2020-11-03 01:19:52 +00:00
|
|
|
// Copyright 2020 Signal Messenger, LLC
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
|
|
|
import Delta from 'quill-delta';
|
2020-11-06 20:11:18 +00:00
|
|
|
import { LeafBlot, DeltaOperation } from 'quill';
|
2020-11-05 21:18:42 +00:00
|
|
|
import Op from 'quill-delta/dist/Op';
|
2020-11-03 01:19:52 +00:00
|
|
|
|
2020-11-04 02:04:22 +00:00
|
|
|
import { BodyRangeType } from '../types/Util';
|
2020-11-05 21:18:42 +00:00
|
|
|
import { MentionBlot } from './mentions/blot';
|
|
|
|
|
|
|
|
export interface MentionBlotValue {
|
|
|
|
uuid: string;
|
|
|
|
title: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export const isMentionBlot = (blot: LeafBlot): blot is MentionBlot =>
|
|
|
|
blot.value() && blot.value().mention;
|
|
|
|
|
|
|
|
export type RetainOp = Op & { retain: number };
|
|
|
|
export type InsertOp<K extends string, T> = Op & { insert: { [V in K]: T } };
|
|
|
|
|
|
|
|
export type InsertMentionOp = InsertOp<'mention', MentionBlotValue>;
|
|
|
|
export type InsertEmojiOp = InsertOp<'emoji', string>;
|
|
|
|
|
|
|
|
export const isRetainOp = (op?: Op): op is RetainOp =>
|
|
|
|
op !== undefined && op.retain !== undefined;
|
|
|
|
|
|
|
|
export const isSpecificInsertOp = (op: Op, type: string): boolean => {
|
|
|
|
return (
|
|
|
|
op.insert !== undefined &&
|
|
|
|
typeof op.insert === 'object' &&
|
|
|
|
Object.hasOwnProperty.call(op.insert, type)
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export const isInsertEmojiOp = (op: Op): op is InsertEmojiOp =>
|
|
|
|
isSpecificInsertOp(op, 'emoji');
|
|
|
|
|
|
|
|
export const isInsertMentionOp = (op: Op): op is InsertMentionOp =>
|
|
|
|
isSpecificInsertOp(op, 'mention');
|
2020-11-03 01:19:52 +00:00
|
|
|
|
2020-11-06 20:11:18 +00:00
|
|
|
export const getTextFromOps = (ops: Array<DeltaOperation>): string =>
|
|
|
|
ops.reduce((acc, { insert }, index) => {
|
|
|
|
if (typeof insert === 'string') {
|
|
|
|
let textToAdd;
|
|
|
|
switch (index) {
|
|
|
|
case 0: {
|
|
|
|
textToAdd = insert.trimLeft();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case ops.length - 1: {
|
|
|
|
textToAdd = insert.trimRight();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default: {
|
|
|
|
textToAdd = insert;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2020-11-11 22:56:20 +00:00
|
|
|
const textWithoutNewlines = textToAdd.replace(/\n+$/, '');
|
|
|
|
return acc + textWithoutNewlines;
|
2020-11-06 20:11:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (insert.emoji) {
|
|
|
|
return acc + insert.emoji;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (insert.mention) {
|
|
|
|
return `${acc}@${insert.mention.title}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
return acc;
|
|
|
|
}, '');
|
|
|
|
|
2020-11-04 02:04:22 +00:00
|
|
|
export const getTextAndMentionsFromOps = (
|
2020-11-05 21:18:42 +00:00
|
|
|
ops: Array<Op>
|
2020-11-04 02:04:22 +00:00
|
|
|
): [string, Array<BodyRangeType>] => {
|
|
|
|
const mentions: Array<BodyRangeType> = [];
|
|
|
|
|
2020-11-05 21:18:42 +00:00
|
|
|
const text = ops.reduce((acc, op, index) => {
|
|
|
|
if (typeof op.insert === 'string') {
|
2020-11-04 02:04:22 +00:00
|
|
|
let textToAdd;
|
|
|
|
switch (index) {
|
|
|
|
case 0: {
|
2020-11-05 21:18:42 +00:00
|
|
|
textToAdd = op.insert.trimLeft();
|
2020-11-04 02:04:22 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case ops.length - 1: {
|
2020-11-05 21:18:42 +00:00
|
|
|
textToAdd = op.insert.trimRight();
|
2020-11-04 02:04:22 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
default: {
|
2020-11-05 21:18:42 +00:00
|
|
|
textToAdd = op.insert;
|
2020-11-04 02:04:22 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2020-11-11 22:56:20 +00:00
|
|
|
|
|
|
|
const textWithoutNewlines = textToAdd.replace(/\n+$/, '');
|
|
|
|
return acc + textWithoutNewlines;
|
2020-11-04 02:04:22 +00:00
|
|
|
}
|
|
|
|
|
2020-11-05 21:18:42 +00:00
|
|
|
if (isInsertEmojiOp(op)) {
|
|
|
|
return acc + op.insert.emoji;
|
2020-11-04 02:04:22 +00:00
|
|
|
}
|
|
|
|
|
2020-11-05 21:18:42 +00:00
|
|
|
if (isInsertMentionOp(op)) {
|
2020-11-04 02:04:22 +00:00
|
|
|
mentions.push({
|
|
|
|
length: 1, // The length of `\uFFFC`
|
2020-11-05 21:18:42 +00:00
|
|
|
mentionUuid: op.insert.mention.uuid,
|
|
|
|
replacementText: op.insert.mention.title,
|
2020-11-04 02:04:22 +00:00
|
|
|
start: acc.length,
|
|
|
|
});
|
|
|
|
|
|
|
|
return `${acc}\uFFFC`;
|
|
|
|
}
|
|
|
|
|
|
|
|
return acc;
|
|
|
|
}, '');
|
|
|
|
|
|
|
|
return [text, mentions];
|
|
|
|
};
|
|
|
|
|
2020-11-05 21:18:42 +00:00
|
|
|
export const getBlotTextPartitions = (
|
|
|
|
blot: LeafBlot,
|
|
|
|
index: number
|
|
|
|
): [string, string] => {
|
|
|
|
if (blot !== undefined && blot.text !== undefined) {
|
|
|
|
const leftLeafText = blot.text.substr(0, index);
|
|
|
|
const rightLeafText = blot.text.substr(index);
|
|
|
|
|
|
|
|
return [leftLeafText, rightLeafText];
|
|
|
|
}
|
|
|
|
|
|
|
|
return ['', ''];
|
|
|
|
};
|
|
|
|
|
|
|
|
export const matchBlotTextPartitions = (
|
|
|
|
blot: LeafBlot,
|
|
|
|
index: number,
|
|
|
|
leftRegExp: RegExp,
|
|
|
|
rightRegExp?: RegExp
|
|
|
|
): Array<RegExpMatchArray | null> => {
|
|
|
|
const [leftText, rightText] = getBlotTextPartitions(blot, index);
|
|
|
|
|
|
|
|
const leftMatch = leftRegExp.exec(leftText);
|
|
|
|
let rightMatch = null;
|
|
|
|
|
|
|
|
if (rightRegExp) {
|
|
|
|
rightMatch = rightRegExp.exec(rightText);
|
|
|
|
}
|
|
|
|
|
|
|
|
return [leftMatch, rightMatch];
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getDeltaToRestartMention = (ops: Array<Op>): Delta => {
|
|
|
|
const changes = ops.reduce((acc, op): Array<Op> => {
|
|
|
|
if (op.insert && typeof op.insert === 'string') {
|
|
|
|
acc.push({ retain: op.insert.length });
|
|
|
|
} else {
|
|
|
|
acc.push({ retain: 1 });
|
|
|
|
}
|
|
|
|
return acc;
|
|
|
|
}, Array<Op>());
|
|
|
|
changes.push({ delete: 1 });
|
|
|
|
changes.push({ insert: '@' });
|
|
|
|
return new Delta(changes);
|
|
|
|
};
|
|
|
|
|
2020-11-03 01:19:52 +00:00
|
|
|
export const getDeltaToRemoveStaleMentions = (
|
2020-11-05 21:18:42 +00:00
|
|
|
ops: Array<Op>,
|
2020-11-03 01:19:52 +00:00
|
|
|
memberUuids: Array<string>
|
|
|
|
): Delta => {
|
|
|
|
const newOps = ops.reduce((memo, op) => {
|
|
|
|
if (op.insert) {
|
2020-11-05 21:18:42 +00:00
|
|
|
if (
|
|
|
|
isInsertMentionOp(op) &&
|
|
|
|
!memberUuids.includes(op.insert.mention.uuid)
|
|
|
|
) {
|
2020-11-03 01:19:52 +00:00
|
|
|
const deleteOp = { delete: 1 };
|
|
|
|
const textOp = { insert: `@${op.insert.mention.title}` };
|
|
|
|
return [...memo, deleteOp, textOp];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof op.insert === 'string') {
|
|
|
|
const retainStringOp = { retain: op.insert.length };
|
|
|
|
return [...memo, retainStringOp];
|
|
|
|
}
|
|
|
|
|
|
|
|
const retainEmbedOp = { retain: 1 };
|
|
|
|
return [...memo, retainEmbedOp];
|
|
|
|
}
|
|
|
|
|
|
|
|
return [...memo, op];
|
2020-11-05 21:18:42 +00:00
|
|
|
}, Array<Op>());
|
2020-11-03 01:19:52 +00:00
|
|
|
|
|
|
|
return new Delta(newOps);
|
|
|
|
};
|