Use binary proto fields in staging

This commit is contained in:
Fedor Indutny
2025-11-03 10:41:49 -08:00
committed by GitHub
parent 6bf79848c1
commit 4436184f95
30 changed files with 242 additions and 154 deletions

View File

@@ -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,

View File

@@ -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<InputApi | undefined>();
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 (

View File

@@ -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={() => []}
/>
);
}

View File

@@ -70,7 +70,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
ourConversationId="me"
platform="darwin"
emojiSkinToneDefault={EmojiSkinTone.None}
conversationSelector={() => getDefaultConversation()}
convertDraftBodyRangesIntoHydrated={() => []}
/>
),
showToast: action('showToast'),

View File

@@ -31,6 +31,7 @@ export default {
onTextTooLong: action('onTextTooLong'),
platform: 'darwin',
emojiSkinToneDefault: EmojiSkinTone.None,
convertDraftBodyRangesIntoHydrated: () => undefined,
},
} satisfies Meta<PropsType>;

View File

@@ -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<fabric.Canvas | undefined>();
@@ -181,8 +184,8 @@ export function MediaEditor({
useState<DraftBodyRanges | null>(draftBodyRanges);
const hydratedBodyRanges = useMemo(
() => hydrateRanges(captionBodyRanges ?? undefined, conversationSelector),
[captionBodyRanges, conversationSelector]
() => convertDraftBodyRangesIntoHydrated(captionBodyRanges ?? undefined),
[captionBodyRanges, convertDraftBodyRangesIntoHydrated]
);
const inputApiRef = useRef<InputApi | undefined>();

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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';

View File

@@ -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';

View File

@@ -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;

View File

@@ -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';

View File

@@ -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 (
<CompositionTextArea
{...props}
@@ -58,7 +72,7 @@ export const SmartCompositionTextArea = memo(function SmartCompositionTextArea(
onTextTooLong={onTextTooLong}
platform={platform}
ourConversationId={ourConversationId}
conversationSelector={conversationSelector}
convertDraftBodyRangesIntoHydrated={convertDraftBodyRangesIntoHydrated}
/>
);
});

View File

@@ -86,7 +86,7 @@ import {
getSearchConversation,
getSearchResults,
getStartSearchCounter,
} from '../selectors/search.dom.js';
} from '../selectors/search.preload.js';
import {
isUpdateDownloaded as getIsUpdateDownloaded,
isOSUnsupported,

View File

@@ -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<RenderConversationListItemContextMenuProps> =
memo(function SmartLeftPaneConversationListItemContextMenu(props) {

View File

@@ -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';

View File

@@ -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 (
<StoryCreator
candidateConversations={candidateConversations}
conversationSelector={conversationSelector}
convertDraftBodyRangesIntoHydrated={convertDraftBodyRangesIntoHydrated}
debouncedMaybeGrabLinkPreview={debouncedMaybeGrabLinkPreview}
distributionLists={distributionLists}
file={file}

View File

@@ -19,7 +19,7 @@ import {
getIsSearchingInAConversation,
getMessageSearchResultSelector,
getSearchResults,
} from '../../../state/selectors/search.dom.js';
} from '../../../state/selectors/search.preload.js';
import { makeLookup } from '../../../util/makeLookup.std.js';
import { generateAci } from '../../../types/ServiceId.std.js';
import {

View File

@@ -153,7 +153,7 @@ import { isNotNil } from '../util/isNotNil.std.js';
import { chunk } from '../util/iterables.std.js';
import { inspectUnknownFieldTags } from '../util/inspectProtobufs.std.js';
import { incrementMessageCounter } from '../util/incrementMessageCounter.preload.js';
import { filterAndClean } from '../types/BodyRange.std.js';
import { filterAndClean } from '../util/BodyRange.node.js';
import {
getCallEventForProto,
getCallLogEventForProto,

View File

@@ -99,7 +99,7 @@ import {
getProtoForCallHistory,
} from '../util/callDisposition.preload.js';
import { MAX_MESSAGE_COUNT } from '../util/deleteForMe.types.std.js';
import { isProtoBinaryEncodingEnabled } from '../util/isProtoBinaryEncodingEnabled.std.js';
import { isProtoBinaryEncodingEnabled } from '../util/isProtoBinaryEncodingEnabled.dom.js';
import type { GroupSendToken } from '../types/GroupSendEndorsements.std.js';
import type { OutgoingPollVote, PollCreateType } from '../types/Polls.dom.js';
import { itemStorage } from './Storage.preload.js';
@@ -433,11 +433,12 @@ class Message {
proto.reaction = new Proto.DataMessage.Reaction();
proto.reaction.emoji = this.reaction.emoji || null;
proto.reaction.remove = this.reaction.remove || false;
proto.reaction.targetAuthorAci = this.reaction.targetAuthorAci || null;
if (isProtoBinaryEncodingEnabled()) {
proto.reaction.targetAuthorAciBinary = this.reaction.targetAuthorAci
? toAciObject(this.reaction.targetAuthorAci).getRawUuidBytes()
: null;
} else {
proto.reaction.targetAuthorAci = this.reaction.targetAuthorAci || null;
}
proto.reaction.targetSentTimestamp =
this.reaction.targetTimestamp === undefined
@@ -543,11 +544,12 @@ class Message {
quote.id =
this.quote.id === undefined ? null : Long.fromNumber(this.quote.id);
quote.authorAci = this.quote.authorAci || null;
if (isProtoBinaryEncodingEnabled()) {
quote.authorAciBinary = this.quote.authorAci
? toAciObject(this.quote.authorAci).getRawUuidBytes()
: null;
} else {
quote.authorAci = this.quote.authorAci || null;
}
quote.text = this.quote.text || null;
quote.attachments = this.quote.attachments.slice() || [];
@@ -557,11 +559,12 @@ class Message {
bodyRange.start = range.start;
bodyRange.length = range.length;
if (BodyRange.isMention(range)) {
bodyRange.mentionAci = range.mentionAci;
if (isProtoBinaryEncodingEnabled()) {
bodyRange.mentionAciBinary = toAciObject(
range.mentionAci
).getRawUuidBytes();
} else {
bodyRange.mentionAci = range.mentionAci;
}
} else if (BodyRange.isFormatting(range)) {
bodyRange.style = range.style;
@@ -601,6 +604,15 @@ class Message {
const { start, length } = bodyRange;
if (BodyRange.isMention(bodyRange)) {
if (isProtoBinaryEncodingEnabled()) {
return {
start,
length,
mentionAciBinary: toAciObject(
bodyRange.mentionAci
).getRawUuidBytes(),
};
}
return {
start,
length,
@@ -632,11 +644,12 @@ class Message {
const storyContext = new StoryContext();
if (this.storyContext.authorAci) {
storyContext.authorAci = this.storyContext.authorAci;
if (isProtoBinaryEncodingEnabled()) {
storyContext.authorAciBinary = toAciObject(
this.storyContext.authorAci
).getRawUuidBytes();
} else {
storyContext.authorAci = this.storyContext.authorAci;
}
}
storyContext.sentTimestamp = Long.fromNumber(this.storyContext.timestamp);
@@ -1424,10 +1437,11 @@ export class MessageSender {
sentMessage.destinationE164 = destinationE164;
}
if (destinationServiceId) {
sentMessage.destinationServiceId = destinationServiceId;
if (isProtoBinaryEncodingEnabled()) {
sentMessage.destinationServiceIdBinary =
toServiceIdObject(destinationServiceId).getServiceIdBinary();
} else {
sentMessage.destinationServiceId = destinationServiceId;
}
}
if (expirationStartTimestamp) {
@@ -1458,10 +1472,11 @@ export class MessageSender {
if (conv) {
const serviceId = conv.getServiceId();
if (serviceId) {
status.destinationServiceId = serviceId;
if (isProtoBinaryEncodingEnabled()) {
status.destinationServiceIdBinary =
toServiceIdObject(serviceId).getServiceIdBinary();
} else {
status.destinationServiceId = serviceId;
}
}
if (isPniString(serviceId)) {

View File

@@ -37,7 +37,7 @@ import {
import { SECOND, DurationInSeconds } from '../util/durations/index.std.js';
import type { AnyPaymentEvent } from '../types/Payment.std.js';
import { PaymentEventKind } from '../types/Payment.std.js';
import { filterAndClean } from '../types/BodyRange.std.js';
import { filterAndClean } from '../util/BodyRange.node.js';
import { bytesToUuid } from '../util/uuidToBytes.std.js';
import { createName } from '../util/attachmentPath.node.js';
import { partitionBodyAndNormalAttachments } from '../util/Attachment.std.js';

View File

@@ -16,7 +16,6 @@ import {
} from '../util/search.std.js';
import { assertDev } from '../util/assert.std.js';
import type { AciString } from './ServiceId.std.js';
import { normalizeAci } from '../util/normalizeAci.std.js';
const { isEqual, isNumber, omit, orderBy, partition } = lodash;
@@ -143,102 +142,6 @@ export type RangeNode = BodyRange<
}
>;
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<Proto.IBodyRange | RawBodyRange> | undefined | null
): ReadonlyArray<RawBodyRange> | 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<BodyRange<object>> | undefined,
conversationSelector: (id: string) => { id: string; title: string }
): Array<HydratedBodyRangeType> | 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
);
});
}

129
ts/util/BodyRange.node.ts Normal file
View File

@@ -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<Proto.IBodyRange | RawBodyRange> | undefined | null
): ReadonlyArray<RawBodyRange> | 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<BodyRange<object>> | undefined,
conversationSelector: (id: string) => { id: string; title: string }
): Array<HydratedBodyRangeType> | 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;
});
}

View File

@@ -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';

View File

@@ -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(

View File

@@ -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';

View File

@@ -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;
}