Make most message attribute uses readonly

Co-authored-by: Jamie Kyle <jamie@signal.org>
This commit is contained in:
Fedor Indutny 2024-07-24 13:14:11 -07:00 committed by GitHub
parent c619a7b6fd
commit 3555ccc629
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 342 additions and 258 deletions

21
package-lock.json generated
View file

@ -105,7 +105,7 @@
"sanitize.css": "11.0.0", "sanitize.css": "11.0.0",
"semver": "5.7.2", "semver": "5.7.2",
"split2": "4.0.0", "split2": "4.0.0",
"type-fest": "3.5.0", "type-fest": "4.23.0",
"urlpattern-polyfill": "9.0.0", "urlpattern-polyfill": "9.0.0",
"uuid": "3.3.2", "uuid": "3.3.2",
"uuid-browser": "3.1.0", "uuid-browser": "3.1.0",
@ -7184,6 +7184,17 @@
"uuid": "^8.3.0" "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": { "node_modules/@signalapp/libsignal-client/node_modules/uuid": {
"version": "8.3.2", "version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
@ -34835,11 +34846,11 @@
} }
}, },
"node_modules/type-fest": { "node_modules/type-fest": {
"version": "3.5.0", "version": "4.23.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.5.0.tgz", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.23.0.tgz",
"integrity": "sha512-bI3zRmZC8K0tUz1HjbIOAGQwR2CoPQG68N5IF7gm0LBl8QSNXzkmaWnkWccCUL5uG9mCsp4sBwC8SBrNSISWew==", "integrity": "sha512-ZiBujro2ohr5+Z/hZWHESLz3g08BBdrdLMieYFULJO+tWc437sn8kQsWLJoZErY8alNhxre9K4p3GURAG11n+w==",
"engines": { "engines": {
"node": ">=14.16" "node": ">=16"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"

View file

@ -187,7 +187,7 @@
"sanitize.css": "11.0.0", "sanitize.css": "11.0.0",
"semver": "5.7.2", "semver": "5.7.2",
"split2": "4.0.0", "split2": "4.0.0",
"type-fest": "3.5.0", "type-fest": "4.23.0",
"urlpattern-polyfill": "9.0.0", "urlpattern-polyfill": "9.0.0",
"uuid": "3.3.2", "uuid": "3.3.2",
"uuid-browser": "3.1.0", "uuid-browser": "3.1.0",

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import React from 'react'; import React from 'react';
import type { ReadonlyDeep } from 'type-fest';
import type { import type {
EmbeddedContactType, EmbeddedContactType,
@ -21,7 +22,7 @@ import {
import type { LocalizerType } from '../../types/Util'; import type { LocalizerType } from '../../types/Util';
export type Props = { export type Props = {
contact: EmbeddedContactType; contact: ReadonlyDeep<EmbeddedContactType>;
hasSignalAccount: boolean; hasSignalAccount: boolean;
i18n: LocalizerType; i18n: LocalizerType;
onSendMessage: () => void; onSendMessage: () => void;

View file

@ -3,6 +3,7 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import type { ReadonlyDeep } from 'type-fest';
import type { EmbeddedContactType } from '../../types/EmbeddedContact'; import type { EmbeddedContactType } from '../../types/EmbeddedContact';
@ -14,7 +15,7 @@ import {
} from './contactUtil'; } from './contactUtil';
export type Props = { export type Props = {
contact: EmbeddedContactType; contact: ReadonlyDeep<EmbeddedContactType>;
i18n: LocalizerType; i18n: LocalizerType;
isIncoming: boolean; isIncoming: boolean;
withContentAbove: boolean; withContentAbove: boolean;

View file

@ -4,6 +4,7 @@
import type { ReactElement, ReactNode } from 'react'; import type { ReactElement, ReactNode } from 'react';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { get } from 'lodash'; import { get } from 'lodash';
import type { ReadonlyDeep } from 'type-fest';
import * as log from '../../logging/log'; import * as log from '../../logging/log';
import { I18n } from '../I18n'; import { I18n } from '../I18n';
@ -27,7 +28,7 @@ import { renderChange } from '../../groupChange';
import { Modal } from '../Modal'; import { Modal } from '../Modal';
import { ConfirmationDialog } from '../ConfirmationDialog'; import { ConfirmationDialog } from '../ConfirmationDialog';
export type PropsDataType = { export type PropsDataType = ReadonlyDeep<{
areWeAdmin: boolean; areWeAdmin: boolean;
change: GroupV2ChangeType; change: GroupV2ChangeType;
conversationId: string; conversationId: string;
@ -39,7 +40,7 @@ export type PropsDataType = {
groupName?: string; groupName?: string;
ourAci: AciString | undefined; ourAci: AciString | undefined;
ourPni: PniString | undefined; ourPni: PniString | undefined;
}; }>;
export type PropsActionsType = { export type PropsActionsType = {
blockGroupLinkRequests: ( blockGroupLinkRequests: (

View file

@ -16,6 +16,7 @@ import getDirection from 'direction';
import { drop, groupBy, noop, orderBy, take, unescape } from 'lodash'; import { drop, groupBy, noop, orderBy, take, unescape } from 'lodash';
import { Manager, Popper, Reference } from 'react-popper'; import { Manager, Popper, Reference } from 'react-popper';
import type { PreventOverflowModifier } from '@popperjs/core/lib/modifiers/preventOverflow'; import type { PreventOverflowModifier } from '@popperjs/core/lib/modifiers/preventOverflow';
import type { ReadonlyDeep } from 'type-fest';
import type { import type {
ConversationType, ConversationType,
@ -229,7 +230,7 @@ export type PropsData = {
timestamp: number; timestamp: number;
receivedAtMS?: number; receivedAtMS?: number;
status?: MessageStatusType; status?: MessageStatusType;
contact?: EmbeddedContactType; contact?: ReadonlyDeep<EmbeddedContactType>;
author: Pick< author: Pick<
ConversationType, ConversationType,
| 'acceptedMessageRequest' | 'acceptedMessageRequest'

View file

@ -3,6 +3,7 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import type { ReadonlyDeep } from 'type-fest';
import { Avatar, AvatarBlur } from '../Avatar'; import { Avatar, AvatarBlur } from '../Avatar';
import { Spinner } from '../Spinner'; import { Spinner } from '../Spinner';
@ -18,7 +19,7 @@ export function renderAvatar({
size, size,
direction, direction,
}: { }: {
contact: EmbeddedContactType; contact: ReadonlyDeep<EmbeddedContactType>;
i18n: LocalizerType; i18n: LocalizerType;
size: 28 | 52 | 80; size: 28 | 52 | 80;
direction?: 'outgoing' | 'incoming'; direction?: 'outgoing' | 'incoming';
@ -65,7 +66,7 @@ export function renderName({
isIncoming, isIncoming,
module, module,
}: { }: {
contact: EmbeddedContactType; contact: ReadonlyDeep<EmbeddedContactType>;
isIncoming: boolean; isIncoming: boolean;
module: string; module: string;
}): JSX.Element { }): JSX.Element {
@ -86,7 +87,7 @@ export function renderContactShorthand({
isIncoming, isIncoming,
module, module,
}: { }: {
contact: EmbeddedContactType; contact: ReadonlyDeep<EmbeddedContactType>;
isIncoming: boolean; isIncoming: boolean;
module: string; module: string;
}): JSX.Element { }): JSX.Element {

View file

@ -1,11 +1,11 @@
// Copyright 2018 Signal Messenger, LLC // Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // 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'; import type { AttachmentType } from '../../../../types/Attachment';
export type ItemClickEvent = { export type ItemClickEvent = {
message: Pick<MessageAttributesType, 'sent_at'>; message: Pick<ReadonlyMessageAttributesType, 'sent_at'>;
attachment: AttachmentType; attachment: AttachmentType;
index: number; index: number;
type: 'media' | 'documents'; type: 'media' | 'documents';

View file

@ -1,6 +1,7 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { ReadonlyDeep } from 'type-fest';
import type { import type {
LocalizerType, LocalizerType,
ICUStringMessageParamsByKeyType, ICUStringMessageParamsByKeyType,
@ -56,7 +57,7 @@ export type RenderChangeResultType<T extends string | JSX.Element> =
>; >;
export function renderChange<T extends string | JSX.Element>( export function renderChange<T extends string | JSX.Element>(
change: GroupV2ChangeType, change: ReadonlyDeep<GroupV2ChangeType>,
options: RenderOptionsType<T> options: RenderOptionsType<T>
): RenderChangeResultType<T> { ): RenderChangeResultType<T> {
const { details, from } = change; const { details, from } = change;
@ -79,7 +80,7 @@ export function renderChange<T extends string | JSX.Element>(
} }
function renderChangeDetail<T extends string | JSX.Element>( function renderChangeDetail<T extends string | JSX.Element>(
detail: GroupV2ChangeDetailType, detail: ReadonlyDeep<GroupV2ChangeDetailType>,
options: RenderOptionsType<T> options: RenderOptionsType<T>
): string | T | ReadonlyArray<string | T> { ): string | T | ReadonlyArray<string | T> {
const { const {

View file

@ -36,10 +36,7 @@ import { copyCdnFields } from '../../util/attachments';
import { LONG_MESSAGE } from '../../types/MIME'; import { LONG_MESSAGE } from '../../types/MIME';
import { LONG_ATTACHMENT_LIMIT } from '../../types/Message'; import { LONG_ATTACHMENT_LIMIT } from '../../types/Message';
import type { RawBodyRange } from '../../types/BodyRange'; import type { RawBodyRange } from '../../types/BodyRange';
import type { import type { EmbeddedContactWithUploadedAvatar } from '../../types/EmbeddedContact';
EmbeddedContactWithHydratedAvatar,
EmbeddedContactWithUploadedAvatar,
} from '../../types/EmbeddedContact';
import type { StoryContextType } from '../../types/Util'; import type { StoryContextType } from '../../types/Util';
import type { LoggerType } from '../../types/Logging'; import type { LoggerType } from '../../types/Logging';
import type { import type {
@ -1058,8 +1055,7 @@ async function uploadMessageContacts(
strictAssert(oldContact, `${logId}: Contacts are gone after upload`); strictAssert(oldContact, `${logId}: Contacts are gone after upload`);
const newContact = oldContact.map((contact, index) => { const newContact = oldContact.map((contact, index) => {
const loaded: EmbeddedContactWithHydratedAvatar | undefined = const loaded = contacts.at(index);
contacts.at(index);
if (!contact.avatar) { if (!contact.avatar) {
strictAssert( strictAssert(
loaded?.avatar === undefined, loaded?.avatar === undefined,

View file

@ -1,11 +1,13 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { ReadonlyDeep } from 'type-fest';
import * as log from '../logging/log'; import * as log from '../logging/log';
import type { ConversationModel } from '../models/conversations'; import type { ConversationModel } from '../models/conversations';
import type { import type {
CustomError, CustomError,
MessageAttributesType, ReadonlyMessageAttributesType,
QuotedAttachmentType, QuotedAttachmentType,
QuotedMessageType, QuotedMessageType,
} from '../model-types.d'; } from '../model-types.d';
@ -16,33 +18,36 @@ import type { LocalizerType } from '../types/Util';
import { missingCaseError } from '../util/missingCaseError'; import { missingCaseError } from '../util/missingCaseError';
export function isIncoming( export function isIncoming(
message: Pick<MessageAttributesType, 'type'> message: Pick<ReadonlyMessageAttributesType, 'type'>
): boolean { ): boolean {
return message.type === 'incoming'; return message.type === 'incoming';
} }
export function isOutgoing( export function isOutgoing(
message: Pick<MessageAttributesType, 'type'> message: Pick<ReadonlyMessageAttributesType, 'type'>
): boolean { ): boolean {
return message.type === 'outgoing'; return message.type === 'outgoing';
} }
export function isStory(message: Pick<MessageAttributesType, 'type'>): boolean { export function isStory(
message: Pick<ReadonlyMessageAttributesType, 'type'>
): boolean {
return message.type === 'story'; return message.type === 'story';
} }
export type MessageAttributesWithPaymentEvent = MessageAttributesType & { export type MessageAttributesWithPaymentEvent = ReadonlyMessageAttributesType &
payment: AnyPaymentEvent; ReadonlyDeep<{
}; payment: AnyPaymentEvent;
}>;
export function messageHasPaymentEvent( export function messageHasPaymentEvent(
message: MessageAttributesType message: ReadonlyMessageAttributesType
): message is MessageAttributesWithPaymentEvent { ): message is MessageAttributesWithPaymentEvent {
return message.payment != null; return message.payment != null;
} }
export function getPaymentEventNotificationText( export function getPaymentEventNotificationText(
payment: AnyPaymentEvent, payment: ReadonlyDeep<AnyPaymentEvent>,
senderTitle: string, senderTitle: string,
conversationTitle: string | null, conversationTitle: string | null,
senderIsMe: boolean, senderIsMe: boolean,
@ -61,7 +66,7 @@ export function getPaymentEventNotificationText(
} }
export function getPaymentEventDescription( export function getPaymentEventDescription(
payment: AnyPaymentEvent, payment: ReadonlyDeep<AnyPaymentEvent>,
senderTitle: string, senderTitle: string,
conversationTitle: string | null, conversationTitle: string | null,
senderIsMe: boolean, senderIsMe: boolean,
@ -110,10 +115,10 @@ export function getPaymentEventDescription(
} }
export function isQuoteAMatch( export function isQuoteAMatch(
message: MessageAttributesType | null | undefined, message: ReadonlyMessageAttributesType | null | undefined,
conversationId: string, conversationId: string,
quote: Pick<QuotedMessageType, 'id' | 'authorAci' | 'author'> quote: ReadonlyDeep<Pick<QuotedMessageType, 'id' | 'authorAci' | 'author'>>
): message is MessageAttributesType { ): message is ReadonlyMessageAttributesType {
if (!message) { if (!message) {
return false; return false;
} }
@ -142,7 +147,7 @@ export const shouldTryToCopyFromQuotedMessage = ({
quoteAttachment, quoteAttachment,
}: { }: {
referencedMessageNotFound: boolean; referencedMessageNotFound: boolean;
quoteAttachment: QuotedAttachmentType | undefined; quoteAttachment: ReadonlyDeep<QuotedAttachmentType> | undefined;
}): boolean => { }): boolean => {
// If we've tried and can't find the message, try again. // If we've tried and can't find the message, try again.
if (referencedMessageNotFound === true) { if (referencedMessageNotFound === true) {
@ -163,7 +168,10 @@ export const shouldTryToCopyFromQuotedMessage = ({
}; };
export function getAuthorId( export function getAuthorId(
message: Pick<MessageAttributesType, 'type' | 'source' | 'sourceServiceId'> message: Pick<
ReadonlyMessageAttributesType,
'type' | 'source' | 'sourceServiceId'
>
): string | undefined { ): string | undefined {
const source = getSource(message); const source = getSource(message);
const sourceServiceId = getSourceServiceId(message); const sourceServiceId = getSourceServiceId(message);
@ -181,14 +189,14 @@ export function getAuthorId(
} }
export function getAuthor( export function getAuthor(
message: MessageAttributesType message: ReadonlyMessageAttributesType
): ConversationModel | undefined { ): ConversationModel | undefined {
const id = getAuthorId(message); const id = getAuthorId(message);
return window.ConversationController.get(id); return window.ConversationController.get(id);
} }
export function getSource( export function getSource(
message: Pick<MessageAttributesType, 'type' | 'source'> message: Pick<ReadonlyMessageAttributesType, 'type' | 'source'>
): string | undefined { ): string | undefined {
if (isIncoming(message) || isStory(message)) { if (isIncoming(message) || isStory(message)) {
return message.source; return message.source;
@ -201,7 +209,7 @@ export function getSource(
} }
export function getSourceDevice( export function getSourceDevice(
message: Pick<MessageAttributesType, 'type' | 'sourceDevice'> message: Pick<ReadonlyMessageAttributesType, 'type' | 'sourceDevice'>
): string | number | undefined { ): string | number | undefined {
const { sourceDevice } = message; const { sourceDevice } = message;
@ -218,7 +226,7 @@ export function getSourceDevice(
} }
export function getSourceServiceId( export function getSourceServiceId(
message: Pick<MessageAttributesType, 'type' | 'sourceServiceId'> message: Pick<ReadonlyMessageAttributesType, 'type' | 'sourceServiceId'>
): ServiceIdString | undefined { ): ServiceIdString | undefined {
if (isIncoming(message) || isStory(message)) { if (isIncoming(message) || isStory(message)) {
return message.sourceServiceId; return message.sourceServiceId;

3
ts/model-types.d.ts vendored
View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import * as Backbone from 'backbone'; import * as Backbone from 'backbone';
import type { ReadonlyDeep } from 'type-fest';
import type { GroupV2ChangeType } from './groups'; import type { GroupV2ChangeType } from './groups';
import type { DraftBodyRanges, RawBodyRange } from './types/BodyRange'; import type { DraftBodyRanges, RawBodyRange } from './types/BodyRange';
@ -291,6 +292,8 @@ export type MessageAttributesType = {
deletedForEveryoneFailed?: boolean; deletedForEveryoneFailed?: boolean;
}; };
export type ReadonlyMessageAttributesType = ReadonlyDeep<MessageAttributesType>;
export type ConversationAttributesTypeType = 'private' | 'group'; export type ConversationAttributesTypeType = 'private' | 'group';
export type ConversationLastProfileType = Readonly<{ export type ConversationLastProfileType = Readonly<{

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { pick } from 'lodash'; 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 type { StoryDataType } from '../state/ducks/stories';
import * as durations from '../util/durations'; import * as durations from '../util/durations';
import * as log from '../logging/log'; import * as log from '../logging/log';
@ -28,10 +28,11 @@ export async function loadStories(): Promise<void> {
} }
export function getStoryDataFromMessageAttributes( export function getStoryDataFromMessageAttributes(
message: MessageAttributesType & { message: ReadonlyMessageAttributesType &
hasReplies?: boolean; Readonly<{
hasRepliesFromSelf?: boolean; hasReplies?: boolean;
} hasRepliesFromSelf?: boolean;
}>
): StoryDataType | undefined { ): StoryDataType | undefined {
const { attachments, deletedForEveryone } = message; const { attachments, deletedForEveryone } = message;
const unresolvedAttachment = attachments ? attachments[0] : undefined; const unresolvedAttachment = attachments ? attachments[0] : undefined;

View file

@ -3,6 +3,8 @@
// The idea with this file is to make it webpackable for the style guide // 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 Crypto from './Crypto';
import * as Curve from './Curve'; import * as Curve from './Curve';
import { start as conversationControllerStart } from './ConversationController'; import { start as conversationControllerStart } from './ConversationController';
@ -84,13 +86,13 @@ type MigrationsModuleType = {
attachment: Partial<AttachmentType> attachment: Partial<AttachmentType>
) => Promise<AttachmentWithHydratedData>; ) => Promise<AttachmentWithHydratedData>;
loadContactData: ( loadContactData: (
contact: Array<EmbeddedContactType> | undefined contact: ReadonlyArray<ReadonlyDeep<EmbeddedContactType>> | undefined
) => Promise<Array<EmbeddedContactWithHydratedAvatar> | undefined>; ) => Promise<Array<EmbeddedContactWithHydratedAvatar> | undefined>;
loadMessage: ( loadMessage: (
message: MessageAttributesType message: MessageAttributesType
) => Promise<MessageAttributesType>; ) => Promise<MessageAttributesType>;
loadPreviewData: ( loadPreviewData: (
preview: Array<LinkPreviewType> | undefined preview: ReadonlyArray<ReadonlyDeep<LinkPreviewType>> | undefined
) => Promise<Array<LinkPreviewWithHydratedData>>; ) => Promise<Array<LinkPreviewWithHydratedData>>;
loadQuoteData: ( loadQuoteData: (
quote: QuotedMessageType | null | undefined quote: QuotedMessageType | null | undefined

View file

@ -4,6 +4,7 @@
import { ipcRenderer as ipc } from 'electron'; import { ipcRenderer as ipc } from 'electron';
import { groupBy, isTypedArray, last, map, omit } from 'lodash'; import { groupBy, isTypedArray, last, map, omit } from 'lodash';
import type { ReadonlyDeep } from 'type-fest';
import { deleteExternalFiles } from '../types/Conversation'; import { deleteExternalFiles } from '../types/Conversation';
import { update as updateExpiringMessagesService } from '../services/expiringMessagesDeletion'; import { update as updateExpiringMessagesService } from '../services/expiringMessagesDeletion';
@ -222,7 +223,9 @@ function _cleanData(
return cleaned; return cleaned;
} }
export function _cleanMessageData(data: MessageType): MessageType { export function _cleanMessageData(
data: ReadonlyDeep<MessageType>
): ReadonlyDeep<MessageType> {
const result = { ...data }; const result = { ...data };
// Ensure that all messages have the received_at set properly // Ensure that all messages have the received_at set properly
if (!data.received_at) { if (!data.received_at) {
@ -586,7 +589,7 @@ async function searchMessages({
// Message // Message
async function saveMessage( async function saveMessage(
data: MessageType, data: ReadonlyDeep<MessageType>,
options: { options: {
jobToInsert?: Readonly<StoredJob>; jobToInsert?: Readonly<StoredJob>;
forceSave?: boolean; forceSave?: boolean;
@ -607,7 +610,7 @@ async function saveMessage(
} }
async function saveMessages( async function saveMessages(
arrayOfMessages: ReadonlyArray<MessageType>, arrayOfMessages: ReadonlyArray<ReadonlyDeep<MessageType>>,
options: { forceSave?: boolean; ourAci: AciString } options: { forceSave?: boolean; ourAci: AciString }
): Promise<Array<string>> { ): Promise<Array<string>> {
const result = await writableChannel.saveMessages( const result = await writableChannel.saveMessages(

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { Database } from '@signalapp/better-sqlite3'; import type { Database } from '@signalapp/better-sqlite3';
import type { ReadonlyDeep } from 'type-fest';
import type { import type {
ConversationAttributesType, ConversationAttributesType,
MessageAttributesType, MessageAttributesType,
@ -730,7 +731,7 @@ type WritableInterface = {
deleteAllEndorsementsForGroup: (groupId: string) => void; deleteAllEndorsementsForGroup: (groupId: string) => void;
saveMessage: ( saveMessage: (
data: MessageType, data: ReadonlyDeep<MessageType>,
options: { options: {
jobToInsert?: StoredJob; jobToInsert?: StoredJob;
forceSave?: boolean; forceSave?: boolean;
@ -738,7 +739,7 @@ type WritableInterface = {
} }
) => string; ) => string;
saveMessages: ( saveMessages: (
arrayOfMessages: ReadonlyArray<MessageType>, arrayOfMessages: ReadonlyArray<ReadonlyDeep<MessageType>>,
options: { forceSave?: boolean; ourAci: AciString } options: { forceSave?: boolean; ourAci: AciString }
) => Array<string>; ) => Array<string>;
@ -795,14 +796,14 @@ type WritableInterface = {
): CallLinkType; ): CallLinkType;
migrateConversationMessages: (obsoleteId: string, currentId: string) => void; migrateConversationMessages: (obsoleteId: string, currentId: string) => void;
saveEditedMessage: ( saveEditedMessage: (
mainMessage: MessageType, mainMessage: ReadonlyDeep<MessageType>,
ourAci: AciString, ourAci: AciString,
opts: EditedMessageType opts: ReadonlyDeep<EditedMessageType>
) => void; ) => void;
saveEditedMessages: ( saveEditedMessages: (
mainMessage: MessageType, mainMessage: ReadonlyDeep<MessageType>,
ourAci: AciString, ourAci: AciString,
history: ReadonlyArray<EditedMessageType> history: ReadonlyArray<ReadonlyDeep<EditedMessageType>>
) => void; ) => void;
removeSyncTaskById: (id: string) => void; removeSyncTaskById: (id: string) => void;
@ -1047,13 +1048,13 @@ export type ClientOnlyReadableInterface = ClientInterfaceWrap<{
getRecentStoryReplies( getRecentStoryReplies(
storyId: string, storyId: string,
options?: GetRecentStoryRepliesOptionsType options?: GetRecentStoryRepliesOptionsType
): Array<MessageAttributesType>; ): Array<MessageType>;
getOlderMessagesByConversation: ( getOlderMessagesByConversation: (
options: AdjacentMessagesByConversationOptionsType options: AdjacentMessagesByConversationOptionsType
) => Array<MessageAttributesType>; ) => Array<MessageType>;
getNewerMessagesByConversation: ( getNewerMessagesByConversation: (
options: AdjacentMessagesByConversationOptionsType options: AdjacentMessagesByConversationOptionsType
) => Array<MessageAttributesType>; ) => Array<MessageType>;
getConversationRangeCenteredOnMessage: ( getConversationRangeCenteredOnMessage: (
options: AdjacentMessagesByConversationOptionsType options: AdjacentMessagesByConversationOptionsType
) => GetConversationRangeCenteredOnMessageResultType<MessageType>; ) => GetConversationRangeCenteredOnMessageResultType<MessageType>;

View file

@ -11,6 +11,7 @@ import type { Database, Statement } from '@signalapp/better-sqlite3';
import SQL from '@signalapp/better-sqlite3'; import SQL from '@signalapp/better-sqlite3';
import { v4 as generateUuid } from 'uuid'; import { v4 as generateUuid } from 'uuid';
import { z } from 'zod'; import { z } from 'zod';
import type { ReadonlyDeep } from 'type-fest';
import type { Dictionary } from 'lodash'; import type { Dictionary } from 'lodash';
import { import {
@ -2119,7 +2120,7 @@ export function getAllSyncTasks(db: WritableDB): Array<SyncTaskType> {
export function saveMessage( export function saveMessage(
db: WritableDB, db: WritableDB,
data: MessageType, data: ReadonlyDeep<MessageType>,
options: { options: {
alreadyInTransaction?: boolean; alreadyInTransaction?: boolean;
forceSave?: boolean; forceSave?: boolean;
@ -2337,7 +2338,7 @@ export function saveMessage(
function saveMessages( function saveMessages(
db: WritableDB, db: WritableDB,
arrayOfMessages: ReadonlyArray<MessageType>, arrayOfMessages: ReadonlyArray<ReadonlyDeep<MessageType>>,
options: { forceSave?: boolean; ourAci: AciString } options: { forceSave?: boolean; ourAci: AciString }
): Array<string> { ): Array<string> {
return db.transaction(() => { return db.transaction(() => {
@ -6964,9 +6965,9 @@ function removeAllProfileKeyCredentials(db: WritableDB): void {
function saveEditedMessages( function saveEditedMessages(
db: WritableDB, db: WritableDB,
mainMessage: MessageType, mainMessage: ReadonlyDeep<MessageType>,
ourAci: AciString, ourAci: AciString,
history: ReadonlyArray<EditedMessageType> history: ReadonlyArray<ReadonlyDeep<EditedMessageType>>
): void { ): void {
db.transaction(() => { db.transaction(() => {
saveMessage(db, mainMessage, { saveMessage(db, mainMessage, {
@ -6996,9 +6997,9 @@ function saveEditedMessages(
function saveEditedMessage( function saveEditedMessage(
db: WritableDB, db: WritableDB,
mainMessage: MessageType, mainMessage: ReadonlyDeep<MessageType>,
ourAci: AciString, ourAci: AciString,
editedMessage: EditedMessageType editedMessage: ReadonlyDeep<EditedMessageType>
): void { ): void {
return saveEditedMessages(db, mainMessage, ourAci, [editedMessage]); return saveEditedMessages(db, mainMessage, ourAci, [editedMessage]);
} }

View file

@ -22,7 +22,7 @@ import { DataReader, DataWriter } from '../../sql/Client';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import type { DraftBodyRanges } from '../../types/BodyRange'; import type { DraftBodyRanges } from '../../types/BodyRange';
import type { LinkPreviewType } from '../../types/message/LinkPreviews'; 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 { NoopActionType } from './noop';
import type { ShowToastActionType } from './toast'; import type { ShowToastActionType } from './toast';
import type { StateType as RootStateType } from '../reducer'; import type { StateType as RootStateType } from '../reducer';
@ -104,14 +104,17 @@ type ComposerStateByConversationType = {
linkPreviewLoading: boolean; linkPreviewLoading: boolean;
linkPreviewResult?: LinkPreviewType; linkPreviewResult?: LinkPreviewType;
messageCompositionId: string; messageCompositionId: string;
quotedMessage?: Pick<MessageAttributesType, 'conversationId' | 'quote'>; quotedMessage?: Pick<
ReadonlyMessageAttributesType,
'conversationId' | 'quote'
>;
sendCounter: number; sendCounter: number;
shouldSendHighQualityAttachments?: boolean; shouldSendHighQualityAttachments?: boolean;
}; };
// eslint-disable-next-line local-rules/type-alias-readonlydeep // eslint-disable-next-line local-rules/type-alias-readonlydeep
export type QuotedMessageType = Pick< export type QuotedMessageType = Pick<
MessageAttributesType, ReadonlyMessageAttributesType,
'conversationId' | 'quote' 'conversationId' | 'quote'
>; >;

View file

@ -64,6 +64,7 @@ import type {
DraftEditMessageType, DraftEditMessageType,
LastMessageStatus, LastMessageStatus,
MessageAttributesType, MessageAttributesType,
ReadonlyMessageAttributesType,
} from '../../model-types.d'; } from '../../model-types.d';
import type { import type {
DraftBodyRanges, DraftBodyRanges,
@ -214,18 +215,20 @@ export type InteractionModeType = ReadonlyDeep<
>; >;
export type MessageTimestamps = ReadonlyDeep< export type MessageTimestamps = ReadonlyDeep<
Pick<MessageAttributesType, 'sent_at' | 'received_at'> Pick<ReadonlyMessageAttributesType, 'sent_at' | 'received_at'>
>; >;
// eslint-disable-next-line local-rules/type-alias-readonlydeep export type MessageType = ReadonlyDeep<
export type MessageType = MessageAttributesType & { ReadonlyMessageAttributesType & {
interactionType?: InteractionModeType; interactionType?: InteractionModeType;
}; }
// eslint-disable-next-line local-rules/type-alias-readonlydeep >;
export type MessageWithUIFieldsType = MessageAttributesType & { export type MessageWithUIFieldsType = ReadonlyDeep<
displayLimit?: number; ReadonlyMessageAttributesType & {
isSpoilerExpanded?: Record<number, boolean>; displayLimit?: number;
}; isSpoilerExpanded?: Record<number, boolean>;
}
>;
export const ConversationTypes = ['direct', 'group'] as const; export const ConversationTypes = ['direct', 'group'] as const;
export type ConversationTypeType = ReadonlyDeep< export type ConversationTypeType = ReadonlyDeep<
@ -420,10 +423,9 @@ type MessageMetricsType = ReadonlyDeep<{
totalUnseen: number; totalUnseen: number;
}>; }>;
// eslint-disable-next-line local-rules/type-alias-readonlydeep export type MessageLookupType = ReadonlyDeep<{
export type MessageLookupType = {
[key: string]: MessageWithUIFieldsType; [key: string]: MessageWithUIFieldsType;
}; }>;
export type ConversationMessageType = ReadonlyDeep<{ export type ConversationMessageType = ReadonlyDeep<{
isNearBottom?: boolean; isNearBottom?: boolean;
messageChangeCounter: number; messageChangeCounter: number;
@ -510,8 +512,7 @@ type ComposerStateType = ReadonlyDeep<
)) ))
>; >;
// eslint-disable-next-line local-rules/type-alias-readonlydeep -- FIXME export type ConversationsStateType = ReadonlyDeep<{
export type ConversationsStateType = Readonly<{
preJoinConversation?: PreJoinConversationType; preJoinConversation?: PreJoinConversationType;
invitedServiceIdsForNewlyCreatedGroup?: ReadonlyArray<ServiceIdString>; invitedServiceIdsForNewlyCreatedGroup?: ReadonlyArray<ServiceIdString>;
conversationLookup: ConversationLookupType; conversationLookup: ConversationLookupType;
@ -530,7 +531,7 @@ export type ConversationsStateType = Readonly<{
stack: ReadonlyArray<PanelRenderType>; stack: ReadonlyArray<PanelRenderType>;
watermark: number; watermark: number;
}; };
targetedMessageForDetails?: MessageAttributesType; targetedMessageForDetails?: ReadonlyMessageAttributesType;
lastSelectedMessage: MessageTimestamps | undefined; lastSelectedMessage: MessageTimestamps | undefined;
selectedMessageIds: ReadonlyArray<string> | undefined; selectedMessageIds: ReadonlyArray<string> | undefined;
@ -771,15 +772,14 @@ type ConversationStoppedByMissingVerificationActionType = ReadonlyDeep<{
untrustedServiceIds: ReadonlyArray<ServiceIdString>; untrustedServiceIds: ReadonlyArray<ServiceIdString>;
}; };
}>; }>;
// eslint-disable-next-line local-rules/type-alias-readonlydeep -- FIXME export type MessageChangedActionType = ReadonlyDeep<{
export type MessageChangedActionType = {
type: typeof MESSAGE_CHANGED; type: typeof MESSAGE_CHANGED;
payload: { payload: {
id: string; id: string;
conversationId: string; conversationId: string;
data: MessageAttributesType; data: ReadonlyMessageAttributesType;
}; };
}; }>;
export type MessageDeletedActionType = ReadonlyDeep<{ export type MessageDeletedActionType = ReadonlyDeep<{
type: typeof MESSAGE_DELETED; type: typeof MESSAGE_DELETED;
payload: { payload: {
@ -802,15 +802,14 @@ export type ShowSpoilerActionType = ReadonlyDeep<{
}; };
}>; }>;
// eslint-disable-next-line local-rules/type-alias-readonlydeep -- FIXME export type MessagesAddedActionType = ReadonlyDeep<{
export type MessagesAddedActionType = Readonly<{
type: 'MESSAGES_ADDED'; type: 'MESSAGES_ADDED';
payload: { payload: {
conversationId: string; conversationId: string;
isActive: boolean; isActive: boolean;
isJustSent: boolean; isJustSent: boolean;
isNewMessage: boolean; isNewMessage: boolean;
messages: ReadonlyArray<MessageAttributesType>; messages: ReadonlyArray<ReadonlyMessageAttributesType>;
}; };
}>; }>;
@ -833,19 +832,18 @@ export type RepairOldestMessageActionType = ReadonlyDeep<{
conversationId: string; conversationId: string;
}; };
}>; }>;
// eslint-disable-next-line local-rules/type-alias-readonlydeep export type MessagesResetActionType = ReadonlyDeep<{
export type MessagesResetActionType = {
type: 'MESSAGES_RESET'; type: 'MESSAGES_RESET';
payload: { payload: {
conversationId: string; conversationId: string;
messages: ReadonlyArray<MessageAttributesType>; messages: ReadonlyArray<ReadonlyMessageAttributesType>;
metrics: MessageMetricsType; metrics: MessageMetricsType;
scrollToMessageId?: string; scrollToMessageId?: string;
// The set of provided messages should be trusted, even if it conflicts with metrics, // 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. // because we weren't looking for a specific time window of messages with our query.
unboundedFetch: boolean; unboundedFetch: boolean;
}; };
}; }>;
export type SetMessageLoadingStateActionType = ReadonlyDeep<{ export type SetMessageLoadingStateActionType = ReadonlyDeep<{
type: 'SET_MESSAGE_LOADING_STATE'; type: 'SET_MESSAGE_LOADING_STATE';
payload: { payload: {
@ -957,8 +955,7 @@ export type ToggleConversationInChooseMembersActionType = ReadonlyDeep<{
}; };
}>; }>;
// eslint-disable-next-line local-rules/type-alias-readonlydeep -- FIXME type PushPanelActionType = ReadonlyDeep<{
type PushPanelActionType = Readonly<{
type: typeof PUSH_PANEL; type: typeof PUSH_PANEL;
payload: PanelRenderType; 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 = export type ConversationActionType =
| CancelVerificationDataByConversationActionType | CancelVerificationDataByConversationActionType
| ClearCancelledVerificationActionType | ClearCancelledVerificationActionType
@ -2865,7 +2862,7 @@ function conversationStoppedByMissingVerification(payload: {
export function messageChanged( export function messageChanged(
id: string, id: string,
conversationId: string, conversationId: string,
data: MessageAttributesType data: ReadonlyMessageAttributesType
): MessageChangedActionType { ): MessageChangedActionType {
return { return {
type: MESSAGE_CHANGED, type: MESSAGE_CHANGED,
@ -2935,7 +2932,7 @@ function messagesAdded({
isActive: boolean; isActive: boolean;
isJustSent: boolean; isJustSent: boolean;
isNewMessage: boolean; isNewMessage: boolean;
messages: ReadonlyArray<MessageAttributesType>; messages: ReadonlyArray<ReadonlyMessageAttributesType>;
}): ThunkAction<void, RootStateType, unknown, MessagesAddedActionType> { }): ThunkAction<void, RootStateType, unknown, MessagesAddedActionType> {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
@ -2990,14 +2987,13 @@ function reviewConversationNameCollision(): ReviewConversationNameCollisionActio
}; };
} }
// eslint-disable-next-line local-rules/type-alias-readonlydeep export type MessageResetOptionsType = ReadonlyDeep<{
export type MessageResetOptionsType = {
conversationId: string; conversationId: string;
messages: ReadonlyArray<MessageAttributesType>; messages: ReadonlyArray<ReadonlyMessageAttributesType>;
metrics: MessageMetricsType; metrics: MessageMetricsType;
scrollToMessageId?: string; scrollToMessageId?: string;
unboundedFetch?: boolean; unboundedFetch?: boolean;
}; }>;
function messagesReset({ function messagesReset({
conversationId, conversationId,
@ -4716,7 +4712,7 @@ function maybeUpdateSelectedMessageForDetails(
targetedMessageForDetails, targetedMessageForDetails,
}: { }: {
messageId: string; messageId: string;
targetedMessageForDetails: MessageAttributesType | undefined; targetedMessageForDetails: ReadonlyMessageAttributesType | undefined;
}, },
state: ConversationsStateType state: ConversationsStateType
): ConversationsStateType { ): ConversationsStateType {

View file

@ -6,7 +6,7 @@ import type { ReadonlyDeep } from 'type-fest';
import type { ExplodePromiseResultType } from '../../util/explodePromise'; import type { ExplodePromiseResultType } from '../../util/explodePromise';
import type { import type {
GroupV2PendingMemberType, GroupV2PendingMemberType,
MessageAttributesType, ReadonlyMessageAttributesType,
} from '../../model-types.d'; } from '../../model-types.d';
import type { import type {
MessageChangedActionType, MessageChangedActionType,
@ -52,7 +52,7 @@ import { linkCallRoute } from '../../util/signalRoutes';
// State // State
export type EditHistoryMessagesType = ReadonlyDeep< export type EditHistoryMessagesType = ReadonlyDeep<
Array<MessageAttributesType> Array<ReadonlyMessageAttributesType>
>; >;
export type EditNicknameAndNoteModalPropsType = ReadonlyDeep<{ export type EditNicknameAndNoteModalPropsType = ReadonlyDeep<{
conversationId: string; conversationId: string;
@ -882,7 +882,7 @@ function showShortcutGuideModal(): ShowShortcutGuideModalActionType {
} }
function copyOverMessageAttributesIntoEditHistory( function copyOverMessageAttributesIntoEditHistory(
messageAttributes: ReadonlyDeep<MessageAttributesType> messageAttributes: ReadonlyDeep<ReadonlyMessageAttributesType>
): EditHistoryMessagesType | undefined { ): EditHistoryMessagesType | undefined {
if (!messageAttributes.editHistory) { if (!messageAttributes.editHistory) {
return; return;
@ -934,7 +934,7 @@ function closeEditHistoryModal(): CloseEditHistoryModalActionType {
function copyOverMessageAttributesIntoForwardMessages( function copyOverMessageAttributesIntoForwardMessages(
messageDrafts: ReadonlyArray<MessageForwardDraft>, messageDrafts: ReadonlyArray<MessageForwardDraft>,
attributes: ReadonlyDeep<MessageAttributesType> attributes: ReadonlyDeep<ReadonlyMessageAttributesType>
): ReadonlyArray<MessageForwardDraft> { ): ReadonlyArray<MessageForwardDraft> {
return messageDrafts.map(messageDraft => { return messageDrafts.map(messageDraft => {
if (messageDraft.originalMessageId !== attributes.id) { if (messageDraft.originalMessageId !== attributes.id) {

View file

@ -18,7 +18,7 @@ import type { StateType as RootStateType } from '../reducer';
import * as log from '../../logging/log'; import * as log from '../../logging/log';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById'; 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 { isGIF } from '../../types/Attachment';
import { import {
isImageTypeSupported, isImageTypeSupported,
@ -245,7 +245,7 @@ function showLightboxForViewOnceMedia(
} }
function filterValidAttachments( function filterValidAttachments(
attributes: MessageAttributesType attributes: ReadonlyMessageAttributesType
): Array<AttachmentType> { ): Array<AttachmentType> {
return (attributes.attachments ?? []).filter( return (attributes.attachments ?? []).filter(
item => item.thumbnail && !item.pending && !item.error item => item.thumbnail && !item.pending && !item.error

View file

@ -8,7 +8,7 @@ import type { ReadonlyDeep } from 'type-fest';
import * as Errors from '../../types/errors'; import * as Errors from '../../types/errors';
import type { AttachmentType } from '../../types/Attachment'; import type { AttachmentType } from '../../types/Attachment';
import type { DraftBodyRanges } from '../../types/BodyRange'; import type { DraftBodyRanges } from '../../types/BodyRange';
import type { MessageAttributesType } from '../../model-types.d'; import type { ReadonlyMessageAttributesType } from '../../model-types.d';
import type { import type {
MessageChangedActionType, MessageChangedActionType,
MessageDeletedActionType, MessageDeletedActionType,
@ -79,7 +79,7 @@ export type StoryDataType = ReadonlyDeep<
messageId: string; messageId: string;
startedDownload?: boolean; startedDownload?: boolean;
} & Pick< } & Pick<
MessageAttributesType, ReadonlyMessageAttributesType,
| 'bodyRanges' | 'bodyRanges'
| 'canReplyToStory' | 'canReplyToStory'
| 'conversationId' | 'conversationId'
@ -124,31 +124,33 @@ export type AddStoryData = ReadonlyDeep<
| undefined | undefined
>; >;
// eslint-disable-next-line local-rules/type-alias-readonlydeep export type RecipientEntry = ReadonlyDeep<{
export type RecipientsByConversation = Record< serviceIds: Array<ServiceIdString>;
string, // conversationId
{
serviceIds: Array<ServiceIdString>;
byDistributionId?: Record< byDistributionId?: Record<
StoryDistributionIdString, StoryDistributionIdString,
{ {
serviceIds: Array<ServiceIdString>; serviceIds: Array<ServiceIdString>;
} }
>; >;
} }>;
export type RecipientsByConversation = ReadonlyDeep<
Record<
string, // conversationId
RecipientEntry
>
>; >;
// State // State
// eslint-disable-next-line local-rules/type-alias-readonlydeep export type StoriesStateType = ReadonlyDeep<{
export type StoriesStateType = Readonly<{
addStoryData: AddStoryData; addStoryData: AddStoryData;
hasAllStoriesUnmuted: boolean; hasAllStoriesUnmuted: boolean;
lastOpenedAtTimestamp: number | undefined; lastOpenedAtTimestamp: number | undefined;
replyState?: Readonly<{ replyState?: Readonly<{
messageId: string; messageId: string;
replies: Array<MessageAttributesType>; replies: Array<ReadonlyMessageAttributesType>;
}>; }>;
selectedStoryData?: SelectedStoryDataType; selectedStoryData?: SelectedStoryDataType;
sendStoryModalData?: RecipientsByConversation; sendStoryModalData?: RecipientsByConversation;
@ -188,14 +190,13 @@ type ListMembersVerified = ReadonlyDeep<{
}; };
}>; }>;
// eslint-disable-next-line local-rules/type-alias-readonlydeep type LoadStoryRepliesActionType = ReadonlyDeep<{
type LoadStoryRepliesActionType = {
type: typeof LOAD_STORY_REPLIES; type: typeof LOAD_STORY_REPLIES;
payload: { payload: {
messageId: string; messageId: string;
replies: Array<MessageAttributesType>; replies: Array<ReadonlyMessageAttributesType>;
}; };
}; }>;
type MarkStoryReadActionType = ReadonlyDeep<{ type MarkStoryReadActionType = ReadonlyDeep<{
type: typeof MARK_STORY_READ; type: typeof MARK_STORY_READ;

View file

@ -19,7 +19,7 @@ import type { StateType } from '../reducer';
import * as log from '../../logging/log'; import * as log from '../../logging/log';
import { getLocalAttachmentUrl } from '../../util/getLocalAttachmentUrl'; import { getLocalAttachmentUrl } from '../../util/getLocalAttachmentUrl';
import type { MessageWithUIFieldsType } from '../ducks/conversations'; 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 { getMessageIdForLogging } from '../../util/idForLogging';
import * as Attachment from '../../types/Attachment'; import * as Attachment from '../../types/Attachment';
import type { ActiveAudioPlayerStateType } from '../ducks/audioPlayer'; import type { ActiveAudioPlayerStateType } from '../ducks/audioPlayer';
@ -57,7 +57,7 @@ export const selectVoiceNoteTitle = createSelector(
(ourNumber, ourAci, ourConversationId, conversationSelector, i18n) => { (ourNumber, ourAci, ourConversationId, conversationSelector, i18n) => {
return ( return (
message: Pick< message: Pick<
MessageAttributesType, ReadonlyMessageAttributesType,
'type' | 'source' | 'sourceServiceId' 'type' | 'source' | 'sourceServiceId'
> >
) => { ) => {
@ -75,7 +75,7 @@ export const selectVoiceNoteTitle = createSelector(
); );
export function extractVoiceNoteForPlayback( export function extractVoiceNoteForPlayback(
message: MessageAttributesType, message: ReadonlyMessageAttributesType,
ourConversationId: string | undefined ourConversationId: string | undefined
): VoiceNoteForPlayback | undefined { ): VoiceNoteForPlayback | undefined {
const { type } = message; const { type } = message;

View file

@ -11,7 +11,7 @@ import type { ReadonlyDeep } from 'type-fest';
import type { StateType } from '../reducer'; import type { StateType } from '../reducer';
import type { import type {
LastMessageStatus, LastMessageStatus,
MessageAttributesType, ReadonlyMessageAttributesType,
MessageReactionType, MessageReactionType,
QuotedAttachmentType, QuotedAttachmentType,
ShallowChallengeError, ShallowChallengeError,
@ -201,7 +201,7 @@ export function hasErrors(
} }
export function getSource( export function getSource(
message: Pick<MessageAttributesType, 'type' | 'source'>, message: Pick<ReadonlyMessageAttributesType, 'type' | 'source'>,
ourNumber: string | undefined ourNumber: string | undefined
): string | undefined { ): string | undefined {
if (isIncoming(message)) { if (isIncoming(message)) {
@ -233,7 +233,7 @@ export function getSourceDevice(
} }
export function getSourceServiceId( export function getSourceServiceId(
message: Pick<MessageAttributesType, 'type' | 'sourceServiceId'>, message: Pick<ReadonlyMessageAttributesType, 'type' | 'sourceServiceId'>,
ourAci: AciString | undefined ourAci: AciString | undefined
): ServiceIdString | undefined { ): ServiceIdString | undefined {
if (isIncoming(message)) { if (isIncoming(message)) {
@ -1523,13 +1523,13 @@ function getPropsForProfileChange(
// Message Request Response Event // Message Request Response Event
export function isMessageRequestResponse( export function isMessageRequestResponse(
message: MessageAttributesType message: ReadonlyMessageAttributesType
): boolean { ): boolean {
return message.type === 'message-request-response-event'; return message.type === 'message-request-response-event';
} }
function getPropsForMessageRequestResponse( function getPropsForMessageRequestResponse(
message: MessageAttributesType message: ReadonlyMessageAttributesType
): MessageRequestResponseNotificationData { ): MessageRequestResponseNotificationData {
const { messageRequestResponseEvent } = message; const { messageRequestResponseEvent } = message;
if (!messageRequestResponseEvent) { if (!messageRequestResponseEvent) {
@ -1805,7 +1805,7 @@ export function getPropsForEmbeddedContact(
message: MessageWithUIFieldsType, message: MessageWithUIFieldsType,
regionCode: string | undefined, regionCode: string | undefined,
accountSelector: (identifier?: string) => ServiceIdString | undefined accountSelector: (identifier?: string) => ServiceIdString | undefined
): EmbeddedContactType | undefined { ): ReadonlyDeep<EmbeddedContactType> | undefined {
const contacts = message.contact; const contacts = message.contact;
if (!contacts || !contacts.length) { if (!contacts || !contacts.length) {
return undefined; return undefined;
@ -2091,7 +2091,7 @@ export function getLastChallengeError(
const getTargetedMessageForDetails = ( const getTargetedMessageForDetails = (
state: StateType state: StateType
): MessageAttributesType | undefined => ): ReadonlyMessageAttributesType | undefined =>
state.conversations.targetedMessageForDetails; state.conversations.targetedMessageForDetails;
const OUTGOING_KEY_ERROR = 'OutgoingIdentityKeyError'; const OUTGOING_KEY_ERROR = 'OutgoingIdentityKeyError';

View file

@ -3,7 +3,7 @@
import React, { memo, useMemo } from 'react'; import React, { memo, useMemo } from 'react';
import { useSelector } from 'react-redux'; 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 { EditHistoryMessagesModal } from '../../components/EditHistoryMessagesModal';
import { getIntl, getPlatform } from '../selectors/user'; import { getIntl, getPlatform } from '../selectors/user';
import { getMessagePropsSelector } from '../selectors/message'; import { getMessagePropsSelector } from '../selectors/message';
@ -31,7 +31,9 @@ export const SmartEditHistoryMessagesModal = memo(
const editHistoryMessages = useMemo(() => { const editHistoryMessages = useMemo(() => {
return messagesAttributes.map(messageAttributes => ({ return messagesAttributes.map(messageAttributes => ({
...messagePropsSelector(messageAttributes as MessageAttributesType), ...messagePropsSelector(
messageAttributes as ReadonlyMessageAttributesType
),
// Make sure the messages don't get an "edited" badge // Make sure the messages don't get an "edited" badge
isEditedMessage: false, isEditedMessage: false,
// Do not show the same reactions in the message history UI // Do not show the same reactions in the message history UI

View file

@ -5,6 +5,7 @@ import { assert } from 'chai';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import { v4 as generateUuid } from 'uuid'; import { v4 as generateUuid } from 'uuid';
import { times } from 'lodash'; import { times } from 'lodash';
import type { ReadonlyDeep } from 'type-fest';
import { reducer as rootReducer } from '../../../state/reducer'; import { reducer as rootReducer } from '../../../state/reducer';
import { noopAction } from '../../../state/ducks/noop'; import { noopAction } from '../../../state/ducks/noop';
@ -60,7 +61,7 @@ import {
VIEWERS_CHANGED, VIEWERS_CHANGED,
} from '../../../state/ducks/storyDistributionLists'; } from '../../../state/ducks/storyDistributionLists';
import { MY_STORY_ID } from '../../../types/Stories'; import { MY_STORY_ID } from '../../../types/Stories';
import type { MessageAttributesType } from '../../../model-types.d'; import type { ReadonlyMessageAttributesType } from '../../../model-types.d';
const { const {
clearGroupCreationError, clearGroupCreationError,
@ -92,8 +93,8 @@ const {
function messageChanged( function messageChanged(
messageId: string, messageId: string,
conversationId: string, conversationId: string,
data: MessageAttributesType data: ReadonlyMessageAttributesType
): MessageChangedActionType { ): ReadonlyDeep<MessageChangedActionType> {
return { return {
type: 'MESSAGE_CHANGED', type: 'MESSAGE_CHANGED',
payload: { payload: {

View file

@ -2,9 +2,10 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { omit } from 'lodash'; import { omit } from 'lodash';
import type { ReadonlyDeep } from 'type-fest';
import { SignalService as Proto } from '../protobuf'; 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 { isNotNil } from '../util/isNotNil';
import { import {
@ -147,13 +148,13 @@ export function numberToAddressType(
} }
export function embeddedContactSelector( export function embeddedContactSelector(
contact: EmbeddedContactType, contact: ReadonlyDeep<EmbeddedContactType>,
options: { options: {
regionCode?: string; regionCode?: string;
firstNumber?: string; firstNumber?: string;
serviceId?: ServiceIdString; serviceId?: ServiceIdString;
} }
): EmbeddedContactType { ): ReadonlyDeep<EmbeddedContactType> {
const { firstNumber, serviceId, regionCode } = options; const { firstNumber, serviceId, regionCode } = options;
let { avatar } = contact; let { avatar } = contact;
@ -189,7 +190,9 @@ export function embeddedContactSelector(
}; };
} }
export function getName(contact: EmbeddedContactType): string | undefined { export function getName(
contact: ReadonlyDeep<EmbeddedContactType>
): string | undefined {
const { name, organization } = contact; const { name, organization } = contact;
const displayName = (name && name.displayName) || undefined; const displayName = (name && name.displayName) || undefined;
const givenName = (name && name.givenName) || undefined; const givenName = (name && name.givenName) || undefined;
@ -206,7 +209,7 @@ export function parseAndWriteAvatar(
return async ( return async (
contact: EmbeddedContactType, contact: EmbeddedContactType,
context: { context: {
message: MessageAttributesType; message: ReadonlyMessageAttributesType;
getRegionCode: () => string | undefined; getRegionCode: () => string | undefined;
logger: LoggerType; logger: LoggerType;
writeNewAttachmentData: ( writeNewAttachmentData: (
@ -280,7 +283,7 @@ function parseContact(
return result; return result;
} }
function idForLogging(message: MessageAttributesType): string { function idForLogging(message: ReadonlyMessageAttributesType): string {
return `${message.source}.${message.sourceDevice} ${message.sent_at}`; return `${message.source}.${message.sourceDevice} ${message.sent_at}`;
} }

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { orderBy } from 'lodash'; import { orderBy } from 'lodash';
import type { MessageAttributesType } from '../model-types'; import type { ReadonlyMessageAttributesType } from '../model-types';
import { import {
isVoiceMessage, isVoiceMessage,
type AttachmentType, type AttachmentType,
@ -23,7 +23,7 @@ export type MessageForwardDraft = Readonly<{
export type ForwardMessageData = Readonly<{ export type ForwardMessageData = Readonly<{
// only null for new messages // only null for new messages
originalMessage: MessageAttributesType | null; originalMessage: ReadonlyMessageAttributesType | null;
draft: MessageForwardDraft; draft: MessageForwardDraft;
}>; }>;
@ -72,7 +72,7 @@ export function sortByMessageOrder<T>(
items: ReadonlyArray<T>, items: ReadonlyArray<T>,
getMesssage: ( getMesssage: (
item: T item: T
) => Pick<MessageAttributesType, 'sent_at' | 'received_at'> | null ) => Pick<ReadonlyMessageAttributesType, 'sent_at' | 'received_at'> | null
): Array<T> { ): Array<T> {
return orderBy( return orderBy(
items, items,

View file

@ -1,12 +1,12 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // 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 { AttachmentType } from './Attachment';
import type { MIMEType } from './MIME'; import type { MIMEType } from './MIME';
export type MediaItemMessageType = Pick< export type MediaItemMessageType = Pick<
MessageAttributesType, ReadonlyMessageAttributesType,
| 'attachments' | 'attachments'
| 'conversationId' | 'conversationId'
| 'id' | 'id'

View file

@ -2,6 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { isFunction, isObject } from 'lodash'; import { isFunction, isObject } from 'lodash';
import type { ReadonlyDeep } from 'type-fest';
import * as Contact from './EmbeddedContact'; import * as Contact from './EmbeddedContact';
import type { import type {
@ -43,6 +44,7 @@ import {
AttachmentDisposition, AttachmentDisposition,
} from '../util/getLocalAttachmentUrl'; } from '../util/getLocalAttachmentUrl';
import { encryptLegacyAttachment } from '../util/encryptLegacyAttachment'; import { encryptLegacyAttachment } from '../util/encryptLegacyAttachment';
import { deepClone } from '../util/deepClone';
export { hasExpiration } from './Message'; export { hasExpiration } from './Message';
@ -831,14 +833,14 @@ export const loadQuoteData = (
export const loadContactData = ( export const loadContactData = (
loadAttachmentData: LoadAttachmentType loadAttachmentData: LoadAttachmentType
): (( ): ((
contact: Array<EmbeddedContactType> | undefined contact: ReadonlyArray<ReadonlyDeep<EmbeddedContactType>> | undefined
) => Promise<Array<EmbeddedContactWithHydratedAvatar> | undefined>) => { ) => Promise<Array<EmbeddedContactWithHydratedAvatar> | undefined>) => {
if (!isFunction(loadAttachmentData)) { if (!isFunction(loadAttachmentData)) {
throw new TypeError('loadContactData: loadAttachmentData is required'); throw new TypeError('loadContactData: loadAttachmentData is required');
} }
return async ( return async (
contact: Array<EmbeddedContactType> | undefined contact: ReadonlyArray<ReadonlyDeep<EmbeddedContactType>> | undefined
): Promise<Array<EmbeddedContactWithHydratedAvatar> | undefined> => { ): Promise<Array<EmbeddedContactWithHydratedAvatar> | undefined> => {
if (!contact) { if (!contact) {
return undefined; return undefined;
@ -847,27 +849,23 @@ export const loadContactData = (
return Promise.all( return Promise.all(
contact.map( contact.map(
async ( async (
item: EmbeddedContactType item: ReadonlyDeep<EmbeddedContactType>
): Promise<EmbeddedContactWithHydratedAvatar> => { ): Promise<EmbeddedContactWithHydratedAvatar> => {
if ( const copy = deepClone(item);
!item || if (!copy?.avatar?.avatar?.path) {
!item.avatar ||
!item.avatar.avatar ||
!item.avatar.avatar.path
) {
return { return {
...item, ...copy,
avatar: undefined, avatar: undefined,
}; };
} }
return { return {
...item, ...copy,
avatar: { avatar: {
...item.avatar, ...copy.avatar,
avatar: { avatar: {
...item.avatar.avatar, ...copy.avatar.avatar,
...(await loadAttachmentData(item.avatar.avatar)), ...(await loadAttachmentData(copy.avatar.avatar)),
}, },
}, },
}; };
@ -880,13 +878,15 @@ export const loadContactData = (
export const loadPreviewData = ( export const loadPreviewData = (
loadAttachmentData: LoadAttachmentType loadAttachmentData: LoadAttachmentType
): (( ): ((
preview: Array<LinkPreviewType> | undefined preview: ReadonlyArray<ReadonlyDeep<LinkPreviewType>> | undefined
) => Promise<Array<LinkPreviewWithHydratedData>>) => { ) => Promise<Array<LinkPreviewWithHydratedData>>) => {
if (!isFunction(loadAttachmentData)) { if (!isFunction(loadAttachmentData)) {
throw new TypeError('loadPreviewData: loadAttachmentData is required'); throw new TypeError('loadPreviewData: loadAttachmentData is required');
} }
return async (preview: Array<LinkPreviewType> | undefined) => { return async (
preview: ReadonlyArray<ReadonlyDeep<LinkPreviewType>> | undefined
) => {
if (!preview || !preview.length) { if (!preview || !preview.length) {
return []; return [];
} }
@ -894,17 +894,19 @@ export const loadPreviewData = (
return Promise.all( return Promise.all(
preview.map( preview.map(
async (item: LinkPreviewType): Promise<LinkPreviewWithHydratedData> => { async (item: LinkPreviewType): Promise<LinkPreviewWithHydratedData> => {
if (!item.image) { const copy = deepClone(item);
if (!copy.image) {
return { return {
...item, ...copy,
// Pacify typescript // Pacify typescript
image: undefined, image: undefined,
}; };
} }
return { return {
...item, ...copy,
image: await loadAttachmentData(item.image), image: await loadAttachmentData(copy.image),
}; };
} }
) )

View file

@ -1,8 +1,10 @@
// Copyright 2022 Signal Messenger, LLC // Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { ReadonlyDeep } from 'type-fest';
import type { EmbeddedContactType } from './EmbeddedContact'; import type { EmbeddedContactType } from './EmbeddedContact';
import type { MessageAttributesType } from '../model-types.d'; import type { ReadonlyMessageAttributesType } from '../model-types.d';
import type { ServiceIdString } from './ServiceId'; import type { ServiceIdString } from './ServiceId';
export enum PanelType { export enum PanelType {
@ -19,7 +21,7 @@ export enum PanelType {
StickerManager = 'StickerManager', StickerManager = 'StickerManager',
} }
export type PanelRequestType = export type PanelRequestType = ReadonlyDeep<
| { type: PanelType.AllMedia } | { type: PanelType.AllMedia }
| { type: PanelType.ChatColorEditor } | { type: PanelType.ChatColorEditor }
| { | {
@ -39,9 +41,10 @@ export type PanelRequestType =
| { type: PanelType.GroupV1Members } | { type: PanelType.GroupV1Members }
| { type: PanelType.MessageDetails; args: { messageId: string } } | { type: PanelType.MessageDetails; args: { messageId: string } }
| { type: PanelType.NotificationSettings } | { type: PanelType.NotificationSettings }
| { type: PanelType.StickerManager }; | { type: PanelType.StickerManager }
>;
export type PanelRenderType = export type PanelRenderType = ReadonlyDeep<
| { type: PanelType.AllMedia } | { type: PanelType.AllMedia }
| { type: PanelType.ChatColorEditor } | { type: PanelType.ChatColorEditor }
| { | {
@ -59,6 +62,10 @@ export type PanelRenderType =
| { type: PanelType.GroupLinkManagement } | { type: PanelType.GroupLinkManagement }
| { type: PanelType.GroupPermissions } | { type: PanelType.GroupPermissions }
| { type: PanelType.GroupV1Members } | { type: PanelType.GroupV1Members }
| { type: PanelType.MessageDetails; args: { message: MessageAttributesType } } | {
type: PanelType.MessageDetails;
args: { message: ReadonlyMessageAttributesType };
}
| { type: PanelType.NotificationSettings } | { type: PanelType.NotificationSettings }
| { type: PanelType.StickerManager }; | { type: PanelType.StickerManager }
>;

View file

@ -9,7 +9,7 @@ export enum PaymentEventKind {
// Cancellation = 5, -- disabled // Cancellation = 5, -- disabled
} }
export type PaymentNotificationEvent = { export type PaymentNotificationEvent = Readonly<{
kind: PaymentEventKind.Notification; kind: PaymentEventKind.Notification;
note: string | null; note: string | null;
@ -17,15 +17,15 @@ export type PaymentNotificationEvent = {
transactionDetailsBase64?: string; transactionDetailsBase64?: string;
amountMob?: string; amountMob?: string;
feeMob?: string; feeMob?: string;
}; }>;
export type PaymentActivationRequestEvent = { export type PaymentActivationRequestEvent = Readonly<{
kind: PaymentEventKind.ActivationRequest; kind: PaymentEventKind.ActivationRequest;
}; }>;
export type PaymentActivatedEvent = { export type PaymentActivatedEvent = Readonly<{
kind: PaymentEventKind.Activation; kind: PaymentEventKind.Activation;
}; }>;
export type AnyPaymentEvent = export type AnyPaymentEvent =
| PaymentNotificationEvent | PaymentNotificationEvent

View file

@ -4,7 +4,10 @@
import type { SafetyNumberChangeSource } from '../components/SafetyNumberChangeDialog'; import type { SafetyNumberChangeSource } from '../components/SafetyNumberChangeDialog';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { explodePromise } from './explodePromise'; import { explodePromise } from './explodePromise';
import type { RecipientsByConversation } from '../state/ducks/stories'; import type {
RecipientsByConversation,
RecipientEntry,
} from '../state/ducks/stories';
import { isNotNil } from './isNotNil'; import { isNotNil } from './isNotNil';
import type { ServiceIdString } from '../types/ServiceId'; import type { ServiceIdString } from '../types/ServiceId';
import { waitForAll } from './waitForAll'; import { waitForAll } from './waitForAll';
@ -105,7 +108,7 @@ export function filterServiceIds(
byConversation: RecipientsByConversation, byConversation: RecipientsByConversation,
predicate: (serviceId: ServiceIdString) => boolean predicate: (serviceId: ServiceIdString) => boolean
): RecipientsByConversation { ): RecipientsByConversation {
const filteredByConversation: RecipientsByConversation = {}; const filteredByConversation: Record<string, RecipientEntry> = {};
Object.entries(byConversation).forEach( Object.entries(byConversation).forEach(
([conversationId, conversationData]) => { ([conversationId, conversationData]) => {
const conversationFiltered = conversationData.serviceIds const conversationFiltered = conversationData.serviceIds

View file

@ -1,14 +1,16 @@
// Copyright 2023 Signal Messenger, LLC // Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // 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 { DAY } from './durations';
import { isMoreRecentThan } from './timestamp'; import { isMoreRecentThan } from './timestamp';
import { isOutgoing } from '../messages/helpers'; import { isOutgoing } from '../messages/helpers';
export const MESSAGE_MAX_EDIT_COUNT = 10; export const MESSAGE_MAX_EDIT_COUNT = 10;
export function canEditMessage(message: MessageAttributesType): boolean { export function canEditMessage(
message: ReadonlyMessageAttributesType
): boolean {
const result = const result =
!message.deletedForEveryone && !message.deletedForEveryone &&
isOutgoing(message) && isOutgoing(message) &&
@ -29,6 +31,8 @@ export function canEditMessage(message: MessageAttributesType): boolean {
return false; return false;
} }
export function isWithinMaxEdits(message: MessageAttributesType): boolean { export function isWithinMaxEdits(
message: ReadonlyMessageAttributesType
): boolean {
return (message.editHistory?.length ?? 0) <= MESSAGE_MAX_EDIT_COUNT; return (message.editHistory?.length ?? 0) <= MESSAGE_MAX_EDIT_COUNT;
} }

11
ts/util/deepClone.ts Normal file
View file

@ -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<T>(value: T): WritableDeep<T> {
return structuredClone(value) as WritableDeep<T>;
}

View file

@ -2,10 +2,15 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { isNumber, sortBy } from 'lodash'; import { isNumber, sortBy } from 'lodash';
import type { ReadonlyDeep } from 'type-fest';
import { strictAssert } from './assert'; 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'; import type { LoggerType } from '../types/Logging';
// The tricky bit for this function is if we are on our second+ attempt to send a given // 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, message,
targetTimestamp, targetTimestamp,
}: { }: {
message: MessageAttributesType; message: ReadonlyMessageAttributesType;
targetTimestamp: number; targetTimestamp: number;
}): number { }): number {
const { timestamp: originalTimestamp, editHistory } = message; const { timestamp: originalTimestamp, editHistory } = message;
@ -27,7 +32,7 @@ export function getTargetOfThisEditTimestamp({
}); });
const mostRecent = sortBy( const mostRecent = sortBy(
sentItems, sentItems,
(item: EditHistoryType) => item.timestamp (item: ReadonlyDeep<EditHistoryType>) => item.timestamp
); );
const { length } = mostRecent; const { length } = mostRecent;

View file

@ -1,7 +1,7 @@
// Copyright 2022 Signal Messenger, LLC // Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // 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 { MessageModel } from '../models/messages';
import type { SignalService as Proto } from '../protobuf'; import type { SignalService as Proto } from '../protobuf';
import type { AciString } from '../types/ServiceId'; import type { AciString } from '../types/ServiceId';
@ -72,12 +72,12 @@ export async function findStoryMessages(
} }
function isStoryAMatch( function isStoryAMatch(
message: MessageAttributesType | null | undefined, message: ReadonlyMessageAttributesType | null | undefined,
conversationId: string, conversationId: string,
ourConversationId: string, ourConversationId: string,
authorAci: AciString, authorAci: AciString,
sentTimestamp: number sentTimestamp: number
): message is MessageAttributesType { ): message is ReadonlyMessageAttributesType {
if (!message) { if (!message) {
return false; return false;
} }

View file

@ -3,13 +3,13 @@
import type { import type {
ConversationAttributesType, ConversationAttributesType,
MessageAttributesType, ReadonlyMessageAttributesType,
} from '../model-types.d'; } from '../model-types.d';
import { isIncoming, isOutgoing } from '../state/selectors/message'; import { isIncoming, isOutgoing } from '../state/selectors/message';
import { getTitle } from './getTitle'; import { getTitle } from './getTitle';
function getIncomingContact( function getIncomingContact(
messageAttributes: MessageAttributesType messageAttributes: ReadonlyMessageAttributesType
): ConversationAttributesType | undefined { ): ConversationAttributesType | undefined {
if (!isIncoming(messageAttributes)) { if (!isIncoming(messageAttributes)) {
return undefined; return undefined;
@ -24,7 +24,7 @@ function getIncomingContact(
} }
export function getMessageAuthorText( export function getMessageAuthorText(
messageAttributes?: MessageAttributesType messageAttributes?: ReadonlyMessageAttributesType
): string | undefined { ): string | undefined {
if (!messageAttributes) { if (!messageAttributes) {
return undefined; return undefined;

View file

@ -1,12 +1,12 @@
// Copyright 2023 Signal Messenger, LLC // Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // 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'; import type { ConversationModel } from '../models/conversations';
export function getMessageConversation({ export function getMessageConversation({
conversationId, conversationId,
}: Pick<MessageAttributesType, 'conversationId'>): }: Pick<ReadonlyMessageAttributesType, 'conversationId'>):
| ConversationModel | ConversationModel
| undefined { | undefined {
return window.ConversationController.get(conversationId); return window.ConversationController.get(conversationId);

View file

@ -1,7 +1,7 @@
// Copyright 2023 Signal Messenger, LLC // Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // 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 type { LoggerType } from '../types/Logging';
import { assertDev } from './assert'; import { assertDev } from './assert';
@ -16,7 +16,7 @@ export function getMessageSentTimestamp(
sent_at: sentAt, sent_at: sentAt,
timestamp, timestamp,
}: Pick< }: Pick<
MessageAttributesType, ReadonlyMessageAttributesType,
'editMessageTimestamp' | 'sent_at' | 'timestamp' 'editMessageTimestamp' | 'sent_at' | 'timestamp'
>, >,
{ includeEdits = true, log }: GetMessageSentTimestampOptionsType { includeEdits = true, log }: GetMessageSentTimestampOptionsType

View file

@ -1,13 +1,13 @@
// Copyright 2023 Signal Messenger, LLC // Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { MessageAttributesType } from '../model-types.d'; import type { ReadonlyMessageAttributesType } from '../model-types.d';
export function getMessageSentTimestampSet({ export function getMessageSentTimestampSet({
sent_at: sentAt, sent_at: sentAt,
editHistory, editHistory,
}: Pick< }: Pick<
MessageAttributesType, ReadonlyMessageAttributesType,
'sent_at' | 'editHistory' 'sent_at' | 'editHistory'
>): ReadonlySet<number> { >): ReadonlySet<number> {
return new Set([ return new Set([

View file

@ -1,10 +1,10 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { MessageAttributesType } from '../model-types.d'; import type { ReadonlyMessageAttributesType } from '../model-types.d';
export function getMessageTimestamp( export function getMessageTimestamp(
message: Pick<MessageAttributesType, 'received_at' | 'received_at_ms'> message: Pick<ReadonlyMessageAttributesType, 'received_at' | 'received_at_ms'>
): number { ): number {
return message.received_at_ms || message.received_at; return message.received_at_ms || message.received_at;
} }

View file

@ -1,8 +1,10 @@
// Copyright 2022 Signal Messenger, LLC // Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { ReadonlyDeep } from 'type-fest';
import type { RawBodyRange } from '../types/BodyRange'; 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 type { ICUStringMessageParamsByKeyType } from '../types/Util';
import * as Attachment from '../types/Attachment'; import * as Attachment from '../types/Attachment';
import * as EmbeddedContact from '../types/EmbeddedContact'; import * as EmbeddedContact from '../types/EmbeddedContact';
@ -64,9 +66,9 @@ function getNameForNumber(e164: string): string {
} }
export function getNotificationDataForMessage( export function getNotificationDataForMessage(
attributes: MessageAttributesType attributes: ReadonlyMessageAttributesType
): { ): {
bodyRanges?: ReadonlyArray<RawBodyRange>; bodyRanges?: ReadonlyArray<ReadonlyDeep<RawBodyRange>>;
emoji?: string; emoji?: string;
text: string; text: string;
} { } {

View file

@ -1,7 +1,7 @@
// Copyright 2023 Signal Messenger, LLC // Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // 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 { applyRangesToText, hydrateRanges } from '../types/BodyRange';
import { findAndFormatContact } from './findAndFormatContact'; import { findAndFormatContact } from './findAndFormatContact';
import { getNotificationDataForMessage } from './getNotificationDataForMessage'; import { getNotificationDataForMessage } from './getNotificationDataForMessage';
@ -9,7 +9,7 @@ import { isConversationAccepted } from './isConversationAccepted';
import { strictAssert } from './assert'; import { strictAssert } from './assert';
export function getNotificationTextForMessage( export function getNotificationTextForMessage(
attributes: MessageAttributesType attributes: ReadonlyMessageAttributesType
): string { ): string {
const { text, emoji, bodyRanges } = getNotificationDataForMessage(attributes); const { text, emoji, bodyRanges } = getNotificationDataForMessage(attributes);

View file

@ -1,11 +1,11 @@
// Copyright 2023 Signal Messenger, LLC // Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // 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'; import * as EmbeddedContact from '../types/EmbeddedContact';
export function getQuoteBodyText( export function getQuoteBodyText(
messageAttributes: MessageAttributesType, messageAttributes: ReadonlyMessageAttributesType,
id: number id: number
): string | undefined { ): string | undefined {
const storyReactionEmoji = messageAttributes.storyReaction?.emoji; const storyReactionEmoji = messageAttributes.storyReaction?.emoji;

View file

@ -3,6 +3,7 @@
import type { ConversationAttributesType } from '../model-types'; import type { ConversationAttributesType } from '../model-types';
import type { RecipientsByConversation } from '../state/ducks/stories'; import type { RecipientsByConversation } from '../state/ducks/stories';
import type { ServiceIdString } from '../types/ServiceId';
import { getConversationMembers } from './getConversationMembers'; import { getConversationMembers } from './getConversationMembers';
import { isNotNil } from './isNotNil'; import { isNotNil } from './isNotNil';
@ -10,7 +11,12 @@ import { isNotNil } from './isNotNil';
export function getRecipientsByConversation( export function getRecipientsByConversation(
conversations: Array<ConversationAttributesType> conversations: Array<ConversationAttributesType>
): RecipientsByConversation { ): RecipientsByConversation {
const recipientsByConversation: RecipientsByConversation = {}; const recipientsByConversation: Record<
string,
{
serviceIds: Array<ServiceIdString>;
}
> = {};
conversations.forEach(attributes => { conversations.forEach(attributes => {
recipientsByConversation[attributes.id] = { recipientsByConversation[attributes.id] = {

View file

@ -1,7 +1,7 @@
// Copyright 2023 Signal Messenger, LLC // Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { MessageAttributesType } from '../model-types.d'; import type { ReadonlyMessageAttributesType } from '../model-types.d';
export function getSenderIdentifier({ export function getSenderIdentifier({
sent_at: sentAt, sent_at: sentAt,
@ -9,7 +9,7 @@ export function getSenderIdentifier({
sourceServiceId, sourceServiceId,
sourceDevice, sourceDevice,
}: Pick< }: Pick<
MessageAttributesType, ReadonlyMessageAttributesType,
'sent_at' | 'source' | 'sourceServiceId' | 'sourceDevice' 'sent_at' | 'source' | 'sourceServiceId' | 'sourceDevice'
>): string { >): string {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion

View file

@ -2,13 +2,13 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { partition } from 'lodash'; import { partition } from 'lodash';
import type { MessageAttributesType } from '../model-types.d'; import type { ReadonlyMessageAttributesType } from '../model-types.d';
import { isLongMessage } from '../types/MIME'; import { isLongMessage } from '../types/MIME';
// NOTE: If you're modifying this function then you'll likely also need // NOTE: If you're modifying this function then you'll likely also need
// to modify ./queueAttachmentDownloads // to modify ./queueAttachmentDownloads
export function hasAttachmentDownloads( export function hasAttachmentDownloads(
message: MessageAttributesType message: ReadonlyMessageAttributesType
): boolean { ): boolean {
const attachments = message.attachments || []; const attachments = message.attachments || [];
@ -83,7 +83,7 @@ export function hasAttachmentDownloads(
} }
function hasPreviewDownloads( function hasPreviewDownloads(
previews: MessageAttributesType['preview'] previews: ReadonlyMessageAttributesType['preview']
): boolean { ): boolean {
return (previews || []).some(item => { return (previews || []).some(item => {
if (!item.image) { if (!item.image) {
@ -98,7 +98,7 @@ function hasPreviewDownloads(
} }
function hasNormalAttachmentDownloads( function hasNormalAttachmentDownloads(
attachments: MessageAttributesType['attachments'] attachments: ReadonlyMessageAttributesType['attachments']
): boolean { ): boolean {
return (attachments || []).some(attachment => { return (attachments || []).some(attachment => {
if (!attachment) { if (!attachment) {

View file

@ -3,7 +3,7 @@
import type { import type {
ConversationAttributesType, ConversationAttributesType,
MessageAttributesType, ReadonlyMessageAttributesType,
} from '../model-types.d'; } from '../model-types.d';
import { import {
getSource, getSource,
@ -16,7 +16,7 @@ import type { ConversationType } from '../state/ducks/conversations';
export function getMessageIdForLogging( export function getMessageIdForLogging(
message: Pick< message: Pick<
MessageAttributesType, ReadonlyMessageAttributesType,
'type' | 'sourceServiceId' | 'sourceDevice' | 'sent_at' 'type' | 'sourceServiceId' | 'sourceDevice' | 'sent_at'
> >
): string { ): string {

View file

@ -2,8 +2,8 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { ReadStatus } from '../messages/MessageReadStatus'; import { ReadStatus } from '../messages/MessageReadStatus';
import type { MessageAttributesType } from '../model-types.d'; import type { ReadonlyMessageAttributesType } from '../model-types.d';
export const isMessageUnread = ( export const isMessageUnread = (
message: Readonly<Pick<MessageAttributesType, 'readStatus'>> message: Pick<ReadonlyMessageAttributesType, 'readStatus'>
): boolean => message.readStatus === ReadStatus.Unread; ): boolean => message.readStatus === ReadStatus.Unread;

View file

@ -1,12 +1,12 @@
// Copyright 2023 Signal Messenger, LLC // Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // 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 { DAY } from './durations';
export function isTooOldToModifyMessage( export function isTooOldToModifyMessage(
serverTimestamp: number, serverTimestamp: number,
message: MessageAttributesType message: Pick<ReadonlyMessageAttributesType, 'serverTimestamp' | 'sent_at'>
): boolean { ): boolean {
const messageTimestamp = message.serverTimestamp || message.sent_at || 0; const messageTimestamp = message.serverTimestamp || message.sent_at || 0;
const delta = Math.abs(serverTimestamp - messageTimestamp); const delta = Math.abs(serverTimestamp - messageTimestamp);

View file

@ -1,17 +1,17 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // 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 { createBatcher } from './batcher';
import { createWaitBatcher } from './waitBatcher'; import { createWaitBatcher } from './waitBatcher';
import { DataWriter } from '../sql/Client'; import { DataWriter } from '../sql/Client';
import * as log from '../logging/log'; import * as log from '../logging/log';
const updateMessageBatcher = createBatcher<MessageAttributesType>({ const updateMessageBatcher = createBatcher<ReadonlyMessageAttributesType>({
name: 'messageBatcher.updateMessageBatcher', name: 'messageBatcher.updateMessageBatcher',
wait: 75, wait: 75,
maxSize: 50, maxSize: 50,
processBatch: async (messageAttrs: Array<MessageAttributesType>) => { processBatch: async (messageAttrs: Array<ReadonlyMessageAttributesType>) => {
log.info('updateMessageBatcher', messageAttrs.length); log.info('updateMessageBatcher', messageAttrs.length);
// Grab the latest from the cache in case they've changed // Grab the latest from the cache in case they've changed
@ -27,7 +27,9 @@ const updateMessageBatcher = createBatcher<MessageAttributesType>({
let shouldBatch = true; let shouldBatch = true;
export function queueUpdateMessage(messageAttr: MessageAttributesType): void { export function queueUpdateMessage(
messageAttr: ReadonlyMessageAttributesType
): void {
if (shouldBatch) { if (shouldBatch) {
updateMessageBatcher.add(messageAttr); updateMessageBatcher.add(messageAttr);
} else { } else {
@ -41,21 +43,24 @@ export function setBatchingStrategy(keepBatching = false): void {
shouldBatch = keepBatching; shouldBatch = keepBatching;
} }
export const saveNewMessageBatcher = createWaitBatcher<MessageAttributesType>({ export const saveNewMessageBatcher =
name: 'messageBatcher.saveNewMessageBatcher', createWaitBatcher<ReadonlyMessageAttributesType>({
wait: 75, name: 'messageBatcher.saveNewMessageBatcher',
maxSize: 30, wait: 75,
processBatch: async (messageAttrs: Array<MessageAttributesType>) => { maxSize: 30,
log.info('saveNewMessageBatcher', messageAttrs.length); processBatch: async (
messageAttrs: Array<ReadonlyMessageAttributesType>
) => {
log.info('saveNewMessageBatcher', messageAttrs.length);
// Grab the latest from the cache in case they've changed // Grab the latest from the cache in case they've changed
const messagesToSave = messageAttrs.map( const messagesToSave = messageAttrs.map(
message => window.MessageCache.accessAttributes(message.id) ?? message message => window.MessageCache.accessAttributes(message.id) ?? message
); );
await DataWriter.saveMessages(messagesToSave, { await DataWriter.saveMessages(messagesToSave, {
forceSave: true, forceSave: true,
ourAci: window.textsecure.storage.user.getCheckedAci(), ourAci: window.textsecure.storage.user.getCheckedAci(),
}); });
}, },
}); });

View file

@ -2,15 +2,16 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { ConversationModel } from '../models/conversations'; 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 * as log from '../logging/log';
import { DataReader } from '../sql/Client'; import { DataReader } from '../sql/Client';
import { isGroup } from './whatTypeOfConversation'; import { isGroup } from './whatTypeOfConversation';
import { isMessageUnread } from './isMessageUnread'; import { isMessageUnread } from './isMessageUnread';
export async function shouldReplyNotifyUser( export async function shouldReplyNotifyUser(
messageAttributes: Readonly< messageAttributes: Pick<
Pick<MessageAttributesType, 'readStatus' | 'storyId'> ReadonlyMessageAttributesType,
'readStatus' | 'storyId'
>, >,
conversation: ConversationModel conversation: ConversationModel
): Promise<boolean> { ): Promise<boolean> {