diff --git a/package-lock.json b/package-lock.json index 56d9ac6468..503bcb0fba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -105,7 +105,7 @@ "sanitize.css": "11.0.0", "semver": "5.7.2", "split2": "4.0.0", - "type-fest": "3.5.0", + "type-fest": "4.23.0", "urlpattern-polyfill": "9.0.0", "uuid": "3.3.2", "uuid-browser": "3.1.0", @@ -7184,6 +7184,17 @@ "uuid": "^8.3.0" } }, + "node_modules/@signalapp/libsignal-client/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@signalapp/libsignal-client/node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -34835,11 +34846,11 @@ } }, "node_modules/type-fest": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.5.0.tgz", - "integrity": "sha512-bI3zRmZC8K0tUz1HjbIOAGQwR2CoPQG68N5IF7gm0LBl8QSNXzkmaWnkWccCUL5uG9mCsp4sBwC8SBrNSISWew==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.23.0.tgz", + "integrity": "sha512-ZiBujro2ohr5+Z/hZWHESLz3g08BBdrdLMieYFULJO+tWc437sn8kQsWLJoZErY8alNhxre9K4p3GURAG11n+w==", "engines": { - "node": ">=14.16" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/package.json b/package.json index d5f8afe751..9753786538 100644 --- a/package.json +++ b/package.json @@ -187,7 +187,7 @@ "sanitize.css": "11.0.0", "semver": "5.7.2", "split2": "4.0.0", - "type-fest": "3.5.0", + "type-fest": "4.23.0", "urlpattern-polyfill": "9.0.0", "uuid": "3.3.2", "uuid-browser": "3.1.0", diff --git a/ts/components/conversation/ContactDetail.tsx b/ts/components/conversation/ContactDetail.tsx index 493ecc5dd7..6ee7880011 100644 --- a/ts/components/conversation/ContactDetail.tsx +++ b/ts/components/conversation/ContactDetail.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; +import type { ReadonlyDeep } from 'type-fest'; import type { EmbeddedContactType, @@ -21,7 +22,7 @@ import { import type { LocalizerType } from '../../types/Util'; export type Props = { - contact: EmbeddedContactType; + contact: ReadonlyDeep; hasSignalAccount: boolean; i18n: LocalizerType; onSendMessage: () => void; diff --git a/ts/components/conversation/EmbeddedContact.tsx b/ts/components/conversation/EmbeddedContact.tsx index 395a4ae2b0..85ab9df011 100644 --- a/ts/components/conversation/EmbeddedContact.tsx +++ b/ts/components/conversation/EmbeddedContact.tsx @@ -3,6 +3,7 @@ import React from 'react'; import classNames from 'classnames'; +import type { ReadonlyDeep } from 'type-fest'; import type { EmbeddedContactType } from '../../types/EmbeddedContact'; @@ -14,7 +15,7 @@ import { } from './contactUtil'; export type Props = { - contact: EmbeddedContactType; + contact: ReadonlyDeep; i18n: LocalizerType; isIncoming: boolean; withContentAbove: boolean; diff --git a/ts/components/conversation/GroupV2Change.tsx b/ts/components/conversation/GroupV2Change.tsx index 9d8460a1d1..9c1f491e9f 100644 --- a/ts/components/conversation/GroupV2Change.tsx +++ b/ts/components/conversation/GroupV2Change.tsx @@ -4,6 +4,7 @@ import type { ReactElement, ReactNode } from 'react'; import React, { useState } from 'react'; import { get } from 'lodash'; +import type { ReadonlyDeep } from 'type-fest'; import * as log from '../../logging/log'; import { I18n } from '../I18n'; @@ -27,7 +28,7 @@ import { renderChange } from '../../groupChange'; import { Modal } from '../Modal'; import { ConfirmationDialog } from '../ConfirmationDialog'; -export type PropsDataType = { +export type PropsDataType = ReadonlyDeep<{ areWeAdmin: boolean; change: GroupV2ChangeType; conversationId: string; @@ -39,7 +40,7 @@ export type PropsDataType = { groupName?: string; ourAci: AciString | undefined; ourPni: PniString | undefined; -}; +}>; export type PropsActionsType = { blockGroupLinkRequests: ( diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 9d69d7b75e..732b4c3443 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -16,6 +16,7 @@ import getDirection from 'direction'; import { drop, groupBy, noop, orderBy, take, unescape } from 'lodash'; import { Manager, Popper, Reference } from 'react-popper'; import type { PreventOverflowModifier } from '@popperjs/core/lib/modifiers/preventOverflow'; +import type { ReadonlyDeep } from 'type-fest'; import type { ConversationType, @@ -229,7 +230,7 @@ export type PropsData = { timestamp: number; receivedAtMS?: number; status?: MessageStatusType; - contact?: EmbeddedContactType; + contact?: ReadonlyDeep; author: Pick< ConversationType, | 'acceptedMessageRequest' diff --git a/ts/components/conversation/contactUtil.tsx b/ts/components/conversation/contactUtil.tsx index 6a30b5cf1e..b93293c569 100644 --- a/ts/components/conversation/contactUtil.tsx +++ b/ts/components/conversation/contactUtil.tsx @@ -3,6 +3,7 @@ import React from 'react'; import classNames from 'classnames'; +import type { ReadonlyDeep } from 'type-fest'; import { Avatar, AvatarBlur } from '../Avatar'; import { Spinner } from '../Spinner'; @@ -18,7 +19,7 @@ export function renderAvatar({ size, direction, }: { - contact: EmbeddedContactType; + contact: ReadonlyDeep; i18n: LocalizerType; size: 28 | 52 | 80; direction?: 'outgoing' | 'incoming'; @@ -65,7 +66,7 @@ export function renderName({ isIncoming, module, }: { - contact: EmbeddedContactType; + contact: ReadonlyDeep; isIncoming: boolean; module: string; }): JSX.Element { @@ -86,7 +87,7 @@ export function renderContactShorthand({ isIncoming, module, }: { - contact: EmbeddedContactType; + contact: ReadonlyDeep; isIncoming: boolean; module: string; }): JSX.Element { diff --git a/ts/components/conversation/media-gallery/types/ItemClickEvent.ts b/ts/components/conversation/media-gallery/types/ItemClickEvent.ts index 553d3f2290..1d938f06a4 100644 --- a/ts/components/conversation/media-gallery/types/ItemClickEvent.ts +++ b/ts/components/conversation/media-gallery/types/ItemClickEvent.ts @@ -1,11 +1,11 @@ // Copyright 2018 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { MessageAttributesType } from '../../../../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../../../../model-types.d'; import type { AttachmentType } from '../../../../types/Attachment'; export type ItemClickEvent = { - message: Pick; + message: Pick; attachment: AttachmentType; index: number; type: 'media' | 'documents'; diff --git a/ts/groupChange.ts b/ts/groupChange.ts index f6cd4640c8..b05902b064 100644 --- a/ts/groupChange.ts +++ b/ts/groupChange.ts @@ -1,6 +1,7 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import type { ReadonlyDeep } from 'type-fest'; import type { LocalizerType, ICUStringMessageParamsByKeyType, @@ -56,7 +57,7 @@ export type RenderChangeResultType = >; export function renderChange( - change: GroupV2ChangeType, + change: ReadonlyDeep, options: RenderOptionsType ): RenderChangeResultType { const { details, from } = change; @@ -79,7 +80,7 @@ export function renderChange( } function renderChangeDetail( - detail: GroupV2ChangeDetailType, + detail: ReadonlyDeep, options: RenderOptionsType ): string | T | ReadonlyArray { const { diff --git a/ts/jobs/helpers/sendNormalMessage.ts b/ts/jobs/helpers/sendNormalMessage.ts index 444487233c..72d1832864 100644 --- a/ts/jobs/helpers/sendNormalMessage.ts +++ b/ts/jobs/helpers/sendNormalMessage.ts @@ -36,10 +36,7 @@ import { copyCdnFields } from '../../util/attachments'; import { LONG_MESSAGE } from '../../types/MIME'; import { LONG_ATTACHMENT_LIMIT } from '../../types/Message'; import type { RawBodyRange } from '../../types/BodyRange'; -import type { - EmbeddedContactWithHydratedAvatar, - EmbeddedContactWithUploadedAvatar, -} from '../../types/EmbeddedContact'; +import type { EmbeddedContactWithUploadedAvatar } from '../../types/EmbeddedContact'; import type { StoryContextType } from '../../types/Util'; import type { LoggerType } from '../../types/Logging'; import type { @@ -1058,8 +1055,7 @@ async function uploadMessageContacts( strictAssert(oldContact, `${logId}: Contacts are gone after upload`); const newContact = oldContact.map((contact, index) => { - const loaded: EmbeddedContactWithHydratedAvatar | undefined = - contacts.at(index); + const loaded = contacts.at(index); if (!contact.avatar) { strictAssert( loaded?.avatar === undefined, diff --git a/ts/messages/helpers.ts b/ts/messages/helpers.ts index 59a9f8f42e..4322507354 100644 --- a/ts/messages/helpers.ts +++ b/ts/messages/helpers.ts @@ -1,11 +1,13 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import type { ReadonlyDeep } from 'type-fest'; + import * as log from '../logging/log'; import type { ConversationModel } from '../models/conversations'; import type { CustomError, - MessageAttributesType, + ReadonlyMessageAttributesType, QuotedAttachmentType, QuotedMessageType, } from '../model-types.d'; @@ -16,33 +18,36 @@ import type { LocalizerType } from '../types/Util'; import { missingCaseError } from '../util/missingCaseError'; export function isIncoming( - message: Pick + message: Pick ): boolean { return message.type === 'incoming'; } export function isOutgoing( - message: Pick + message: Pick ): boolean { return message.type === 'outgoing'; } -export function isStory(message: Pick): boolean { +export function isStory( + message: Pick +): boolean { return message.type === 'story'; } -export type MessageAttributesWithPaymentEvent = MessageAttributesType & { - payment: AnyPaymentEvent; -}; +export type MessageAttributesWithPaymentEvent = ReadonlyMessageAttributesType & + ReadonlyDeep<{ + payment: AnyPaymentEvent; + }>; export function messageHasPaymentEvent( - message: MessageAttributesType + message: ReadonlyMessageAttributesType ): message is MessageAttributesWithPaymentEvent { return message.payment != null; } export function getPaymentEventNotificationText( - payment: AnyPaymentEvent, + payment: ReadonlyDeep, senderTitle: string, conversationTitle: string | null, senderIsMe: boolean, @@ -61,7 +66,7 @@ export function getPaymentEventNotificationText( } export function getPaymentEventDescription( - payment: AnyPaymentEvent, + payment: ReadonlyDeep, senderTitle: string, conversationTitle: string | null, senderIsMe: boolean, @@ -110,10 +115,10 @@ export function getPaymentEventDescription( } export function isQuoteAMatch( - message: MessageAttributesType | null | undefined, + message: ReadonlyMessageAttributesType | null | undefined, conversationId: string, - quote: Pick -): message is MessageAttributesType { + quote: ReadonlyDeep> +): message is ReadonlyMessageAttributesType { if (!message) { return false; } @@ -142,7 +147,7 @@ export const shouldTryToCopyFromQuotedMessage = ({ quoteAttachment, }: { referencedMessageNotFound: boolean; - quoteAttachment: QuotedAttachmentType | undefined; + quoteAttachment: ReadonlyDeep | undefined; }): boolean => { // If we've tried and can't find the message, try again. if (referencedMessageNotFound === true) { @@ -163,7 +168,10 @@ export const shouldTryToCopyFromQuotedMessage = ({ }; export function getAuthorId( - message: Pick + message: Pick< + ReadonlyMessageAttributesType, + 'type' | 'source' | 'sourceServiceId' + > ): string | undefined { const source = getSource(message); const sourceServiceId = getSourceServiceId(message); @@ -181,14 +189,14 @@ export function getAuthorId( } export function getAuthor( - message: MessageAttributesType + message: ReadonlyMessageAttributesType ): ConversationModel | undefined { const id = getAuthorId(message); return window.ConversationController.get(id); } export function getSource( - message: Pick + message: Pick ): string | undefined { if (isIncoming(message) || isStory(message)) { return message.source; @@ -201,7 +209,7 @@ export function getSource( } export function getSourceDevice( - message: Pick + message: Pick ): string | number | undefined { const { sourceDevice } = message; @@ -218,7 +226,7 @@ export function getSourceDevice( } export function getSourceServiceId( - message: Pick + message: Pick ): ServiceIdString | undefined { if (isIncoming(message) || isStory(message)) { return message.sourceServiceId; diff --git a/ts/model-types.d.ts b/ts/model-types.d.ts index bb727ad3dc..580b149c3b 100644 --- a/ts/model-types.d.ts +++ b/ts/model-types.d.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import * as Backbone from 'backbone'; +import type { ReadonlyDeep } from 'type-fest'; import type { GroupV2ChangeType } from './groups'; import type { DraftBodyRanges, RawBodyRange } from './types/BodyRange'; @@ -291,6 +292,8 @@ export type MessageAttributesType = { deletedForEveryoneFailed?: boolean; }; +export type ReadonlyMessageAttributesType = ReadonlyDeep; + export type ConversationAttributesTypeType = 'private' | 'group'; export type ConversationLastProfileType = Readonly<{ diff --git a/ts/services/storyLoader.ts b/ts/services/storyLoader.ts index e95b4d8c22..faee925c88 100644 --- a/ts/services/storyLoader.ts +++ b/ts/services/storyLoader.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import { pick } from 'lodash'; -import type { MessageAttributesType } from '../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../model-types.d'; import type { StoryDataType } from '../state/ducks/stories'; import * as durations from '../util/durations'; import * as log from '../logging/log'; @@ -28,10 +28,11 @@ export async function loadStories(): Promise { } export function getStoryDataFromMessageAttributes( - message: MessageAttributesType & { - hasReplies?: boolean; - hasRepliesFromSelf?: boolean; - } + message: ReadonlyMessageAttributesType & + Readonly<{ + hasReplies?: boolean; + hasRepliesFromSelf?: boolean; + }> ): StoryDataType | undefined { const { attachments, deletedForEveryone } = message; const unresolvedAttachment = attachments ? attachments[0] : undefined; diff --git a/ts/signal.ts b/ts/signal.ts index 9e4f9a579b..33123a1f7b 100644 --- a/ts/signal.ts +++ b/ts/signal.ts @@ -3,6 +3,8 @@ // The idea with this file is to make it webpackable for the style guide +import type { ReadonlyDeep } from 'type-fest'; + import * as Crypto from './Crypto'; import * as Curve from './Curve'; import { start as conversationControllerStart } from './ConversationController'; @@ -84,13 +86,13 @@ type MigrationsModuleType = { attachment: Partial ) => Promise; loadContactData: ( - contact: Array | undefined + contact: ReadonlyArray> | undefined ) => Promise | undefined>; loadMessage: ( message: MessageAttributesType ) => Promise; loadPreviewData: ( - preview: Array | undefined + preview: ReadonlyArray> | undefined ) => Promise>; loadQuoteData: ( quote: QuotedMessageType | null | undefined diff --git a/ts/sql/Client.ts b/ts/sql/Client.ts index d1140b72e9..1b979ca9ad 100644 --- a/ts/sql/Client.ts +++ b/ts/sql/Client.ts @@ -4,6 +4,7 @@ import { ipcRenderer as ipc } from 'electron'; import { groupBy, isTypedArray, last, map, omit } from 'lodash'; +import type { ReadonlyDeep } from 'type-fest'; import { deleteExternalFiles } from '../types/Conversation'; import { update as updateExpiringMessagesService } from '../services/expiringMessagesDeletion'; @@ -222,7 +223,9 @@ function _cleanData( return cleaned; } -export function _cleanMessageData(data: MessageType): MessageType { +export function _cleanMessageData( + data: ReadonlyDeep +): ReadonlyDeep { const result = { ...data }; // Ensure that all messages have the received_at set properly if (!data.received_at) { @@ -586,7 +589,7 @@ async function searchMessages({ // Message async function saveMessage( - data: MessageType, + data: ReadonlyDeep, options: { jobToInsert?: Readonly; forceSave?: boolean; @@ -607,7 +610,7 @@ async function saveMessage( } async function saveMessages( - arrayOfMessages: ReadonlyArray, + arrayOfMessages: ReadonlyArray>, options: { forceSave?: boolean; ourAci: AciString } ): Promise> { const result = await writableChannel.saveMessages( diff --git a/ts/sql/Interface.ts b/ts/sql/Interface.ts index 1691fdb114..231b64863c 100644 --- a/ts/sql/Interface.ts +++ b/ts/sql/Interface.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { Database } from '@signalapp/better-sqlite3'; +import type { ReadonlyDeep } from 'type-fest'; import type { ConversationAttributesType, MessageAttributesType, @@ -730,7 +731,7 @@ type WritableInterface = { deleteAllEndorsementsForGroup: (groupId: string) => void; saveMessage: ( - data: MessageType, + data: ReadonlyDeep, options: { jobToInsert?: StoredJob; forceSave?: boolean; @@ -738,7 +739,7 @@ type WritableInterface = { } ) => string; saveMessages: ( - arrayOfMessages: ReadonlyArray, + arrayOfMessages: ReadonlyArray>, options: { forceSave?: boolean; ourAci: AciString } ) => Array; @@ -795,14 +796,14 @@ type WritableInterface = { ): CallLinkType; migrateConversationMessages: (obsoleteId: string, currentId: string) => void; saveEditedMessage: ( - mainMessage: MessageType, + mainMessage: ReadonlyDeep, ourAci: AciString, - opts: EditedMessageType + opts: ReadonlyDeep ) => void; saveEditedMessages: ( - mainMessage: MessageType, + mainMessage: ReadonlyDeep, ourAci: AciString, - history: ReadonlyArray + history: ReadonlyArray> ) => void; removeSyncTaskById: (id: string) => void; @@ -1047,13 +1048,13 @@ export type ClientOnlyReadableInterface = ClientInterfaceWrap<{ getRecentStoryReplies( storyId: string, options?: GetRecentStoryRepliesOptionsType - ): Array; + ): Array; getOlderMessagesByConversation: ( options: AdjacentMessagesByConversationOptionsType - ) => Array; + ) => Array; getNewerMessagesByConversation: ( options: AdjacentMessagesByConversationOptionsType - ) => Array; + ) => Array; getConversationRangeCenteredOnMessage: ( options: AdjacentMessagesByConversationOptionsType ) => GetConversationRangeCenteredOnMessageResultType; diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index c9761e89a2..3f23970696 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -11,6 +11,7 @@ import type { Database, Statement } from '@signalapp/better-sqlite3'; import SQL from '@signalapp/better-sqlite3'; import { v4 as generateUuid } from 'uuid'; import { z } from 'zod'; +import type { ReadonlyDeep } from 'type-fest'; import type { Dictionary } from 'lodash'; import { @@ -2119,7 +2120,7 @@ export function getAllSyncTasks(db: WritableDB): Array { export function saveMessage( db: WritableDB, - data: MessageType, + data: ReadonlyDeep, options: { alreadyInTransaction?: boolean; forceSave?: boolean; @@ -2337,7 +2338,7 @@ export function saveMessage( function saveMessages( db: WritableDB, - arrayOfMessages: ReadonlyArray, + arrayOfMessages: ReadonlyArray>, options: { forceSave?: boolean; ourAci: AciString } ): Array { return db.transaction(() => { @@ -6964,9 +6965,9 @@ function removeAllProfileKeyCredentials(db: WritableDB): void { function saveEditedMessages( db: WritableDB, - mainMessage: MessageType, + mainMessage: ReadonlyDeep, ourAci: AciString, - history: ReadonlyArray + history: ReadonlyArray> ): void { db.transaction(() => { saveMessage(db, mainMessage, { @@ -6996,9 +6997,9 @@ function saveEditedMessages( function saveEditedMessage( db: WritableDB, - mainMessage: MessageType, + mainMessage: ReadonlyDeep, ourAci: AciString, - editedMessage: EditedMessageType + editedMessage: ReadonlyDeep ): void { return saveEditedMessages(db, mainMessage, ourAci, [editedMessage]); } diff --git a/ts/state/ducks/composer.ts b/ts/state/ducks/composer.ts index b6e2785135..8660aada4f 100644 --- a/ts/state/ducks/composer.ts +++ b/ts/state/ducks/composer.ts @@ -22,7 +22,7 @@ import { DataReader, DataWriter } from '../../sql/Client'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions'; import type { DraftBodyRanges } from '../../types/BodyRange'; import type { LinkPreviewType } from '../../types/message/LinkPreviews'; -import type { MessageAttributesType } from '../../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../../model-types.d'; import type { NoopActionType } from './noop'; import type { ShowToastActionType } from './toast'; import type { StateType as RootStateType } from '../reducer'; @@ -104,14 +104,17 @@ type ComposerStateByConversationType = { linkPreviewLoading: boolean; linkPreviewResult?: LinkPreviewType; messageCompositionId: string; - quotedMessage?: Pick; + quotedMessage?: Pick< + ReadonlyMessageAttributesType, + 'conversationId' | 'quote' + >; sendCounter: number; shouldSendHighQualityAttachments?: boolean; }; // eslint-disable-next-line local-rules/type-alias-readonlydeep export type QuotedMessageType = Pick< - MessageAttributesType, + ReadonlyMessageAttributesType, 'conversationId' | 'quote' >; diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 468f3c8d00..f2615aa007 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -64,6 +64,7 @@ import type { DraftEditMessageType, LastMessageStatus, MessageAttributesType, + ReadonlyMessageAttributesType, } from '../../model-types.d'; import type { DraftBodyRanges, @@ -214,18 +215,20 @@ export type InteractionModeType = ReadonlyDeep< >; export type MessageTimestamps = ReadonlyDeep< - Pick + Pick >; -// eslint-disable-next-line local-rules/type-alias-readonlydeep -export type MessageType = MessageAttributesType & { - interactionType?: InteractionModeType; -}; -// eslint-disable-next-line local-rules/type-alias-readonlydeep -export type MessageWithUIFieldsType = MessageAttributesType & { - displayLimit?: number; - isSpoilerExpanded?: Record; -}; +export type MessageType = ReadonlyDeep< + ReadonlyMessageAttributesType & { + interactionType?: InteractionModeType; + } +>; +export type MessageWithUIFieldsType = ReadonlyDeep< + ReadonlyMessageAttributesType & { + displayLimit?: number; + isSpoilerExpanded?: Record; + } +>; export const ConversationTypes = ['direct', 'group'] as const; export type ConversationTypeType = ReadonlyDeep< @@ -420,10 +423,9 @@ type MessageMetricsType = ReadonlyDeep<{ totalUnseen: number; }>; -// eslint-disable-next-line local-rules/type-alias-readonlydeep -export type MessageLookupType = { +export type MessageLookupType = ReadonlyDeep<{ [key: string]: MessageWithUIFieldsType; -}; +}>; export type ConversationMessageType = ReadonlyDeep<{ isNearBottom?: boolean; messageChangeCounter: number; @@ -510,8 +512,7 @@ type ComposerStateType = ReadonlyDeep< )) >; -// eslint-disable-next-line local-rules/type-alias-readonlydeep -- FIXME -export type ConversationsStateType = Readonly<{ +export type ConversationsStateType = ReadonlyDeep<{ preJoinConversation?: PreJoinConversationType; invitedServiceIdsForNewlyCreatedGroup?: ReadonlyArray; conversationLookup: ConversationLookupType; @@ -530,7 +531,7 @@ export type ConversationsStateType = Readonly<{ stack: ReadonlyArray; watermark: number; }; - targetedMessageForDetails?: MessageAttributesType; + targetedMessageForDetails?: ReadonlyMessageAttributesType; lastSelectedMessage: MessageTimestamps | undefined; selectedMessageIds: ReadonlyArray | undefined; @@ -771,15 +772,14 @@ type ConversationStoppedByMissingVerificationActionType = ReadonlyDeep<{ untrustedServiceIds: ReadonlyArray; }; }>; -// eslint-disable-next-line local-rules/type-alias-readonlydeep -- FIXME -export type MessageChangedActionType = { +export type MessageChangedActionType = ReadonlyDeep<{ type: typeof MESSAGE_CHANGED; payload: { id: string; conversationId: string; - data: MessageAttributesType; + data: ReadonlyMessageAttributesType; }; -}; +}>; export type MessageDeletedActionType = ReadonlyDeep<{ type: typeof MESSAGE_DELETED; payload: { @@ -802,15 +802,14 @@ export type ShowSpoilerActionType = ReadonlyDeep<{ }; }>; -// eslint-disable-next-line local-rules/type-alias-readonlydeep -- FIXME -export type MessagesAddedActionType = Readonly<{ +export type MessagesAddedActionType = ReadonlyDeep<{ type: 'MESSAGES_ADDED'; payload: { conversationId: string; isActive: boolean; isJustSent: boolean; isNewMessage: boolean; - messages: ReadonlyArray; + messages: ReadonlyArray; }; }>; @@ -833,19 +832,18 @@ export type RepairOldestMessageActionType = ReadonlyDeep<{ conversationId: string; }; }>; -// eslint-disable-next-line local-rules/type-alias-readonlydeep -export type MessagesResetActionType = { +export type MessagesResetActionType = ReadonlyDeep<{ type: 'MESSAGES_RESET'; payload: { conversationId: string; - messages: ReadonlyArray; + messages: ReadonlyArray; metrics: MessageMetricsType; scrollToMessageId?: string; // The set of provided messages should be trusted, even if it conflicts with metrics, // because we weren't looking for a specific time window of messages with our query. unboundedFetch: boolean; }; -}; +}>; export type SetMessageLoadingStateActionType = ReadonlyDeep<{ type: 'SET_MESSAGE_LOADING_STATE'; payload: { @@ -957,8 +955,7 @@ export type ToggleConversationInChooseMembersActionType = ReadonlyDeep<{ }; }>; -// eslint-disable-next-line local-rules/type-alias-readonlydeep -- FIXME -type PushPanelActionType = Readonly<{ +type PushPanelActionType = ReadonlyDeep<{ type: typeof PUSH_PANEL; payload: PanelRenderType; }>; @@ -983,7 +980,7 @@ type ReplaceAvatarsActionType = ReadonlyDeep<{ }; }>; -// eslint-disable-next-line local-rules/type-alias-readonlydeep -- FIXME +// eslint-disable-next-line local-rules/type-alias-readonlydeep export type ConversationActionType = | CancelVerificationDataByConversationActionType | ClearCancelledVerificationActionType @@ -2865,7 +2862,7 @@ function conversationStoppedByMissingVerification(payload: { export function messageChanged( id: string, conversationId: string, - data: MessageAttributesType + data: ReadonlyMessageAttributesType ): MessageChangedActionType { return { type: MESSAGE_CHANGED, @@ -2935,7 +2932,7 @@ function messagesAdded({ isActive: boolean; isJustSent: boolean; isNewMessage: boolean; - messages: ReadonlyArray; + messages: ReadonlyArray; }): ThunkAction { return (dispatch, getState) => { const state = getState(); @@ -2990,14 +2987,13 @@ function reviewConversationNameCollision(): ReviewConversationNameCollisionActio }; } -// eslint-disable-next-line local-rules/type-alias-readonlydeep -export type MessageResetOptionsType = { +export type MessageResetOptionsType = ReadonlyDeep<{ conversationId: string; - messages: ReadonlyArray; + messages: ReadonlyArray; metrics: MessageMetricsType; scrollToMessageId?: string; unboundedFetch?: boolean; -}; +}>; function messagesReset({ conversationId, @@ -4716,7 +4712,7 @@ function maybeUpdateSelectedMessageForDetails( targetedMessageForDetails, }: { messageId: string; - targetedMessageForDetails: MessageAttributesType | undefined; + targetedMessageForDetails: ReadonlyMessageAttributesType | undefined; }, state: ConversationsStateType ): ConversationsStateType { diff --git a/ts/state/ducks/globalModals.ts b/ts/state/ducks/globalModals.ts index c3e9404fb8..5342a416ac 100644 --- a/ts/state/ducks/globalModals.ts +++ b/ts/state/ducks/globalModals.ts @@ -6,7 +6,7 @@ import type { ReadonlyDeep } from 'type-fest'; import type { ExplodePromiseResultType } from '../../util/explodePromise'; import type { GroupV2PendingMemberType, - MessageAttributesType, + ReadonlyMessageAttributesType, } from '../../model-types.d'; import type { MessageChangedActionType, @@ -52,7 +52,7 @@ import { linkCallRoute } from '../../util/signalRoutes'; // State export type EditHistoryMessagesType = ReadonlyDeep< - Array + Array >; export type EditNicknameAndNoteModalPropsType = ReadonlyDeep<{ conversationId: string; @@ -882,7 +882,7 @@ function showShortcutGuideModal(): ShowShortcutGuideModalActionType { } function copyOverMessageAttributesIntoEditHistory( - messageAttributes: ReadonlyDeep + messageAttributes: ReadonlyDeep ): EditHistoryMessagesType | undefined { if (!messageAttributes.editHistory) { return; @@ -934,7 +934,7 @@ function closeEditHistoryModal(): CloseEditHistoryModalActionType { function copyOverMessageAttributesIntoForwardMessages( messageDrafts: ReadonlyArray, - attributes: ReadonlyDeep + attributes: ReadonlyDeep ): ReadonlyArray { return messageDrafts.map(messageDraft => { if (messageDraft.originalMessageId !== attributes.id) { diff --git a/ts/state/ducks/lightbox.ts b/ts/state/ducks/lightbox.ts index db7ac4f82d..b494614a5b 100644 --- a/ts/state/ducks/lightbox.ts +++ b/ts/state/ducks/lightbox.ts @@ -18,7 +18,7 @@ import type { StateType as RootStateType } from '../reducer'; import * as log from '../../logging/log'; import { __DEPRECATED$getMessageById } from '../../messages/getMessageById'; -import type { MessageAttributesType } from '../../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../../model-types.d'; import { isGIF } from '../../types/Attachment'; import { isImageTypeSupported, @@ -245,7 +245,7 @@ function showLightboxForViewOnceMedia( } function filterValidAttachments( - attributes: MessageAttributesType + attributes: ReadonlyMessageAttributesType ): Array { return (attributes.attachments ?? []).filter( item => item.thumbnail && !item.pending && !item.error diff --git a/ts/state/ducks/stories.ts b/ts/state/ducks/stories.ts index 9306eb7fb3..03b2c812af 100644 --- a/ts/state/ducks/stories.ts +++ b/ts/state/ducks/stories.ts @@ -8,7 +8,7 @@ import type { ReadonlyDeep } from 'type-fest'; import * as Errors from '../../types/errors'; import type { AttachmentType } from '../../types/Attachment'; import type { DraftBodyRanges } from '../../types/BodyRange'; -import type { MessageAttributesType } from '../../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../../model-types.d'; import type { MessageChangedActionType, MessageDeletedActionType, @@ -79,7 +79,7 @@ export type StoryDataType = ReadonlyDeep< messageId: string; startedDownload?: boolean; } & Pick< - MessageAttributesType, + ReadonlyMessageAttributesType, | 'bodyRanges' | 'canReplyToStory' | 'conversationId' @@ -124,31 +124,33 @@ export type AddStoryData = ReadonlyDeep< | undefined >; -// eslint-disable-next-line local-rules/type-alias-readonlydeep -export type RecipientsByConversation = Record< - string, // conversationId - { - serviceIds: Array; +export type RecipientEntry = ReadonlyDeep<{ + serviceIds: Array; - byDistributionId?: Record< - StoryDistributionIdString, - { - serviceIds: Array; - } - >; - } + byDistributionId?: Record< + StoryDistributionIdString, + { + serviceIds: Array; + } + >; +}>; + +export type RecipientsByConversation = ReadonlyDeep< + Record< + string, // conversationId + RecipientEntry + > >; // State -// eslint-disable-next-line local-rules/type-alias-readonlydeep -export type StoriesStateType = Readonly<{ +export type StoriesStateType = ReadonlyDeep<{ addStoryData: AddStoryData; hasAllStoriesUnmuted: boolean; lastOpenedAtTimestamp: number | undefined; replyState?: Readonly<{ messageId: string; - replies: Array; + replies: Array; }>; selectedStoryData?: SelectedStoryDataType; sendStoryModalData?: RecipientsByConversation; @@ -188,14 +190,13 @@ type ListMembersVerified = ReadonlyDeep<{ }; }>; -// eslint-disable-next-line local-rules/type-alias-readonlydeep -type LoadStoryRepliesActionType = { +type LoadStoryRepliesActionType = ReadonlyDeep<{ type: typeof LOAD_STORY_REPLIES; payload: { messageId: string; - replies: Array; + replies: Array; }; -}; +}>; type MarkStoryReadActionType = ReadonlyDeep<{ type: typeof MARK_STORY_READ; diff --git a/ts/state/selectors/audioPlayer.ts b/ts/state/selectors/audioPlayer.ts index 08b59bc807..db236ba9a8 100644 --- a/ts/state/selectors/audioPlayer.ts +++ b/ts/state/selectors/audioPlayer.ts @@ -19,7 +19,7 @@ import type { StateType } from '../reducer'; import * as log from '../../logging/log'; import { getLocalAttachmentUrl } from '../../util/getLocalAttachmentUrl'; import type { MessageWithUIFieldsType } from '../ducks/conversations'; -import type { MessageAttributesType } from '../../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../../model-types.d'; import { getMessageIdForLogging } from '../../util/idForLogging'; import * as Attachment from '../../types/Attachment'; import type { ActiveAudioPlayerStateType } from '../ducks/audioPlayer'; @@ -57,7 +57,7 @@ export const selectVoiceNoteTitle = createSelector( (ourNumber, ourAci, ourConversationId, conversationSelector, i18n) => { return ( message: Pick< - MessageAttributesType, + ReadonlyMessageAttributesType, 'type' | 'source' | 'sourceServiceId' > ) => { @@ -75,7 +75,7 @@ export const selectVoiceNoteTitle = createSelector( ); export function extractVoiceNoteForPlayback( - message: MessageAttributesType, + message: ReadonlyMessageAttributesType, ourConversationId: string | undefined ): VoiceNoteForPlayback | undefined { const { type } = message; diff --git a/ts/state/selectors/message.ts b/ts/state/selectors/message.ts index e03e316e89..d99c819159 100644 --- a/ts/state/selectors/message.ts +++ b/ts/state/selectors/message.ts @@ -11,7 +11,7 @@ import type { ReadonlyDeep } from 'type-fest'; import type { StateType } from '../reducer'; import type { LastMessageStatus, - MessageAttributesType, + ReadonlyMessageAttributesType, MessageReactionType, QuotedAttachmentType, ShallowChallengeError, @@ -201,7 +201,7 @@ export function hasErrors( } export function getSource( - message: Pick, + message: Pick, ourNumber: string | undefined ): string | undefined { if (isIncoming(message)) { @@ -233,7 +233,7 @@ export function getSourceDevice( } export function getSourceServiceId( - message: Pick, + message: Pick, ourAci: AciString | undefined ): ServiceIdString | undefined { if (isIncoming(message)) { @@ -1523,13 +1523,13 @@ function getPropsForProfileChange( // Message Request Response Event export function isMessageRequestResponse( - message: MessageAttributesType + message: ReadonlyMessageAttributesType ): boolean { return message.type === 'message-request-response-event'; } function getPropsForMessageRequestResponse( - message: MessageAttributesType + message: ReadonlyMessageAttributesType ): MessageRequestResponseNotificationData { const { messageRequestResponseEvent } = message; if (!messageRequestResponseEvent) { @@ -1805,7 +1805,7 @@ export function getPropsForEmbeddedContact( message: MessageWithUIFieldsType, regionCode: string | undefined, accountSelector: (identifier?: string) => ServiceIdString | undefined -): EmbeddedContactType | undefined { +): ReadonlyDeep | undefined { const contacts = message.contact; if (!contacts || !contacts.length) { return undefined; @@ -2091,7 +2091,7 @@ export function getLastChallengeError( const getTargetedMessageForDetails = ( state: StateType -): MessageAttributesType | undefined => +): ReadonlyMessageAttributesType | undefined => state.conversations.targetedMessageForDetails; const OUTGOING_KEY_ERROR = 'OutgoingIdentityKeyError'; diff --git a/ts/state/smart/EditHistoryMessagesModal.tsx b/ts/state/smart/EditHistoryMessagesModal.tsx index 16d2e21241..43dcd8a618 100644 --- a/ts/state/smart/EditHistoryMessagesModal.tsx +++ b/ts/state/smart/EditHistoryMessagesModal.tsx @@ -3,7 +3,7 @@ import React, { memo, useMemo } from 'react'; import { useSelector } from 'react-redux'; -import type { MessageAttributesType } from '../../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../../model-types.d'; import { EditHistoryMessagesModal } from '../../components/EditHistoryMessagesModal'; import { getIntl, getPlatform } from '../selectors/user'; import { getMessagePropsSelector } from '../selectors/message'; @@ -31,7 +31,9 @@ export const SmartEditHistoryMessagesModal = memo( const editHistoryMessages = useMemo(() => { return messagesAttributes.map(messageAttributes => ({ - ...messagePropsSelector(messageAttributes as MessageAttributesType), + ...messagePropsSelector( + messageAttributes as ReadonlyMessageAttributesType + ), // Make sure the messages don't get an "edited" badge isEditedMessage: false, // Do not show the same reactions in the message history UI diff --git a/ts/test-electron/state/ducks/conversations_test.ts b/ts/test-electron/state/ducks/conversations_test.ts index 3b722a7495..0174ba9e40 100644 --- a/ts/test-electron/state/ducks/conversations_test.ts +++ b/ts/test-electron/state/ducks/conversations_test.ts @@ -5,6 +5,7 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; import { v4 as generateUuid } from 'uuid'; import { times } from 'lodash'; +import type { ReadonlyDeep } from 'type-fest'; import { reducer as rootReducer } from '../../../state/reducer'; import { noopAction } from '../../../state/ducks/noop'; @@ -60,7 +61,7 @@ import { VIEWERS_CHANGED, } from '../../../state/ducks/storyDistributionLists'; import { MY_STORY_ID } from '../../../types/Stories'; -import type { MessageAttributesType } from '../../../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../../../model-types.d'; const { clearGroupCreationError, @@ -92,8 +93,8 @@ const { function messageChanged( messageId: string, conversationId: string, - data: MessageAttributesType -): MessageChangedActionType { + data: ReadonlyMessageAttributesType +): ReadonlyDeep { return { type: 'MESSAGE_CHANGED', payload: { diff --git a/ts/types/EmbeddedContact.ts b/ts/types/EmbeddedContact.ts index 859d69e09d..701a2c9a78 100644 --- a/ts/types/EmbeddedContact.ts +++ b/ts/types/EmbeddedContact.ts @@ -2,9 +2,10 @@ // SPDX-License-Identifier: AGPL-3.0-only import { omit } from 'lodash'; +import type { ReadonlyDeep } from 'type-fest'; import { SignalService as Proto } from '../protobuf'; -import type { MessageAttributesType } from '../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../model-types.d'; import { isNotNil } from '../util/isNotNil'; import { @@ -147,13 +148,13 @@ export function numberToAddressType( } export function embeddedContactSelector( - contact: EmbeddedContactType, + contact: ReadonlyDeep, options: { regionCode?: string; firstNumber?: string; serviceId?: ServiceIdString; } -): EmbeddedContactType { +): ReadonlyDeep { const { firstNumber, serviceId, regionCode } = options; let { avatar } = contact; @@ -189,7 +190,9 @@ export function embeddedContactSelector( }; } -export function getName(contact: EmbeddedContactType): string | undefined { +export function getName( + contact: ReadonlyDeep +): string | undefined { const { name, organization } = contact; const displayName = (name && name.displayName) || undefined; const givenName = (name && name.givenName) || undefined; @@ -206,7 +209,7 @@ export function parseAndWriteAvatar( return async ( contact: EmbeddedContactType, context: { - message: MessageAttributesType; + message: ReadonlyMessageAttributesType; getRegionCode: () => string | undefined; logger: LoggerType; writeNewAttachmentData: ( @@ -280,7 +283,7 @@ function parseContact( return result; } -function idForLogging(message: MessageAttributesType): string { +function idForLogging(message: ReadonlyMessageAttributesType): string { return `${message.source}.${message.sourceDevice} ${message.sent_at}`; } diff --git a/ts/types/ForwardDraft.ts b/ts/types/ForwardDraft.ts index 06348df898..6b8b70f051 100644 --- a/ts/types/ForwardDraft.ts +++ b/ts/types/ForwardDraft.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import { orderBy } from 'lodash'; -import type { MessageAttributesType } from '../model-types'; +import type { ReadonlyMessageAttributesType } from '../model-types'; import { isVoiceMessage, type AttachmentType, @@ -23,7 +23,7 @@ export type MessageForwardDraft = Readonly<{ export type ForwardMessageData = Readonly<{ // only null for new messages - originalMessage: MessageAttributesType | null; + originalMessage: ReadonlyMessageAttributesType | null; draft: MessageForwardDraft; }>; @@ -72,7 +72,7 @@ export function sortByMessageOrder( items: ReadonlyArray, getMesssage: ( item: T - ) => Pick | null + ) => Pick | null ): Array { return orderBy( items, diff --git a/ts/types/MediaItem.ts b/ts/types/MediaItem.ts index ecbf3e9091..15d8438f51 100644 --- a/ts/types/MediaItem.ts +++ b/ts/types/MediaItem.ts @@ -1,12 +1,12 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { MessageAttributesType } from '../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../model-types.d'; import type { AttachmentType } from './Attachment'; import type { MIMEType } from './MIME'; export type MediaItemMessageType = Pick< - MessageAttributesType, + ReadonlyMessageAttributesType, | 'attachments' | 'conversationId' | 'id' diff --git a/ts/types/Message2.ts b/ts/types/Message2.ts index 9616eceffe..27cd8d0eae 100644 --- a/ts/types/Message2.ts +++ b/ts/types/Message2.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import { isFunction, isObject } from 'lodash'; +import type { ReadonlyDeep } from 'type-fest'; import * as Contact from './EmbeddedContact'; import type { @@ -43,6 +44,7 @@ import { AttachmentDisposition, } from '../util/getLocalAttachmentUrl'; import { encryptLegacyAttachment } from '../util/encryptLegacyAttachment'; +import { deepClone } from '../util/deepClone'; export { hasExpiration } from './Message'; @@ -831,14 +833,14 @@ export const loadQuoteData = ( export const loadContactData = ( loadAttachmentData: LoadAttachmentType ): (( - contact: Array | undefined + contact: ReadonlyArray> | undefined ) => Promise | undefined>) => { if (!isFunction(loadAttachmentData)) { throw new TypeError('loadContactData: loadAttachmentData is required'); } return async ( - contact: Array | undefined + contact: ReadonlyArray> | undefined ): Promise | undefined> => { if (!contact) { return undefined; @@ -847,27 +849,23 @@ export const loadContactData = ( return Promise.all( contact.map( async ( - item: EmbeddedContactType + item: ReadonlyDeep ): Promise => { - if ( - !item || - !item.avatar || - !item.avatar.avatar || - !item.avatar.avatar.path - ) { + const copy = deepClone(item); + if (!copy?.avatar?.avatar?.path) { return { - ...item, + ...copy, avatar: undefined, }; } return { - ...item, + ...copy, avatar: { - ...item.avatar, + ...copy.avatar, avatar: { - ...item.avatar.avatar, - ...(await loadAttachmentData(item.avatar.avatar)), + ...copy.avatar.avatar, + ...(await loadAttachmentData(copy.avatar.avatar)), }, }, }; @@ -880,13 +878,15 @@ export const loadContactData = ( export const loadPreviewData = ( loadAttachmentData: LoadAttachmentType ): (( - preview: Array | undefined + preview: ReadonlyArray> | undefined ) => Promise>) => { if (!isFunction(loadAttachmentData)) { throw new TypeError('loadPreviewData: loadAttachmentData is required'); } - return async (preview: Array | undefined) => { + return async ( + preview: ReadonlyArray> | undefined + ) => { if (!preview || !preview.length) { return []; } @@ -894,17 +894,19 @@ export const loadPreviewData = ( return Promise.all( preview.map( async (item: LinkPreviewType): Promise => { - if (!item.image) { + const copy = deepClone(item); + + if (!copy.image) { return { - ...item, + ...copy, // Pacify typescript image: undefined, }; } return { - ...item, - image: await loadAttachmentData(item.image), + ...copy, + image: await loadAttachmentData(copy.image), }; } ) diff --git a/ts/types/Panels.ts b/ts/types/Panels.ts index 42d4c007c1..b5337d6177 100644 --- a/ts/types/Panels.ts +++ b/ts/types/Panels.ts @@ -1,8 +1,10 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import type { ReadonlyDeep } from 'type-fest'; + import type { EmbeddedContactType } from './EmbeddedContact'; -import type { MessageAttributesType } from '../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../model-types.d'; import type { ServiceIdString } from './ServiceId'; export enum PanelType { @@ -19,7 +21,7 @@ export enum PanelType { StickerManager = 'StickerManager', } -export type PanelRequestType = +export type PanelRequestType = ReadonlyDeep< | { type: PanelType.AllMedia } | { type: PanelType.ChatColorEditor } | { @@ -39,9 +41,10 @@ export type PanelRequestType = | { type: PanelType.GroupV1Members } | { type: PanelType.MessageDetails; args: { messageId: string } } | { type: PanelType.NotificationSettings } - | { type: PanelType.StickerManager }; + | { type: PanelType.StickerManager } +>; -export type PanelRenderType = +export type PanelRenderType = ReadonlyDeep< | { type: PanelType.AllMedia } | { type: PanelType.ChatColorEditor } | { @@ -59,6 +62,10 @@ export type PanelRenderType = | { type: PanelType.GroupLinkManagement } | { type: PanelType.GroupPermissions } | { type: PanelType.GroupV1Members } - | { type: PanelType.MessageDetails; args: { message: MessageAttributesType } } + | { + type: PanelType.MessageDetails; + args: { message: ReadonlyMessageAttributesType }; + } | { type: PanelType.NotificationSettings } - | { type: PanelType.StickerManager }; + | { type: PanelType.StickerManager } +>; diff --git a/ts/types/Payment.ts b/ts/types/Payment.ts index da3d7a773a..3924f79b9c 100644 --- a/ts/types/Payment.ts +++ b/ts/types/Payment.ts @@ -9,7 +9,7 @@ export enum PaymentEventKind { // Cancellation = 5, -- disabled } -export type PaymentNotificationEvent = { +export type PaymentNotificationEvent = Readonly<{ kind: PaymentEventKind.Notification; note: string | null; @@ -17,15 +17,15 @@ export type PaymentNotificationEvent = { transactionDetailsBase64?: string; amountMob?: string; feeMob?: string; -}; +}>; -export type PaymentActivationRequestEvent = { +export type PaymentActivationRequestEvent = Readonly<{ kind: PaymentEventKind.ActivationRequest; -}; +}>; -export type PaymentActivatedEvent = { +export type PaymentActivatedEvent = Readonly<{ kind: PaymentEventKind.Activation; -}; +}>; export type AnyPaymentEvent = | PaymentNotificationEvent diff --git a/ts/util/blockSendUntilConversationsAreVerified.ts b/ts/util/blockSendUntilConversationsAreVerified.ts index 3c17ddd501..6ae4de180a 100644 --- a/ts/util/blockSendUntilConversationsAreVerified.ts +++ b/ts/util/blockSendUntilConversationsAreVerified.ts @@ -4,7 +4,10 @@ import type { SafetyNumberChangeSource } from '../components/SafetyNumberChangeDialog'; import * as log from '../logging/log'; import { explodePromise } from './explodePromise'; -import type { RecipientsByConversation } from '../state/ducks/stories'; +import type { + RecipientsByConversation, + RecipientEntry, +} from '../state/ducks/stories'; import { isNotNil } from './isNotNil'; import type { ServiceIdString } from '../types/ServiceId'; import { waitForAll } from './waitForAll'; @@ -105,7 +108,7 @@ export function filterServiceIds( byConversation: RecipientsByConversation, predicate: (serviceId: ServiceIdString) => boolean ): RecipientsByConversation { - const filteredByConversation: RecipientsByConversation = {}; + const filteredByConversation: Record = {}; Object.entries(byConversation).forEach( ([conversationId, conversationData]) => { const conversationFiltered = conversationData.serviceIds diff --git a/ts/util/canEditMessage.ts b/ts/util/canEditMessage.ts index c9b0c023d3..47137f9fd2 100644 --- a/ts/util/canEditMessage.ts +++ b/ts/util/canEditMessage.ts @@ -1,14 +1,16 @@ // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { MessageAttributesType } from '../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../model-types.d'; import { DAY } from './durations'; import { isMoreRecentThan } from './timestamp'; import { isOutgoing } from '../messages/helpers'; export const MESSAGE_MAX_EDIT_COUNT = 10; -export function canEditMessage(message: MessageAttributesType): boolean { +export function canEditMessage( + message: ReadonlyMessageAttributesType +): boolean { const result = !message.deletedForEveryone && isOutgoing(message) && @@ -29,6 +31,8 @@ export function canEditMessage(message: MessageAttributesType): boolean { return false; } -export function isWithinMaxEdits(message: MessageAttributesType): boolean { +export function isWithinMaxEdits( + message: ReadonlyMessageAttributesType +): boolean { return (message.editHistory?.length ?? 0) <= MESSAGE_MAX_EDIT_COUNT; } diff --git a/ts/util/deepClone.ts b/ts/util/deepClone.ts new file mode 100644 index 0000000000..05c66b4a59 --- /dev/null +++ b/ts/util/deepClone.ts @@ -0,0 +1,11 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +import type { WritableDeep } from 'type-fest'; + +/** + * Takes a readonly object and returns a writable deep clone of it. + * @see https://developer.mozilla.org/en-US/docs/Web/API/structuredClone + */ +export function deepClone(value: T): WritableDeep { + return structuredClone(value) as WritableDeep; +} diff --git a/ts/util/editHelpers.ts b/ts/util/editHelpers.ts index 3b9a169ece..3e60f3cb70 100644 --- a/ts/util/editHelpers.ts +++ b/ts/util/editHelpers.ts @@ -2,10 +2,15 @@ // SPDX-License-Identifier: AGPL-3.0-only import { isNumber, sortBy } from 'lodash'; +import type { ReadonlyDeep } from 'type-fest'; import { strictAssert } from './assert'; -import type { EditHistoryType, MessageAttributesType } from '../model-types'; +import type { + EditHistoryType, + MessageAttributesType, + ReadonlyMessageAttributesType, +} from '../model-types'; import type { LoggerType } from '../types/Logging'; // The tricky bit for this function is if we are on our second+ attempt to send a given @@ -14,7 +19,7 @@ export function getTargetOfThisEditTimestamp({ message, targetTimestamp, }: { - message: MessageAttributesType; + message: ReadonlyMessageAttributesType; targetTimestamp: number; }): number { const { timestamp: originalTimestamp, editHistory } = message; @@ -27,7 +32,7 @@ export function getTargetOfThisEditTimestamp({ }); const mostRecent = sortBy( sentItems, - (item: EditHistoryType) => item.timestamp + (item: ReadonlyDeep) => item.timestamp ); const { length } = mostRecent; diff --git a/ts/util/findStoryMessage.ts b/ts/util/findStoryMessage.ts index b7b6148e3e..2c19a4e744 100644 --- a/ts/util/findStoryMessage.ts +++ b/ts/util/findStoryMessage.ts @@ -1,7 +1,7 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { MessageAttributesType } from '../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../model-types.d'; import type { MessageModel } from '../models/messages'; import type { SignalService as Proto } from '../protobuf'; import type { AciString } from '../types/ServiceId'; @@ -72,12 +72,12 @@ export async function findStoryMessages( } function isStoryAMatch( - message: MessageAttributesType | null | undefined, + message: ReadonlyMessageAttributesType | null | undefined, conversationId: string, ourConversationId: string, authorAci: AciString, sentTimestamp: number -): message is MessageAttributesType { +): message is ReadonlyMessageAttributesType { if (!message) { return false; } diff --git a/ts/util/getMessageAuthorText.ts b/ts/util/getMessageAuthorText.ts index 7e380c76d1..95db2068d1 100644 --- a/ts/util/getMessageAuthorText.ts +++ b/ts/util/getMessageAuthorText.ts @@ -3,13 +3,13 @@ import type { ConversationAttributesType, - MessageAttributesType, + ReadonlyMessageAttributesType, } from '../model-types.d'; import { isIncoming, isOutgoing } from '../state/selectors/message'; import { getTitle } from './getTitle'; function getIncomingContact( - messageAttributes: MessageAttributesType + messageAttributes: ReadonlyMessageAttributesType ): ConversationAttributesType | undefined { if (!isIncoming(messageAttributes)) { return undefined; @@ -24,7 +24,7 @@ function getIncomingContact( } export function getMessageAuthorText( - messageAttributes?: MessageAttributesType + messageAttributes?: ReadonlyMessageAttributesType ): string | undefined { if (!messageAttributes) { return undefined; diff --git a/ts/util/getMessageConversation.ts b/ts/util/getMessageConversation.ts index 5693d440b4..d1db1019df 100644 --- a/ts/util/getMessageConversation.ts +++ b/ts/util/getMessageConversation.ts @@ -1,12 +1,12 @@ // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { MessageAttributesType } from '../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../model-types.d'; import type { ConversationModel } from '../models/conversations'; export function getMessageConversation({ conversationId, -}: Pick): +}: Pick): | ConversationModel | undefined { return window.ConversationController.get(conversationId); diff --git a/ts/util/getMessageSentTimestamp.ts b/ts/util/getMessageSentTimestamp.ts index a6aaf3827b..3ced8ede89 100644 --- a/ts/util/getMessageSentTimestamp.ts +++ b/ts/util/getMessageSentTimestamp.ts @@ -1,7 +1,7 @@ // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { MessageAttributesType } from '../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../model-types.d'; import type { LoggerType } from '../types/Logging'; import { assertDev } from './assert'; @@ -16,7 +16,7 @@ export function getMessageSentTimestamp( sent_at: sentAt, timestamp, }: Pick< - MessageAttributesType, + ReadonlyMessageAttributesType, 'editMessageTimestamp' | 'sent_at' | 'timestamp' >, { includeEdits = true, log }: GetMessageSentTimestampOptionsType diff --git a/ts/util/getMessageSentTimestampSet.ts b/ts/util/getMessageSentTimestampSet.ts index 0d8dc59a78..321fc7d0e9 100644 --- a/ts/util/getMessageSentTimestampSet.ts +++ b/ts/util/getMessageSentTimestampSet.ts @@ -1,13 +1,13 @@ // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { MessageAttributesType } from '../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../model-types.d'; export function getMessageSentTimestampSet({ sent_at: sentAt, editHistory, }: Pick< - MessageAttributesType, + ReadonlyMessageAttributesType, 'sent_at' | 'editHistory' >): ReadonlySet { return new Set([ diff --git a/ts/util/getMessageTimestamp.ts b/ts/util/getMessageTimestamp.ts index 95c87d719e..7aa876c6c3 100644 --- a/ts/util/getMessageTimestamp.ts +++ b/ts/util/getMessageTimestamp.ts @@ -1,10 +1,10 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { MessageAttributesType } from '../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../model-types.d'; export function getMessageTimestamp( - message: Pick + message: Pick ): number { return message.received_at_ms || message.received_at; } diff --git a/ts/util/getNotificationDataForMessage.ts b/ts/util/getNotificationDataForMessage.ts index e6f5416045..7e197287b4 100644 --- a/ts/util/getNotificationDataForMessage.ts +++ b/ts/util/getNotificationDataForMessage.ts @@ -1,8 +1,10 @@ // Copyright 2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import type { ReadonlyDeep } from 'type-fest'; + import type { RawBodyRange } from '../types/BodyRange'; -import type { MessageAttributesType } from '../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../model-types.d'; import type { ICUStringMessageParamsByKeyType } from '../types/Util'; import * as Attachment from '../types/Attachment'; import * as EmbeddedContact from '../types/EmbeddedContact'; @@ -64,9 +66,9 @@ function getNameForNumber(e164: string): string { } export function getNotificationDataForMessage( - attributes: MessageAttributesType + attributes: ReadonlyMessageAttributesType ): { - bodyRanges?: ReadonlyArray; + bodyRanges?: ReadonlyArray>; emoji?: string; text: string; } { diff --git a/ts/util/getNotificationTextForMessage.ts b/ts/util/getNotificationTextForMessage.ts index ccc7b52321..cca536d487 100644 --- a/ts/util/getNotificationTextForMessage.ts +++ b/ts/util/getNotificationTextForMessage.ts @@ -1,7 +1,7 @@ // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { MessageAttributesType } from '../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../model-types.d'; import { applyRangesToText, hydrateRanges } from '../types/BodyRange'; import { findAndFormatContact } from './findAndFormatContact'; import { getNotificationDataForMessage } from './getNotificationDataForMessage'; @@ -9,7 +9,7 @@ import { isConversationAccepted } from './isConversationAccepted'; import { strictAssert } from './assert'; export function getNotificationTextForMessage( - attributes: MessageAttributesType + attributes: ReadonlyMessageAttributesType ): string { const { text, emoji, bodyRanges } = getNotificationDataForMessage(attributes); diff --git a/ts/util/getQuoteBodyText.ts b/ts/util/getQuoteBodyText.ts index e262feafc6..7b9db2a13a 100644 --- a/ts/util/getQuoteBodyText.ts +++ b/ts/util/getQuoteBodyText.ts @@ -1,11 +1,11 @@ // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { MessageAttributesType } from '../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../model-types.d'; import * as EmbeddedContact from '../types/EmbeddedContact'; export function getQuoteBodyText( - messageAttributes: MessageAttributesType, + messageAttributes: ReadonlyMessageAttributesType, id: number ): string | undefined { const storyReactionEmoji = messageAttributes.storyReaction?.emoji; diff --git a/ts/util/getRecipientsByConversation.ts b/ts/util/getRecipientsByConversation.ts index 64e7cf7024..46d80d789e 100644 --- a/ts/util/getRecipientsByConversation.ts +++ b/ts/util/getRecipientsByConversation.ts @@ -3,6 +3,7 @@ import type { ConversationAttributesType } from '../model-types'; import type { RecipientsByConversation } from '../state/ducks/stories'; +import type { ServiceIdString } from '../types/ServiceId'; import { getConversationMembers } from './getConversationMembers'; import { isNotNil } from './isNotNil'; @@ -10,7 +11,12 @@ import { isNotNil } from './isNotNil'; export function getRecipientsByConversation( conversations: Array ): RecipientsByConversation { - const recipientsByConversation: RecipientsByConversation = {}; + const recipientsByConversation: Record< + string, + { + serviceIds: Array; + } + > = {}; conversations.forEach(attributes => { recipientsByConversation[attributes.id] = { diff --git a/ts/util/getSenderIdentifier.ts b/ts/util/getSenderIdentifier.ts index 8cc9abefb6..89171297cc 100644 --- a/ts/util/getSenderIdentifier.ts +++ b/ts/util/getSenderIdentifier.ts @@ -1,7 +1,7 @@ // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { MessageAttributesType } from '../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../model-types.d'; export function getSenderIdentifier({ sent_at: sentAt, @@ -9,7 +9,7 @@ export function getSenderIdentifier({ sourceServiceId, sourceDevice, }: Pick< - MessageAttributesType, + ReadonlyMessageAttributesType, 'sent_at' | 'source' | 'sourceServiceId' | 'sourceDevice' >): string { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion diff --git a/ts/util/hasAttachmentDownloads.ts b/ts/util/hasAttachmentDownloads.ts index 3398a0dbed..733bd98e8e 100644 --- a/ts/util/hasAttachmentDownloads.ts +++ b/ts/util/hasAttachmentDownloads.ts @@ -2,13 +2,13 @@ // SPDX-License-Identifier: AGPL-3.0-only import { partition } from 'lodash'; -import type { MessageAttributesType } from '../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../model-types.d'; import { isLongMessage } from '../types/MIME'; // NOTE: If you're modifying this function then you'll likely also need // to modify ./queueAttachmentDownloads export function hasAttachmentDownloads( - message: MessageAttributesType + message: ReadonlyMessageAttributesType ): boolean { const attachments = message.attachments || []; @@ -83,7 +83,7 @@ export function hasAttachmentDownloads( } function hasPreviewDownloads( - previews: MessageAttributesType['preview'] + previews: ReadonlyMessageAttributesType['preview'] ): boolean { return (previews || []).some(item => { if (!item.image) { @@ -98,7 +98,7 @@ function hasPreviewDownloads( } function hasNormalAttachmentDownloads( - attachments: MessageAttributesType['attachments'] + attachments: ReadonlyMessageAttributesType['attachments'] ): boolean { return (attachments || []).some(attachment => { if (!attachment) { diff --git a/ts/util/idForLogging.ts b/ts/util/idForLogging.ts index 91a67f3ce2..c3844304cd 100644 --- a/ts/util/idForLogging.ts +++ b/ts/util/idForLogging.ts @@ -3,7 +3,7 @@ import type { ConversationAttributesType, - MessageAttributesType, + ReadonlyMessageAttributesType, } from '../model-types.d'; import { getSource, @@ -16,7 +16,7 @@ import type { ConversationType } from '../state/ducks/conversations'; export function getMessageIdForLogging( message: Pick< - MessageAttributesType, + ReadonlyMessageAttributesType, 'type' | 'sourceServiceId' | 'sourceDevice' | 'sent_at' > ): string { diff --git a/ts/util/isMessageUnread.ts b/ts/util/isMessageUnread.ts index 641328e5b5..ae2d56502d 100644 --- a/ts/util/isMessageUnread.ts +++ b/ts/util/isMessageUnread.ts @@ -2,8 +2,8 @@ // SPDX-License-Identifier: AGPL-3.0-only import { ReadStatus } from '../messages/MessageReadStatus'; -import type { MessageAttributesType } from '../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../model-types.d'; export const isMessageUnread = ( - message: Readonly> + message: Pick ): boolean => message.readStatus === ReadStatus.Unread; diff --git a/ts/util/isTooOldToModifyMessage.ts b/ts/util/isTooOldToModifyMessage.ts index 5e7db06f0f..7650f459a6 100644 --- a/ts/util/isTooOldToModifyMessage.ts +++ b/ts/util/isTooOldToModifyMessage.ts @@ -1,12 +1,12 @@ // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { MessageAttributesType } from '../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../model-types.d'; import { DAY } from './durations'; export function isTooOldToModifyMessage( serverTimestamp: number, - message: MessageAttributesType + message: Pick ): boolean { const messageTimestamp = message.serverTimestamp || message.sent_at || 0; const delta = Math.abs(serverTimestamp - messageTimestamp); diff --git a/ts/util/messageBatcher.ts b/ts/util/messageBatcher.ts index 15e28859e4..51ec5fc38d 100644 --- a/ts/util/messageBatcher.ts +++ b/ts/util/messageBatcher.ts @@ -1,17 +1,17 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { MessageAttributesType } from '../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../model-types.d'; import { createBatcher } from './batcher'; import { createWaitBatcher } from './waitBatcher'; import { DataWriter } from '../sql/Client'; import * as log from '../logging/log'; -const updateMessageBatcher = createBatcher({ +const updateMessageBatcher = createBatcher({ name: 'messageBatcher.updateMessageBatcher', wait: 75, maxSize: 50, - processBatch: async (messageAttrs: Array) => { + processBatch: async (messageAttrs: Array) => { log.info('updateMessageBatcher', messageAttrs.length); // Grab the latest from the cache in case they've changed @@ -27,7 +27,9 @@ const updateMessageBatcher = createBatcher({ let shouldBatch = true; -export function queueUpdateMessage(messageAttr: MessageAttributesType): void { +export function queueUpdateMessage( + messageAttr: ReadonlyMessageAttributesType +): void { if (shouldBatch) { updateMessageBatcher.add(messageAttr); } else { @@ -41,21 +43,24 @@ export function setBatchingStrategy(keepBatching = false): void { shouldBatch = keepBatching; } -export const saveNewMessageBatcher = createWaitBatcher({ - name: 'messageBatcher.saveNewMessageBatcher', - wait: 75, - maxSize: 30, - processBatch: async (messageAttrs: Array) => { - log.info('saveNewMessageBatcher', messageAttrs.length); +export const saveNewMessageBatcher = + createWaitBatcher({ + name: 'messageBatcher.saveNewMessageBatcher', + wait: 75, + maxSize: 30, + processBatch: async ( + messageAttrs: Array + ) => { + log.info('saveNewMessageBatcher', messageAttrs.length); - // Grab the latest from the cache in case they've changed - const messagesToSave = messageAttrs.map( - message => window.MessageCache.accessAttributes(message.id) ?? message - ); + // Grab the latest from the cache in case they've changed + const messagesToSave = messageAttrs.map( + message => window.MessageCache.accessAttributes(message.id) ?? message + ); - await DataWriter.saveMessages(messagesToSave, { - forceSave: true, - ourAci: window.textsecure.storage.user.getCheckedAci(), - }); - }, -}); + await DataWriter.saveMessages(messagesToSave, { + forceSave: true, + ourAci: window.textsecure.storage.user.getCheckedAci(), + }); + }, + }); diff --git a/ts/util/shouldReplyNotifyUser.ts b/ts/util/shouldReplyNotifyUser.ts index 3f511ab449..305c0c86ba 100644 --- a/ts/util/shouldReplyNotifyUser.ts +++ b/ts/util/shouldReplyNotifyUser.ts @@ -2,15 +2,16 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { ConversationModel } from '../models/conversations'; -import type { MessageAttributesType } from '../model-types.d'; +import type { ReadonlyMessageAttributesType } from '../model-types.d'; import * as log from '../logging/log'; import { DataReader } from '../sql/Client'; import { isGroup } from './whatTypeOfConversation'; import { isMessageUnread } from './isMessageUnread'; export async function shouldReplyNotifyUser( - messageAttributes: Readonly< - Pick + messageAttributes: Pick< + ReadonlyMessageAttributesType, + 'readStatus' | 'storyId' >, conversation: ConversationModel ): Promise {