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

View file

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

View file

@ -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<EmbeddedContactType>;
hasSignalAccount: boolean;
i18n: LocalizerType;
onSendMessage: () => void;

View file

@ -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<EmbeddedContactType>;
i18n: LocalizerType;
isIncoming: boolean;
withContentAbove: boolean;

View file

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

View file

@ -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<EmbeddedContactType>;
author: Pick<
ConversationType,
| 'acceptedMessageRequest'

View file

@ -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<EmbeddedContactType>;
i18n: LocalizerType;
size: 28 | 52 | 80;
direction?: 'outgoing' | 'incoming';
@ -65,7 +66,7 @@ export function renderName({
isIncoming,
module,
}: {
contact: EmbeddedContactType;
contact: ReadonlyDeep<EmbeddedContactType>;
isIncoming: boolean;
module: string;
}): JSX.Element {
@ -86,7 +87,7 @@ export function renderContactShorthand({
isIncoming,
module,
}: {
contact: EmbeddedContactType;
contact: ReadonlyDeep<EmbeddedContactType>;
isIncoming: boolean;
module: string;
}): JSX.Element {

View file

@ -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<MessageAttributesType, 'sent_at'>;
message: Pick<ReadonlyMessageAttributesType, 'sent_at'>;
attachment: AttachmentType;
index: number;
type: 'media' | 'documents';

View file

@ -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<T extends string | JSX.Element> =
>;
export function renderChange<T extends string | JSX.Element>(
change: GroupV2ChangeType,
change: ReadonlyDeep<GroupV2ChangeType>,
options: RenderOptionsType<T>
): RenderChangeResultType<T> {
const { details, from } = change;
@ -79,7 +80,7 @@ export function renderChange<T extends string | JSX.Element>(
}
function renderChangeDetail<T extends string | JSX.Element>(
detail: GroupV2ChangeDetailType,
detail: ReadonlyDeep<GroupV2ChangeDetailType>,
options: RenderOptionsType<T>
): string | T | ReadonlyArray<string | T> {
const {

View file

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

View file

@ -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<MessageAttributesType, 'type'>
message: Pick<ReadonlyMessageAttributesType, 'type'>
): boolean {
return message.type === 'incoming';
}
export function isOutgoing(
message: Pick<MessageAttributesType, 'type'>
message: Pick<ReadonlyMessageAttributesType, 'type'>
): boolean {
return message.type === 'outgoing';
}
export function isStory(message: Pick<MessageAttributesType, 'type'>): boolean {
export function isStory(
message: Pick<ReadonlyMessageAttributesType, 'type'>
): 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<AnyPaymentEvent>,
senderTitle: string,
conversationTitle: string | null,
senderIsMe: boolean,
@ -61,7 +66,7 @@ export function getPaymentEventNotificationText(
}
export function getPaymentEventDescription(
payment: AnyPaymentEvent,
payment: ReadonlyDeep<AnyPaymentEvent>,
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<QuotedMessageType, 'id' | 'authorAci' | 'author'>
): message is MessageAttributesType {
quote: ReadonlyDeep<Pick<QuotedMessageType, 'id' | 'authorAci' | 'author'>>
): message is ReadonlyMessageAttributesType {
if (!message) {
return false;
}
@ -142,7 +147,7 @@ export const shouldTryToCopyFromQuotedMessage = ({
quoteAttachment,
}: {
referencedMessageNotFound: boolean;
quoteAttachment: QuotedAttachmentType | undefined;
quoteAttachment: ReadonlyDeep<QuotedAttachmentType> | 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<MessageAttributesType, 'type' | 'source' | 'sourceServiceId'>
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<MessageAttributesType, 'type' | 'source'>
message: Pick<ReadonlyMessageAttributesType, 'type' | 'source'>
): string | undefined {
if (isIncoming(message) || isStory(message)) {
return message.source;
@ -201,7 +209,7 @@ export function getSource(
}
export function getSourceDevice(
message: Pick<MessageAttributesType, 'type' | 'sourceDevice'>
message: Pick<ReadonlyMessageAttributesType, 'type' | 'sourceDevice'>
): string | number | undefined {
const { sourceDevice } = message;
@ -218,7 +226,7 @@ export function getSourceDevice(
}
export function getSourceServiceId(
message: Pick<MessageAttributesType, 'type' | 'sourceServiceId'>
message: Pick<ReadonlyMessageAttributesType, 'type' | 'sourceServiceId'>
): ServiceIdString | undefined {
if (isIncoming(message) || isStory(message)) {
return message.sourceServiceId;

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

@ -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<MessageAttributesType>;
export type ConversationAttributesTypeType = 'private' | 'group';
export type ConversationLastProfileType = Readonly<{

View file

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

View file

@ -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<AttachmentType>
) => Promise<AttachmentWithHydratedData>;
loadContactData: (
contact: Array<EmbeddedContactType> | undefined
contact: ReadonlyArray<ReadonlyDeep<EmbeddedContactType>> | undefined
) => Promise<Array<EmbeddedContactWithHydratedAvatar> | undefined>;
loadMessage: (
message: MessageAttributesType
) => Promise<MessageAttributesType>;
loadPreviewData: (
preview: Array<LinkPreviewType> | undefined
preview: ReadonlyArray<ReadonlyDeep<LinkPreviewType>> | undefined
) => Promise<Array<LinkPreviewWithHydratedData>>;
loadQuoteData: (
quote: QuotedMessageType | null | undefined

View file

@ -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<MessageType>
): ReadonlyDeep<MessageType> {
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<MessageType>,
options: {
jobToInsert?: Readonly<StoredJob>;
forceSave?: boolean;
@ -607,7 +610,7 @@ async function saveMessage(
}
async function saveMessages(
arrayOfMessages: ReadonlyArray<MessageType>,
arrayOfMessages: ReadonlyArray<ReadonlyDeep<MessageType>>,
options: { forceSave?: boolean; ourAci: AciString }
): Promise<Array<string>> {
const result = await writableChannel.saveMessages(

View file

@ -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<MessageType>,
options: {
jobToInsert?: StoredJob;
forceSave?: boolean;
@ -738,7 +739,7 @@ type WritableInterface = {
}
) => string;
saveMessages: (
arrayOfMessages: ReadonlyArray<MessageType>,
arrayOfMessages: ReadonlyArray<ReadonlyDeep<MessageType>>,
options: { forceSave?: boolean; ourAci: AciString }
) => Array<string>;
@ -795,14 +796,14 @@ type WritableInterface = {
): CallLinkType;
migrateConversationMessages: (obsoleteId: string, currentId: string) => void;
saveEditedMessage: (
mainMessage: MessageType,
mainMessage: ReadonlyDeep<MessageType>,
ourAci: AciString,
opts: EditedMessageType
opts: ReadonlyDeep<EditedMessageType>
) => void;
saveEditedMessages: (
mainMessage: MessageType,
mainMessage: ReadonlyDeep<MessageType>,
ourAci: AciString,
history: ReadonlyArray<EditedMessageType>
history: ReadonlyArray<ReadonlyDeep<EditedMessageType>>
) => void;
removeSyncTaskById: (id: string) => void;
@ -1047,13 +1048,13 @@ export type ClientOnlyReadableInterface = ClientInterfaceWrap<{
getRecentStoryReplies(
storyId: string,
options?: GetRecentStoryRepliesOptionsType
): Array<MessageAttributesType>;
): Array<MessageType>;
getOlderMessagesByConversation: (
options: AdjacentMessagesByConversationOptionsType
) => Array<MessageAttributesType>;
) => Array<MessageType>;
getNewerMessagesByConversation: (
options: AdjacentMessagesByConversationOptionsType
) => Array<MessageAttributesType>;
) => Array<MessageType>;
getConversationRangeCenteredOnMessage: (
options: AdjacentMessagesByConversationOptionsType
) => GetConversationRangeCenteredOnMessageResultType<MessageType>;

View file

@ -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<SyncTaskType> {
export function saveMessage(
db: WritableDB,
data: MessageType,
data: ReadonlyDeep<MessageType>,
options: {
alreadyInTransaction?: boolean;
forceSave?: boolean;
@ -2337,7 +2338,7 @@ export function saveMessage(
function saveMessages(
db: WritableDB,
arrayOfMessages: ReadonlyArray<MessageType>,
arrayOfMessages: ReadonlyArray<ReadonlyDeep<MessageType>>,
options: { forceSave?: boolean; ourAci: AciString }
): Array<string> {
return db.transaction(() => {
@ -6964,9 +6965,9 @@ function removeAllProfileKeyCredentials(db: WritableDB): void {
function saveEditedMessages(
db: WritableDB,
mainMessage: MessageType,
mainMessage: ReadonlyDeep<MessageType>,
ourAci: AciString,
history: ReadonlyArray<EditedMessageType>
history: ReadonlyArray<ReadonlyDeep<EditedMessageType>>
): void {
db.transaction(() => {
saveMessage(db, mainMessage, {
@ -6996,9 +6997,9 @@ function saveEditedMessages(
function saveEditedMessage(
db: WritableDB,
mainMessage: MessageType,
mainMessage: ReadonlyDeep<MessageType>,
ourAci: AciString,
editedMessage: EditedMessageType
editedMessage: ReadonlyDeep<EditedMessageType>
): void {
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 { 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<MessageAttributesType, 'conversationId' | 'quote'>;
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'
>;

View file

@ -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<MessageAttributesType, 'sent_at' | 'received_at'>
Pick<ReadonlyMessageAttributesType, 'sent_at' | 'received_at'>
>;
// 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<number, boolean>;
};
export type MessageType = ReadonlyDeep<
ReadonlyMessageAttributesType & {
interactionType?: InteractionModeType;
}
>;
export type MessageWithUIFieldsType = ReadonlyDeep<
ReadonlyMessageAttributesType & {
displayLimit?: number;
isSpoilerExpanded?: Record<number, boolean>;
}
>;
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<ServiceIdString>;
conversationLookup: ConversationLookupType;
@ -530,7 +531,7 @@ export type ConversationsStateType = Readonly<{
stack: ReadonlyArray<PanelRenderType>;
watermark: number;
};
targetedMessageForDetails?: MessageAttributesType;
targetedMessageForDetails?: ReadonlyMessageAttributesType;
lastSelectedMessage: MessageTimestamps | undefined;
selectedMessageIds: ReadonlyArray<string> | undefined;
@ -771,15 +772,14 @@ type ConversationStoppedByMissingVerificationActionType = ReadonlyDeep<{
untrustedServiceIds: ReadonlyArray<ServiceIdString>;
};
}>;
// 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<MessageAttributesType>;
messages: ReadonlyArray<ReadonlyMessageAttributesType>;
};
}>;
@ -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<MessageAttributesType>;
messages: ReadonlyArray<ReadonlyMessageAttributesType>;
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<MessageAttributesType>;
messages: ReadonlyArray<ReadonlyMessageAttributesType>;
}): ThunkAction<void, RootStateType, unknown, MessagesAddedActionType> {
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<MessageAttributesType>;
messages: ReadonlyArray<ReadonlyMessageAttributesType>;
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 {

View file

@ -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<MessageAttributesType>
Array<ReadonlyMessageAttributesType>
>;
export type EditNicknameAndNoteModalPropsType = ReadonlyDeep<{
conversationId: string;
@ -882,7 +882,7 @@ function showShortcutGuideModal(): ShowShortcutGuideModalActionType {
}
function copyOverMessageAttributesIntoEditHistory(
messageAttributes: ReadonlyDeep<MessageAttributesType>
messageAttributes: ReadonlyDeep<ReadonlyMessageAttributesType>
): EditHistoryMessagesType | undefined {
if (!messageAttributes.editHistory) {
return;
@ -934,7 +934,7 @@ function closeEditHistoryModal(): CloseEditHistoryModalActionType {
function copyOverMessageAttributesIntoForwardMessages(
messageDrafts: ReadonlyArray<MessageForwardDraft>,
attributes: ReadonlyDeep<MessageAttributesType>
attributes: ReadonlyDeep<ReadonlyMessageAttributesType>
): ReadonlyArray<MessageForwardDraft> {
return messageDrafts.map(messageDraft => {
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 { __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<AttachmentType> {
return (attributes.attachments ?? []).filter(
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 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<ServiceIdString>;
export type RecipientEntry = ReadonlyDeep<{
serviceIds: Array<ServiceIdString>;
byDistributionId?: Record<
StoryDistributionIdString,
{
serviceIds: Array<ServiceIdString>;
}
>;
}
byDistributionId?: Record<
StoryDistributionIdString,
{
serviceIds: Array<ServiceIdString>;
}
>;
}>;
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<MessageAttributesType>;
replies: Array<ReadonlyMessageAttributesType>;
}>;
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<MessageAttributesType>;
replies: Array<ReadonlyMessageAttributesType>;
};
};
}>;
type MarkStoryReadActionType = ReadonlyDeep<{
type: typeof MARK_STORY_READ;

View file

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

View file

@ -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<MessageAttributesType, 'type' | 'source'>,
message: Pick<ReadonlyMessageAttributesType, 'type' | 'source'>,
ourNumber: string | undefined
): string | undefined {
if (isIncoming(message)) {
@ -233,7 +233,7 @@ export function getSourceDevice(
}
export function getSourceServiceId(
message: Pick<MessageAttributesType, 'type' | 'sourceServiceId'>,
message: Pick<ReadonlyMessageAttributesType, 'type' | 'sourceServiceId'>,
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<EmbeddedContactType> | 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';

View file

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

View file

@ -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<MessageChangedActionType> {
return {
type: 'MESSAGE_CHANGED',
payload: {

View file

@ -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<EmbeddedContactType>,
options: {
regionCode?: string;
firstNumber?: string;
serviceId?: ServiceIdString;
}
): EmbeddedContactType {
): ReadonlyDeep<EmbeddedContactType> {
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<EmbeddedContactType>
): 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}`;
}

View file

@ -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<T>(
items: ReadonlyArray<T>,
getMesssage: (
item: T
) => Pick<MessageAttributesType, 'sent_at' | 'received_at'> | null
) => Pick<ReadonlyMessageAttributesType, 'sent_at' | 'received_at'> | null
): Array<T> {
return orderBy(
items,

View file

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

View file

@ -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<EmbeddedContactType> | undefined
contact: ReadonlyArray<ReadonlyDeep<EmbeddedContactType>> | undefined
) => Promise<Array<EmbeddedContactWithHydratedAvatar> | undefined>) => {
if (!isFunction(loadAttachmentData)) {
throw new TypeError('loadContactData: loadAttachmentData is required');
}
return async (
contact: Array<EmbeddedContactType> | undefined
contact: ReadonlyArray<ReadonlyDeep<EmbeddedContactType>> | undefined
): Promise<Array<EmbeddedContactWithHydratedAvatar> | undefined> => {
if (!contact) {
return undefined;
@ -847,27 +849,23 @@ export const loadContactData = (
return Promise.all(
contact.map(
async (
item: EmbeddedContactType
item: ReadonlyDeep<EmbeddedContactType>
): Promise<EmbeddedContactWithHydratedAvatar> => {
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<LinkPreviewType> | undefined
preview: ReadonlyArray<ReadonlyDeep<LinkPreviewType>> | undefined
) => Promise<Array<LinkPreviewWithHydratedData>>) => {
if (!isFunction(loadAttachmentData)) {
throw new TypeError('loadPreviewData: loadAttachmentData is required');
}
return async (preview: Array<LinkPreviewType> | undefined) => {
return async (
preview: ReadonlyArray<ReadonlyDeep<LinkPreviewType>> | undefined
) => {
if (!preview || !preview.length) {
return [];
}
@ -894,17 +894,19 @@ export const loadPreviewData = (
return Promise.all(
preview.map(
async (item: LinkPreviewType): Promise<LinkPreviewWithHydratedData> => {
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),
};
}
)

View file

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

View file

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

View file

@ -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<string, RecipientEntry> = {};
Object.entries(byConversation).forEach(
([conversationId, conversationData]) => {
const conversationFiltered = conversationData.serviceIds

View file

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

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
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<EditHistoryType>) => item.timestamp
);
const { length } = mostRecent;

View file

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

View file

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

View file

@ -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<MessageAttributesType, 'conversationId'>):
}: Pick<ReadonlyMessageAttributesType, 'conversationId'>):
| ConversationModel
| undefined {
return window.ConversationController.get(conversationId);

View file

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

View file

@ -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<number> {
return new Set([

View file

@ -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<MessageAttributesType, 'received_at' | 'received_at_ms'>
message: Pick<ReadonlyMessageAttributesType, 'received_at' | 'received_at_ms'>
): number {
return message.received_at_ms || message.received_at;
}

View file

@ -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<RawBodyRange>;
bodyRanges?: ReadonlyArray<ReadonlyDeep<RawBodyRange>>;
emoji?: string;
text: string;
} {

View file

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

View file

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

View file

@ -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<ConversationAttributesType>
): RecipientsByConversation {
const recipientsByConversation: RecipientsByConversation = {};
const recipientsByConversation: Record<
string,
{
serviceIds: Array<ServiceIdString>;
}
> = {};
conversations.forEach(attributes => {
recipientsByConversation[attributes.id] = {

View file

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

View file

@ -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) {

View file

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

View file

@ -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<Pick<MessageAttributesType, 'readStatus'>>
message: Pick<ReadonlyMessageAttributesType, 'readStatus'>
): boolean => message.readStatus === ReadStatus.Unread;

View file

@ -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<ReadonlyMessageAttributesType, 'serverTimestamp' | 'sent_at'>
): boolean {
const messageTimestamp = message.serverTimestamp || message.sent_at || 0;
const delta = Math.abs(serverTimestamp - messageTimestamp);

View file

@ -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<MessageAttributesType>({
const updateMessageBatcher = createBatcher<ReadonlyMessageAttributesType>({
name: 'messageBatcher.updateMessageBatcher',
wait: 75,
maxSize: 50,
processBatch: async (messageAttrs: Array<MessageAttributesType>) => {
processBatch: async (messageAttrs: Array<ReadonlyMessageAttributesType>) => {
log.info('updateMessageBatcher', messageAttrs.length);
// Grab the latest from the cache in case they've changed
@ -27,7 +27,9 @@ const updateMessageBatcher = createBatcher<MessageAttributesType>({
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<MessageAttributesType>({
name: 'messageBatcher.saveNewMessageBatcher',
wait: 75,
maxSize: 30,
processBatch: async (messageAttrs: Array<MessageAttributesType>) => {
log.info('saveNewMessageBatcher', messageAttrs.length);
export const saveNewMessageBatcher =
createWaitBatcher<ReadonlyMessageAttributesType>({
name: 'messageBatcher.saveNewMessageBatcher',
wait: 75,
maxSize: 30,
processBatch: async (
messageAttrs: Array<ReadonlyMessageAttributesType>
) => {
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(),
});
},
});

View file

@ -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<MessageAttributesType, 'readStatus' | 'storyId'>
messageAttributes: Pick<
ReadonlyMessageAttributesType,
'readStatus' | 'storyId'
>,
conversation: ConversationModel
): Promise<boolean> {