Skip to content

Commit

Permalink
React Quill Markdown (#3246)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
rbennettcw committed Apr 1, 2023
1 parent c024612 commit 1fea352
Show file tree
Hide file tree
Showing 14 changed files with 596 additions and 492 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { User } from '../user/user';
import { EditComment } from './edit_comment';
import { clearEditingLocalStorage } from './helpers';
import { AnonymousUser } from '../user/anonymous_user';
import { QuillRenderer } from '../react_quill_editor/quill_renderer';

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

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

return comment.deleted ? (
<span>[deleted]</span>
) : (
<User avatarSize={24} user={author} popover linkify />
);
return comment.deleted ? <span>[deleted]</span> : <User avatarSize={24} user={author} popover linkify />;
};

type CommentProps = {
Expand All @@ -58,20 +52,11 @@ type CommentProps = {
};

export const Comment = (props: CommentProps) => {
const {
comment,
handleIsReplying,
isLast,
isLocked,
setIsGloballyEditing,
threadLevel,
updatedCommentsCallback,
} = props;

const [isEditingComment, setIsEditingComment] =
React.useState<boolean>(false);
const [shouldRestoreEdits, setShouldRestoreEdits] =
React.useState<boolean>(false);
const { comment, handleIsReplying, isLast, isLocked, setIsGloballyEditing, threadLevel, updatedCommentsCallback } =
props;

const [isEditingComment, setIsEditingComment] = React.useState<boolean>(false);
const [shouldRestoreEdits, setShouldRestoreEdits] = React.useState<boolean>(false);
const [savedEdits, setSavedEdits] = React.useState<string>('');

const handleSetIsEditingComment = (status: boolean) => {
Expand All @@ -83,24 +68,21 @@ export const Comment = (props: CommentProps) => {
app.user.isSiteAdmin ||
app.roles.isRoleOfCommunity({
role: 'admin',
chain: app.activeChainId(),
chain: app.activeChainId()
}) ||
app.roles.isRoleOfCommunity({
role: 'moderator',
chain: app.activeChainId(),
chain: app.activeChainId()
});

const canReply =
!isLast && !isLocked && app.isLoggedIn() && app.user.activeAccount;
const canReply = !isLast && !isLocked && app.isLoggedIn() && app.user.activeAccount;

const canEditAndDelete =
!isLocked &&
(comment.author === app.user.activeAccount?.address || isAdminOrMod);
const canEditAndDelete = !isLocked && (comment.author === app.user.activeAccount?.address || isAdminOrMod);

const deleteComment = async () => {
await app.comments.delete(comment);
updatedCommentsCallback();
}
};

return (
<div className={`Comment comment-${comment.id}`}>
Expand All @@ -120,12 +102,7 @@ export const Comment = (props: CommentProps) => {
{/* <CWText type="caption" className="published-text">
published on
</CWText> */}
<CWText
key={comment.id}
type="caption"
fontWeight="medium"
className="published-text"
>
<CWText key={comment.id} type="caption" fontWeight="medium" className="published-text">
{moment(comment.createdAt).format('l')}
</CWText>
</div>
Expand All @@ -140,7 +117,7 @@ export const Comment = (props: CommentProps) => {
) : (
<>
<CWText className="comment-text">
{renderQuillTextBody(comment.text)}
<QuillRenderer doc={comment.text} />
</CWText>
{!comment.deleted && (
<div className="comment-footer">
Expand All @@ -165,11 +142,7 @@ export const Comment = (props: CommentProps) => {
{canEditAndDelete && (
<PopoverMenu
renderTrigger={(onclick) => (
<CWIconButton
iconName="dotsVertical"
iconSize="small"
onClick={onclick}
/>
<CWIconButton iconName="dotsVertical" iconSize="small" onClick={onclick} />
)}
menuItems={[
{
Expand All @@ -178,32 +151,23 @@ export const Comment = (props: CommentProps) => {
onClick: async (e) => {
e.preventDefault();
setSavedEdits(
localStorage.getItem(
`${app.activeChainId()}-edit-comment-${
comment.id
}-storedText`
)
localStorage.getItem(`${app.activeChainId()}-edit-comment-${comment.id}-storedText`)
);
if (savedEdits) {
clearEditingLocalStorage(
comment.id,
ContentType.Comment
);
clearEditingLocalStorage(comment.id, ContentType.Comment);

const confirmationResult = window.confirm(
'Previous changes found. Restore edits?'
);
const confirmationResult = window.confirm('Previous changes found. Restore edits?');

setShouldRestoreEdits(confirmationResult);
}
handleSetIsEditingComment(true);
},
}
},
{
label: 'Delete',
iconLeft: 'trash',
onClick: deleteComment,
},
onClick: deleteComment
}
]}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,8 @@ import { CWButton } from '../component_kit/cw_button';
import { CWText } from '../component_kit/cw_text';
import { CWValidationText } from '../component_kit/cw_validation_text';
import { jumpHighlightComment } from './helpers';
import {
createDeltaFromText,
getTextFromDelta,
ReactQuillEditor,
} from '../react_quill_editor';
import { createDeltaFromText, getTextFromDelta, ReactQuillEditor } from '../react_quill_editor';
import { serializeDelta } from '../react_quill_editor/utils';

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

export const CreateComment = (props: CreateCommmentProps) => {
const [errorMsg, setErrorMsg] = React.useState<string | null>(null);
const [contentDelta, setContentDelta] = React.useState<DeltaStatic>(
createDeltaFromText('')
);
const [contentDelta, setContentDelta] = React.useState<DeltaStatic>(createDeltaFromText(''));
const [sendingComment, setSendingComment] = React.useState<boolean>(false);

const editorValue = getTextFromDelta(contentDelta);

const {
handleIsReplying,
parentCommentId,
rootProposal,
updatedCommentsCallback,
} = props;
const { handleIsReplying, parentCommentId, rootProposal, updatedCommentsCallback } = props;

const author = app.user.activeAccount;

Expand All @@ -61,7 +51,7 @@ export const CreateComment = (props: CreateCommmentProps) => {
author.address,
rootProposal.uniqueIdentifier,
chainId,
JSON.stringify(contentDelta),
serializeDelta(contentDelta),
parentCommentId
);

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

const activeTopicName =
rootProposal instanceof Thread ? rootProposal?.topic?.name : null;
const activeTopicName = rootProposal instanceof Thread ? rootProposal?.topic?.name : null;

// token balance check if needed
const tokenPostingThreshold: BN =
TopicGateCheck.getTopicThreshold(activeTopicName);
const tokenPostingThreshold: BN = TopicGateCheck.getTopicThreshold(activeTopicName);

const userBalance: BN = TopicGateCheck.getUserBalance();
const userFailsThreshold =
tokenPostingThreshold?.gtn(0) &&
userBalance?.gtn(0) &&
userBalance.lt(tokenPostingThreshold);
tokenPostingThreshold?.gtn(0) && userBalance?.gtn(0) && userBalance.lt(tokenPostingThreshold);

const disabled =
editorValue.length === 0 || sendingComment || userFailsThreshold;
const disabled = editorValue.length === 0 || sendingComment || userFailsThreshold;

const decimals = getDecimals(app.chain);

const cancel = (e) => {
e.preventDefault();
setContentDelta(createDeltaFromText(''))
setContentDelta(createDeltaFromText(''));
if (handleIsReplying) {
handleIsReplying(false)
handleIsReplying(false);
}
}
};

return (
<div className="CreateComment">
<div className="attribution-row">
<div className="attribution-left-content">
<CWText type="caption">
{parentType === ContentType.Comment ? 'Reply as' : 'Comment as'}
</CWText>
<CWText type="caption">{parentType === ContentType.Comment ? 'Reply as' : 'Comment as'}</CWText>
<CWText type="caption" fontWeight="medium" className="user-link-text">
<User user={author} hideAvatar linkify />
</CWText>
</div>
{errorMsg && <CWValidationText message={errorMsg} status="failure" />}
</div>
<ReactQuillEditor
className="editor"
contentDelta={contentDelta}
setContentDelta={setContentDelta}
/>
<ReactQuillEditor className="editor" contentDelta={contentDelta} setContentDelta={setContentDelta} />
{tokenPostingThreshold && tokenPostingThreshold.gt(new BN(0)) && (
<CWText className="token-req-text">
Commenting in {activeTopicName} requires{' '}
{weiToTokens(tokenPostingThreshold.toString(), decimals)}{' '}
Commenting in {activeTopicName} requires {weiToTokens(tokenPostingThreshold.toString(), decimals)}{' '}
{app.chain.meta.default_symbol}.{' '}
{userBalance && app.user.activeAccount && (
<>
You have {weiToTokens(userBalance.toString(), decimals)}{' '}
{app.chain.meta.default_symbol}.
You have {weiToTokens(userBalance.toString(), decimals)} {app.chain.meta.default_symbol}.
</>
)}
</CWText>
)}
<div className="form-bottom">
<div className="form-buttons">
{
editorValue.length > 0 && (
<CWButton
buttonType="secondary-blue"
onClick={cancel}
label="Cancel"
/>
)
}
<CWButton
disabled={disabled}
onClick={handleSubmitComment}
label="Submit"
/>
{editorValue.length > 0 && <CWButton buttonType="secondary-blue" onClick={cancel} label="Cancel" />}
<CWButton disabled={disabled} onClick={handleSubmitComment} label="Submit" />
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { CWButton } from '../component_kit/cw_button';
import { clearEditingLocalStorage } from './helpers';
import type { DeltaStatic } from 'quill';
import { ReactQuillEditor } from '../react_quill_editor';
import { parseDeltaString } from '../react_quill_editor/utils';
import { deserializeDelta, serializeDelta } from '../react_quill_editor/utils';

type EditCommentProps = {
comment: Comment<any>;
Expand All @@ -20,16 +20,10 @@ type EditCommentProps = {
};

export const EditComment = (props: EditCommentProps) => {
const {
comment,
savedEdits,
setIsEditing,
shouldRestoreEdits,
updatedCommentsCallback,
} = props;
const { comment, savedEdits, setIsEditing, shouldRestoreEdits, updatedCommentsCallback } = props;

const commentBody = (shouldRestoreEdits && savedEdits) ? savedEdits : comment.text;
const body = parseDeltaString(commentBody)
const commentBody = shouldRestoreEdits && savedEdits ? savedEdits : comment.text;
const body = deserializeDelta(commentBody);

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

if (JSON.stringify(body) !== JSON.stringify(contentDelta)) {
cancelConfirmed = window.confirm(
'Cancel editing? Changes will not be saved.'
);
cancelConfirmed = window.confirm('Cancel editing? Changes will not be saved.');
}

if (cancelConfirmed) {
setIsEditing(false);
clearEditingLocalStorage(comment.id, ContentType.Comment);
}
}
};

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

setSaving(true);

try {
await app.comments.edit(comment, JSON.stringify(contentDelta))
await app.comments.edit(comment, serializeDelta(contentDelta));
setIsEditing(false);
clearEditingLocalStorage(comment.id, ContentType.Comment);
updatedCommentsCallback();
} catch (err) {
console.error(err)
console.error(err);
} finally {
setSaving(false);
}

}
};

return (
<div className="EditComment">
<ReactQuillEditor
contentDelta={contentDelta}
setContentDelta={setContentDelta}
/>
<ReactQuillEditor contentDelta={contentDelta} setContentDelta={setContentDelta} />
<div className="buttons-row">
<CWButton
label="Cancel"
disabled={saving}
buttonType="secondary-blue"
onClick={cancel}
/>
<CWButton
label="Save"
disabled={saving}
onClick={save}
/>
<CWButton label="Cancel" disabled={saving} buttonType="secondary-blue" onClick={cancel} />
<CWButton label="Save" disabled={saving} onClick={save} />
</div>
</div>
);
Expand Down
Loading

0 comments on commit 1fea352

Please sign in to comment.