Add unblocked timeline event

This commit is contained in:
Jamie Kyle 2024-04-12 10:07:57 -07:00 committed by GitHub
parent 92eb036196
commit ad8020848f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 333 additions and 10 deletions

View file

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

View file

@ -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 && (
<SystemMessage
icon="block"
contents={i18n(
'icu:MessageRequestResponseNotification__Message--Blocked'
)}
contents={
isGroup
? i18n(
'icu:MessageRequestResponseNotification__Message--Blocked--Group'
)
: i18n('icu:MessageRequestResponseNotification__Message--Blocked')
}
/>
)}
{event === MessageRequestResponseEvent.UNBLOCK && (
<SystemMessage
icon="thread"
contents={
isGroup
? i18n(
'icu:MessageRequestResponseNotification__Message--Unblocked--Group'
)
: i18n(
'icu:MessageRequestResponseNotification__Message--Unblocked'
)
}
/>
)}
{event === MessageRequestResponseEvent.SPAM && (

View file

@ -355,6 +355,7 @@ const renderItem = ({
id=""
isTargeted={false}
isBlocked={false}
isGroup={false}
i18n={i18n}
interactionMode="keyboard"
isNextItemCallingNotification={false}

View file

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

View file

@ -61,6 +61,7 @@ const getDefaultProps = () => ({
isNextItemCallingNotification: false,
isTargeted: false,
isBlocked: false,
isGroup: false,
interactionMode: 'keyboard' as const,
theme: ThemeType.light,
platform: 'darwin',

View file

@ -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({
<MessageRequestResponseNotification
{...item.data}
i18n={i18n}
isGroup={isGroup}
isBlocked={isBlocked}
onOpenMessageRequestActionsConfirmation={
onOpenMessageRequestActionsConfirmation

View file

@ -2170,6 +2170,9 @@ export class ConversationModel extends window.Backbone
const didResponseChange = response !== currentMessageRequestState;
const wasPreviouslyAccepted = this.getAccepted();
const didUnblock =
response === messageRequestEnum.ACCEPT && this.isBlocked();
if (didResponseChange) {
if (response === messageRequestEnum.ACCEPT) {
// Only add a message when the user took an explicit action to accept
@ -2177,7 +2180,9 @@ export class ConversationModel extends window.Backbone
if (!viaStorageServiceSync) {
drop(
this.addMessageRequestResponseEventMessage(
MessageRequestResponseEvent.ACCEPT
didUnblock
? MessageRequestResponseEvent.UNBLOCK
: MessageRequestResponseEvent.ACCEPT
)
);
}

View file

@ -0,0 +1,87 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Database } from '@signalapp/better-sqlite3';
import type { LoggerType } from '../../types/Logging';
import { sql, sqlFragment } from '../util';
export const version = 1030;
export function updateToSchemaVersion1030(
currentVersion: number,
db: Database,
logger: LoggerType
): void {
if (currentVersion >= 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!');
}

View file

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

View file

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

View file

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

View file

@ -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, 'type' | 'messageRequestResponseEvent'>
): 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,
},
]);
});
});

View file

@ -3,5 +3,6 @@
export enum MessageRequestResponseEvent {
ACCEPT = 'ACCEPT',
BLOCK = 'BLOCK',
UNBLOCK = 'UNBLOCK',
SPAM = 'SPAM',
}

View file

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