From ad8020848f356853d419d8065ccf15cf3f656641 Mon Sep 17 00:00:00 2001 From: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com> Date: Fri, 12 Apr 2024 10:07:57 -0700 Subject: [PATCH] Add unblocked timeline event --- _locales/en/messages.json | 12 ++ .../MessageRequestResponseNotification.tsx | 26 ++- .../conversation/Timeline.stories.tsx | 1 + ts/components/conversation/Timeline.tsx | 4 + .../conversation/TimelineItem.stories.tsx | 1 + ts/components/conversation/TimelineItem.tsx | 3 + ts/models/conversations.ts | 7 +- ts/sql/migrations/1030-unblock-event.ts | 87 ++++++++++ ts/sql/migrations/index.ts | 6 +- ts/state/smart/Timeline.tsx | 2 + ts/state/smart/TimelineItem.tsx | 3 + ts/test-node/sql/migration_1030_test.ts | 158 ++++++++++++++++++ ts/types/MessageRequestResponseEvent.ts | 1 + ts/util/getNotificationDataForMessage.ts | 32 +++- 14 files changed, 333 insertions(+), 10 deletions(-) create mode 100644 ts/sql/migrations/1030-unblock-event.ts create mode 100644 ts/test-node/sql/migration_1030_test.ts diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 824b46806142..462519e93b89 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -6528,6 +6528,18 @@ "messageformat": "You blocked this person", "description": "Message request response notification message when the user blocked another user" }, + "icu:MessageRequestResponseNotification__Message--Blocked--Group": { + "messageformat": "You blocked the group", + "description": "Message request response notification message when the user blocked a group" + }, + "icu:MessageRequestResponseNotification__Message--Unblocked": { + "messageformat": "You unblocked this person", + "description": "Message request response notification message when the user unblocked another user" + }, + "icu:MessageRequestResponseNotification__Message--Unblocked--Group": { + "messageformat": "You unblocked the group", + "description": "Message request response notification message when the user unblocked a group" + }, "icu:MessageRequestResponseNotification__Button--Options": { "messageformat": "Options", "description": "Message request response notification button to show options" diff --git a/ts/components/conversation/MessageRequestResponseNotification.tsx b/ts/components/conversation/MessageRequestResponseNotification.tsx index fa2b409735b7..9af023c46888 100644 --- a/ts/components/conversation/MessageRequestResponseNotification.tsx +++ b/ts/components/conversation/MessageRequestResponseNotification.tsx @@ -16,12 +16,14 @@ export type MessageRequestResponseNotificationProps = MessageRequestResponseNotificationData & { i18n: LocalizerType; isBlocked: boolean; + isGroup: boolean; onOpenMessageRequestActionsConfirmation(state: MessageRequestState): void; }; export function MessageRequestResponseNotification({ i18n, isBlocked, + isGroup, messageRequestResponseEvent: event, onOpenMessageRequestActionsConfirmation, }: MessageRequestResponseNotificationProps): JSX.Element | null { @@ -58,9 +60,27 @@ export function MessageRequestResponseNotification({ {event === MessageRequestResponseEvent.BLOCK && ( + )} + {event === MessageRequestResponseEvent.UNBLOCK && ( + )} {event === MessageRequestResponseEvent.SPAM && ( diff --git a/ts/components/conversation/Timeline.stories.tsx b/ts/components/conversation/Timeline.stories.tsx index c283d7994063..316ad4977f09 100644 --- a/ts/components/conversation/Timeline.stories.tsx +++ b/ts/components/conversation/Timeline.stories.tsx @@ -355,6 +355,7 @@ const renderItem = ({ id="" isTargeted={false} isBlocked={false} + isGroup={false} i18n={i18n} interactionMode="keyboard" isNextItemCallingNotification={false} diff --git a/ts/components/conversation/Timeline.tsx b/ts/components/conversation/Timeline.tsx index e805cfd05a20..9366f129fb8b 100644 --- a/ts/components/conversation/Timeline.tsx +++ b/ts/components/conversation/Timeline.tsx @@ -124,6 +124,7 @@ type PropsHousekeepingType = { containerWidthBreakpoint: WidthBreakpoint; conversationId: string; isBlocked: boolean; + isGroup: boolean; isOldestTimelineItem: boolean; messageId: string; nextMessageId: undefined | string; @@ -804,6 +805,7 @@ export class Timeline extends React.Component< acknowledgeGroupMemberNameCollisions, clearInvitedServiceIdsForNewlyCreatedGroup, closeContactSpoofingReview, + conversationType, hasContactSpoofingReview, getPreferredBadge, getTimestampForMessage, @@ -847,6 +849,7 @@ export class Timeline extends React.Component< return null; } + const isGroup = conversationType === 'group'; const areThereAnyMessages = items.length > 0; const areAnyMessagesUnread = Boolean(unreadCount); const areAnyMessagesBelowCurrentPosition = @@ -956,6 +959,7 @@ export class Timeline extends React.Component< containerWidthBreakpoint: widthBreakpoint, conversationId: id, isBlocked, + isGroup, isOldestTimelineItem: haveOldest && itemIndex === 0, messageId, nextMessageId, diff --git a/ts/components/conversation/TimelineItem.stories.tsx b/ts/components/conversation/TimelineItem.stories.tsx index 72345db9937d..1e3828c6b9d7 100644 --- a/ts/components/conversation/TimelineItem.stories.tsx +++ b/ts/components/conversation/TimelineItem.stories.tsx @@ -61,6 +61,7 @@ const getDefaultProps = () => ({ isNextItemCallingNotification: false, isTargeted: false, isBlocked: false, + isGroup: false, interactionMode: 'keyboard' as const, theme: ThemeType.light, platform: 'darwin', diff --git a/ts/components/conversation/TimelineItem.tsx b/ts/components/conversation/TimelineItem.tsx index cb194e73fff9..4883a8f13e61 100644 --- a/ts/components/conversation/TimelineItem.tsx +++ b/ts/components/conversation/TimelineItem.tsx @@ -177,6 +177,7 @@ type PropsLocalType = { item?: TimelineItemType; id: string; isBlocked: boolean; + isGroup: boolean; isNextItemCallingNotification: boolean; isTargeted: boolean; targetMessage: (messageId: string, conversationId: string) => unknown; @@ -217,6 +218,7 @@ export const TimelineItem = memo(function TimelineItem({ i18n, id, isBlocked, + isGroup, isNextItemCallingNotification, isTargeted, item, @@ -401,6 +403,7 @@ export const TimelineItem = memo(function TimelineItem({ = 1030) { + return; + } + + db.transaction(() => { + // From migration 81 + const shouldAffectActivityOrPreview = sqlFragment` + type IS NULL + OR + type NOT IN ( + 'change-number-notification', + 'contact-removed-notification', + 'conversation-merge', + 'group-v1-migration', + 'keychange', + 'message-history-unsynced', + 'profile-change', + 'story', + 'universal-timer-notification', + 'verified-change' + ) + AND NOT ( + type IS 'message-request-response-event' + AND json_extract(json, '$.messageRequestResponseEvent') IN ('ACCEPT', 'BLOCK', 'UNBLOCK') + ) + `; + + const [updateShouldAffectPreview] = sql` + --- These will be re-added below + DROP INDEX messages_preview; + DROP INDEX messages_preview_without_story; + DROP INDEX messages_activity; + DROP INDEX message_user_initiated; + + --- These will also be re-added below + ALTER TABLE messages DROP COLUMN shouldAffectActivity; + ALTER TABLE messages DROP COLUMN shouldAffectPreview; + + --- (change: added message-request-response-event->ACCEPT/BLOCK/UNBLOCK) + ALTER TABLE messages + ADD COLUMN shouldAffectPreview INTEGER + GENERATED ALWAYS AS (${shouldAffectActivityOrPreview}); + ALTER TABLE messages + ADD COLUMN shouldAffectActivity INTEGER + GENERATED ALWAYS AS (${shouldAffectActivityOrPreview}); + + --- From migration 88 + CREATE INDEX messages_preview ON messages + (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, + received_at, sent_at); + + --- From migration 88 + CREATE INDEX messages_preview_without_story ON messages + (conversationId, shouldAffectPreview, isGroupLeaveEventFromOther, + received_at, sent_at) WHERE storyId IS NULL; + + --- From migration 88 + CREATE INDEX messages_activity ON messages + (conversationId, shouldAffectActivity, isTimerChangeFromSync, + isGroupLeaveEventFromOther, received_at, sent_at); + + --- From migration 81 + CREATE INDEX message_user_initiated ON messages (conversationId, isUserInitiatedMessage); + `; + + db.exec(updateShouldAffectPreview); + + db.pragma('user_version = 1030'); + })(); + + logger.info('updateToSchemaVersion1030: success!'); +} diff --git a/ts/sql/migrations/index.ts b/ts/sql/migrations/index.ts index f0fbe7f70438..e37c5bec503d 100644 --- a/ts/sql/migrations/index.ts +++ b/ts/sql/migrations/index.ts @@ -77,10 +77,11 @@ import { updateToSchemaVersion980 } from './980-reaction-timestamp'; import { updateToSchemaVersion990 } from './990-phone-number-sharing'; import { updateToSchemaVersion1000 } from './1000-mark-unread-call-history-messages-as-unseen'; import { updateToSchemaVersion1010 } from './1010-call-links-table'; +import { updateToSchemaVersion1020 } from './1020-self-merges'; import { version as MAX_VERSION, - updateToSchemaVersion1020, -} from './1020-self-merges'; + updateToSchemaVersion1030, +} from './1030-unblock-event'; function updateToSchemaVersion1( currentVersion: number, @@ -2025,6 +2026,7 @@ export const SCHEMA_VERSIONS = [ updateToSchemaVersion1000, updateToSchemaVersion1010, updateToSchemaVersion1020, + updateToSchemaVersion1030, ]; export class DBVersionFromFutureError extends Error { diff --git a/ts/state/smart/Timeline.tsx b/ts/state/smart/Timeline.tsx index 1a982c4a4b59..4f0621d2ad0f 100644 --- a/ts/state/smart/Timeline.tsx +++ b/ts/state/smart/Timeline.tsx @@ -52,6 +52,7 @@ function renderItem({ containerWidthBreakpoint, conversationId, isBlocked, + isGroup, isOldestTimelineItem, messageId, nextMessageId, @@ -64,6 +65,7 @@ function renderItem({ containerWidthBreakpoint={containerWidthBreakpoint} conversationId={conversationId} isBlocked={isBlocked} + isGroup={isGroup} isOldestTimelineItem={isOldestTimelineItem} messageId={messageId} previousMessageId={previousMessageId} diff --git a/ts/state/smart/TimelineItem.tsx b/ts/state/smart/TimelineItem.tsx index 5cb46dffb41c..0bd3a42ed2ac 100644 --- a/ts/state/smart/TimelineItem.tsx +++ b/ts/state/smart/TimelineItem.tsx @@ -42,6 +42,7 @@ export type SmartTimelineItemProps = { containerWidthBreakpoint: WidthBreakpoint; conversationId: string; isBlocked: boolean; + isGroup: boolean; isOldestTimelineItem: boolean; messageId: string; nextMessageId: undefined | string; @@ -64,6 +65,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem( containerWidthBreakpoint, conversationId, isBlocked, + isGroup, isOldestTimelineItem, messageId, nextMessageId, @@ -193,6 +195,7 @@ export const SmartTimelineItem = memo(function SmartTimelineItem( i18n={i18n} interactionMode={interactionMode} isBlocked={isBlocked} + isGroup={isGroup} theme={theme} platform={platform} blockGroupLinkRequests={blockGroupLinkRequests} diff --git a/ts/test-node/sql/migration_1030_test.ts b/ts/test-node/sql/migration_1030_test.ts new file mode 100644 index 000000000000..9dfdb62b474c --- /dev/null +++ b/ts/test-node/sql/migration_1030_test.ts @@ -0,0 +1,158 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { assert } from 'chai'; +import type { Database } from '@signalapp/better-sqlite3'; +import SQL from '@signalapp/better-sqlite3'; +import { v4 as generateGuid } from 'uuid'; +import { sql } from '../../sql/util'; +import { updateToVersion } from './helpers'; +import type { MessageType } from '../../sql/Interface'; +import { MessageRequestResponseEvent } from '../../types/MessageRequestResponseEvent'; + +describe('SQL/updateToSchemaVersion1030', () => { + let db: Database; + + beforeEach(() => { + db = new SQL(':memory:'); + updateToVersion(db, 1020); + }); + + afterEach(() => { + db.close(); + }); + + function createMessage( + attrs: Pick + ): MessageType { + const message: MessageType = { + id: generateGuid(), + conversationId: generateGuid(), + received_at: Date.now(), + sent_at: Date.now(), + received_at_ms: Date.now(), + timestamp: Date.now(), + ...attrs, + }; + const json = JSON.stringify(message); + const [query, params] = sql` + INSERT INTO messages + (id, conversationId, type, json) + VALUES + ( + ${message.id}, + ${message.conversationId}, + ${message.type}, + ${json} + ) + `; + db.prepare(query).run(params); + return message; + } + + function getMessages() { + const [query] = sql` + SELECT type, json_extract(json, '$.messageRequestResponseEvent') AS event, shouldAffectActivity, shouldAffectPreview FROM messages; + `; + return db.prepare(query).all(); + } + + const INCLUDED_TYPES = [ + 'call-history', + 'chat-session-refreshed', + 'delivery-issue', + 'group-v2-change', + 'group', + 'incoming', + 'outgoing', + 'phone-number-discovery', + 'timer-notification', + 'title-transition-notification', + ] as const; + + const EXCLUDED_TYPES = [ + 'change-number-notification', + 'contact-removed-notification', + 'conversation-merge', + 'group-v1-migration', + 'keychange', + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- legacy type + 'message-history-unsynced' as any, + 'profile-change', + 'story', + 'universal-timer-notification', + 'verified-change', + ] as const; + + it('marks activity and preview correctly', () => { + for (const type of [...INCLUDED_TYPES, ...EXCLUDED_TYPES]) { + createMessage({ + type, + }); + } + + createMessage({ + type: 'message-request-response-event', + messageRequestResponseEvent: MessageRequestResponseEvent.ACCEPT, + }); + createMessage({ + type: 'message-request-response-event', + messageRequestResponseEvent: MessageRequestResponseEvent.BLOCK, + }); + createMessage({ + type: 'message-request-response-event', + messageRequestResponseEvent: MessageRequestResponseEvent.UNBLOCK, + }); + createMessage({ + type: 'message-request-response-event', + messageRequestResponseEvent: MessageRequestResponseEvent.SPAM, + }); + + updateToVersion(db, 1030); + + const messages = getMessages(); + + assert.deepStrictEqual(messages, [ + ...INCLUDED_TYPES.map(type => { + return { + type, + event: null, + shouldAffectActivity: 1, + shouldAffectPreview: 1, + }; + }), + ...EXCLUDED_TYPES.map(type => { + return { + type, + event: null, + shouldAffectActivity: 0, + shouldAffectPreview: 0, + }; + }), + { + type: 'message-request-response-event', + event: MessageRequestResponseEvent.ACCEPT, + shouldAffectActivity: 0, + shouldAffectPreview: 0, + }, + { + type: 'message-request-response-event', + event: MessageRequestResponseEvent.BLOCK, + shouldAffectActivity: 0, + shouldAffectPreview: 0, + }, + { + type: 'message-request-response-event', + event: MessageRequestResponseEvent.UNBLOCK, + shouldAffectActivity: 0, + shouldAffectPreview: 0, + }, + { + type: 'message-request-response-event', + event: MessageRequestResponseEvent.SPAM, + shouldAffectActivity: 1, + shouldAffectPreview: 1, + }, + ]); + }); +}); diff --git a/ts/types/MessageRequestResponseEvent.ts b/ts/types/MessageRequestResponseEvent.ts index 37de581c7ce2..8cacbf492d54 100644 --- a/ts/types/MessageRequestResponseEvent.ts +++ b/ts/types/MessageRequestResponseEvent.ts @@ -3,5 +3,6 @@ export enum MessageRequestResponseEvent { ACCEPT = 'ACCEPT', BLOCK = 'BLOCK', + UNBLOCK = 'UNBLOCK', SPAM = 'SPAM', } diff --git a/ts/util/getNotificationDataForMessage.ts b/ts/util/getNotificationDataForMessage.ts index a48604d5d3d7..e91961662ced 100644 --- a/ts/util/getNotificationDataForMessage.ts +++ b/ts/util/getNotificationDataForMessage.ts @@ -25,7 +25,7 @@ import { getStringForConversationMerge } from './getStringForConversationMerge'; import { getStringForProfileChange } from './getStringForProfileChange'; import { getTitleNoDefault, getNumber } from './getTitle'; import { findAndFormatContact } from './findAndFormatContact'; -import { isMe } from './whatTypeOfConversation'; +import { isGroup, isMe } from './whatTypeOfConversation'; import { strictAssert } from './assert'; import { getPropsForCallHistory, @@ -186,6 +186,14 @@ export function getNotificationDataForMessage( event, 'getNotificationData: isMessageRequestResponse true, but no messageRequestResponseEvent!' ); + const conversation = window.ConversationController.get( + attributes.conversationId + ); + strictAssert( + conversation, + 'getNotificationData/isConversationMerge/conversation' + ); + const isGroupConversation = isGroup(conversation.attributes); let text: string; if (event === MessageRequestResponseEvent.ACCEPT) { text = window.i18n( @@ -196,9 +204,25 @@ export function getNotificationDataForMessage( 'icu:MessageRequestResponseNotification__Message--Reported' ); } else if (event === MessageRequestResponseEvent.BLOCK) { - text = window.i18n( - 'icu:MessageRequestResponseNotification__Message--Blocked' - ); + if (isGroupConversation) { + text = window.i18n( + 'icu:MessageRequestResponseNotification__Message--Blocked--Group' + ); + } else { + text = window.i18n( + 'icu:MessageRequestResponseNotification__Message--Blocked' + ); + } + } else if (event === MessageRequestResponseEvent.UNBLOCK) { + if (isGroupConversation) { + text = window.i18n( + 'icu:MessageRequestResponseNotification__Message--Unblocked--Group' + ); + } else { + text = window.i18n( + 'icu:MessageRequestResponseNotification__Message--Unblocked' + ); + } } else { throw missingCaseError(event); }