Edit message: Don't allow send unless message contents changed

Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
automated-signal 2024-07-16 21:29:10 -05:00 committed by GitHub
parent 435d80b797
commit d1e2575df8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 70 additions and 8 deletions

View file

@ -82,6 +82,9 @@
&::before {
@include color-svg('../images/icons/v3/check/check.svg', $color-white);
}
&:disabled {
opacity: 0.5;
}
}
}

View file

@ -359,7 +359,15 @@ export const CompositionArea = memo(function CompositionArea({
const editedMessageId = draftEditMessage?.targetMessageId;
const handleSubmit = useCallback(
(message: string, bodyRanges: DraftBodyRanges, timestamp: number) => {
(
message: string,
bodyRanges: DraftBodyRanges,
timestamp: number
): boolean => {
if (!dirty) {
return false;
}
emojiButtonRef.current?.close();
if (editedMessageId) {
@ -380,9 +388,12 @@ export const CompositionArea = memo(function CompositionArea({
});
}
setLarge(false);
return true;
},
[
conversationId,
dirty,
draftAttachments,
editedMessageId,
quotedMessageSentAt,
@ -592,6 +603,7 @@ export const CompositionArea = memo(function CompositionArea({
<button
aria-label={i18n('icu:CompositionArea__edit-action--send')}
className="CompositionArea__edit-button CompositionArea__edit-button--accept"
disabled={!dirty}
onClick={() => inputApiRef.current?.submit()}
type="button"
/>

View file

@ -22,7 +22,12 @@ import type {
HydratedBodyRangesType,
RangeNode,
} from '../types/BodyRange';
import { BodyRange, collapseRangeTree, insertRange } from '../types/BodyRange';
import {
BodyRange,
areBodyRangesEqual,
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';
@ -359,7 +364,11 @@ export function CompositionInput(props: Props): React.ReactElement {
`CompositionInput: Submitting message ${timestamp} with ${bodyRanges.length} ranges`
);
canSendRef.current = false;
onSubmit(text, bodyRanges, timestamp);
const didSend = onSubmit(text, bodyRanges, timestamp);
if (!didSend) {
canSendRef.current = true;
}
};
if (inputApi) {
@ -579,7 +588,19 @@ export function CompositionInput(props: Props): React.ReactElement {
}
if (propsRef.current.onDirtyChange) {
propsRef.current.onDirtyChange(text.length > 0);
let isDirty: boolean = false;
if (!draftEditMessage) {
isDirty = text.length > 0;
} else if (text.trimEnd() !== draftEditMessage.body.trimEnd()) {
isDirty = true;
} else if (bodyRanges.length !== draftEditMessage.bodyRanges?.length) {
isDirty = true;
} else if (!areBodyRangesEqual(bodyRanges, draftEditMessage.bodyRanges)) {
isDirty = true;
}
propsRef.current.onDirtyChange(isDirty);
}
};

View file

@ -1911,18 +1911,20 @@ function setMessageToEdit(
: undefined;
}
const draftBodyRanges = processBodyRanges(message, {
conversationSelector: getConversationSelector(getState()),
});
conversation.set({
draftEditMessage: {
body: message.body,
bodyRanges: draftBodyRanges,
editHistoryLength: message.editHistory?.length ?? 0,
attachmentThumbnail,
preview: message.preview ? message.preview[0] : undefined,
targetMessageId: messageId,
quote: message.quote,
},
draftBodyRanges: processBodyRanges(message, {
conversationSelector: getConversationSelector(getState()),
}),
draftBodyRanges,
});
dispatch({

View file

@ -3,7 +3,7 @@
/* eslint-disable @typescript-eslint/no-namespace */
import { isNumber, omit, partition } from 'lodash';
import { isEqual, isNumber, omit, orderBy, partition } from 'lodash';
import { SignalService as Proto } from '../protobuf';
import * as log from '../logging/log';
@ -849,3 +849,27 @@ export function applyRangesToText(
return state;
}
// For ease of working with draft mentions in Quill, a conversationID field is present.
function normalizeBodyRanges(bodyRanges: DraftBodyRanges) {
return orderBy(bodyRanges, ['start', 'length']).map(item => {
if (BodyRange.isMention(item)) {
return { ...item, conversationID: undefined };
}
return item;
});
}
export function areBodyRangesEqual(
left: DraftBodyRanges,
right: DraftBodyRanges
): boolean {
const normalizedLeft = normalizeBodyRanges(left);
const sortedRight = normalizeBodyRanges(right);
if (normalizedLeft.length !== sortedRight.length) {
return false;
}
return isEqual(normalizedLeft, sortedRight);
}