signal-desktop/ts/messages/helpers.ts
2024-09-10 09:29:19 +10:00

239 lines
6.4 KiB
TypeScript

// 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,
ReadonlyMessageAttributesType,
QuotedAttachmentType,
QuotedMessageType,
} from '../model-types.d';
import type { ServiceIdString } from '../types/ServiceId';
import { PaymentEventKind } from '../types/Payment';
import type { AnyPaymentEvent } from '../types/Payment';
import type { LocalizerType } from '../types/Util';
import { missingCaseError } from '../util/missingCaseError';
export function isIncoming(
message: Pick<ReadonlyMessageAttributesType, 'type'>
): boolean {
return message.type === 'incoming';
}
export function isOutgoing(
message: Pick<ReadonlyMessageAttributesType, 'type'>
): boolean {
return message.type === 'outgoing';
}
export function isStory(
message: Pick<ReadonlyMessageAttributesType, 'type'>
): boolean {
return message.type === 'story';
}
export type MessageAttributesWithPaymentEvent = ReadonlyMessageAttributesType &
ReadonlyDeep<{
payment: AnyPaymentEvent;
}>;
export function messageHasPaymentEvent(
message: ReadonlyMessageAttributesType
): message is MessageAttributesWithPaymentEvent {
return message.payment != null;
}
export function getPaymentEventNotificationText(
payment: ReadonlyDeep<AnyPaymentEvent>,
senderTitle: string,
conversationTitle: string | null,
senderIsMe: boolean,
i18n: LocalizerType
): string {
if (payment.kind === PaymentEventKind.Notification) {
return i18n('icu:payment-event-notification-label');
}
return getPaymentEventDescription(
payment,
senderTitle,
conversationTitle,
senderIsMe,
i18n
);
}
export function getPaymentEventDescription(
payment: ReadonlyDeep<AnyPaymentEvent>,
senderTitle: string,
conversationTitle: string | null,
senderIsMe: boolean,
i18n: LocalizerType
): string {
const { kind } = payment;
if (kind === PaymentEventKind.Notification) {
if (senderIsMe) {
if (conversationTitle != null) {
return i18n('icu:payment-event-notification-message-you-label', {
receiver: conversationTitle,
});
}
return i18n(
'icu:payment-event-notification-message-you-label-without-receiver'
);
}
return i18n('icu:payment-event-notification-message-label', {
sender: senderTitle,
});
}
if (kind === PaymentEventKind.ActivationRequest) {
if (senderIsMe) {
if (conversationTitle != null) {
return i18n('icu:payment-event-activation-request-you-label', {
receiver: conversationTitle,
});
}
return i18n(
'icu:payment-event-activation-request-you-label-without-receiver'
);
}
return i18n('icu:payment-event-activation-request-label', {
sender: senderTitle,
});
}
if (kind === PaymentEventKind.Activation) {
if (senderIsMe) {
return i18n('icu:payment-event-activated-you-label');
}
return i18n('icu:payment-event-activated-label', {
sender: senderTitle,
});
}
throw missingCaseError(kind);
}
export function isQuoteAMatch(
message: ReadonlyMessageAttributesType | null | undefined,
conversationId: string,
quote: ReadonlyDeep<Pick<QuotedMessageType, 'id' | 'authorAci' | 'author'>>
): message is ReadonlyMessageAttributesType {
if (!message) {
return false;
}
const { authorAci, id } = quote;
const isSameTimestamp =
message.sent_at === id ||
message.editHistory?.some(({ timestamp }) => timestamp === id) ||
false;
return (
isSameTimestamp &&
message.conversationId === conversationId &&
getSourceServiceId(message) === authorAci
);
}
export const shouldTryToCopyFromQuotedMessage = ({
referencedMessageNotFound,
quoteAttachment,
}: {
referencedMessageNotFound: boolean;
quoteAttachment: ReadonlyDeep<QuotedAttachmentType> | undefined;
}): boolean => {
// If we've tried and can't find the message, try again.
if (referencedMessageNotFound === true) {
return true;
}
// Otherwise, try again in case we have not yet copied over the thumbnail from the
// original attachment (maybe it had not been downloaded when we first checked)
if (!quoteAttachment?.thumbnail) {
return false;
}
if (quoteAttachment.thumbnail.copied === true) {
return false;
}
return true;
};
export function getAuthorId(
message: Pick<
ReadonlyMessageAttributesType,
'type' | 'source' | 'sourceServiceId'
>
): string | undefined {
const source = getSource(message);
const sourceServiceId = getSourceServiceId(message);
if (!source && !sourceServiceId) {
return window.ConversationController.getOurConversationId();
}
const conversation = window.ConversationController.lookupOrCreate({
e164: source,
serviceId: sourceServiceId,
reason: 'helpers.getAuthorId',
});
return conversation?.id;
}
export function getAuthor(
message: ReadonlyMessageAttributesType
): ConversationModel | undefined {
const id = getAuthorId(message);
return window.ConversationController.get(id);
}
export function getSource(
message: Pick<ReadonlyMessageAttributesType, 'type' | 'source'>
): string | undefined {
if (isIncoming(message) || isStory(message)) {
return message.source;
}
if (!isOutgoing(message)) {
log.warn('Message.getSource: Called for non-incoming/non-outgoing message');
}
return window.textsecure.storage.user.getNumber();
}
export function getSourceDevice(
message: Pick<ReadonlyMessageAttributesType, 'type' | 'sourceDevice'>
): string | number | undefined {
const { sourceDevice } = message;
if (isIncoming(message) || isStory(message)) {
return sourceDevice;
}
if (!isOutgoing(message)) {
log.warn(
'Message.getSourceDevice: Called for non-incoming/non-outgoing message'
);
}
return sourceDevice || window.textsecure.storage.user.getDeviceId();
}
export function getSourceServiceId(
message: Pick<ReadonlyMessageAttributesType, 'type' | 'sourceServiceId'>
): ServiceIdString | undefined {
if (isIncoming(message) || isStory(message)) {
return message.sourceServiceId;
}
if (!isOutgoing(message)) {
log.warn(
'Message.getSourceServiceId: Called for non-incoming/non-outgoing message'
);
}
return window.textsecure.storage.user.getAci();
}
export const isCustomError = (e: unknown): e is CustomError =>
e instanceof Error;