feat: timer notifications expire correctly when disappearing after sending

pull/2660/head
William Grant 3 years ago
parent f36bbb9c7c
commit 4ef3537a53

@ -3,7 +3,7 @@ import React from 'react';
import { missingCaseError } from '../../util/missingCaseError'; import { missingCaseError } from '../../util/missingCaseError';
import { PropsForExpirationTimer } from '../../state/ducks/conversations'; import { PropsForExpirationTimer } from '../../state/ducks/conversations';
import { NotificationBubble } from './message/message-item/notification-bubble/NotificationBubble'; import { NotificationBubble } from './message/message-item/notification-bubble/NotificationBubble';
import { ReadableMessage } from './message/message-item/ReadableMessage'; import { ExpirableReadableMessage } from './message/message-item/ExpirableReadableMessage';
export const TimerNotification = (props: PropsForExpirationTimer) => { export const TimerNotification = (props: PropsForExpirationTimer) => {
const { const {
@ -50,10 +50,15 @@ export const TimerNotification = (props: PropsForExpirationTimer) => {
throw new Error('textToRender invalid key used TimerNotification'); throw new Error('textToRender invalid key used TimerNotification');
} }
return ( return (
<ReadableMessage <ExpirableReadableMessage
convoId={props.convoId}
messageId={messageId} messageId={messageId}
direction={props.direction}
receivedAt={receivedAt} receivedAt={receivedAt}
isUnread={isUnread} isUnread={isUnread}
expirationLength={props.expirationLength}
expirationTimestamp={props.expirationTimestamp}
isExpired={props.isExpired}
key={`readable-message-${messageId}`} key={`readable-message-${messageId}`}
> >
<NotificationBubble <NotificationBubble
@ -61,6 +66,6 @@ export const TimerNotification = (props: PropsForExpirationTimer) => {
iconColor="inherit" iconColor="inherit"
notificationText={textToRender} notificationText={textToRender}
/> />
</ReadableMessage> </ExpirableReadableMessage>
); );
}; };

@ -1097,6 +1097,8 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
source, source,
fromSync, fromSync,
}, },
expirationType,
expireTimer,
}; };
let message: MessageModel | undefined; let message: MessageModel | undefined;

@ -280,6 +280,26 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
await deleteExternalMessageFiles(this.attributes); await deleteExternalMessageFiles(this.attributes);
} }
public getPropsForExpiringMessage(): PropsForExpiringMessage | null {
const expirationType = this.get('expirationType');
const expirationLength = this.get('expireTimer') || null;
const expireTimerStart = this.get('expirationStartTimestamp') || null;
const expirationTimestamp =
expirationType && expireTimerStart && expirationLength
? expireTimerStart + expirationLength * DURATION.SECONDS
: null;
return {
convoId: this.get('conversationId'),
messageId: this.get('id'),
expirationLength,
expirationTimestamp,
isExpired: this.isExpired(),
};
}
public getPropsForTimerNotification(): PropsForExpirationTimer | null { public getPropsForTimerNotification(): PropsForExpirationTimer | null {
if (!this.isExpirationTimerUpdate()) { if (!this.isExpirationTimerUpdate()) {
return null; return null;
@ -289,6 +309,12 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
return null; return null;
} }
// TODO should direction be parts of expiration props?
let direction = this.get('direction');
if (!direction) {
direction = this.get('type') === 'outgoing' ? 'outgoing' : 'incoming';
}
const { expirationType, expireTimer, fromSync, source } = timerUpdate; const { expirationType, expireTimer, fromSync, source } = timerUpdate;
const timespan = ExpirationTimerOptions.getName(expireTimer || 0); const timespan = ExpirationTimerOptions.getName(expireTimer || 0);
const disabled = !expireTimer; const disabled = !expireTimer;
@ -302,29 +328,13 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
receivedAt: this.get('received_at'), receivedAt: this.get('received_at'),
isUnread: this.isUnread(), isUnread: this.isUnread(),
expirationType: expirationType || 'off', expirationType: expirationType || 'off',
direction,
...this.getPropsForExpiringMessage(),
}; };
return basicProps; return basicProps;
} }
public getPropsForExpiringMessage(): PropsForExpiringMessage | null {
const expirationType = this.get('expirationType');
const expireTimerStart = this.get('expirationStartTimestamp') || null;
const expirationLength = this.get('expireTimer') || null;
const expirationTimestamp =
expirationType && expireTimerStart && expirationLength
? expireTimerStart + expirationLength * DURATION.SECONDS
: null;
return {
convoId: this.get('conversationId'),
messageId: this.get('id'),
expirationLength,
expirationTimestamp,
isExpired: this.isExpired(),
};
}
public getPropsForGroupInvitation(): PropsForGroupInvitation | null { public getPropsForGroupInvitation(): PropsForGroupInvitation | null {
if (!this.isGroupInvitation()) { if (!this.isGroupInvitation()) {
return null; return null;

@ -23,7 +23,6 @@ import { toLogFormat } from '../types/attachments/Errors';
import { ConversationTypeEnum } from '../models/conversationAttributes'; import { ConversationTypeEnum } from '../models/conversationAttributes';
import { Reactions } from '../util/reactions'; import { Reactions } from '../util/reactions';
import { Action, Reaction } from '../types/Reaction'; import { Action, Reaction } from '../types/Reaction';
import { setExpirationStartTimestamp } from '../util/expiringMessages';
function cleanAttachment(attachment: any) { function cleanAttachment(attachment: any) {
return { return {
@ -246,12 +245,7 @@ export async function handleSwarmDataMessage(
if (isSyncedMessage) { if (isSyncedMessage) {
// TODO handle sync messages separately // TODO handle sync messages separately
window.log.info('WIP: Sync Message dropping'); window.log.info('WIP: Sync Message dropping');
} else { expireUpdate = null;
if (msgModel.isIncoming() && expireUpdate.expirationType === 'deleteAfterSend') {
msgModel =
setExpirationStartTimestamp(msgModel, 'deleteAfterSend', msgModel.get('sent_at')) ||
msgModel;
}
} }
await handleSwarmMessage( await handleSwarmMessage(

@ -16,7 +16,10 @@ import { GoogleChrome } from '../util';
import { appendFetchAvatarAndProfileJob } from './userProfileImageUpdates'; import { appendFetchAvatarAndProfileJob } from './userProfileImageUpdates';
import { ConversationTypeEnum } from '../models/conversationAttributes'; import { ConversationTypeEnum } from '../models/conversationAttributes';
import { getUsBlindedInThatServer } from '../session/apis/open_group_api/sogsv3/knownBlindedkeys'; import { getUsBlindedInThatServer } from '../session/apis/open_group_api/sogsv3/knownBlindedkeys';
import { DisappearingMessageConversationType } from '../util/expiringMessages'; import {
DisappearingMessageConversationType,
setExpirationStartTimestamp,
} from '../util/expiringMessages';
import { getNowWithNetworkOffset } from '../session/apis/snode_api/SNodeAPI'; import { getNowWithNetworkOffset } from '../session/apis/snode_api/SNodeAPI';
function contentTypeSupported(type: string): boolean { function contentTypeSupported(type: string): boolean {
@ -351,26 +354,43 @@ export async function handleMessageJob(
try { try {
messageModel.set({ flags: regularDataMessage.flags }); messageModel.set({ flags: regularDataMessage.flags });
if (messageModel.isExpirationTimerUpdate()) { if (!isEmpty(expireUpdate)) {
// TODO account for lastDisappearingMessageChangeTimestamp messageModel.set({
expirationType: expireUpdate.expirationType,
// TODO in the future we will remove the dataMessage expireTimer and the expirationTimerUpdate expireTimer: expireUpdate.expireTimer,
let expirationType = expireUpdate.expirationType; });
let expireTimer = expireUpdate.expireTimer;
if (!expirationType) { if (messageModel.isIncoming() && messageModel.get('expirationType') === 'deleteAfterSend') {
expirationType = conversation.isPrivate() ? 'deleteAfterRead' : 'deleteAfterSend'; messageModel =
setExpirationStartTimestamp(
messageModel,
'deleteAfterSend',
messageModel.get('sent_at')
) || messageModel;
}
} }
if (messageModel.isExpirationTimerUpdate()) {
// TODO in the future we will remove the dataMessage expireTimer and the expirationTimerUpdate
// Backwards compatibility for Disappearing Messages in old clients // Backwards compatibility for Disappearing Messages in old clients
if (regularDataMessage.expireTimer) { if (regularDataMessage.expireTimer) {
const expirationTimerUpdate = messageModel.get('expirationTimerUpdate'); const expirationTimerUpdate = messageModel.get('expirationTimerUpdate');
if (!isEmpty(expirationTimerUpdate)) { if (!isEmpty(expirationTimerUpdate)) {
expirationType = expirationTimerUpdate?.expirationType; messageModel.set({
expireTimer = expirationTimerUpdate?.expireTimer; expirationType: expirationTimerUpdate?.expirationType,
expireTimer: expirationTimerUpdate?.expireTimer,
});
} }
} }
// TODO account for lastDisappearingMessageChangeTimestamp
let expirationType = messageModel.get('expirationType');
const expireTimer = messageModel.get('expireTimer');
if (!expirationType) {
expirationType = conversation.isPrivate() ? 'deleteAfterRead' : 'deleteAfterSend';
}
// TODO compare types and change timestamps // TODO compare types and change timestamps
const oldTypeValue = conversation.get('expirationType'); const oldTypeValue = conversation.get('expirationType');
const oldTimerValue = conversation.get('expireTimer'); const oldTimerValue = conversation.get('expireTimer');
@ -390,13 +410,6 @@ export async function handleMessageJob(
expireTimer expireTimer
); );
} else { } else {
// NOTE this is a disappearing message NOT a expiration timer update
if (!isEmpty(expireUpdate)) {
messageModel.set({
expirationType: expireUpdate.expirationType,
expireTimer: expireUpdate.expireTimer,
});
}
// this does not commit to db nor UI unless we need to approve a convo // this does not commit to db nor UI unless we need to approve a convo
await handleRegularMessage( await handleRegularMessage(
conversation, conversation,

@ -1,9 +1,9 @@
import { DataMessage } from '..';
import { SignalService } from '../../../../protobuf'; import { SignalService } from '../../../../protobuf';
import { DisappearingMessageType } from '../../../../util/expiringMessages'; import { DisappearingMessageType } from '../../../../util/expiringMessages';
import { PubKey } from '../../../types'; import { PubKey } from '../../../types';
import { StringUtils } from '../../../utils'; import { StringUtils } from '../../../utils';
import { MessageParams } from '../Message'; import { MessageParams } from '../Message';
import { VisibleMessage } from '../visibleMessage/VisibleMessage';
interface ExpirationTimerUpdateMessageParams extends MessageParams { interface ExpirationTimerUpdateMessageParams extends MessageParams {
groupId?: string | PubKey; groupId?: string | PubKey;
@ -16,22 +16,23 @@ interface ExpirationTimerUpdateMessageParams extends MessageParams {
// Note the old disappearing messages used a data message for the expiration time. // Note the old disappearing messages used a data message for the expiration time.
// The new ones use properties on the Content Message // The new ones use properties on the Content Message
// We will remove support for the old one 2 weeks after the release // We will remove support for the old one 2 weeks after the release
export class ExpirationTimerUpdateMessage extends DataMessage { export class ExpirationTimerUpdateMessage extends VisibleMessage {
public readonly groupId?: PubKey; public readonly groupId?: PubKey;
public readonly syncTarget?: string;
public readonly expirationType: DisappearingMessageType | null;
public readonly expireTimer: number | null;
public readonly lastDisappearingMessageChangeTimestamp: number | null; public readonly lastDisappearingMessageChangeTimestamp: number | null;
constructor(params: ExpirationTimerUpdateMessageParams) { constructor(params: ExpirationTimerUpdateMessageParams) {
super({ timestamp: params.timestamp, identifier: params.identifier }); super({
this.expirationType = params.expirationType; timestamp: params.timestamp,
this.expireTimer = params.expireTimer; identifier: params.identifier,
expirationType: params.expirationType,
expireTimer: params.expireTimer || undefined,
syncTarget: params.syncTarget ? PubKey.cast(params.syncTarget).key : undefined,
});
this.lastDisappearingMessageChangeTimestamp = params.lastDisappearingMessageChangeTimestamp; this.lastDisappearingMessageChangeTimestamp = params.lastDisappearingMessageChangeTimestamp;
const { groupId, syncTarget } = params; const { groupId } = params;
this.groupId = groupId ? PubKey.cast(groupId) : undefined; this.groupId = groupId ? PubKey.cast(groupId) : undefined;
this.syncTarget = syncTarget ? PubKey.cast(syncTarget).key : undefined;
} }
public contentProto(): SignalService.Content { public contentProto(): SignalService.Content {
@ -47,7 +48,7 @@ export class ExpirationTimerUpdateMessage extends DataMessage {
} }
public dataProto(): SignalService.DataMessage { public dataProto(): SignalService.DataMessage {
const data = new SignalService.DataMessage(); const data = super.dataProto();
data.flags = SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE; data.flags = SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE;
@ -64,10 +65,6 @@ export class ExpirationTimerUpdateMessage extends DataMessage {
data.group = groupMessage; data.group = groupMessage;
} }
if (this.syncTarget) {
data.syncTarget = this.syncTarget;
}
// TODO remove 2 weeks after the release // TODO remove 2 weeks after the release
if (this.expireTimer) { if (this.expireTimer) {
data.expireTimer = this.expireTimer; data.expireTimer = this.expireTimer;

@ -136,9 +136,10 @@ async function handleMessageSentSuccess(
if (!shouldMarkMessageAsSynced) { if (!shouldMarkMessageAsSynced) {
const expirationType = fetchedMessage.get('expirationType'); const expirationType = fetchedMessage.get('expirationType');
if (expirationType) { if (expirationType === 'deleteAfterSend') {
fetchedMessage = fetchedMessage =
setExpirationStartTimestamp(fetchedMessage, expirationType) || fetchedMessage; setExpirationStartTimestamp(fetchedMessage, expirationType, effectiveTimestamp) ||
fetchedMessage;
} }
} }
@ -169,11 +170,6 @@ async function handleMessageSentFailure(
} }
} }
const expirationType = fetchedMessage.get('expirationType');
if (expirationType) {
fetchedMessage = setExpirationStartTimestamp(fetchedMessage, expirationType) || fetchedMessage;
}
// always mark the message as sent. // always mark the message as sent.
// the fact that we have errors on the sent is based on the saveErrors() // the fact that we have errors on the sent is based on the saveErrors()
fetchedMessage.set({ fetchedMessage.set({

@ -83,7 +83,7 @@ export type PropsForExpiringMessage = {
isExpired?: boolean; isExpired?: boolean;
}; };
export type PropsForExpirationTimer = { export interface PropsForExpirationTimer extends PropsForExpiringMessage {
expirationType: DisappearingMessageConversationType; expirationType: DisappearingMessageConversationType;
timespan: string; timespan: string;
disabled: boolean; disabled: boolean;
@ -96,7 +96,8 @@ export type PropsForExpirationTimer = {
messageId: string; messageId: string;
isUnread: boolean; isUnread: boolean;
receivedAt: number | undefined; receivedAt: number | undefined;
}; direction: MessageModelType;
}
export type PropsForGroupUpdateGeneral = { export type PropsForGroupUpdateGeneral = {
type: 'general'; type: 'general';

@ -203,33 +203,42 @@ export function setExpirationStartTimestamp(
timestamp?: number timestamp?: number
): MessageModel | null { ): MessageModel | null {
if (message.get('expirationStartTimestamp') > 0) { if (message.get('expirationStartTimestamp') > 0) {
window.log.info(`WIP: Expiration Timer already set. Ignoring.`); window.log.info(`WIP: Expiration Timer already set. Ignoring new value.`);
return null; return null;
} }
let expirationStartTimestamp = getNowWithNetworkOffset(); let expirationStartTimestamp = getNowWithNetworkOffset();
if (timestamp) { if (timestamp) {
expirationStartTimestamp = Math.min(getNowWithNetworkOffset(), timestamp); window.log.info(
`WIP: We compare 2 timestamps for a delete after ${
mode === 'deleteAfterRead' ? 'read' : 'send'
} message: \expirationStartTimestamp `,
new Date(expirationStartTimestamp).toLocaleTimeString(),
'\ntimestamp ',
new Date(timestamp).toLocaleTimeString()
);
expirationStartTimestamp = Math.min(expirationStartTimestamp, timestamp);
} }
message.set('expirationStartTimestamp', expirationStartTimestamp); message.set('expirationStartTimestamp', expirationStartTimestamp);
if (mode === 'deleteAfterRead') { if (mode === 'deleteAfterRead') {
window.log.info( window.log.info(
`WIP: setExpirationStartTimestamp we set the start timestamp for a delete after read message`, `WIP: We set the start timestamp for a delete after read message to ${new Date(
expirationStartTimestamp
).toLocaleTimeString()}`,
message message
); );
} else if (mode === 'deleteAfterSend') { } else if (mode === 'deleteAfterSend') {
window.log.info( window.log.info(
`WIP: setExpirationStartTimestamp we set the start timestamp for a delete after send message`, `WIP: We set the start timestamp for a delete after send message to ${new Date(
expirationStartTimestamp
).toLocaleTimeString()}`,
message message
); );
} else { } else {
console.log( console.log(`WIP: Invalid disappearing message mode set. Ignoring.`, message);
`WIP: setExpirationStartTimestamp Invalid disappearing message mode set. Ignoring.`,
message
);
return null; return null;
} }

Loading…
Cancel
Save