Skip to content

Commit 1fea352

Browse files
authored
React Quill Markdown (#3246)
* react quill markdown WIP * render rich text preview works * convert mithril components WIP * renders markdown and richtext in preview modal * fix remove formatting on paste * remove formatting on markdown enabled * add serializable delta static for persistence * handle serialization and deserialization in parent components * fix refresh bug when react quill modules prop is updated * fix upload markdown state bug * fix markdown state sync bug * hide toolbar buttons if markdown enabled
1 parent c024612 commit 1fea352

File tree

14 files changed

+596
-492
lines changed

14 files changed

+596
-492
lines changed

packages/commonwealth/client/scripts/views/components/comments/comment.tsx

Lines changed: 22 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { User } from '../user/user';
1818
import { EditComment } from './edit_comment';
1919
import { clearEditingLocalStorage } from './helpers';
2020
import { AnonymousUser } from '../user/anonymous_user';
21+
import { QuillRenderer } from '../react_quill_editor/quill_renderer';
2122

2223
type CommentAuthorProps = {
2324
comment: CommentType<any>;
@@ -29,21 +30,14 @@ const CommentAuthor = (props: CommentAuthorProps) => {
2930
// Check for accounts on forums that originally signed up on a different base chain,
3031
// Render them as anonymous as the forum is unable to support them.
3132
if (app.chain.meta.type === ChainType.Offchain) {
32-
if (
33-
comment.authorChain !== app.chain.id &&
34-
comment.authorChain !== app.chain.base
35-
) {
33+
if (comment.authorChain !== app.chain.id && comment.authorChain !== app.chain.base) {
3634
return <AnonymousUser distinguishingKey={comment.author} />;
3735
}
3836
}
3937

4038
const author: Account = app.chain.accounts.get(comment.author);
4139

42-
return comment.deleted ? (
43-
<span>[deleted]</span>
44-
) : (
45-
<User avatarSize={24} user={author} popover linkify />
46-
);
40+
return comment.deleted ? <span>[deleted]</span> : <User avatarSize={24} user={author} popover linkify />;
4741
};
4842

4943
type CommentProps = {
@@ -58,20 +52,11 @@ type CommentProps = {
5852
};
5953

6054
export const Comment = (props: CommentProps) => {
61-
const {
62-
comment,
63-
handleIsReplying,
64-
isLast,
65-
isLocked,
66-
setIsGloballyEditing,
67-
threadLevel,
68-
updatedCommentsCallback,
69-
} = props;
70-
71-
const [isEditingComment, setIsEditingComment] =
72-
React.useState<boolean>(false);
73-
const [shouldRestoreEdits, setShouldRestoreEdits] =
74-
React.useState<boolean>(false);
55+
const { comment, handleIsReplying, isLast, isLocked, setIsGloballyEditing, threadLevel, updatedCommentsCallback } =
56+
props;
57+
58+
const [isEditingComment, setIsEditingComment] = React.useState<boolean>(false);
59+
const [shouldRestoreEdits, setShouldRestoreEdits] = React.useState<boolean>(false);
7560
const [savedEdits, setSavedEdits] = React.useState<string>('');
7661

7762
const handleSetIsEditingComment = (status: boolean) => {
@@ -83,24 +68,21 @@ export const Comment = (props: CommentProps) => {
8368
app.user.isSiteAdmin ||
8469
app.roles.isRoleOfCommunity({
8570
role: 'admin',
86-
chain: app.activeChainId(),
71+
chain: app.activeChainId()
8772
}) ||
8873
app.roles.isRoleOfCommunity({
8974
role: 'moderator',
90-
chain: app.activeChainId(),
75+
chain: app.activeChainId()
9176
});
9277

93-
const canReply =
94-
!isLast && !isLocked && app.isLoggedIn() && app.user.activeAccount;
78+
const canReply = !isLast && !isLocked && app.isLoggedIn() && app.user.activeAccount;
9579

96-
const canEditAndDelete =
97-
!isLocked &&
98-
(comment.author === app.user.activeAccount?.address || isAdminOrMod);
80+
const canEditAndDelete = !isLocked && (comment.author === app.user.activeAccount?.address || isAdminOrMod);
9981

10082
const deleteComment = async () => {
10183
await app.comments.delete(comment);
10284
updatedCommentsCallback();
103-
}
85+
};
10486

10587
return (
10688
<div className={`Comment comment-${comment.id}`}>
@@ -120,12 +102,7 @@ export const Comment = (props: CommentProps) => {
120102
{/* <CWText type="caption" className="published-text">
121103
published on
122104
</CWText> */}
123-
<CWText
124-
key={comment.id}
125-
type="caption"
126-
fontWeight="medium"
127-
className="published-text"
128-
>
105+
<CWText key={comment.id} type="caption" fontWeight="medium" className="published-text">
129106
{moment(comment.createdAt).format('l')}
130107
</CWText>
131108
</div>
@@ -140,7 +117,7 @@ export const Comment = (props: CommentProps) => {
140117
) : (
141118
<>
142119
<CWText className="comment-text">
143-
{renderQuillTextBody(comment.text)}
120+
<QuillRenderer doc={comment.text} />
144121
</CWText>
145122
{!comment.deleted && (
146123
<div className="comment-footer">
@@ -165,11 +142,7 @@ export const Comment = (props: CommentProps) => {
165142
{canEditAndDelete && (
166143
<PopoverMenu
167144
renderTrigger={(onclick) => (
168-
<CWIconButton
169-
iconName="dotsVertical"
170-
iconSize="small"
171-
onClick={onclick}
172-
/>
145+
<CWIconButton iconName="dotsVertical" iconSize="small" onClick={onclick} />
173146
)}
174147
menuItems={[
175148
{
@@ -178,32 +151,23 @@ export const Comment = (props: CommentProps) => {
178151
onClick: async (e) => {
179152
e.preventDefault();
180153
setSavedEdits(
181-
localStorage.getItem(
182-
`${app.activeChainId()}-edit-comment-${
183-
comment.id
184-
}-storedText`
185-
)
154+
localStorage.getItem(`${app.activeChainId()}-edit-comment-${comment.id}-storedText`)
186155
);
187156
if (savedEdits) {
188-
clearEditingLocalStorage(
189-
comment.id,
190-
ContentType.Comment
191-
);
157+
clearEditingLocalStorage(comment.id, ContentType.Comment);
192158

193-
const confirmationResult = window.confirm(
194-
'Previous changes found. Restore edits?'
195-
);
159+
const confirmationResult = window.confirm('Previous changes found. Restore edits?');
196160

197161
setShouldRestoreEdits(confirmationResult);
198162
}
199163
handleSetIsEditingComment(true);
200-
},
164+
}
201165
},
202166
{
203167
label: 'Delete',
204168
iconLeft: 'trash',
205-
onClick: deleteComment,
206-
},
169+
onClick: deleteComment
170+
}
207171
]}
208172
/>
209173
)}

packages/commonwealth/client/scripts/views/components/comments/create_comment.tsx

Lines changed: 18 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,8 @@ import { CWButton } from '../component_kit/cw_button';
1717
import { CWText } from '../component_kit/cw_text';
1818
import { CWValidationText } from '../component_kit/cw_validation_text';
1919
import { jumpHighlightComment } from './helpers';
20-
import {
21-
createDeltaFromText,
22-
getTextFromDelta,
23-
ReactQuillEditor,
24-
} from '../react_quill_editor';
20+
import { createDeltaFromText, getTextFromDelta, ReactQuillEditor } from '../react_quill_editor';
21+
import { serializeDelta } from '../react_quill_editor/utils';
2522

2623
type CreateCommmentProps = {
2724
handleIsReplying?: (isReplying: boolean, id?: number) => void;
@@ -32,19 +29,12 @@ type CreateCommmentProps = {
3229

3330
export const CreateComment = (props: CreateCommmentProps) => {
3431
const [errorMsg, setErrorMsg] = React.useState<string | null>(null);
35-
const [contentDelta, setContentDelta] = React.useState<DeltaStatic>(
36-
createDeltaFromText('')
37-
);
32+
const [contentDelta, setContentDelta] = React.useState<DeltaStatic>(createDeltaFromText(''));
3833
const [sendingComment, setSendingComment] = React.useState<boolean>(false);
3934

4035
const editorValue = getTextFromDelta(contentDelta);
4136

42-
const {
43-
handleIsReplying,
44-
parentCommentId,
45-
rootProposal,
46-
updatedCommentsCallback,
47-
} = props;
37+
const { handleIsReplying, parentCommentId, rootProposal, updatedCommentsCallback } = props;
4838

4939
const author = app.user.activeAccount;
5040

@@ -61,7 +51,7 @@ export const CreateComment = (props: CreateCommmentProps) => {
6151
author.address,
6252
rootProposal.uniqueIdentifier,
6353
chainId,
64-
JSON.stringify(contentDelta),
54+
serializeDelta(contentDelta),
6555
parentCommentId
6656
);
6757

@@ -88,79 +78,54 @@ export const CreateComment = (props: CreateCommmentProps) => {
8878
}
8979
};
9080

91-
const activeTopicName =
92-
rootProposal instanceof Thread ? rootProposal?.topic?.name : null;
81+
const activeTopicName = rootProposal instanceof Thread ? rootProposal?.topic?.name : null;
9382

9483
// token balance check if needed
95-
const tokenPostingThreshold: BN =
96-
TopicGateCheck.getTopicThreshold(activeTopicName);
84+
const tokenPostingThreshold: BN = TopicGateCheck.getTopicThreshold(activeTopicName);
9785

9886
const userBalance: BN = TopicGateCheck.getUserBalance();
9987
const userFailsThreshold =
100-
tokenPostingThreshold?.gtn(0) &&
101-
userBalance?.gtn(0) &&
102-
userBalance.lt(tokenPostingThreshold);
88+
tokenPostingThreshold?.gtn(0) && userBalance?.gtn(0) && userBalance.lt(tokenPostingThreshold);
10389

104-
const disabled =
105-
editorValue.length === 0 || sendingComment || userFailsThreshold;
90+
const disabled = editorValue.length === 0 || sendingComment || userFailsThreshold;
10691

10792
const decimals = getDecimals(app.chain);
10893

10994
const cancel = (e) => {
11095
e.preventDefault();
111-
setContentDelta(createDeltaFromText(''))
96+
setContentDelta(createDeltaFromText(''));
11297
if (handleIsReplying) {
113-
handleIsReplying(false)
98+
handleIsReplying(false);
11499
}
115-
}
100+
};
116101

117102
return (
118103
<div className="CreateComment">
119104
<div className="attribution-row">
120105
<div className="attribution-left-content">
121-
<CWText type="caption">
122-
{parentType === ContentType.Comment ? 'Reply as' : 'Comment as'}
123-
</CWText>
106+
<CWText type="caption">{parentType === ContentType.Comment ? 'Reply as' : 'Comment as'}</CWText>
124107
<CWText type="caption" fontWeight="medium" className="user-link-text">
125108
<User user={author} hideAvatar linkify />
126109
</CWText>
127110
</div>
128111
{errorMsg && <CWValidationText message={errorMsg} status="failure" />}
129112
</div>
130-
<ReactQuillEditor
131-
className="editor"
132-
contentDelta={contentDelta}
133-
setContentDelta={setContentDelta}
134-
/>
113+
<ReactQuillEditor className="editor" contentDelta={contentDelta} setContentDelta={setContentDelta} />
135114
{tokenPostingThreshold && tokenPostingThreshold.gt(new BN(0)) && (
136115
<CWText className="token-req-text">
137-
Commenting in {activeTopicName} requires{' '}
138-
{weiToTokens(tokenPostingThreshold.toString(), decimals)}{' '}
116+
Commenting in {activeTopicName} requires {weiToTokens(tokenPostingThreshold.toString(), decimals)}{' '}
139117
{app.chain.meta.default_symbol}.{' '}
140118
{userBalance && app.user.activeAccount && (
141119
<>
142-
You have {weiToTokens(userBalance.toString(), decimals)}{' '}
143-
{app.chain.meta.default_symbol}.
120+
You have {weiToTokens(userBalance.toString(), decimals)} {app.chain.meta.default_symbol}.
144121
</>
145122
)}
146123
</CWText>
147124
)}
148125
<div className="form-bottom">
149126
<div className="form-buttons">
150-
{
151-
editorValue.length > 0 && (
152-
<CWButton
153-
buttonType="secondary-blue"
154-
onClick={cancel}
155-
label="Cancel"
156-
/>
157-
)
158-
}
159-
<CWButton
160-
disabled={disabled}
161-
onClick={handleSubmitComment}
162-
label="Submit"
163-
/>
127+
{editorValue.length > 0 && <CWButton buttonType="secondary-blue" onClick={cancel} label="Cancel" />}
128+
<CWButton disabled={disabled} onClick={handleSubmitComment} label="Submit" />
164129
</div>
165130
</div>
166131
</div>

packages/commonwealth/client/scripts/views/components/comments/edit_comment.tsx

Lines changed: 12 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { CWButton } from '../component_kit/cw_button';
99
import { clearEditingLocalStorage } from './helpers';
1010
import type { DeltaStatic } from 'quill';
1111
import { ReactQuillEditor } from '../react_quill_editor';
12-
import { parseDeltaString } from '../react_quill_editor/utils';
12+
import { deserializeDelta, serializeDelta } from '../react_quill_editor/utils';
1313

1414
type EditCommentProps = {
1515
comment: Comment<any>;
@@ -20,16 +20,10 @@ type EditCommentProps = {
2020
};
2121

2222
export const EditComment = (props: EditCommentProps) => {
23-
const {
24-
comment,
25-
savedEdits,
26-
setIsEditing,
27-
shouldRestoreEdits,
28-
updatedCommentsCallback,
29-
} = props;
23+
const { comment, savedEdits, setIsEditing, shouldRestoreEdits, updatedCommentsCallback } = props;
3024

31-
const commentBody = (shouldRestoreEdits && savedEdits) ? savedEdits : comment.text;
32-
const body = parseDeltaString(commentBody)
25+
const commentBody = shouldRestoreEdits && savedEdits ? savedEdits : comment.text;
26+
const body = deserializeDelta(commentBody);
3327

3428
const [contentDelta, setContentDelta] = React.useState<DeltaStatic>(body);
3529
const [saving, setSaving] = React.useState<boolean>();
@@ -40,53 +34,38 @@ export const EditComment = (props: EditCommentProps) => {
4034
let cancelConfirmed = true;
4135

4236
if (JSON.stringify(body) !== JSON.stringify(contentDelta)) {
43-
cancelConfirmed = window.confirm(
44-
'Cancel editing? Changes will not be saved.'
45-
);
37+
cancelConfirmed = window.confirm('Cancel editing? Changes will not be saved.');
4638
}
4739

4840
if (cancelConfirmed) {
4941
setIsEditing(false);
5042
clearEditingLocalStorage(comment.id, ContentType.Comment);
5143
}
52-
}
44+
};
5345

5446
const save = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
5547
e.preventDefault();
5648

5749
setSaving(true);
5850

5951
try {
60-
await app.comments.edit(comment, JSON.stringify(contentDelta))
52+
await app.comments.edit(comment, serializeDelta(contentDelta));
6153
setIsEditing(false);
6254
clearEditingLocalStorage(comment.id, ContentType.Comment);
6355
updatedCommentsCallback();
6456
} catch (err) {
65-
console.error(err)
57+
console.error(err);
6658
} finally {
6759
setSaving(false);
6860
}
69-
70-
}
61+
};
7162

7263
return (
7364
<div className="EditComment">
74-
<ReactQuillEditor
75-
contentDelta={contentDelta}
76-
setContentDelta={setContentDelta}
77-
/>
65+
<ReactQuillEditor contentDelta={contentDelta} setContentDelta={setContentDelta} />
7866
<div className="buttons-row">
79-
<CWButton
80-
label="Cancel"
81-
disabled={saving}
82-
buttonType="secondary-blue"
83-
onClick={cancel}
84-
/>
85-
<CWButton
86-
label="Save"
87-
disabled={saving}
88-
onClick={save}
89-
/>
67+
<CWButton label="Cancel" disabled={saving} buttonType="secondary-blue" onClick={cancel} />
68+
<CWButton label="Save" disabled={saving} onClick={save} />
9069
</div>
9170
</div>
9271
);

0 commit comments

Comments
 (0)