Allow copy/paste of formatting and mentions
This commit is contained in:
parent
320ac044a8
commit
b4caf67bf9
55 changed files with 1003 additions and 446 deletions
|
@ -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,
|
||||
]
|
||||
: []),
|
||||
];
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue