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