Prevent forward of at-mentions, don't render in 1:1 conversations

Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
automated-signal
2026-04-07 12:01:04 -05:00
committed by GitHub
parent c53ea6d4cd
commit 8dfdcfaf9d
6 changed files with 69 additions and 35 deletions
+1 -23
View File
@@ -33,7 +33,6 @@ import { LinkPreviewSourceType } from '../types/LinkPreview.std.ts';
import { ToastType } from '../types/Toast.dom.tsx';
import type { ShowToastAction } from '../state/ducks/toast.preload.ts';
import type { HydratedBodyRangesType } from '../types/BodyRange.std.ts';
import { applyRangesToText } from '../types/BodyRange.std.ts';
import { UserText } from './UserText.dom.tsx';
import { Modal } from './Modal.dom.tsx';
import { SizeObserver } from '../hooks/useSizeObserver.dom.tsx';
@@ -146,28 +145,7 @@ export function ForwardMessagesModal({
const previews = lonelyLinkPreview?.domain ? [lonelyLinkPreview] : [];
doForwardMessages(conversationIds, [{ ...lonelyDraft, previews }]);
} else {
doForwardMessages(
conversationIds,
drafts.map(draft => {
// We don't keep @mention bodyRanges in multi-forward scenarios
const result = applyRangesToText(
{
body: draft.messageBody ?? '',
bodyRanges: draft.bodyRanges ?? [],
},
{
replaceMentions: true,
replaceSpoilers: false,
}
);
return {
...draft,
messageBody: result.body,
bodyRanges: result.bodyRanges,
};
})
);
doForwardMessages(conversationIds, drafts);
}
}, [
drafts,
+19 -3
View File
@@ -16,6 +16,7 @@ import type { LocalizerType } from '../types/Util.std.ts';
import { getClassNamesFor } from '../util/getClassNamesFor.std.ts';
import { useRefMerger } from '../hooks/useRefMerger.std.ts';
import { byteLength } from '../Bytes.std.ts';
import { truncateString } from '../util/truncateString.std.ts';
export type PropsType = {
autoFocus?: boolean;
@@ -181,17 +182,32 @@ export const Input = forwardRef<
const pastedText = event.clipboardData.getData('Text');
const pastedLength = countLength(pastedText);
const newLengthCount =
countLength(textBeforeSelection) +
countLength(pastedText) +
pastedLength +
countLength(textAfterSelection);
const pastedBytes = countBytes(pastedText);
const newByteCount =
countBytes(textBeforeSelection) +
countBytes(pastedText) +
pastedBytes +
countBytes(textAfterSelection);
if (newLengthCount > maxLengthCount || newByteCount > maxByteCount) {
const lengthDelta = newLengthCount - maxLengthCount;
const byteDelta = newByteCount - maxByteCount;
if (lengthDelta > 0 || byteDelta > 0) {
event.preventDefault();
const newPastedLength = pastedLength - lengthDelta;
const newPastedBytes = pastedBytes - byteDelta;
const truncatedPaste = truncateString(pastedText, {
byteLimit: newPastedBytes,
graphemeLimit: newPastedLength,
});
inputEl.value =
textBeforeSelection + truncatedPaste + textAfterSelection;
}
maybeSetLarge();
+7 -3
View File
@@ -2099,9 +2099,13 @@ function setMessageToEdit(
: undefined;
}
const draftBodyRanges = processBodyRanges(message, {
conversationSelector: getConversationSelector(getState()),
});
const draftBodyRanges = processBodyRanges(
message,
isGroup(conversation.attributes),
{
conversationSelector: getConversationSelector(getState()),
}
);
conversation.set({
draftEditMessage: {
body: message.body,
+19 -5
View File
@@ -56,7 +56,10 @@ import type {
import type { EmbeddedContactForUIType } from '../../types/EmbeddedContact.std.ts';
import { embeddedContactSelector } from '../../types/EmbeddedContact.std.ts';
import type { HydratedBodyRangesType } from '../../types/BodyRange.std.ts';
import {
BodyRange,
type HydratedBodyRangesType,
} from '../../types/BodyRange.std.ts';
import { hydrateRanges } from '../../util/BodyRange.node.ts';
import type { AssertProps, LocalizerType } from '../../types/Util.std.ts';
import type { LinkPreviewForUIType } from '../../types/message/LinkPreviews.std.ts';
@@ -356,13 +359,20 @@ export const getAttachmentsForMessage = (
export const processBodyRanges = (
{ bodyRanges }: Pick<MessageWithUIFieldsType, 'bodyRanges'>,
isGroup: boolean,
options: { conversationSelector: GetConversationByIdType }
): HydratedBodyRangesType | undefined => {
if (!bodyRanges) {
return undefined;
}
return hydrateRanges(bodyRanges, options.conversationSelector)?.sort(
let toHydrate = bodyRanges;
if (!isGroup) {
toHydrate = toHydrate.filter(range => !BodyRange.isMention(range));
}
return hydrateRanges(toHydrate, options.conversationSelector)?.sort(
(a, b) => b.start - a.start
);
};
@@ -702,10 +712,12 @@ export const getPropsForQuote = (
conversationSelector,
ourConversationId,
defaultConversationColor,
isGroup,
}: {
conversationSelector: GetConversationByIdType;
ourConversationId?: string;
defaultConversationColor: DefaultConversationColorType;
isGroup: boolean;
}
): PropsData['quote'] => {
const { quote } = message;
@@ -758,7 +770,7 @@ export const getPropsForQuote = (
authorPhoneNumber,
authorProfileName,
authorTitle,
bodyRanges: processBodyRanges(quote, { conversationSelector }),
bodyRanges: processBodyRanges(quote, isGroup, { conversationSelector }),
conversationColor,
conversationTitle: conversation.title,
customColor,
@@ -869,11 +881,10 @@ export const getPropsForMessage = (
item => item.wasTooBig
);
const attachments = getAttachmentsForMessage(message);
const bodyRanges = processBodyRanges(message, options);
const author = getAuthorForMessage(message, options);
const previews = getPreviewsForMessage(message);
const reactions = getReactionsForMessage(message, options);
const quote = getPropsForQuote(message, options);
const storyReplyContext = getPropsForStoryReplyContext(message, options);
const textAttachment = getTextAttachment(message);
const payment = getPayment(message);
@@ -901,6 +912,9 @@ export const getPropsForMessage = (
const conversation = getConversation(message, conversationSelector);
const isGroup = conversation.type === 'group';
const bodyRanges = processBodyRanges(message, isGroup, options);
const quote = getPropsForQuote(message, { ...options, isGroup });
const isGroupTerminated = isGroup && conversation.terminated;
const { sticker } = message;
@@ -81,6 +81,7 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
}) {
const conversationSelector = useSelector(getConversationSelector);
const conversation = conversationSelector(id);
const isGroup = conversation.type === 'group';
strictAssert(conversation, `Conversation id ${id} not found!`);
const i18n = useSelector(getIntl);
@@ -189,6 +190,7 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
conversationSelector,
ourConversationId,
defaultConversationColor,
isGroup,
})
: undefined;
}, [
@@ -196,6 +198,7 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
conversationSelector,
ourConversationId,
defaultConversationColor,
isGroup,
]);
const {
@@ -30,6 +30,7 @@ import type {
MessageForwardDraft,
} from '../../types/ForwardDraft.std.ts';
import { getForwardMessagesProps } from '../selectors/globalModals.std.ts';
import { applyRangesToText } from '../../types/BodyRange.std.ts';
const log = createLogger('ForwardMessagesModal');
@@ -76,7 +77,25 @@ function SmartForwardMessagesModalInner({
const [drafts, setDrafts] = useState<ReadonlyArray<MessageForwardDraft>>(
() => {
return forwardMessagesProps.messageDrafts;
return forwardMessagesProps.messageDrafts.map(draft => {
// We don't keep @mention bodyRanges when forwarding, so we turn them to text
const result = applyRangesToText(
{
body: draft.messageBody ?? '',
bodyRanges: draft.bodyRanges ?? [],
},
{
replaceMentions: true,
replaceSpoilers: false,
}
);
return {
...draft,
messageBody: result.body,
bodyRanges: result.bodyRanges,
};
});
}
);