Add unblocked timeline event
This commit is contained in:
parent
92eb036196
commit
ad8020848f
14 changed files with 333 additions and 10 deletions
|
@ -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"
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -355,6 +355,7 @@ const renderItem = ({
|
|||
id=""
|
||||
isTargeted={false}
|
||||
isBlocked={false}
|
||||
isGroup={false}
|
||||
i18n={i18n}
|
||||
interactionMode="keyboard"
|
||||
isNextItemCallingNotification={false}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -61,6 +61,7 @@ const getDefaultProps = () => ({
|
|||
isNextItemCallingNotification: false,
|
||||
isTargeted: false,
|
||||
isBlocked: false,
|
||||
isGroup: false,
|
||||
interactionMode: 'keyboard' as const,
|
||||
theme: ThemeType.light,
|
||||
platform: 'darwin',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
87
ts/sql/migrations/1030-unblock-event.ts
Normal file
87
ts/sql/migrations/1030-unblock-event.ts
Normal 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!');
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
158
ts/test-node/sql/migration_1030_test.ts
Normal file
158
ts/test-node/sql/migration_1030_test.ts
Normal 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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -3,5 +3,6 @@
|
|||
export enum MessageRequestResponseEvent {
|
||||
ACCEPT = 'ACCEPT',
|
||||
BLOCK = 'BLOCK',
|
||||
UNBLOCK = 'UNBLOCK',
|
||||
SPAM = 'SPAM',
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue