Ignore delivery receipts for outgoing reactions

This commit is contained in:
Fedor Indutny 2023-12-19 15:57:15 +01:00 committed by GitHub
parent c8099171e2
commit e46b1f7958
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 151 additions and 34 deletions

View file

@ -105,6 +105,28 @@ const processReceiptBatcher = createWaitBatcher({
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await window.Signal.Data.getMessagesBySentAt(sentAt); await window.Signal.Data.getMessagesBySentAt(sentAt);
if (messagesMatchingTimestamp.length === 0) {
// eslint-disable-next-line no-await-in-loop
const reaction = await window.Signal.Data.getReactionByTimestamp(
window.ConversationController.getOurConversationIdOrThrow(),
sentAt
);
if (reaction) {
for (const receipt of receiptsForMessageSentAt) {
log.info(
'MesageReceipts.processReceiptBatcher: Got receipt for reaction',
receipt.messageSentAt,
receipt.type,
receipt.sourceConversationId,
receipt.sourceServiceId
);
remove(receipt);
}
continue;
}
}
for (const receipt of receiptsForMessageSentAt) { for (const receipt of receiptsForMessageSentAt) {
const targetMessage = getTargetMessage({ const targetMessage = getTargetMessage({
sourceConversationId: receipt.sourceConversationId, sourceConversationId: receipt.sourceConversationId,

View file

@ -42,6 +42,7 @@ import { getUserLanguages } from '../util/userLanguages';
import { copyCdnFields } from '../util/attachments'; import { copyCdnFields } from '../util/attachments';
import type { ReactionType } from '../types/Reactions'; import type { ReactionType } from '../types/Reactions';
import { ReactionReadStatus } from '../types/Reactions';
import type { ServiceIdString } from '../types/ServiceId'; import type { ServiceIdString } from '../types/ServiceId';
import { normalizeServiceId } from '../types/ServiceId'; import { normalizeServiceId } from '../types/ServiceId';
import { isAciString } from '../util/isAciString'; import { isAciString } from '../util/isAciString';
@ -2615,13 +2616,6 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
); );
} }
this.set({ reactions }); this.set({ reactions });
await window.Signal.Data.removeReactionFromConversation({
emoji: reaction.emoji,
fromId: reaction.fromId,
targetAuthorServiceId: reaction.targetAuthorAci,
targetTimestamp: reaction.targetTimestamp,
});
} else { } else {
log.info( log.info(
'handleReaction: adding reaction for message', 'handleReaction: adding reaction for message',
@ -2648,8 +2642,19 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
if (isOutgoing(this.attributes) && isFromSomeoneElse) { if (isOutgoing(this.attributes) && isFromSomeoneElse) {
void conversation.notify(this, reaction); void conversation.notify(this, reaction);
} }
}
}
await window.Signal.Data.addReaction({ if (reaction.remove) {
await window.Signal.Data.removeReactionFromConversation({
emoji: reaction.emoji,
fromId: reaction.fromId,
targetAuthorServiceId: reaction.targetAuthorAci,
targetTimestamp: reaction.targetTimestamp,
});
} else {
await window.Signal.Data.addReaction(
{
conversationId: this.get('conversationId'), conversationId: this.get('conversationId'),
emoji: reaction.emoji, emoji: reaction.emoji,
fromId: reaction.fromId, fromId: reaction.fromId,
@ -2657,8 +2662,14 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
messageReceivedAt: this.get('received_at'), messageReceivedAt: this.get('received_at'),
targetAuthorAci: reaction.targetAuthorAci, targetAuthorAci: reaction.targetAuthorAci,
targetTimestamp: reaction.targetTimestamp, targetTimestamp: reaction.targetTimestamp,
}); timestamp: reaction.timestamp,
} },
{
readStatus: isFromThisDevice
? ReactionReadStatus.Read
: ReactionReadStatus.Unread,
}
);
} }
const currentLength = (this.get('reactions') || []).length; const currentLength = (this.get('reactions') || []).length;

View file

@ -7,7 +7,7 @@ import type {
SenderKeyInfoType, SenderKeyInfoType,
} from '../model-types.d'; } from '../model-types.d';
import type { StoredJob } from '../jobs/types'; import type { StoredJob } from '../jobs/types';
import type { ReactionType } from '../types/Reactions'; import type { ReactionType, ReactionReadStatus } from '../types/Reactions';
import type { ConversationColorType, CustomColorType } from '../types/Colors'; import type { ConversationColorType, CustomColorType } from '../types/Colors';
import type { StorageAccessType } from '../types/Storage.d'; import type { StorageAccessType } from '../types/Storage.d';
import type { AttachmentType } from '../types/Attachment'; import type { AttachmentType } from '../types/Attachment';
@ -586,7 +586,16 @@ export type DataInterface = {
targetAuthorServiceId: ServiceIdString; targetAuthorServiceId: ServiceIdString;
targetTimestamp: number; targetTimestamp: number;
}) => Promise<void>; }) => Promise<void>;
addReaction: (reactionObj: ReactionType) => Promise<void>; getReactionByTimestamp: (
fromId: string,
timestamp: number
) => Promise<ReactionType | undefined>;
addReaction: (
reactionObj: ReactionType,
options: {
readStatus: ReactionReadStatus;
}
) => Promise<void>;
_getAllReactions: () => Promise<Array<ReactionType>>; _getAllReactions: () => Promise<Array<ReactionType>>;
_removeAllReactions: () => Promise<void>; _removeAllReactions: () => Promise<void>;
getMessageBySender: (options: { getMessageBySender: (options: {

View file

@ -33,6 +33,7 @@ import * as Errors from '../types/errors';
import { ReadStatus } from '../messages/MessageReadStatus'; import { ReadStatus } from '../messages/MessageReadStatus';
import type { GroupV2MemberType } from '../model-types.d'; import type { GroupV2MemberType } from '../model-types.d';
import type { ReactionType } from '../types/Reactions'; import type { ReactionType } from '../types/Reactions';
import { ReactionReadStatus } from '../types/Reactions';
import { STORAGE_UI_KEYS } from '../types/StorageUIKeys'; import { STORAGE_UI_KEYS } from '../types/StorageUIKeys';
import type { StoryDistributionIdString } from '../types/StoryDistributionId'; import type { StoryDistributionIdString } from '../types/StoryDistributionId';
import type { ServiceIdString, AciString } from '../types/ServiceId'; import type { ServiceIdString, AciString } from '../types/ServiceId';
@ -274,6 +275,7 @@ const dataInterface: ServerInterface = {
getUnreadByConversationAndMarkRead, getUnreadByConversationAndMarkRead,
getUnreadReactionsAndMarkRead, getUnreadReactionsAndMarkRead,
markReactionAsRead, markReactionAsRead,
getReactionByTimestamp,
addReaction, addReaction,
removeReactionFromConversation, removeReactionFromConversation,
_getAllReactions, _getAllReactions,
@ -2537,15 +2539,32 @@ async function markReactionAsRead(
})(); })();
} }
async function addReaction({ async function getReactionByTimestamp(
conversationId, fromId: string,
emoji, timestamp: number
fromId, ): Promise<ReactionType | undefined> {
messageId, const db = getReadonlyInstance();
messageReceivedAt, const [query, params] = sql`
targetAuthorAci, SELECT * FROM reactions
targetTimestamp, WHERE fromId IS ${fromId} AND timestamp IS ${timestamp}
}: ReactionType): Promise<void> { `;
return db.prepare(query).get(params);
}
async function addReaction(
{
conversationId,
emoji,
fromId,
messageId,
messageReceivedAt,
targetAuthorAci,
targetTimestamp,
timestamp,
}: ReactionType,
{ readStatus }: { readStatus: ReactionReadStatus }
): Promise<void> {
const db = await getWritableInstance(); const db = await getWritableInstance();
await db await db
.prepare( .prepare(
@ -2557,6 +2576,7 @@ async function addReaction({
messageReceivedAt, messageReceivedAt,
targetAuthorAci, targetAuthorAci,
targetTimestamp, targetTimestamp,
timestamp,
unread unread
) VALUES ( ) VALUES (
$conversationId, $conversationId,
@ -2566,6 +2586,7 @@ async function addReaction({
$messageReceivedAt, $messageReceivedAt,
$targetAuthorAci, $targetAuthorAci,
$targetTimestamp, $targetTimestamp,
$timestamp,
$unread $unread
);` );`
) )
@ -2577,7 +2598,8 @@ async function addReaction({
messageReceivedAt, messageReceivedAt,
targetAuthorAci, targetAuthorAci,
targetTimestamp, targetTimestamp,
unread: 1, timestamp,
unread: readStatus === ReactionReadStatus.Unread ? 1 : 0,
}); });
} }

View file

@ -0,0 +1,32 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Database } from '@signalapp/better-sqlite3';
import type { LoggerType } from '../../types/Logging';
export const version = 980;
export function updateToSchemaVersion980(
currentVersion: number,
db: Database,
logger: LoggerType
): void {
if (currentVersion >= 980) {
return;
}
db.transaction(() => {
db.exec(`
ALTER TABLE reactions ADD COLUMN timestamp NUMBER;
CREATE INDEX reactions_byTimestamp
ON reactions
(fromId, timestamp);
`);
})();
db.pragma('user_version = 980');
logger.info('updateToSchemaVersion980: success!');
}

View file

@ -72,10 +72,11 @@ import { updateToSchemaVersion930 } from './930-fts5-secure-delete';
import { updateToSchemaVersion940 } from './940-fts5-revert'; import { updateToSchemaVersion940 } from './940-fts5-revert';
import { updateToSchemaVersion950 } from './950-fts5-secure-delete'; import { updateToSchemaVersion950 } from './950-fts5-secure-delete';
import { updateToSchemaVersion960 } from './960-untag-pni'; import { updateToSchemaVersion960 } from './960-untag-pni';
import { updateToSchemaVersion970 } from './970-fts5-optimize';
import { import {
version as MAX_VERSION, version as MAX_VERSION,
updateToSchemaVersion970, updateToSchemaVersion980,
} from './970-fts5-optimize'; } from './980-reaction-timestamp';
function updateToSchemaVersion1( function updateToSchemaVersion1(
currentVersion: number, currentVersion: number,
@ -2015,6 +2016,7 @@ export const SCHEMA_VERSIONS = [
updateToSchemaVersion950, updateToSchemaVersion950,
updateToSchemaVersion960, updateToSchemaVersion960,
updateToSchemaVersion970, updateToSchemaVersion970,
updateToSchemaVersion980,
]; ];
export class DBVersionFromFutureError extends Error { export class DBVersionFromFutureError extends Error {

View file

@ -8,6 +8,7 @@ import dataInterface from '../../sql/Client';
import { generateAci } from '../../types/ServiceId'; import { generateAci } from '../../types/ServiceId';
import type { ReactionType } from '../../types/Reactions'; import type { ReactionType } from '../../types/Reactions';
import { ReactionReadStatus } from '../../types/Reactions';
import { DurationInSeconds } from '../../util/durations'; import { DurationInSeconds } from '../../util/durations';
import type { MessageAttributesType } from '../../model-types.d'; import type { MessageAttributesType } from '../../model-types.d';
import { ReadStatus } from '../../messages/MessageReadStatus'; import { ReadStatus } from '../../messages/MessageReadStatus';
@ -24,6 +25,8 @@ const {
getUnreadReactionsAndMarkRead, getUnreadReactionsAndMarkRead,
} = dataInterface; } = dataInterface;
const UNREAD_REACTION = { readStatus: ReactionReadStatus.Unread };
describe('sql/markRead', () => { describe('sql/markRead', () => {
beforeEach(async () => { beforeEach(async () => {
await _removeAllMessages(); await _removeAllMessages();
@ -528,6 +531,7 @@ describe('sql/markRead', () => {
messageReceivedAt: message1.received_at, messageReceivedAt: message1.received_at,
targetAuthorAci: generateAci(), targetAuthorAci: generateAci(),
targetTimestamp: start, targetTimestamp: start,
timestamp: start,
}; };
const reaction2: ReactionType = { const reaction2: ReactionType = {
conversationId, conversationId,
@ -537,6 +541,7 @@ describe('sql/markRead', () => {
messageReceivedAt: message2.received_at, messageReceivedAt: message2.received_at,
targetAuthorAci: generateAci(), targetAuthorAci: generateAci(),
targetTimestamp: start, targetTimestamp: start,
timestamp: start,
}; };
const reaction3: ReactionType = { const reaction3: ReactionType = {
conversationId: generateUuid(), conversationId: generateUuid(),
@ -546,6 +551,7 @@ describe('sql/markRead', () => {
messageReceivedAt: message3.received_at, messageReceivedAt: message3.received_at,
targetAuthorAci: generateAci(), targetAuthorAci: generateAci(),
targetTimestamp: start, targetTimestamp: start,
timestamp: start,
}; };
const reaction4: ReactionType = { const reaction4: ReactionType = {
conversationId, conversationId,
@ -555,6 +561,7 @@ describe('sql/markRead', () => {
messageReceivedAt: message4.received_at, messageReceivedAt: message4.received_at,
targetAuthorAci: generateAci(), targetAuthorAci: generateAci(),
targetTimestamp: start, targetTimestamp: start,
timestamp: start,
}; };
const reaction5: ReactionType = { const reaction5: ReactionType = {
conversationId, conversationId,
@ -564,13 +571,14 @@ describe('sql/markRead', () => {
messageReceivedAt: message5.received_at, messageReceivedAt: message5.received_at,
targetAuthorAci: generateAci(), targetAuthorAci: generateAci(),
targetTimestamp: start, targetTimestamp: start,
timestamp: start,
}; };
await addReaction(reaction1); await addReaction(reaction1, UNREAD_REACTION);
await addReaction(reaction2); await addReaction(reaction2, UNREAD_REACTION);
await addReaction(reaction3); await addReaction(reaction3, UNREAD_REACTION);
await addReaction(reaction4); await addReaction(reaction4, UNREAD_REACTION);
await addReaction(reaction5); await addReaction(reaction5, UNREAD_REACTION);
assert.lengthOf(await _getAllReactions(), 5); assert.lengthOf(await _getAllReactions(), 5);
const markedRead = await getUnreadReactionsAndMarkRead({ const markedRead = await getUnreadReactionsAndMarkRead({
@ -677,6 +685,7 @@ describe('sql/markRead', () => {
messageReceivedAt: message1.received_at, messageReceivedAt: message1.received_at,
targetAuthorAci: generateAci(), targetAuthorAci: generateAci(),
targetTimestamp: start, targetTimestamp: start,
timestamp: start,
}; };
const reaction2: ReactionType = { const reaction2: ReactionType = {
conversationId, conversationId,
@ -686,6 +695,7 @@ describe('sql/markRead', () => {
messageReceivedAt: message2.received_at, messageReceivedAt: message2.received_at,
targetAuthorAci: generateAci(), targetAuthorAci: generateAci(),
targetTimestamp: start, targetTimestamp: start,
timestamp: start,
}; };
const reaction3: ReactionType = { const reaction3: ReactionType = {
conversationId: generateUuid(), conversationId: generateUuid(),
@ -695,6 +705,7 @@ describe('sql/markRead', () => {
messageReceivedAt: message3.received_at, messageReceivedAt: message3.received_at,
targetAuthorAci: generateAci(), targetAuthorAci: generateAci(),
targetTimestamp: start, targetTimestamp: start,
timestamp: start,
}; };
const reaction4: ReactionType = { const reaction4: ReactionType = {
conversationId, conversationId,
@ -704,6 +715,7 @@ describe('sql/markRead', () => {
messageReceivedAt: message4.received_at, messageReceivedAt: message4.received_at,
targetAuthorAci: generateAci(), targetAuthorAci: generateAci(),
targetTimestamp: start, targetTimestamp: start,
timestamp: start,
}; };
const reaction5: ReactionType = { const reaction5: ReactionType = {
conversationId, conversationId,
@ -713,13 +725,14 @@ describe('sql/markRead', () => {
messageReceivedAt: message5.received_at, messageReceivedAt: message5.received_at,
targetAuthorAci: generateAci(), targetAuthorAci: generateAci(),
targetTimestamp: start, targetTimestamp: start,
timestamp: start,
}; };
await addReaction(reaction1); await addReaction(reaction1, UNREAD_REACTION);
await addReaction(reaction2); await addReaction(reaction2, UNREAD_REACTION);
await addReaction(reaction3); await addReaction(reaction3, UNREAD_REACTION);
await addReaction(reaction4); await addReaction(reaction4, UNREAD_REACTION);
await addReaction(reaction5); await addReaction(reaction5, UNREAD_REACTION);
assert.lengthOf(await _getAllReactions(), 5); assert.lengthOf(await _getAllReactions(), 5);
const markedRead = await getUnreadReactionsAndMarkRead({ const markedRead = await getUnreadReactionsAndMarkRead({

View file

@ -11,4 +11,10 @@ export type ReactionType = Readonly<{
messageReceivedAt: number; messageReceivedAt: number;
targetAuthorAci: AciString; targetAuthorAci: AciString;
targetTimestamp: number; targetTimestamp: number;
timestamp: number;
}>; }>;
export enum ReactionReadStatus {
Unread = 'Unread',
Read = 'Read',
}