From af8e7a39a50330da4dfc352d49e30850a8a85517 Mon Sep 17 00:00:00 2001 From: Jamie <113370520+jamiebuilds-signal@users.noreply.github.com> Date: Wed, 1 Jul 2026 10:03:17 -0700 Subject: [PATCH] Fix polls terminate notification title --- ts/messageModifiers/Polls.preload.ts | 2 + ts/messageModifiers/Reactions.preload.ts | 2 + ts/messages/maybeNotify.preload.ts | 152 ++++++++++++++++------- ts/messages/saveAndNotify.preload.ts | 8 +- ts/models/conversations.preload.ts | 17 ++- 5 files changed, 134 insertions(+), 47 deletions(-) diff --git a/ts/messageModifiers/Polls.preload.ts b/ts/messageModifiers/Polls.preload.ts index 19ecd647dd..925a2d5ef0 100644 --- a/ts/messageModifiers/Polls.preload.ts +++ b/ts/messageModifiers/Polls.preload.ts @@ -484,6 +484,7 @@ export async function handlePollVote( if (shouldMarkAsUnread) { drop( maybeNotify({ + kind: 'pollVote', pollVote: vote, targetMessage: message.attributes, conversation: conversationContainingThisPoll, @@ -576,6 +577,7 @@ export async function handlePollTerminate( await conversation.addPollTerminateNotification({ pollQuestion: poll.question, pollTimestamp: message.attributes.timestamp, + pollSource: terminate.source, terminatorId: terminate.fromConversationId, timestamp: terminate.timestamp, isMeTerminating: isMe(author.attributes), diff --git a/ts/messageModifiers/Reactions.preload.ts b/ts/messageModifiers/Reactions.preload.ts index 44500e4b41..d77c1d5104 100644 --- a/ts/messageModifiers/Reactions.preload.ts +++ b/ts/messageModifiers/Reactions.preload.ts @@ -439,6 +439,7 @@ export async function handleReaction( if (isFromSomeoneElse) { drop( maybeNotify({ + kind: 'normalMessage', message: generatedMessage.attributes, conversation: targetConversation, }) @@ -517,6 +518,7 @@ export async function handleReaction( if (isOutgoing(message.attributes) && isFromSomeoneElse) { drop( maybeNotify({ + kind: 'reaction', targetMessage: message.attributes, conversation, reaction, diff --git a/ts/messages/maybeNotify.preload.ts b/ts/messages/maybeNotify.preload.ts index caaeb49134..113f379b12 100644 --- a/ts/messages/maybeNotify.preload.ts +++ b/ts/messages/maybeNotify.preload.ts @@ -24,29 +24,56 @@ import { import { shouldStoryReplyNotifyUser } from '../util/shouldStoryReplyNotifyUser.preload.ts'; import { ReactionSource } from '../reactions/ReactionSource.std.ts'; import { itemStorage } from '../textsecure/Storage.preload.ts'; +import { missingCaseError } from '../util/missingCaseError.std.ts'; const log = createLogger('maybeNotify'); +type ReactionNotifyData = Readonly<{ + kind: 'reaction'; + reaction: Readonly; + targetMessage: Readonly; +}>; + +type PollVoteNotifyData = Readonly<{ + kind: 'pollVote'; + pollVote: Readonly; + targetMessage: Readonly; +}>; + +type PollTerminateNotifyData = Readonly<{ + kind: 'pollTerminate'; + pollSource: PollSource; + pollTerminatorId: string; + message: Readonly; +}>; + +type DeliveryIssueNotifyData = Readonly<{ + kind: 'deliveryIssue'; + message: Readonly; +}>; + +type NormalMessageNotifyData = Readonly<{ + kind: 'normalMessage'; + message: Readonly; +}>; + +type NotifyData = + | ReactionNotifyData + | PollVoteNotifyData + | PollTerminateNotifyData + | DeliveryIssueNotifyData + | NormalMessageNotifyData; + type MaybeNotifyArgs = { conversation: ConversationModel; -} & ( - | { - reaction: Readonly; - targetMessage: Readonly; - } - | { - pollVote: Readonly; - targetMessage: Readonly; - } - | { - message: Readonly; - reaction?: never; - pollVote?: never; - } -); +} & NotifyData; function isMentionOrReply(args: MaybeNotifyArgs): boolean { - if ('reaction' in args || 'pollVote' in args) { + if ( + args.kind === 'reaction' || + args.kind === 'pollVote' || + args.kind === 'pollTerminate' + ) { return false; } @@ -70,25 +97,34 @@ export async function maybeNotify(args: MaybeNotifyArgs): Promise { const { i18n } = window.SignalContext; const { conversation } = args; - const reaction = 'reaction' in args ? args.reaction : undefined; - const pollVote = 'pollVote' in args ? args.pollVote : undefined; let warrantsNotification: boolean; - if ('reaction' in args && 'targetMessage' in args) { + if (args.kind === 'reaction') { warrantsNotification = doesReactionWarrantNotification({ reaction: args.reaction, targetMessage: args.targetMessage, }); - } else if ('pollVote' in args && 'targetMessage' in args) { + } else if (args.kind === 'pollVote') { warrantsNotification = doesPollVoteWarrantNotification({ pollVote: args.pollVote, targetMessage: args.targetMessage, }); - } else { + } else if (args.kind === 'pollTerminate') { + warrantsNotification = doesPollTerminateWarrantNotification({ + pollSource: args.pollSource, + }); + } else if (args.kind === 'deliveryIssue') { warrantsNotification = await doesMessageWarrantNotification({ message: args.message, conversation, }); + } else if (args.kind === 'normalMessage') { + warrantsNotification = await doesMessageWarrantNotification({ + message: args.message, + conversation, + }); + } else { + throw missingCaseError(args); } if (!warrantsNotification) { @@ -114,20 +150,35 @@ export async function maybeNotify(args: MaybeNotifyArgs): Promise { } const conversationId = conversation.get('id'); - const messageForNotification = - 'targetMessage' in args ? args.targetMessage : args.message; + const isMessageInDirectConversation = isDirectConversation( conversation.attributes ); let sender: ConversationModel | undefined; - if (reaction) { - sender = window.ConversationController.get(reaction.fromId); - } else if (pollVote) { - sender = window.ConversationController.get(pollVote.fromConversationId); - } else if ('message' in args) { + let messageForNotification: MessageAttributesType | undefined; + + if (args.kind === 'reaction') { + sender = window.ConversationController.get(args.reaction.fromId); + messageForNotification = args.targetMessage; + } else if (args.kind === 'pollVote') { + sender = window.ConversationController.get( + args.pollVote.fromConversationId + ); + messageForNotification = args.targetMessage; + } else if (args.kind === 'pollTerminate') { + sender = window.ConversationController.get(args.pollTerminatorId); + messageForNotification = args.message; + } else if (args.kind === 'deliveryIssue') { sender = getAuthor(args.message); + messageForNotification = args.message; + } else if (args.kind === 'normalMessage') { + sender = getAuthor(args.message); + messageForNotification = args.message; + } else { + throw missingCaseError(args); } + const senderName = sender ? sender.getTitle() : i18n('icu:unknownContact'); const senderTitle = isMessageInDirectConversation ? senderName @@ -151,22 +202,27 @@ export async function maybeNotify(args: MaybeNotifyArgs): Promise { isExpiringMessage: isExpiringMessage(messageForNotification), message: getNotificationTextForMessage(messageForNotification), messageId, - reaction: reaction - ? { - emoji: reaction.emoji, - targetAuthorAci: reaction.targetAuthorAci, - targetTimestamp: reaction.targetTimestamp, - } - : undefined, - pollVote: pollVote - ? { - voterConversationId: pollVote.fromConversationId, - targetAuthorAci: pollVote.targetAuthorAci, - targetTimestamp: pollVote.targetTimestamp, - } - : undefined, + reaction: + args.kind === 'reaction' + ? { + emoji: args.reaction.emoji, + targetAuthorAci: args.reaction.targetAuthorAci, + targetTimestamp: args.reaction.targetTimestamp, + } + : undefined, + pollVote: + args.kind === 'pollVote' + ? { + voterConversationId: args.pollVote.fromConversationId, + targetAuthorAci: args.pollVote.targetAuthorAci, + targetTimestamp: args.pollVote.targetTimestamp, + } + : undefined, sentAt: messageForNotification.timestamp, - type: reaction ? NotificationType.Reaction : NotificationType.Message, + type: + args.kind === 'reaction' + ? NotificationType.Reaction + : NotificationType.Message, }); } @@ -195,6 +251,14 @@ function doesPollVoteWarrantNotification({ ); } +function doesPollTerminateWarrantNotification({ + pollSource, +}: { + pollSource: Readonly; +}): boolean { + return pollSource === PollSource.FromSomeoneElse; +} + async function doesMessageWarrantNotification({ message, conversation, @@ -202,7 +266,7 @@ async function doesMessageWarrantNotification({ message: MessageAttributesType; conversation: ConversationModel; }): Promise { - if (!(message.type === 'incoming' || message.type === 'poll-terminate')) { + if (message.type !== 'incoming') { return false; } diff --git a/ts/messages/saveAndNotify.preload.ts b/ts/messages/saveAndNotify.preload.ts index 25621cdcdc..c103638948 100644 --- a/ts/messages/saveAndNotify.preload.ts +++ b/ts/messages/saveAndNotify.preload.ts @@ -50,7 +50,13 @@ export async function saveAndNotify( drop(conversation.onNewMessage(message)); - drop(maybeNotify({ message: message.attributes, conversation })); + drop( + maybeNotify({ + kind: 'normalMessage', + message: message.attributes, + conversation, + }) + ); // Increment the sent message count if this is an outgoing message if (message.get('type') === 'outgoing') { diff --git a/ts/models/conversations.preload.ts b/ts/models/conversations.preload.ts index 2e08412b39..12e19c3be7 100644 --- a/ts/models/conversations.preload.ts +++ b/ts/models/conversations.preload.ts @@ -277,6 +277,7 @@ import { isUsernameValid } from '../util/Username.dom.ts'; import type { Emoji } from '../axo/emoji.std.ts'; import { canConversationOnlyBeMutedAlways } from '../conversations/canConversationOnlyBeMutedAlways.dom.ts'; import { keyTransparency } from '../services/keyTransparency.preload.ts'; +import type { PollSource } from '../messageModifiers/Polls.preload.ts'; const { compact, isNumber, throttle, debounce } = lodash; @@ -3349,7 +3350,11 @@ export class ConversationModel { drop(this.onNewMessage(message)); this.throttledUpdateUnread(); - await maybeNotify({ message: message.attributes, conversation: this }); + await maybeNotify({ + kind: 'deliveryIssue', + message: message.attributes, + conversation: this, + }); } async addKeyChange( @@ -3572,6 +3577,7 @@ export class ConversationModel { async addPollTerminateNotification(params: { pollQuestion: string; pollTimestamp: number; + pollSource: PollSource; terminatorId: string; timestamp: number; isMeTerminating: boolean; @@ -3608,7 +3614,14 @@ export class ConversationModel { drop(this.onNewMessage(message)); this.throttledUpdateUnread(); - await maybeNotify({ message: message.attributes, conversation: this }); + + await maybeNotify({ + kind: 'pollTerminate', + pollSource: params.pollSource, + pollTerminatorId: params.terminatorId, + message: message.attributes, + conversation: this, + }); } async addPinnedMessageNotification(params: {