Prevent >64k text in composition box; truncate too-large drafts
This commit is contained in:
parent
87ae65c852
commit
095cd884a2
5 changed files with 116 additions and 7 deletions
js
ts
|
@ -8,6 +8,8 @@
|
|||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
|
||||
|
||||
const conversations = new Whisper.ConversationCollection();
|
||||
const inboxCollection = new (Backbone.Collection.extend({
|
||||
initialize() {
|
||||
|
@ -183,12 +185,25 @@
|
|||
|
||||
this._initialFetchComplete = true;
|
||||
await Promise.all(
|
||||
conversations.map(conversation => {
|
||||
conversations.map(async conversation => {
|
||||
if (!conversation.get('lastMessage')) {
|
||||
return conversation.updateLastMessage();
|
||||
await conversation.updateLastMessage();
|
||||
}
|
||||
|
||||
return null;
|
||||
// In case a too-large draft was saved to the database
|
||||
const draft = conversation.get('draft');
|
||||
if (draft && draft.length > MAX_MESSAGE_BODY_LENGTH) {
|
||||
this.model.set({
|
||||
draft: draft.slice(0, MAX_MESSAGE_BODY_LENGTH),
|
||||
});
|
||||
await window.Signal.Data.updateConversation(
|
||||
conversation.id,
|
||||
conversation.attributes,
|
||||
{
|
||||
Conversation: Whisper.Conversation,
|
||||
}
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
window.log.info('ConversationController: done with initial fetch');
|
||||
|
|
|
@ -323,6 +323,7 @@
|
|||
onSubmit: message => this.sendMessage(message),
|
||||
onEditorStateChange: (msg, caretLocation) =>
|
||||
this.onEditorStateChange(msg, caretLocation),
|
||||
onTextTooLong: () => this.showToast(Whisper.MessageBodyTooLongToast),
|
||||
onChooseAttachment: this.onChooseAttachment.bind(this),
|
||||
micCellEl,
|
||||
attachmentListEl,
|
||||
|
|
|
@ -38,7 +38,11 @@ export type OwnProps = {
|
|||
|
||||
export type Props = Pick<
|
||||
CompositionInputProps,
|
||||
'onSubmit' | 'onEditorSizeChange' | 'onEditorStateChange' | 'startingText'
|
||||
| 'onSubmit'
|
||||
| 'onEditorSizeChange'
|
||||
| 'onEditorStateChange'
|
||||
| 'onTextTooLong'
|
||||
| 'startingText'
|
||||
> &
|
||||
Pick<
|
||||
EmojiButtonProps,
|
||||
|
@ -76,6 +80,7 @@ export const CompositionArea = ({
|
|||
compositionApi,
|
||||
onEditorSizeChange,
|
||||
onEditorStateChange,
|
||||
onTextTooLong,
|
||||
startingText,
|
||||
// EmojiButton
|
||||
onPickEmoji,
|
||||
|
@ -336,6 +341,7 @@ export const CompositionArea = ({
|
|||
onSubmit={handleSubmit}
|
||||
onEditorSizeChange={onEditorSizeChange}
|
||||
onEditorStateChange={onEditorStateChange}
|
||||
onTextTooLong={onTextTooLong}
|
||||
onDirtyChange={setDirty}
|
||||
skinTone={skinTone}
|
||||
startingText={startingText}
|
||||
|
|
|
@ -29,6 +29,7 @@ import {
|
|||
} from './emoji/lib';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
const MAX_LENGTH = 64 * 1024;
|
||||
const colonsRegex = /(?:^|\s):[a-z0-9-_+]+:?/gi;
|
||||
const triggerEmojiRegex = /^(?:[-+]\d|[a-z]{2})/i;
|
||||
|
||||
|
@ -43,6 +44,7 @@ export type Props = {
|
|||
onDirtyChange?(dirty: boolean): unknown;
|
||||
onEditorStateChange?(messageText: string, caretLocation: number): unknown;
|
||||
onEditorSizeChange?(rect: ContentRect): unknown;
|
||||
onTextTooLong(): unknown;
|
||||
onPickEmoji(o: EmojiPickDataType): unknown;
|
||||
onSubmit(message: string): unknown;
|
||||
};
|
||||
|
@ -78,6 +80,43 @@ function getTrimmedMatchAtIndex(str: string, index: number, pattern: RegExp) {
|
|||
return null;
|
||||
}
|
||||
|
||||
function getLengthOfSelectedText(state: EditorState): number {
|
||||
const currentSelection = state.getSelection();
|
||||
let length = 0;
|
||||
|
||||
const currentContent = state.getCurrentContent();
|
||||
const startKey = currentSelection.getStartKey();
|
||||
const endKey = currentSelection.getEndKey();
|
||||
const startBlock = currentContent.getBlockForKey(startKey);
|
||||
const isStartAndEndBlockAreTheSame = startKey === endKey;
|
||||
const startBlockTextLength = startBlock.getLength();
|
||||
const startSelectedTextLength =
|
||||
startBlockTextLength - currentSelection.getStartOffset();
|
||||
const endSelectedTextLength = currentSelection.getEndOffset();
|
||||
const keyAfterEnd = currentContent.getKeyAfter(endKey);
|
||||
|
||||
if (isStartAndEndBlockAreTheSame) {
|
||||
length +=
|
||||
currentSelection.getEndOffset() - currentSelection.getStartOffset();
|
||||
} else {
|
||||
let currentKey = startKey;
|
||||
|
||||
while (currentKey && currentKey !== keyAfterEnd) {
|
||||
if (currentKey === startKey) {
|
||||
length += startSelectedTextLength + 1;
|
||||
} else if (currentKey === endKey) {
|
||||
length += endSelectedTextLength;
|
||||
} else {
|
||||
length += currentContent.getBlockForKey(currentKey).getLength() + 1;
|
||||
}
|
||||
|
||||
currentKey = currentContent.getKeyAfter(currentKey);
|
||||
}
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
function getWordAtIndex(str: string, index: number) {
|
||||
const start = str
|
||||
.slice(0, index + 1)
|
||||
|
@ -172,6 +211,7 @@ export const CompositionInput = ({
|
|||
onDirtyChange,
|
||||
onEditorStateChange,
|
||||
onEditorSizeChange,
|
||||
onTextTooLong,
|
||||
onPickEmoji,
|
||||
onSubmit,
|
||||
skinTone,
|
||||
|
@ -298,6 +338,51 @@ export const CompositionInput = ({
|
|||
]
|
||||
);
|
||||
|
||||
const handleBeforeInput = React.useCallback(
|
||||
(): DraftHandleValue => {
|
||||
if (!editorStateRef.current) {
|
||||
return 'not-handled';
|
||||
}
|
||||
|
||||
const editorState = editorStateRef.current;
|
||||
const plainText = editorState.getCurrentContent().getPlainText();
|
||||
const selectedTextLength = getLengthOfSelectedText(editorState);
|
||||
|
||||
if (plainText.length - selectedTextLength > MAX_LENGTH - 1) {
|
||||
onTextTooLong();
|
||||
|
||||
return 'handled';
|
||||
}
|
||||
|
||||
return 'not-handled';
|
||||
},
|
||||
[onTextTooLong, editorStateRef]
|
||||
);
|
||||
|
||||
const handlePastedText = React.useCallback(
|
||||
(pastedText: string): DraftHandleValue => {
|
||||
if (!editorStateRef.current) {
|
||||
return 'not-handled';
|
||||
}
|
||||
|
||||
const editorState = editorStateRef.current;
|
||||
const plainText = editorState.getCurrentContent().getPlainText();
|
||||
const selectedTextLength = getLengthOfSelectedText(editorState);
|
||||
|
||||
if (
|
||||
plainText.length + pastedText.length - selectedTextLength >
|
||||
MAX_LENGTH
|
||||
) {
|
||||
onTextTooLong();
|
||||
|
||||
return 'handled';
|
||||
}
|
||||
|
||||
return 'not-handled';
|
||||
},
|
||||
[onTextTooLong, editorStateRef]
|
||||
);
|
||||
|
||||
const resetEditorState = React.useCallback(
|
||||
() => {
|
||||
const newEmptyState = EditorState.createEmpty(compositeDecorator);
|
||||
|
@ -694,6 +779,8 @@ export const CompositionInput = ({
|
|||
onEscape={handleEscapeKey}
|
||||
onTab={onTab}
|
||||
handleKeyCommand={handleEditorCommand}
|
||||
handleBeforeInput={handleBeforeInput}
|
||||
handlePastedText={handlePastedText}
|
||||
keyBindingFn={editorKeybindingFn}
|
||||
spellCheck={true}
|
||||
stripPastedStyles={true}
|
||||
|
|
|
@ -164,7 +164,7 @@
|
|||
"rule": "jQuery-load(",
|
||||
"path": "js/conversation_controller.js",
|
||||
"line": " async load() {",
|
||||
"lineNumber": 169,
|
||||
"lineNumber": 171,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2019-07-31T00:19:18.696Z"
|
||||
},
|
||||
|
@ -172,7 +172,7 @@
|
|||
"rule": "jQuery-load(",
|
||||
"path": "js/conversation_controller.js",
|
||||
"line": " this._initialPromise = load();",
|
||||
"lineNumber": 204,
|
||||
"lineNumber": 219,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2019-07-31T00:19:18.696Z"
|
||||
},
|
||||
|
@ -7475,7 +7475,7 @@
|
|||
"rule": "DOM-innerHTML",
|
||||
"path": "ts/components/CompositionArea.tsx",
|
||||
"line": " el.innerHTML = '';",
|
||||
"lineNumber": 65,
|
||||
"lineNumber": 69,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-08-01T14:10:37.481Z",
|
||||
"reasonDetail": "Our code, no user input, only clearing out the dom"
|
||||
|
|
Loading…
Add table
Reference in a new issue