Allow copy/paste of formatting and mentions

This commit is contained in:
Scott Nonnenberg 2023-05-09 17:40:19 -07:00 committed by GitHub
parent 320ac044a8
commit b4caf67bf9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 1003 additions and 446 deletions

View file

@ -22,7 +22,7 @@ import type {
HydratedBodyRangesType,
RangeNode,
} from '../types/BodyRange';
import { collapseRangeTree, insertRange } from '../types/BodyRange';
import { BodyRange, collapseRangeTree, insertRange } from '../types/BodyRange';
import type { LocalizerType, ThemeType } from '../types/Util';
import type { ConversationType } from '../state/ducks/conversations';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
@ -31,7 +31,6 @@ import { MentionBlot } from '../quill/mentions/blot';
import {
matchEmojiImage,
matchEmojiBlot,
matchReactEmoji,
matchEmojiText,
} from '../quill/emoji/matchers';
import { matchMention } from '../quill/mentions/matchers';
@ -53,6 +52,14 @@ import type { LinkPreviewType } from '../types/message/LinkPreviews';
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
import type { DraftEditMessageType } from '../model-types.d';
import { usePrevious } from '../hooks/usePrevious';
import {
matchBold,
matchItalic,
matchMonospace,
matchSpoiler,
matchStrikethrough,
} from '../quill/formatting/matchers';
import { missingCaseError } from '../util/missingCaseError';
Quill.register('formats/emoji', EmojiBlot);
Quill.register('formats/mention', MentionBlot);
@ -91,7 +98,8 @@ export type Props = Readonly<{
large?: boolean;
inputApi?: React.MutableRefObject<InputApi | undefined>;
isFormattingEnabled: boolean;
isFormattingSpoilersEnabled: boolean;
isFormattingFlagEnabled: boolean;
isFormattingSpoilersFlagEnabled: boolean;
sendCounter: number;
skinTone?: EmojiPickDataType['skinTone'];
draftText?: string;
@ -117,6 +125,7 @@ export type Props = Readonly<{
timestamp: number
): unknown;
onScroll?: (ev: React.UIEvent<HTMLElement>) => void;
platform: string;
getQuotedMessage?(): unknown;
clearQuotedMessage?(): unknown;
linkPreviewLoading?: boolean;
@ -141,7 +150,8 @@ export function CompositionInput(props: Props): React.ReactElement {
i18n,
inputApi,
isFormattingEnabled,
isFormattingSpoilersEnabled,
isFormattingFlagEnabled,
isFormattingSpoilersFlagEnabled,
large,
linkPreviewLoading,
linkPreviewResult,
@ -151,6 +161,7 @@ export function CompositionInput(props: Props): React.ReactElement {
onScroll,
onSubmit,
placeholder,
platform,
skinTone,
sendCounter,
sortedGroupMembers,
@ -220,7 +231,29 @@ export function CompositionInput(props: Props): React.ReactElement {
return { text: '', bodyRanges: [] };
}
return getTextAndRangesFromOps(ops);
const { text, bodyRanges } = getTextAndRangesFromOps(ops);
return {
text,
bodyRanges: bodyRanges.filter(range => {
if (BodyRange.isMention(range)) {
return true;
}
if (BodyRange.isFormatting(range)) {
if (!isFormattingFlagEnabled) {
return false;
}
if (
range.style === BodyRange.Style.SPOILER &&
!isFormattingSpoilersFlagEnabled
) {
return false;
}
return true;
}
throw missingCaseError(range);
}),
};
};
const focus = () => {
@ -352,32 +385,46 @@ export function CompositionInput(props: Props): React.ReactElement {
isFormattingEnabled,
isFormattingEnabled
);
const previousFormattingSpoilersEnabled = usePrevious(
isFormattingSpoilersEnabled,
isFormattingSpoilersEnabled
const previousFormattingFlagEnabled = usePrevious(
isFormattingFlagEnabled,
isFormattingFlagEnabled
);
const previousFormattingSpoilersFlagEnabled = usePrevious(
isFormattingSpoilersFlagEnabled,
isFormattingSpoilersFlagEnabled
);
React.useEffect(() => {
const formattingChanged =
typeof previousFormattingEnabled === 'boolean' &&
previousFormattingEnabled !== isFormattingEnabled;
const spoilersChanged =
typeof previousFormattingSpoilersEnabled === 'boolean' &&
previousFormattingSpoilersEnabled !== isFormattingSpoilersEnabled;
const flagChanged =
typeof previousFormattingFlagEnabled === 'boolean' &&
previousFormattingFlagEnabled !== isFormattingFlagEnabled;
const spoilersFlagChanged =
typeof previousFormattingSpoilersFlagEnabled === 'boolean' &&
previousFormattingSpoilersFlagEnabled !== isFormattingSpoilersFlagEnabled;
const quill = quillRef.current;
const changed = formattingChanged || spoilersChanged;
const changed = formattingChanged || flagChanged || spoilersFlagChanged;
if (quill && changed) {
quill.getModule('formattingMenu').updateOptions({
isEnabled: isFormattingEnabled,
isSpoilersEnabled: isFormattingSpoilersEnabled,
isMenuEnabled: isFormattingEnabled,
isEnabled: isFormattingFlagEnabled,
isSpoilersEnabled: isFormattingSpoilersFlagEnabled,
});
quill.options.formats = getQuillFormats({
isFormattingFlagEnabled,
isFormattingSpoilersFlagEnabled,
});
}
}, [
isFormattingEnabled,
isFormattingSpoilersEnabled,
isFormattingFlagEnabled,
isFormattingSpoilersFlagEnabled,
previousFormattingEnabled,
previousFormattingSpoilersEnabled,
previousFormattingFlagEnabled,
previousFormattingSpoilersFlagEnabled,
quillRef,
]);
@ -643,7 +690,11 @@ export function CompositionInput(props: Props): React.ReactElement {
matchers: [
['IMG', matchEmojiImage],
['IMG', matchEmojiBlot],
['SPAN', matchReactEmoji],
['STRONG', matchBold],
['EM', matchItalic],
['SPAN', matchMonospace],
['S', matchStrikethrough],
['SPAN', matchSpoiler],
[Node.TEXT_NODE, matchEmojiText],
['SPAN', matchMention(memberRepositoryRef)],
],
@ -677,8 +728,10 @@ export function CompositionInput(props: Props): React.ReactElement {
},
formattingMenu: {
i18n,
isEnabled: isFormattingEnabled,
isSpoilersEnabled: isFormattingSpoilersEnabled,
isMenuEnabled: isFormattingEnabled,
isEnabled: isFormattingFlagEnabled,
isSpoilersEnabled: isFormattingSpoilersFlagEnabled,
platform,
setFormattingChooserElement,
},
mentionCompletion: {
@ -692,25 +745,10 @@ export function CompositionInput(props: Props): React.ReactElement {
theme,
},
}}
formats={[
// For image replacement (local-only)
'emoji',
// @mentions
'mention',
...(isFormattingEnabled
? [
// Custom
...(isFormattingSpoilersEnabled
? [QuillFormattingStyle.spoiler]
: []),
QuillFormattingStyle.monospace,
// Built-in
QuillFormattingStyle.bold,
QuillFormattingStyle.italic,
QuillFormattingStyle.strike,
]
: []),
]}
formats={getQuillFormats({
isFormattingFlagEnabled,
isFormattingSpoilersFlagEnabled,
})}
placeholder={placeholder || i18n('icu:sendMessage')}
readOnly={disabled}
ref={element => {
@ -838,3 +876,31 @@ export function CompositionInput(props: Props): React.ReactElement {
</Manager>
);
}
function getQuillFormats({
isFormattingFlagEnabled,
isFormattingSpoilersFlagEnabled,
}: {
isFormattingFlagEnabled: boolean;
isFormattingSpoilersFlagEnabled: boolean;
}): Array<string> {
return [
// For image replacement (local-only)
'emoji',
// @mentions
'mention',
...(isFormattingFlagEnabled
? [
// Custom
...(isFormattingSpoilersFlagEnabled
? [QuillFormattingStyle.spoiler]
: []),
QuillFormattingStyle.monospace,
// Built-in
QuillFormattingStyle.bold,
QuillFormattingStyle.italic,
QuillFormattingStyle.strike,
]
: []),
];
}