From 4436184f95dbb8f2d860df4bdb893a14ecc71b3e Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Mon, 3 Nov 2025 10:41:49 -0800 Subject: [PATCH] Use binary proto fields in staging --- ts/components/CompositionArea.dom.tsx | 6 +- ts/components/CompositionTextArea.dom.tsx | 18 +-- .../DraftGifMessageSendModal.dom.stories.tsx | 3 +- .../ForwardMessagesModal.dom.stories.tsx | 2 +- ts/components/MediaEditor.dom.stories.tsx | 1 + ts/components/MediaEditor.dom.tsx | 17 ++- ts/components/StoryCreator.dom.tsx | 8 +- ts/services/backups/import.preload.ts | 6 +- ts/services/storageRecordOps.preload.ts | 2 +- ts/state/ducks/globalModals.preload.ts | 2 +- ts/state/ducks/search.preload.ts | 2 +- ts/state/selectors/message.preload.ts | 2 +- .../{search.dom.ts => search.preload.ts} | 2 +- ts/state/selectors/stories.preload.ts | 3 +- ts/state/smart/CompositionArea.preload.tsx | 2 +- .../smart/CompositionTextArea.preload.tsx | 18 ++- ts/state/smart/LeftPane.preload.tsx | 2 +- ...onversationListItemContextMenu.preload.tsx | 2 +- .../smart/MessageSearchResult.preload.tsx | 2 +- ts/state/smart/StoryCreator.preload.tsx | 18 ++- .../state/selectors/search_test.preload.ts | 2 +- ts/textsecure/MessageReceiver.preload.ts | 2 +- ts/textsecure/SendMessage.preload.ts | 29 +++- ts/textsecure/processDataMessage.preload.ts | 2 +- ts/types/BodyRange.std.ts | 102 +------------- ts/util/BodyRange.node.ts | 129 ++++++++++++++++++ ts/util/getDraftPreview.preload.ts | 2 +- ts/util/getLastMessage.preload.ts | 2 +- .../getNotificationTextForMessage.preload.ts | 3 +- ...ts => isProtoBinaryEncodingEnabled.dom.ts} | 5 + 30 files changed, 242 insertions(+), 154 deletions(-) rename ts/state/selectors/{search.dom.ts => search.preload.ts} (99%) create mode 100644 ts/util/BodyRange.node.ts rename ts/util/{isProtoBinaryEncodingEnabled.std.ts => isProtoBinaryEncodingEnabled.dom.ts} (73%) diff --git a/ts/components/CompositionArea.dom.tsx b/ts/components/CompositionArea.dom.tsx index 7c8f380ac0..db4f2d3564 100644 --- a/ts/components/CompositionArea.dom.tsx +++ b/ts/components/CompositionArea.dom.tsx @@ -252,8 +252,6 @@ export const CompositionArea = memo(function CompositionArea({ theme, setMuteExpiration, - // MediaEditor - conversationSelector, // AttachmentList draftAttachments, onClearAttachments, @@ -967,7 +965,9 @@ export const CompositionArea = memo(function CompositionArea({ isCreatingStory={false} isFormattingEnabled={isFormattingEnabled} isSending={false} - conversationSelector={conversationSelector} + convertDraftBodyRangesIntoHydrated={ + convertDraftBodyRangesIntoHydrated + } onClose={() => setAttachmentToEdit(undefined)} onDone={({ caption, diff --git a/ts/components/CompositionTextArea.dom.tsx b/ts/components/CompositionTextArea.dom.tsx index 7c2e09088d..9f537baf7b 100644 --- a/ts/components/CompositionTextArea.dom.tsx +++ b/ts/components/CompositionTextArea.dom.tsx @@ -5,10 +5,9 @@ import React, { useRef, useCallback, useState } from 'react'; import type { LocalizerType } from '../types/I18N.std.js'; import type { InputApi } from './CompositionInput.dom.js'; import { CompositionInput } from './CompositionInput.dom.js'; -import { - hydrateRanges, - type DraftBodyRanges, - type HydratedBodyRangesType, +import type { + DraftBodyRanges, + HydratedBodyRangesType, } from '../types/BodyRange.std.js'; import type { ThemeType } from '../types/Util.std.js'; import type { PreferredBadgeSelectorType } from '../state/selectors/badges.preload.js'; @@ -17,7 +16,6 @@ import { FunEmojiPicker } from './fun/FunEmojiPicker.dom.js'; import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.dom.js'; import type { EmojiSkinTone } from './fun/data/emojis.std.js'; import { FunEmojiPickerButton } from './fun/FunButton.dom.js'; -import type { GetConversationByIdType } from '../state/selectors/conversations.dom.js'; export type CompositionTextAreaProps = { bodyRanges: HydratedBodyRangesType | null; @@ -47,7 +45,9 @@ export type CompositionTextAreaProps = { getPreferredBadge: PreferredBadgeSelectorType; draftText: string; theme: ThemeType; - conversationSelector: GetConversationByIdType; + convertDraftBodyRangesIntoHydrated: ( + bodyRanges: DraftBodyRanges | undefined + ) => HydratedBodyRangesType | undefined; }; /** @@ -76,7 +76,7 @@ export function CompositionTextArea({ emojiSkinToneDefault, theme, whenToShowRemainingCount = Infinity, - conversationSelector, + convertDraftBodyRangesIntoHydrated, }: CompositionTextAreaProps): JSX.Element { const inputApiRef = useRef(); const [characterCount, setCharacterCount] = useState( @@ -132,7 +132,7 @@ export function CompositionTextArea({ ); const hydratedBodyRanges = - hydrateRanges(updatedBodyRanges, conversationSelector) ?? []; + convertDraftBodyRangesIntoHydrated(updatedBodyRanges) ?? []; if (maxLength !== undefined) { // if we had to truncate @@ -150,7 +150,7 @@ export function CompositionTextArea({ setCharacterCount(newCharacterCount); onChange(newValue, hydratedBodyRanges, caretLocation); }, - [maxLength, onChange, conversationSelector] + [maxLength, onChange, convertDraftBodyRangesIntoHydrated] ); return ( diff --git a/ts/components/DraftGifMessageSendModal.dom.stories.tsx b/ts/components/DraftGifMessageSendModal.dom.stories.tsx index 6ab9316289..c2f10bff27 100644 --- a/ts/components/DraftGifMessageSendModal.dom.stories.tsx +++ b/ts/components/DraftGifMessageSendModal.dom.stories.tsx @@ -14,7 +14,6 @@ import { EmojiSkinTone } from './fun/data/emojis.std.js'; import { LoadingState } from '../util/loadable.std.js'; import { VIDEO_MP4 } from '../types/MIME.std.js'; import { drop } from '../util/drop.std.js'; -import { getDefaultConversation } from '../test-helpers/getDefaultConversation.std.js'; const { i18n } = window.SignalContext; @@ -39,7 +38,7 @@ function RenderCompositionTextArea(props: SmartCompositionTextAreaProps) { ourConversationId="me" platform="darwin" emojiSkinToneDefault={EmojiSkinTone.None} - conversationSelector={() => getDefaultConversation()} + convertDraftBodyRangesIntoHydrated={() => []} /> ); } diff --git a/ts/components/ForwardMessagesModal.dom.stories.tsx b/ts/components/ForwardMessagesModal.dom.stories.tsx index 1a07e4f87f..d0e715e06f 100644 --- a/ts/components/ForwardMessagesModal.dom.stories.tsx +++ b/ts/components/ForwardMessagesModal.dom.stories.tsx @@ -70,7 +70,7 @@ const useProps = (overrideProps: Partial = {}): PropsType => ({ ourConversationId="me" platform="darwin" emojiSkinToneDefault={EmojiSkinTone.None} - conversationSelector={() => getDefaultConversation()} + convertDraftBodyRangesIntoHydrated={() => []} /> ), showToast: action('showToast'), diff --git a/ts/components/MediaEditor.dom.stories.tsx b/ts/components/MediaEditor.dom.stories.tsx index 4e8509884b..628a548bd6 100644 --- a/ts/components/MediaEditor.dom.stories.tsx +++ b/ts/components/MediaEditor.dom.stories.tsx @@ -31,6 +31,7 @@ export default { onTextTooLong: action('onTextTooLong'), platform: 'darwin', emojiSkinToneDefault: EmojiSkinTone.None, + convertDraftBodyRangesIntoHydrated: () => undefined, }, } satisfies Meta; diff --git a/ts/components/MediaEditor.dom.tsx b/ts/components/MediaEditor.dom.tsx index 0672b66682..36464cef2a 100644 --- a/ts/components/MediaEditor.dom.tsx +++ b/ts/components/MediaEditor.dom.tsx @@ -13,7 +13,10 @@ import classNames from 'classnames'; import { createPortal } from 'react-dom'; import { fabric } from 'fabric'; import lodash from 'lodash'; -import type { DraftBodyRanges } from '../types/BodyRange.std.js'; +import type { + DraftBodyRanges, + HydratedBodyRangesType, +} from '../types/BodyRange.std.js'; import type { ImageStateType } from '../mediaEditor/ImageStateType.std.js'; import type { InputApi, @@ -47,7 +50,6 @@ import { ThemeType } from '../types/Util.std.js'; import { arrow } from '../util/keyboard.dom.js'; import { canvasToBytes } from '../util/canvasToBytes.std.js'; import { loadImage } from '../util/loadImage.std.js'; -import { hydrateRanges } from '../types/BodyRange.std.js'; import { useConfirmDiscard } from '../hooks/useConfirmDiscard.dom.js'; import { useFabricHistory } from '../mediaEditor/useFabricHistory.dom.js'; import { usePortal } from '../hooks/usePortal.dom.js'; @@ -62,7 +64,6 @@ import type { FunStickerSelection } from './fun/panels/FunPanelStickers.dom.js'; import { drop } from '../util/drop.std.js'; import type { FunTimeStickerStyle } from './fun/constants.dom.js'; import * as Errors from '../types/errors.std.js'; -import type { GetConversationByIdType } from '../state/selectors/conversations.dom.js'; const { get, has, noop } = lodash; @@ -85,7 +86,9 @@ export type PropsType = { imageToBlurHash: typeof imageToBlurHash; onClose: () => unknown; onDone: (result: MediaEditorResultType) => unknown; - conversationSelector: GetConversationByIdType; + convertDraftBodyRangesIntoHydrated: ( + bodyRanges: DraftBodyRanges | undefined + ) => HydratedBodyRangesType | undefined; } & Pick< CompositionInputProps, | 'draftText' @@ -168,7 +171,7 @@ export function MediaEditor({ ourConversationId, platform, sortedGroupMembers, - conversationSelector, + convertDraftBodyRangesIntoHydrated, imageToBlurHash, }: PropsType): JSX.Element | null { const [fabricCanvas, setFabricCanvas] = useState(); @@ -181,8 +184,8 @@ export function MediaEditor({ useState(draftBodyRanges); const hydratedBodyRanges = useMemo( - () => hydrateRanges(captionBodyRanges ?? undefined, conversationSelector), - [captionBodyRanges, conversationSelector] + () => convertDraftBodyRangesIntoHydrated(captionBodyRanges ?? undefined), + [captionBodyRanges, convertDraftBodyRangesIntoHydrated] ); const inputApiRef = useRef(); diff --git a/ts/components/StoryCreator.dom.tsx b/ts/components/StoryCreator.dom.tsx index a9bc5efef9..d777b405cc 100644 --- a/ts/components/StoryCreator.dom.tsx +++ b/ts/components/StoryCreator.dom.tsx @@ -93,12 +93,12 @@ export type PropsType = { | 'onTextTooLong' | 'platform' | 'sortedGroupMembers' - | 'conversationSelector' + | 'convertDraftBodyRangesIntoHydrated' >; export function StoryCreator({ candidateConversations, - conversationSelector, + convertDraftBodyRangesIntoHydrated, debouncedMaybeGrabLinkPreview, distributionLists, file, @@ -265,7 +265,9 @@ export function StoryCreator({ isCreatingStory isFormattingEnabled={isFormattingEnabled} isSending={isSending} - conversationSelector={conversationSelector} + convertDraftBodyRangesIntoHydrated={ + convertDraftBodyRangesIntoHydrated + } onClose={onClose} onDone={({ contentType, diff --git a/ts/services/backups/import.preload.ts b/ts/services/backups/import.preload.ts index 4743bbd12b..34e6c3e975 100644 --- a/ts/services/backups/import.preload.ts +++ b/ts/services/backups/import.preload.ts @@ -107,10 +107,8 @@ import { convertBackupMessageAttachmentToAttachment, convertFilePointerToAttachment, } from './util/filePointers.preload.js'; -import { - filterAndClean, - trimMessageWhitespace, -} from '../../types/BodyRange.std.js'; +import { trimMessageWhitespace } from '../../types/BodyRange.std.js'; +import { filterAndClean } from '../../util/BodyRange.node.js'; import { APPLICATION_OCTET_STREAM, stringToMIMEType, diff --git a/ts/services/storageRecordOps.preload.ts b/ts/services/storageRecordOps.preload.ts index cf93029583..b5f304f352 100644 --- a/ts/services/storageRecordOps.preload.ts +++ b/ts/services/storageRecordOps.preload.ts @@ -96,7 +96,7 @@ import { fromAciUuidBytesOrString, fromPniUuidBytesOrUntaggedString, } from '../util/ServiceId.node.js'; -import { isProtoBinaryEncodingEnabled } from '../util/isProtoBinaryEncodingEnabled.std.js'; +import { isProtoBinaryEncodingEnabled } from '../util/isProtoBinaryEncodingEnabled.dom.js'; import { getLinkPreviewSetting, getReadReceiptSetting, diff --git a/ts/state/ducks/globalModals.preload.ts b/ts/state/ducks/globalModals.preload.ts index d7dcd087aa..b4d1a1ca0b 100644 --- a/ts/state/ducks/globalModals.preload.ts +++ b/ts/state/ducks/globalModals.preload.ts @@ -44,7 +44,7 @@ import { isPermanentlyUndownloadable } from '../../jobs/AttachmentDownloadManage import type { ButtonVariant } from '../../components/Button.dom.js'; import type { MessageRequestState } from '../../components/conversation/MessageRequestActionsConfirmation.dom.js'; import type { MessageForwardDraft } from '../../types/ForwardDraft.std.js'; -import { hydrateRanges } from '../../types/BodyRange.std.js'; +import { hydrateRanges } from '../../util/BodyRange.node.js'; import { getConversationSelector, type GetConversationByIdType, diff --git a/ts/state/ducks/search.preload.ts b/ts/state/ducks/search.preload.ts index 895d26ec53..eda06221fa 100644 --- a/ts/state/ducks/search.preload.ts +++ b/ts/state/ducks/search.preload.ts @@ -30,7 +30,7 @@ import { getIsActivelySearching, getQuery, getSearchConversation, -} from '../selectors/search.dom.js'; +} from '../selectors/search.preload.js'; import { getAllConversations } from '../selectors/conversations.dom.js'; import { getIntl, diff --git a/ts/state/selectors/message.preload.ts b/ts/state/selectors/message.preload.ts index 3a84710312..724586c9eb 100644 --- a/ts/state/selectors/message.preload.ts +++ b/ts/state/selectors/message.preload.ts @@ -57,7 +57,7 @@ import type { import type { EmbeddedContactForUIType } from '../../types/EmbeddedContact.std.js'; import { embeddedContactSelector } from '../../types/EmbeddedContact.std.js'; import type { HydratedBodyRangesType } from '../../types/BodyRange.std.js'; -import { hydrateRanges } from '../../types/BodyRange.std.js'; +import { hydrateRanges } from '../../util/BodyRange.node.js'; import type { AssertProps } from '../../types/Util.std.js'; import type { LinkPreviewForUIType } from '../../types/message/LinkPreviews.std.js'; import { getMentionsRegex } from '../../types/Message.std.js'; diff --git a/ts/state/selectors/search.dom.ts b/ts/state/selectors/search.preload.ts similarity index 99% rename from ts/state/selectors/search.dom.ts rename to ts/state/selectors/search.preload.ts index 43f1749813..85723d6ff2 100644 --- a/ts/state/selectors/search.dom.ts +++ b/ts/state/selectors/search.preload.ts @@ -29,7 +29,7 @@ import { getSelectedConversationId, } from './conversations.dom.js'; -import { hydrateRanges } from '../../types/BodyRange.std.js'; +import { hydrateRanges } from '../../util/BodyRange.node.js'; import { createLogger } from '../../logging/log.std.js'; import { getOwn } from '../../util/getOwn.std.js'; diff --git a/ts/state/selectors/stories.preload.ts b/ts/state/selectors/stories.preload.ts index d32b6ad49d..5acb0d611f 100644 --- a/ts/state/selectors/stories.preload.ts +++ b/ts/state/selectors/stories.preload.ts @@ -43,7 +43,8 @@ import { reduceStorySendStatus, resolveStorySendStatus, } from '../../util/resolveStorySendStatus.std.js'; -import { BodyRange, hydrateRanges } from '../../types/BodyRange.std.js'; +import { BodyRange } from '../../types/BodyRange.std.js'; +import { hydrateRanges } from '../../util/BodyRange.node.js'; import { getStoriesEnabled } from './items.dom.js'; const { pick } = lodash; diff --git a/ts/state/smart/CompositionArea.preload.tsx b/ts/state/smart/CompositionArea.preload.tsx index ef6281bc87..e2777d3f12 100644 --- a/ts/state/smart/CompositionArea.preload.tsx +++ b/ts/state/smart/CompositionArea.preload.tsx @@ -9,7 +9,7 @@ import type { DraftBodyRanges, HydratedBodyRangesType, } from '../../types/BodyRange.std.js'; -import { hydrateRanges } from '../../types/BodyRange.std.js'; +import { hydrateRanges } from '../../util/BodyRange.node.js'; import { strictAssert } from '../../util/assert.std.js'; import { getAddedByForOurPendingInvitation } from '../../util/getAddedByForOurPendingInvitation.preload.js'; import { AutoSubstituteAsciiEmojis } from '../../quill/auto-substitute-ascii-emojis/index.dom.js'; diff --git a/ts/state/smart/CompositionTextArea.preload.tsx b/ts/state/smart/CompositionTextArea.preload.tsx index 6c8d802bd0..ac34d4f1a7 100644 --- a/ts/state/smart/CompositionTextArea.preload.tsx +++ b/ts/state/smart/CompositionTextArea.preload.tsx @@ -1,9 +1,14 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React, { memo } from 'react'; +import React, { memo, useCallback } from 'react'; import { useSelector } from 'react-redux'; import type { CompositionTextAreaProps } from '../../components/CompositionTextArea.dom.js'; import { CompositionTextArea } from '../../components/CompositionTextArea.dom.js'; +import type { + DraftBodyRanges, + HydratedBodyRangesType, +} from '../../types/BodyRange.std.js'; +import { hydrateRanges } from '../../util/BodyRange.node.js'; import { getIntl, getPlatform, @@ -46,6 +51,15 @@ export const SmartCompositionTextArea = memo(function SmartCompositionTextArea( const isFormattingEnabled = useSelector(getTextFormattingEnabled); const conversationSelector = useSelector(getConversationSelector); + const convertDraftBodyRangesIntoHydrated = useCallback( + ( + bodyRanges: DraftBodyRanges | undefined + ): HydratedBodyRangesType | undefined => { + return hydrateRanges(bodyRanges, conversationSelector); + }, + [conversationSelector] + ); + return ( ); }); diff --git a/ts/state/smart/LeftPane.preload.tsx b/ts/state/smart/LeftPane.preload.tsx index 24ce78a567..8e90cc74fc 100644 --- a/ts/state/smart/LeftPane.preload.tsx +++ b/ts/state/smart/LeftPane.preload.tsx @@ -86,7 +86,7 @@ import { getSearchConversation, getSearchResults, getStartSearchCounter, -} from '../selectors/search.dom.js'; +} from '../selectors/search.preload.js'; import { isUpdateDownloaded as getIsUpdateDownloaded, isOSUnsupported, diff --git a/ts/state/smart/LeftPaneConversationListItemContextMenu.preload.tsx b/ts/state/smart/LeftPaneConversationListItemContextMenu.preload.tsx index 676ef5a503..e8f3639ddb 100644 --- a/ts/state/smart/LeftPaneConversationListItemContextMenu.preload.tsx +++ b/ts/state/smart/LeftPaneConversationListItemContextMenu.preload.tsx @@ -22,7 +22,7 @@ import { useNavActions } from '../ducks/nav.std.js'; import { NavTab, SettingsPage } from '../../types/Nav.std.js'; import type { ChatFolderParams } from '../../types/ChatFolder.std.js'; import { getSelectedLocation } from '../selectors/nav.preload.js'; -import { getIsActivelySearching } from '../selectors/search.dom.js'; +import { getIsActivelySearching } from '../selectors/search.preload.js'; export const SmartLeftPaneConversationListItemContextMenu: FC = memo(function SmartLeftPaneConversationListItemContextMenu(props) { diff --git a/ts/state/smart/MessageSearchResult.preload.tsx b/ts/state/smart/MessageSearchResult.preload.tsx index b6543d3009..968a5cea9c 100644 --- a/ts/state/smart/MessageSearchResult.preload.tsx +++ b/ts/state/smart/MessageSearchResult.preload.tsx @@ -5,7 +5,7 @@ import { useSelector } from 'react-redux'; import { MessageSearchResult } from '../../components/conversationList/MessageSearchResult.dom.js'; import { getPreferredBadgeSelector } from '../selectors/badges.preload.js'; import { getIntl, getTheme } from '../selectors/user.std.js'; -import { getMessageSearchResultSelector } from '../selectors/search.dom.js'; +import { getMessageSearchResultSelector } from '../selectors/search.preload.js'; import { createLogger } from '../../logging/log.std.js'; import { useConversationsActions } from '../ducks/conversations.preload.js'; diff --git a/ts/state/smart/StoryCreator.preload.tsx b/ts/state/smart/StoryCreator.preload.tsx index fafb0af664..79e30c1db1 100644 --- a/ts/state/smart/StoryCreator.preload.tsx +++ b/ts/state/smart/StoryCreator.preload.tsx @@ -1,10 +1,14 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React, { memo, useMemo } from 'react'; +import React, { memo, useMemo, useCallback } from 'react'; import { useSelector } from 'react-redux'; import { ThemeType } from '../../types/Util.std.js'; import { LinkPreviewSourceType } from '../../types/LinkPreview.std.js'; +import type { + DraftBodyRanges, + HydratedBodyRangesType, +} from '../../types/BodyRange.std.js'; import { StoryCreator } from '../../components/StoryCreator.dom.js'; import { getCandidateContactsForNewGroup, @@ -31,6 +35,7 @@ import { } from '../selectors/items.dom.js'; import { imageToBlurHash } from '../../util/imageToBlurHash.dom.js'; import { processAttachment } from '../../util/processAttachment.preload.js'; +import { hydrateRanges } from '../../util/BodyRange.node.js'; import { useEmojisActions } from '../ducks/emojis.preload.js'; import { useAudioPlayerActions } from '../ducks/audioPlayer.preload.js'; import { useComposerActions } from '../ducks/composer.preload.js'; @@ -103,10 +108,19 @@ export const SmartStoryCreator = memo(function SmartStoryCreator() { return linkPreviewForSource(LinkPreviewSourceType.StoryCreator); }, [linkPreviewForSource]); + const convertDraftBodyRangesIntoHydrated = useCallback( + ( + bodyRanges: DraftBodyRanges | undefined + ): HydratedBodyRangesType | undefined => { + return hydrateRanges(bodyRanges, conversationSelector); + }, + [conversationSelector] + ); + return ( ; -const { BOLD, ITALIC, MONOSPACE, SPOILER, STRIKETHROUGH, NONE } = - BodyRange.Style; -const MAX_PER_TYPE = 250; -const MENTION_NAME = 'mention'; - -// We drop unknown bodyRanges and remove extra stuff so they serialize properly -export function filterAndClean( - ranges: ReadonlyArray | undefined | null -): ReadonlyArray | undefined { - if (!ranges) { - return undefined; - } - - const countByTypeRecord: Record< - BodyRange.Style | typeof MENTION_NAME, - number - > = { - [MENTION_NAME]: 0, - [BOLD]: 0, - [ITALIC]: 0, - [MONOSPACE]: 0, - [SPOILER]: 0, - [STRIKETHROUGH]: 0, - [NONE]: 0, - }; - - return ranges - .map(range => { - const { start: startFromRange, length, ...restOfRange } = range; - - const start = startFromRange ?? 0; - if (!isNumber(length)) { - log.warn('filterAndClean: Dropping bodyRange with non-number length'); - return undefined; - } - - let mentionAci: AciString | undefined; - if ('mentionAci' in range && range.mentionAci) { - mentionAci = normalizeAci(range.mentionAci, 'BodyRange.mentionAci'); - } - - if (mentionAci) { - countByTypeRecord[MENTION_NAME] += 1; - if (countByTypeRecord[MENTION_NAME] > MAX_PER_TYPE) { - return undefined; - } - - return { - ...restOfRange, - start, - length, - mentionAci, - }; - } - if ('style' in range && range.style) { - countByTypeRecord[range.style] += 1; - if (countByTypeRecord[range.style] > MAX_PER_TYPE) { - return undefined; - } - return { - ...restOfRange, - start, - length, - style: range.style, - }; - } - - log.warn('filterAndClean: Dropping unknown bodyRange'); - return undefined; - }) - .filter(isNotNil); -} - -export function hydrateRanges( - ranges: ReadonlyArray> | undefined, - conversationSelector: (id: string) => { id: string; title: string } -): Array | undefined { - if (!ranges) { - return undefined; - } - - return filterAndClean(ranges)?.map(range => { - if (BodyRange.isMention(range)) { - const conversation = conversationSelector(range.mentionAci); - - return { - ...range, - conversationID: conversation.id, - replacementText: conversation.title, - }; - } - - return range; - }); -} - /** * Insert a range into an existing range tree, splitting up the range if it intersects * with an existing range @@ -835,7 +738,10 @@ export function applyRangesToText( if (options.replaceSpoilers) { state = _applyRangeOfType(state, bodyRange => { - return BodyRange.isFormatting(bodyRange) && bodyRange.style === SPOILER; + return ( + BodyRange.isFormatting(bodyRange) && + bodyRange.style === BodyRange.Style.SPOILER + ); }); } diff --git a/ts/util/BodyRange.node.ts b/ts/util/BodyRange.node.ts new file mode 100644 index 0000000000..fe1a07a249 --- /dev/null +++ b/ts/util/BodyRange.node.ts @@ -0,0 +1,129 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import lodash from 'lodash'; + +import type { SignalService as Proto } from '../protobuf/index.std.js'; +import { + BodyRange, + type RawBodyRange, + type HydratedBodyRangeType, +} from '../types/BodyRange.std.js'; +import type { AciString } from '../types/ServiceId.std.js'; +import { createLogger } from '../logging/log.std.js'; +import { isNotNil } from './isNotNil.std.js'; +import { dropNull } from './dropNull.std.js'; +import { fromAciUuidBytesOrString } from './ServiceId.node.js'; + +const { isNumber } = lodash; + +const log = createLogger('BodyRange'); + +const { BOLD, ITALIC, MONOSPACE, SPOILER, STRIKETHROUGH, NONE } = + BodyRange.Style; +const MENTION_NAME = 'mention'; +const MAX_PER_TYPE = 250; + +// We drop unknown bodyRanges and remove extra stuff so they serialize properly +export function filterAndClean( + ranges: ReadonlyArray | undefined | null +): ReadonlyArray | undefined { + if (!ranges) { + return undefined; + } + + const countByTypeRecord: Record< + BodyRange.Style | typeof MENTION_NAME, + number + > = { + [MENTION_NAME]: 0, + [BOLD]: 0, + [ITALIC]: 0, + [MONOSPACE]: 0, + [SPOILER]: 0, + [STRIKETHROUGH]: 0, + [NONE]: 0, + }; + + return ranges + .map(range => { + const { start: startFromRange, length, ...restOfRange } = range; + + const start = startFromRange ?? 0; + if (!isNumber(length)) { + log.warn('filterAndClean: Dropping bodyRange with non-number length'); + return undefined; + } + + let rawMentionAci: string | undefined; + let mentionAciBinary: Uint8Array | undefined; + if ('mentionAci' in range) { + rawMentionAci = dropNull(range.mentionAci); + } + if ('mentionAciBinary' in range) { + mentionAciBinary = dropNull(range.mentionAciBinary); + } + + let mentionAci: AciString | undefined; + if (rawMentionAci != null || mentionAciBinary?.length) { + mentionAci = fromAciUuidBytesOrString( + mentionAciBinary, + rawMentionAci, + 'BodyRange.mentionAci' + ); + } + + if (mentionAci) { + countByTypeRecord[MENTION_NAME] += 1; + if (countByTypeRecord[MENTION_NAME] > MAX_PER_TYPE) { + return undefined; + } + + return { + ...restOfRange, + start, + length, + mentionAci, + }; + } + if ('style' in range && range.style) { + countByTypeRecord[range.style] += 1; + if (countByTypeRecord[range.style] > MAX_PER_TYPE) { + return undefined; + } + return { + ...restOfRange, + start, + length, + style: range.style, + }; + } + + log.warn('filterAndClean: Dropping unknown bodyRange'); + return undefined; + }) + .filter(isNotNil); +} + +export function hydrateRanges( + ranges: ReadonlyArray> | undefined, + conversationSelector: (id: string) => { id: string; title: string } +): Array | undefined { + if (!ranges) { + return undefined; + } + + return filterAndClean(ranges)?.map(range => { + if (BodyRange.isMention(range)) { + const conversation = conversationSelector(range.mentionAci); + + return { + ...range, + conversationID: conversation.id, + replacementText: conversation.title, + }; + } + + return range; + }); +} diff --git a/ts/util/getDraftPreview.preload.ts b/ts/util/getDraftPreview.preload.ts index e9507e49a9..dd0dc38386 100644 --- a/ts/util/getDraftPreview.preload.ts +++ b/ts/util/getDraftPreview.preload.ts @@ -4,7 +4,7 @@ import type { ConversationAttributesType } from '../model-types.d.ts'; import type { DraftPreviewType } from '../state/ducks/conversations.preload.js'; import { findAndFormatContact } from './findAndFormatContact.preload.js'; -import { hydrateRanges } from '../types/BodyRange.std.js'; +import { hydrateRanges } from './BodyRange.node.js'; import { isVoiceMessage } from './Attachment.std.js'; import { stripNewlinesForLeftPane } from './stripNewlinesForLeftPane.std.js'; diff --git a/ts/util/getLastMessage.preload.ts b/ts/util/getLastMessage.preload.ts index f478260574..a5d9e0a947 100644 --- a/ts/util/getLastMessage.preload.ts +++ b/ts/util/getLastMessage.preload.ts @@ -5,7 +5,7 @@ import type { ConversationAttributesType } from '../model-types.d.ts'; import type { LastMessageType } from '../state/ducks/conversations.preload.js'; import { dropNull } from './dropNull.std.js'; import { findAndFormatContact } from './findAndFormatContact.preload.js'; -import { hydrateRanges } from '../types/BodyRange.std.js'; +import { hydrateRanges } from './BodyRange.node.js'; import { stripNewlinesForLeftPane } from './stripNewlinesForLeftPane.std.js'; export function getLastMessage( diff --git a/ts/util/getNotificationTextForMessage.preload.ts b/ts/util/getNotificationTextForMessage.preload.ts index 86ebba2196..4c2df404ff 100644 --- a/ts/util/getNotificationTextForMessage.preload.ts +++ b/ts/util/getNotificationTextForMessage.preload.ts @@ -2,7 +2,8 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { ReadonlyMessageAttributesType } from '../model-types.d.ts'; -import { applyRangesToText, hydrateRanges } from '../types/BodyRange.std.js'; +import { applyRangesToText } from '../types/BodyRange.std.js'; +import { hydrateRanges } from './BodyRange.node.js'; import { findAndFormatContact } from './findAndFormatContact.preload.js'; import { getNotificationDataForMessage } from './getNotificationDataForMessage.preload.js'; import { isConversationAccepted } from './isConversationAccepted.preload.js'; diff --git a/ts/util/isProtoBinaryEncodingEnabled.std.ts b/ts/util/isProtoBinaryEncodingEnabled.dom.ts similarity index 73% rename from ts/util/isProtoBinaryEncodingEnabled.std.ts rename to ts/util/isProtoBinaryEncodingEnabled.dom.ts index e9a9c35cad..73f1298db5 100644 --- a/ts/util/isProtoBinaryEncodingEnabled.std.ts +++ b/ts/util/isProtoBinaryEncodingEnabled.dom.ts @@ -2,12 +2,17 @@ // SPDX-License-Identifier: AGPL-3.0-only import { isTestOrMockEnvironment } from '../environment.std.js'; +import { isStagingServer } from './isStagingServer.dom.js'; export function isProtoBinaryEncodingEnabled(): boolean { if (isTestOrMockEnvironment()) { return true; } + if (isStagingServer()) { + return true; + } + // TODO: DESKTOP-8938 return false; }