Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Replace Draft with Slate #1890

Merged
merged 108 commits into from
Jul 16, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
75a2be1
WIP (doesn't build yet) replacing draft with slate
ara4n Apr 23, 2018
0294706
Merge branch 'develop' into matthew/slate
ara4n May 5, 2018
e62e43d
comment out more draft stuff
ara4n May 5, 2018
f4ed820
fix stubbing
ara4n May 5, 2018
05eba3f
stub out more until it loads...
ara4n May 5, 2018
a2233a4
stub out yet more
ara4n May 6, 2018
190f6d9
make slate actually work as a textarea
ara4n May 6, 2018
ff42ef4
make it work for MD mode (modulo history)
ara4n May 6, 2018
8b2eb2c
make history work again
ara4n May 8, 2018
984961a
blind fix to the overlapping sticker bug
ara4n May 8, 2018
cbb8432
unbreak switching from draft to slate
ara4n May 9, 2018
410a168
make autocomplete selection work
ara4n May 12, 2018
d7c2c8b
include the plaintext representation of a pill within it
ara4n May 12, 2018
9c0c806
correctly send pills in messages
ara4n May 12, 2018
c91dcff
fix cursor behaviour around pills
ara4n May 12, 2018
33eaa84
fix NPEs when deleting mentions
ara4n May 12, 2018
877a619
unbreak history scrolling for pills & emoji
ara4n May 12, 2018
c967ecc
autocomplete polishing
ara4n May 13, 2018
5605439
autocomplete polishing
ara4n May 13, 2018
e06763c
show all slashcommands on /
ara4n May 13, 2018
4c3588d
don't lose focus after a / command
ara4n May 13, 2018
79f7c5d
remove // support, as it never worked
ara4n May 13, 2018
721410b
Merge branch 'develop' into matthew/slate
ara4n May 13, 2018
dd0726f
fix navigating history downwards on tall messages; remove obsolete code
ara4n May 13, 2018
ddfe069
fix insert_mention
ara4n May 13, 2018
a247ea2
delete duplicate propTypes(!!!)
ara4n May 13, 2018
7405b49
unify setState() and onChange()
ara4n May 13, 2018
7ecb4e3
remove dead removeMDLinks code
ara4n May 13, 2018
b10f9a9
remove spurious vendor prefixing
ara4n May 14, 2018
c1000a7
emojioneify the composer
ara4n May 14, 2018
12a56e8
remove spurious comment
ara4n May 14, 2018
b60ccad
Merge branch 'develop' into matthew/slate
ara4n May 15, 2018
4eb6942
let onChange set originalEditorState
ara4n May 15, 2018
ae208da
nudge towards supporting formatting buttons in MD
ara4n May 16, 2018
e51554c
actually hook up RTE
ara4n May 17, 2018
089ac33
remove unused html serializer
ara4n May 18, 2018
4972a23
Merge branch 'develop' into matthew/slate
ara4n May 19, 2018
167742d
make RTE sending work
ara4n May 19, 2018
a4d9338
let backspace delete list nodes in RTE
ara4n May 19, 2018
58670cc
exit list more sanely on backspace
ara4n May 19, 2018
d426c34
fix strikethough & code, improve shift-return & backspace
ara4n May 19, 2018
1536ab4
make file pasting work again
ara4n May 19, 2018
1f05aea
make HTML pasting work
ara4n May 19, 2018
572a313
add h4, h5 and h6
ara4n May 19, 2018
65f0b05
fix typo
ara4n May 19, 2018
1175195
remove HRs from H1/H2s
ara4n May 19, 2018
f211694
switch schema to match the MD serializer
ara4n May 19, 2018
c3a6a41
support links in RTE
ara4n May 19, 2018
d76a2ab
use <p/> as our root node everywhere and fix blank roundtrip bug
ara4n May 20, 2018
a0d88a8
support sending inlines from the RTE.
ara4n May 20, 2018
e9cabf0
add pill and emoji serialisation to Md
ara4n May 20, 2018
ad7782b
remove the remaining Draft specific stuff from RichText
ara4n May 20, 2018
c5676ee
comment out more old draft stuff
ara4n May 20, 2018
9aba046
fix MD pill serialization
ara4n May 20, 2018
aac6866
switch back to using commonmark for serialising MD when roundtripping
ara4n May 20, 2018
d799b7e
refactor roundtripping into a single place
ara4n May 20, 2018
f981d7b
unify buttons on the node type names, and make them work
ara4n May 20, 2018
e460cf3
hide formatting bar for MD editor
ara4n May 20, 2018
b616fd0
comment out all the tests for now
ara4n May 20, 2018
4439a04
fix lint
ara4n May 20, 2018
7de45f8
make quoting work
ara4n May 21, 2018
11cea61
refocus editor after clicking on autocompletes
ara4n May 21, 2018
cace5e8
fix bug where selection breaks after inserting emoji
ara4n May 22, 2018
e7a4ffa
fix emojioneifying autoconverted emoji
ara4n May 22, 2018
794a60b
refocus editor immediately after executing commands
ara4n May 23, 2018
6ac2324
fix wordwrap and pre formatting
ara4n May 23, 2018
6fba831
escape blockquotes correctly
ara4n May 23, 2018
fc1c499
slate-md-serializer 3.1.0 now escapes correctly
ara4n May 23, 2018
edc8264
shift to custom slate-md-serializer
ara4n May 23, 2018
6f3634c
bump slate-md-serializer
ara4n May 23, 2018
a822a1c
bump dep
ara4n May 23, 2018
079b123
bump right dep
ara4n May 23, 2018
95bffac
comment out broken logic for stripping p tags & bump dep
ara4n May 23, 2018
f40af43
unbreak Markdown.toPlaintext
ara4n May 23, 2018
5b4a036
bump dep
ara4n May 23, 2018
87c3e92
bump dep
ara4n May 23, 2018
fb5240a
explain why we're now including <p/>s
ara4n May 23, 2018
b0ff61f
switch code schema to match slate-md-serializer
ara4n May 23, 2018
294565c
switch code schema to match slate-md-serializer
ara4n May 23, 2018
864a33f
switch to using 'code' for both blocks & marks to match md-serializer
ara4n May 23, 2018
ab41212
fix double-nested code blocks & bump md-serializer
ara4n May 25, 2018
6299e18
unbreak keyboard shortcuts & ctrl-backspace
ara4n May 26, 2018
efdc543
merge develop
ara4n Jul 9, 2018
c6837af
import-type Change from slate
t3chguy Jul 3, 2018
372fa29
take edge into consideration when moving focus region on arrow keys
t3chguy Jul 3, 2018
483116f
add rule to slate-md-serializer: make underlined and removed work for CM
t3chguy Jul 3, 2018
faf17f0
remove debugger statement
t3chguy Jul 3, 2018
43204ea
fix Control-Backspace after select-all
t3chguy Jul 4, 2018
5b74c61
add missing import
t3chguy Jul 4, 2018
5bd4104
modify ComposerHistoryManager
t3chguy Jul 4, 2018
8665f10
pin slate to 0.33.4 to avoid https://github.com/ianstormtaylor/slate/…
ara4n Jul 8, 2018
83f2614
add guide to slate's data formats and how we convert
ara4n Jul 8, 2018
021409a
apply review feedback from @lukebarnard1
ara4n Jul 8, 2018
0d0934a
unbreak modifier+space (e.g. emoji insert on macOS)
ara4n Jul 8, 2018
8bcb987
delint
t3chguy Jul 9, 2018
51591a4
fix lint
ara4n Jul 9, 2018
58301e5
navigateHistory only when at edges of document, to prevent Firefox bug
t3chguy Jul 10, 2018
100ecfe
remove trailing spaces to make linter happy (no-trailing-spaces)
t3chguy Jul 10, 2018
abbb69d
fix fn call, fixes usage of SlashCommands
t3chguy Jul 10, 2018
fd4f967
convert md<->rt if the stored editorState was in a different state
t3chguy Jul 11, 2018
c3aef6e
workaround for tommoor/slate-md-serializer#14
t3chguy Jul 11, 2018
95909de
fix MessageComposer not marking translatable strings. run gen-i18n
t3chguy Jul 11, 2018
3e05bf1
hide autocomplete when moving caret to match existing behaviour
t3chguy Jul 11, 2018
b4bc09c
null-guard savedState since now we're accessing its props
t3chguy Jul 11, 2018
7405c5e
specify alternate history storage key to prevent conflicts with draft
t3chguy Jul 12, 2018
59a14f2
re-hydrate Values which have been serialized into LocalStorage
t3chguy Jul 15, 2018
d7ff7cd
stupid thinkotypo
ara4n Jul 16, 2018
eb497d4
Merge pull request #2049 from matrix-org/t3chguy/slate_cont2
dbkr Jul 16, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ module.exports = {
"new-cap": ["warn"],
"key-spacing": ["warn"],
"prefer-const": ["warn"],
"arrow-parens": "off",

// crashes currently: https://github.com/eslint/eslint/issues/6274
"generator-star-spacing": "off",
Expand Down
88 changes: 88 additions & 0 deletions docs/slate-formats.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
Guide to data types used by the Slate-based Rich Text Editor
------------------------------------------------------------

We always store the Slate editor state in its Value form.

The schema for the Value is the same whether the editor is in MD or rich text mode, and is currently (rather arbitrarily)
dictated by the schema expected by slate-md-serializer, simply because it was the only bit of the pipeline which
has opinions on the schema. (slate-html-serializer lets you define how to serialize whatever schema you like).

The BLOCK_TAGS and MARK_TAGS give the mapping from HTML tags to the schema's node types (for blocks, which describe
block content like divs, and marks, which describe inline formatted sections like spans).

We use <p/> as the parent tag for the message (XXX: although some tags are technically not allowed to be nested within p's)

Various conversions are performed as content is moved between HTML, MD, and plaintext representations of HTML and MD.

The primitives used are:

* Markdown.js - models commonmark-formatted MD strings (as entered by the composer in MD mode)
* toHtml() - renders them to HTML suitable for sending on the wire
* isPlainText() - checks whether the parsed MD contains anything other than simple text.
* toPlainText() - renders MD to plain text in order to remove backslashes. Only works if the MD is already plaintext (otherwise it just emits HTML)

* slate-html-serializer
* converts Values to HTML (serialising) using our schema rules
* converts HTML to Values (deserialising) using our schema rules

* slate-md-serializer
* converts rich Values to MD strings (serialising) but using a non-commonmark generic MD dialect.
* This should use commonmark, but we use the serializer here for expedience rather than writing a commonmark one.

* slate-plain-serializer
* converts Values to plain text strings (serialising them) by concatenating the strings together
* converts Values from plain text strings (deserialiasing them).
* Used to initialise the editor by deserializing "" into a Value. Apparently this is the idiomatic way to initialise a blank editor.
* Used (as a bodge) to turn a rich text editor into a MD editor, when deserialising the converted MD string of the editor into a value

* PlainWithPillsSerializer
* A fork of slate-plain-serializer which is aware of Pills (hence the name) and Emoji.
* It can be configured to output Pills as:
* "plain": Pills are rendered via their 'completion' text - e.g. 'Matthew'; used for sending messages)
* "md": Pills are rendered as MD, e.g. [Matthew](https://matrix.to/#/@matthew:matrix.org) )
* "id": Pills are rendered as IDs, e.g. '@matthew:matrix.org' (used for authoring / commands)
* Emoji nodes are converted to inline utf8 emoji.

The actual conversion transitions are:

* Quoting:
* The message being quoted is taken as HTML
* ...and deserialised into a Value
* ...and then serialised into MD via slate-md-serializer if the editor is in MD mode

* Roundtripping between MD and rich text editor mode
* From MD to richtext (mdToRichEditorState):
* Serialise the MD-format Value to a MD string (converting pills to MD) with PlainWithPillsSerializer in 'md' mode
* Convert that MD string to HTML via Markdown.js
* Deserialise that Value to HTML via slate-html-serializer
* From richtext to MD (richToMdEditorState):
* Serialise the richtext-format Value to a MD string with slate-md-serializer (XXX: this should use commonmark)
* Deserialise that to a plain text value via slate-plain-serializer

* Loading history in one format into an editor which is in the other format
* Uses the same functions as for roundtripping

* Scanning the editor for a slash command
* If the editor is a single line node starting with /, then serialize it to a string with PlainWithPillsSerializer in 'id' mode
So that pills get converted to IDs suitable for commands being passed around

* Sending messages
* In RT mode:
* If there is rich content, serialize the RT-format Value to HTML body via slate-html-serializer
* Serialize the RT-format Value to the plain text fallback via PlainWithPillsSerializer in 'plain' mode
* In MD mode:
* Serialize the MD-format Value into an MD string with PlainWithPillsSerializer in 'md' mode
* Parse the string with Markdown.js
* If it contains no formatting:
* Send as plaintext (as taken from Markdown.toPlainText())
* Otherwise
* Send as HTML (as taken from Markdown.toHtml())
* Serialize the RT-format Value to the plain text fallback via PlainWithPillsSerializer in 'plain' mode

* Pasting HTML
* Deserialize HTML to a RT Value via slate-html-serializer
* In RT mode, insert it straight into the editor as a fragment
* In MD mode, serialise it to an MD string via slate-md-serializer and then insert the string into the editor as a fragment.

The various scenarios and transitions could be drawn into a pretty diagram if one felt the urge, but hopefully the above
gives sufficient detail on how it's all meant to work.
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,6 @@
"classnames": "^2.1.2",
"commonmark": "^0.28.1",
"counterpart": "^0.18.0",
"draft-js": "^0.11.0-alpha",
"draft-js-export-html": "^0.6.0",
"draft-js-export-markdown": "^0.3.0",
"emojione": "2.2.7",
"file-saver": "^1.3.3",
"filesize": "3.5.6",
Expand All @@ -87,6 +84,10 @@
"react-beautiful-dnd": "^4.0.1",
"react-dom": "^15.6.0",
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
"slate": "0.33.4",
"slate-react": "^0.12.4",
"slate-html-serializer": "^0.6.1",
"slate-md-serializer": "matrix-org/slate-md-serializer#f7c4ad3",
"sanitize-html": "^1.14.1",
"text-encoding-utf-8": "^1.0.1",
"url": "^0.11.0",
Expand Down
4 changes: 4 additions & 0 deletions res/css/_common.scss
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,10 @@ textarea {
vertical-align: middle;
}

.mx_emojione_selected {
background-color: $accent-color;
}

::-moz-selection {
background-color: $accent-color;
color: $selection-fg-color;
Expand Down
5 changes: 1 addition & 4 deletions res/css/structures/_RoomView.scss
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,7 @@ hr.mx_RoomView_myReadMarker {
z-index: 1000;
overflow: hidden;

-webkit-transition: all .2s ease-out;
-moz-transition: all .2s ease-out;
-ms-transition: all .2s ease-out;
-o-transition: all .2s ease-out;
transition: all .2s ease-out;
}

.mx_RoomView_statusArea_expanded {
Expand Down
4 changes: 4 additions & 0 deletions res/css/views/elements/_RichText.scss
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
padding-right: 5px;
}

.mx_UserPill_selected {
background-color: $accent-color ! important;
}

.mx_EventTile_highlight .mx_EventTile_content .markdown-body a.mx_UserPill_me,
.mx_EventTile_content .mx_AtRoomPill,
.mx_MessageComposer_input .mx_AtRoomPill {
Expand Down
1 change: 1 addition & 0 deletions res/css/views/rooms/_EventTile.scss
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ limitations under the License.
.mx_EventTile_content .markdown-body h2
{
font-size: 1.5em;
border-bottom: none ! important; // override GFM
}

.mx_EventTile_content .markdown-body a {
Expand Down
43 changes: 19 additions & 24 deletions res/css/views/rooms/_MessageComposer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,29 @@ limitations under the License.
display: flex;
flex-direction: column;
min-height: 60px;
justify-content: center;
justify-content: start;
align-items: flex-start;
font-size: 14px;
margin-right: 6px;
}

.mx_MessageComposer_editor {
width: 100%;
max-height: 120px;
min-height: 19px;
overflow: auto;
word-break: break-word;
}

// FIXME: rather unpleasant hack to get rid of <p/> margins.
// really we should be mixing in markdown-body from gfm.css instead
.mx_MessageComposer_editor > :first-child {
margin-top: 0 ! important;
}
.mx_MessageComposer_editor > :last-child {
margin-bottom: 0 ! important;
}

@keyframes visualbell
{
from { background-color: #faa }
Expand All @@ -95,36 +112,14 @@ limitations under the License.
animation: 0.2s visualbell;
}

.mx_MessageComposer_input_empty .public-DraftEditorPlaceholder-root {
display: none;
}

.mx_MessageComposer_input .DraftEditor-root {
width: 100%;
flex: 1;
word-break: break-word;
max-height: 120px;
min-height: 21px;
overflow: auto;
}

.mx_MessageComposer_input .DraftEditor-root .DraftEditor-editorContainer {
/* Ensure mx_UserPill and mx_RoomPill (see _RichText) are not obscured from the top */
padding-top: 2px;
}

.mx_MessageComposer .public-DraftStyleDefault-block {
overflow-x: hidden;
}

.mx_MessageComposer_input blockquote {
color: $blockquote-fg-color;
margin: 0 0 16px;
padding: 0 15px;
border-left: 4px solid $blockquote-bar-color;
}

.mx_MessageComposer_input pre.public-DraftStyleDefault-pre pre {
.mx_MessageComposer_input pre {
background-color: $rte-code-bg-color;
border-radius: 3px;
padding: 10px;
Expand Down
75 changes: 39 additions & 36 deletions src/ComposerHistoryManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,70 +15,73 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import {ContentState, convertToRaw, convertFromRaw} from 'draft-js';
import * as RichText from './RichText';
import Markdown from './Markdown';
import { Value } from 'slate';

import _clamp from 'lodash/clamp';

type MessageFormat = 'html' | 'markdown';
type MessageFormat = 'rich' | 'markdown';

class HistoryItem {

// Keeping message for backwards-compatibility
message: string;
rawContentState: RawDraftContentState;
format: MessageFormat = 'html';
// We store history items in their native format to ensure history is accurate
// and then convert them if our RTE has subsequently changed format.
value: Value;
format: MessageFormat = 'rich';

constructor(contentState: ?ContentState, format: ?MessageFormat) {
this.rawContentState = contentState ? convertToRaw(contentState) : null;
constructor(value: ?Value, format: ?MessageFormat) {
this.value = value;
this.format = format;
}

toContentState(outputFormat: MessageFormat): ContentState {
const contentState = convertFromRaw(this.rawContentState);
if (outputFormat === 'markdown') {
if (this.format === 'html') {
return ContentState.createFromText(RichText.stateToMarkdown(contentState));
}
} else {
if (this.format === 'markdown') {
return RichText.htmlToContentState(new Markdown(contentState.getPlainText()).toHTML());
}
}
// history item has format === outputFormat
return contentState;
static fromJSON(obj: Object): HistoryItem {
return new HistoryItem(
Value.fromJSON(obj.value),
obj.format,
);
}

toJSON(): Object {
return {
value: this.value.toJSON(),
format: this.format,
};
}
}

export default class ComposerHistoryManager {
history: Array<HistoryItem> = [];
prefix: string;
lastIndex: number = 0;
currentIndex: number = 0;
lastIndex: number = 0; // used for indexing the storage
currentIndex: number = 0; // used for indexing the loaded validated history Array

constructor(roomId: string, prefix: string = 'mx_composer_history_') {
this.prefix = prefix + roomId;

// TODO: Performance issues?
let item;
for (; item = sessionStorage.getItem(`${this.prefix}[${this.currentIndex}]`); this.currentIndex++) {
this.history.push(
Object.assign(new HistoryItem(), JSON.parse(item)),
);
try {
this.history.push(
HistoryItem.fromJSON(JSON.parse(item)),
);
} catch (e) {
console.warn("Throwing away unserialisable history", e);
}
}
this.lastIndex = this.currentIndex;
// reset currentIndex to account for any unserialisable history
this.currentIndex = this.history.length;
}

save(contentState: ContentState, format: MessageFormat) {
const item = new HistoryItem(contentState, format);
save(value: Value, format: MessageFormat) {
const item = new HistoryItem(value, format);
this.history.push(item);
this.currentIndex = this.lastIndex + 1;
sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item));
this.currentIndex = this.history.length;
sessionStorage.setItem(`${this.prefix}[${this.lastIndex++}]`, JSON.stringify(item.toJSON()));
}

getItem(offset: number, format: MessageFormat): ?ContentState {
this.currentIndex = _clamp(this.currentIndex + offset, 0, this.lastIndex - 1);
const item = this.history[this.currentIndex];
return item ? item.toContentState(format) : null;
getItem(offset: number): ?HistoryItem {
this.currentIndex = _clamp(this.currentIndex + offset, 0, this.history.length - 1);
return this.history[this.currentIndex];
}
}
Loading