mirror of
https://github.com/lux-tel/session-desktop.git
synced 2026-10-30 20:33:24 +00:00
634 lines
21 KiB
TypeScript
634 lines
21 KiB
TypeScript
import { isEmpty, isNumber, throttle, uniq } from 'lodash';
|
|
import { messagesExpired } from '../../state/ducks/conversations';
|
|
import { initWallClockListener } from '../../util/wallClockListener';
|
|
|
|
import { Data } from '../../data/data';
|
|
import { ConversationModel } from '../../models/conversation';
|
|
import { READ_MESSAGE_STATE } from '../../models/conversationAttributes';
|
|
import { MessageModel } from '../../models/message';
|
|
import { SignalService } from '../../protobuf';
|
|
import { ExpiringDetails, expireMessagesOnSnode } from '../apis/snode_api/expireRequest';
|
|
import { ConvoHub } from '../conversations';
|
|
import { isValidUnixTimestamp } from '../utils/Timestamps';
|
|
import { UpdateMsgExpirySwarm } from '../utils/job_runners/jobs/UpdateMsgExpirySwarmJob';
|
|
import {
|
|
DisappearingMessageConversationModeType,
|
|
DisappearingMessageMode,
|
|
DisappearingMessageType,
|
|
DisappearingMessageUpdate,
|
|
ReadyToDisappearMsgUpdate,
|
|
} from './types';
|
|
import { PubKey } from '../types';
|
|
import { NetworkTime } from '../../util/NetworkTime';
|
|
|
|
export async function destroyMessagesAndUpdateRedux(
|
|
messages: Array<{
|
|
conversationKey: string;
|
|
messageId: string;
|
|
}>
|
|
) {
|
|
if (!messages.length) {
|
|
return;
|
|
}
|
|
const conversationWithChanges = uniq(messages.map(m => m.conversationKey));
|
|
|
|
try {
|
|
const messageIds = messages.map(m => m.messageId);
|
|
|
|
// Delete any attachments
|
|
for (let i = 0; i < messageIds.length; i++) {
|
|
/* eslint-disable no-await-in-loop */
|
|
// TODO make this use getMessagesById and not getMessageById
|
|
|
|
const message = await Data.getMessageById(messageIds[i]);
|
|
await message?.cleanup();
|
|
/* eslint-enable no-await-in-loop */
|
|
}
|
|
|
|
// Delete all those messages in a single sql call
|
|
await Data.removeMessagesByIds(messageIds);
|
|
} catch (e) {
|
|
window.log.error('destroyMessages: removeMessagesByIds failed', e && e.message ? e.message : e);
|
|
}
|
|
// trigger a redux update if needed for all those messages
|
|
window.inboxStore?.dispatch(
|
|
messagesExpired(
|
|
messages.map(m => ({ conversationId: m.conversationKey, messageId: m.messageId }))
|
|
)
|
|
);
|
|
|
|
// trigger a refresh the last message for all those uniq conversation
|
|
conversationWithChanges.forEach(convoIdToUpdate => {
|
|
ConvoHub.use().get(convoIdToUpdate)?.updateLastMessage();
|
|
});
|
|
}
|
|
|
|
async function destroyExpiredMessages() {
|
|
try {
|
|
window.log.info('destroyExpiredMessages: Loading messages...');
|
|
const messages = await Data.getExpiredMessages();
|
|
window.log.debug('destroyExpiredMessages: count:', messages.length);
|
|
|
|
const messagesExpiredDetails: Array<{
|
|
conversationKey: string;
|
|
messageId: string;
|
|
}> = messages.map(m => ({
|
|
conversationKey: m.get('conversationId'),
|
|
messageId: m.id,
|
|
}));
|
|
|
|
messages.forEach(expired => {
|
|
window.log.info('Message expired', {
|
|
sentAt: expired.get('sent_at'),
|
|
hash: expired.getMessageHash(),
|
|
});
|
|
});
|
|
|
|
await destroyMessagesAndUpdateRedux(messagesExpiredDetails);
|
|
const convosToRefresh = uniq(messagesExpiredDetails.map(m => m.conversationKey));
|
|
window.log.info('destroyExpiredMessages: convosToRefresh:', convosToRefresh);
|
|
await Promise.all(
|
|
convosToRefresh.map(async c => {
|
|
ConvoHub.use().get(c)?.updateLastMessage();
|
|
return ConvoHub.use().get(c)?.refreshInMemoryDetails();
|
|
})
|
|
);
|
|
} catch (error) {
|
|
window.log.error(
|
|
'destroyExpiredMessages: Error deleting expired messages',
|
|
error && error.stack ? error.stack : error
|
|
);
|
|
}
|
|
|
|
window.log.info('destroyExpiredMessages: complete');
|
|
void checkExpiringMessages();
|
|
}
|
|
|
|
let timeout: NodeJS.Timeout | undefined;
|
|
|
|
async function checkExpiringMessages() {
|
|
// Look up the next expiring message and set a timer to destroy it
|
|
const messages = await Data.getNextExpiringMessage();
|
|
const next = messages.at(0);
|
|
if (!next) {
|
|
return;
|
|
}
|
|
|
|
const expiresAt = next.getExpiresAt();
|
|
if (!expiresAt || !isNumber(expiresAt)) {
|
|
return;
|
|
}
|
|
|
|
const ms = expiresAt - Date.now();
|
|
window.log.info(
|
|
`message with hash:${next.getMessageHash()} expires in ${ms}ms, or ${Math.floor(
|
|
ms / 1000
|
|
)}s, or ${Math.floor(ms / (3600 * 1000))}h`
|
|
);
|
|
|
|
let wait = expiresAt - Date.now();
|
|
|
|
// In the past
|
|
if (wait < 0) {
|
|
wait = 0;
|
|
}
|
|
|
|
// Too far in the future, since it's limited to a 32-bit value
|
|
if (wait > 2147483647) {
|
|
wait = 2147483647;
|
|
}
|
|
|
|
if (timeout) {
|
|
global.clearTimeout(timeout);
|
|
}
|
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
timeout = global.setTimeout(async () => destroyExpiredMessages(), wait);
|
|
}
|
|
const throttledCheckExpiringMessages = throttle(checkExpiringMessages, 1000);
|
|
|
|
let isInit = false;
|
|
|
|
const initExpiringMessageListener = () => {
|
|
if (isInit) {
|
|
throw new Error('expiring messages listener is already init');
|
|
}
|
|
|
|
void checkExpiringMessages();
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
initWallClockListener(async () => throttledCheckExpiringMessages());
|
|
isInit = true;
|
|
};
|
|
|
|
const updateExpiringMessagesCheck = () => {
|
|
void throttledCheckExpiringMessages();
|
|
};
|
|
|
|
function setExpirationStartTimestamp(
|
|
mode: DisappearingMessageConversationModeType,
|
|
timestamp?: number,
|
|
// these are for debugging purposes
|
|
callLocation?: string,
|
|
messageId?: string
|
|
) {
|
|
let expirationStartTimestamp: number | undefined = NetworkTime.now();
|
|
|
|
if (callLocation) {
|
|
// window.log.debug(
|
|
// `[setExpirationStartTimestamp] called from: ${callLocation} ${
|
|
// messageId ? `messageId: ${messageId} ` : ''
|
|
// }`
|
|
// );
|
|
}
|
|
|
|
if (timestamp) {
|
|
if (!isValidUnixTimestamp(timestamp)) {
|
|
window.log.debug(
|
|
`[setExpirationStartTimestamp] We compared 2 timestamps for a disappearing message (${mode}) and the argument timestamp is an invalid unix timestamp.${
|
|
messageId ? `messageId: ${messageId} ` : ''
|
|
}`
|
|
);
|
|
return undefined;
|
|
}
|
|
expirationStartTimestamp = Math.min(expirationStartTimestamp, timestamp);
|
|
}
|
|
|
|
switch (mode) {
|
|
case 'deleteAfterRead':
|
|
// window.log.debug(
|
|
// `[setExpirationStartTimestamp] We set the start timestamp for a delete after read message to ${new Date(
|
|
// expirationStartTimestamp
|
|
// ).toLocaleTimeString()}${messageId ? `messageId: ${messageId} ` : ''}`
|
|
// );
|
|
break;
|
|
case 'deleteAfterSend':
|
|
// window.log.debug(
|
|
// `[setExpirationStartTimestamp] We set the start timestamp for a delete after send message to ${new Date(
|
|
// expirationStartTimestamp
|
|
// ).toLocaleTimeString()}${messageId ? `messageId: ${messageId} ` : ''}`
|
|
// );
|
|
break;
|
|
case 'off':
|
|
// window.log.debug(
|
|
// `[setExpirationStartTimestamp] Disappearing message mode has been turned off. We can safely ignore this. ${messageId ? `messageId: ${messageId} ` : ''}`
|
|
// );
|
|
expirationStartTimestamp = undefined;
|
|
break;
|
|
default:
|
|
window.log.warn(
|
|
`[setExpirationStartTimestamp] Invalid disappearing message mode "${mode}" set. Ignoring.${
|
|
messageId ? `messageId: ${messageId} ` : ''
|
|
}`
|
|
);
|
|
expirationStartTimestamp = undefined;
|
|
}
|
|
|
|
return expirationStartTimestamp;
|
|
}
|
|
|
|
/**
|
|
* Converts DisappearingMessageConversationModeType to DisappearingMessageType
|
|
*
|
|
* NOTE Used for sending or receiving data messages (protobuf)
|
|
*
|
|
* @param convo Conversation we want to set
|
|
* @param expirationMode DisappearingMessageConversationModeType
|
|
* @returns Disappearing mode we should use
|
|
*/
|
|
function changeToDisappearingMessageType(
|
|
convo: ConversationModel,
|
|
expireTimer: number,
|
|
expirationMode?: DisappearingMessageConversationModeType
|
|
): DisappearingMessageType {
|
|
if (expirationMode === 'off') {
|
|
// NOTE we would want this to be undefined but because of an issue with the protobuf implementation we need to have a value
|
|
return 'unknown';
|
|
}
|
|
|
|
if (expireTimer > 0) {
|
|
if (convo.isMe() || convo.isClosedGroup()) {
|
|
return 'deleteAfterSend';
|
|
}
|
|
|
|
return expirationMode === 'deleteAfterSend' ? 'deleteAfterSend' : 'deleteAfterRead';
|
|
}
|
|
|
|
return 'unknown';
|
|
}
|
|
|
|
/**
|
|
* Forces a private DaS to be a DaR.
|
|
* This should only be used for DataExtractionNotification and CallMessages (the ones saved to the DB) currently.
|
|
* Note: this can only be called for private conversations, excluding ourselves as it throws otherwise (this wouldn't be right)
|
|
* */
|
|
function forcedDeleteAfterReadMsgSetting(convo: ConversationModel): {
|
|
expirationType: Exclude<DisappearingMessageType, 'deleteAfterSend'>;
|
|
expireTimer: number;
|
|
} {
|
|
if (convo.isMe() || !convo.isPrivate()) {
|
|
throw new Error(
|
|
'forcedDeleteAfterReadMsgSetting can only be called with a private chat (excluding ourselves)'
|
|
);
|
|
}
|
|
const expirationMode = convo.getExpirationMode();
|
|
const expireTimer = convo.getExpireTimer();
|
|
if (expirationMode === 'off' || expireTimer <= 0) {
|
|
return { expirationType: 'unknown', expireTimer: 0 };
|
|
}
|
|
|
|
return {
|
|
expirationType: expirationMode === 'deleteAfterSend' ? 'deleteAfterRead' : expirationMode,
|
|
expireTimer,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Forces a private DaR to be a DaS.
|
|
* This should only be used for the outgoing CallMessages that we keep locally only (not synced, just the "you started a call" notification)
|
|
* Note: this can only be called for private conversations, excluding ourselves as it throws otherwise (this wouldn't be right)
|
|
* */
|
|
function forcedDeleteAfterSendMsgSetting(convo: ConversationModel): {
|
|
expirationType: Exclude<DisappearingMessageType, 'deleteAfterRead'>;
|
|
expireTimer: number;
|
|
} {
|
|
if (convo.isMe() || !convo.isPrivate()) {
|
|
throw new Error(
|
|
'forcedDeleteAfterSendMsgSetting can only be called with a private chat (excluding ourselves)'
|
|
);
|
|
}
|
|
const expirationMode = convo.getExpirationMode();
|
|
const expireTimer = convo.getExpireTimer();
|
|
if (expirationMode === 'off' || expireTimer <= 0) {
|
|
return { expirationType: 'unknown', expireTimer: 0 };
|
|
}
|
|
|
|
return {
|
|
expirationType: expirationMode === 'deleteAfterRead' ? 'deleteAfterSend' : expirationMode,
|
|
expireTimer,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Converts DisappearingMessageType to DisappearingMessageConversationModeType
|
|
*
|
|
* NOTE Used for the UI
|
|
*
|
|
* @param convo Conversation we want to set
|
|
* @param expirationType DisappearingMessageType
|
|
* @param expireTimer in seconds, 0 means no expiration
|
|
* @returns
|
|
*/
|
|
function changeToDisappearingConversationMode(
|
|
convo: ConversationModel,
|
|
expirationType?: DisappearingMessageType,
|
|
expireTimer?: number
|
|
): DisappearingMessageConversationModeType {
|
|
if (expirationType === 'unknown' || !expireTimer || expireTimer < 0) {
|
|
return 'off';
|
|
}
|
|
|
|
if (convo.isMe() || convo.isClosedGroup()) {
|
|
return 'deleteAfterSend';
|
|
}
|
|
|
|
return expirationType === 'deleteAfterSend' ? 'deleteAfterSend' : 'deleteAfterRead';
|
|
}
|
|
|
|
async function checkForExpireUpdateInContentMessage(
|
|
content: SignalService.Content,
|
|
convoToUpdate: ConversationModel,
|
|
messageExpirationFromRetrieve: number | null
|
|
): Promise<DisappearingMessageUpdate> {
|
|
const expirationTimer = content.expirationTimer;
|
|
|
|
// NOTE we don't use the expirationType directly from the Content Message because
|
|
// we need to resolve it to the correct convo type first in case it has errors
|
|
const expirationMode = changeToDisappearingConversationMode(
|
|
convoToUpdate,
|
|
DisappearingMessageMode[content.expirationType],
|
|
expirationTimer
|
|
);
|
|
|
|
const expireUpdate: DisappearingMessageUpdate = {
|
|
expirationType: changeToDisappearingMessageType(convoToUpdate, expirationTimer, expirationMode),
|
|
expirationTimer,
|
|
messageExpirationFromRetrieve,
|
|
};
|
|
|
|
return expireUpdate;
|
|
}
|
|
|
|
/**
|
|
* Checks if an outgoing message is meant to disappear and if so trigger the timer
|
|
*/
|
|
function checkForExpiringOutgoingMessage({
|
|
effectivelyStoredAtMs,
|
|
location,
|
|
message,
|
|
}: {
|
|
message: MessageModel;
|
|
location: string;
|
|
effectivelyStoredAtMs: number;
|
|
}) {
|
|
const convo = message.getConversation();
|
|
const expireTimer = message.getExpireTimerSeconds();
|
|
const expirationType = message.getExpirationType();
|
|
|
|
const isGroupConvo = !!convo?.isClosedGroup();
|
|
const isControlMessage = message.isControlMessage();
|
|
|
|
if (
|
|
convo &&
|
|
expirationType &&
|
|
expireTimer > 0 &&
|
|
!message.getExpirationStartTimestamp() &&
|
|
!(isGroupConvo && isControlMessage && !PubKey.is03Pubkey(convo.id))
|
|
) {
|
|
const expirationMode = changeToDisappearingConversationMode(convo, expirationType, expireTimer);
|
|
|
|
if (expirationMode !== 'off') {
|
|
const expirationStartTimestamp = setExpirationStartTimestamp(
|
|
expirationMode,
|
|
effectivelyStoredAtMs,
|
|
location
|
|
);
|
|
if (expirationStartTimestamp) {
|
|
message.set({
|
|
expirationStartTimestamp,
|
|
expires_at: expirationStartTimestamp + expireTimer * 1000,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function getMessageReadyToDisappear(
|
|
conversationModel: ConversationModel,
|
|
messageModel: MessageModel,
|
|
messageFlags: number,
|
|
expireUpdate?: ReadyToDisappearMsgUpdate
|
|
) {
|
|
if (conversationModel.isOpenGroupV2()) {
|
|
throw Error(
|
|
`getMessageReadyToDisappear() Disappearing messages aren't supported in communities`
|
|
);
|
|
}
|
|
if (!expireUpdate) {
|
|
window.log.debug(
|
|
`[getMessageReadyToDisappear] called getMessageReadyToDisappear() without an expireUpdate`
|
|
);
|
|
return messageModel;
|
|
}
|
|
|
|
const {
|
|
expirationType,
|
|
expirationTimer: expireTimer,
|
|
messageExpirationFromRetrieve,
|
|
} = expireUpdate;
|
|
|
|
// This message is an ExpirationTimerUpdate
|
|
if (messageFlags === SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE) {
|
|
const expirationTimerUpdate = {
|
|
expirationType,
|
|
expireTimer,
|
|
source: messageModel.get('source'),
|
|
};
|
|
|
|
messageModel.set({
|
|
expirationTimerUpdate,
|
|
});
|
|
}
|
|
|
|
// Note: We agreed that a control message for legacy groups does not expire
|
|
if (conversationModel.isClosedGroup() && messageModel.isControlMessage()) {
|
|
return messageModel;
|
|
}
|
|
|
|
/**
|
|
* This is quite tricky, but when we receive a message from the network, it might be a disappearing after read one, which was already read by another device.
|
|
* If that's the case, we need to not only mark the message as read, but also mark it as read at the right time.
|
|
* So that a message read 20h ago, and expiring 24h after read, has only 4h to live on this device too.
|
|
*
|
|
* A message is marked as read when created, if the convo volatile update reports that it should have been read (check `markAttributesAsReadIfNeeded()` if needed).
|
|
* That means that here, if we have a message
|
|
* - read,
|
|
* - incoming,
|
|
* - and disappearing after read,
|
|
* we have to force its expirationStartTimestamp and expire_at fields so they are in sync with our other devices.
|
|
*/
|
|
messageModel.set({
|
|
expirationType,
|
|
expireTimer,
|
|
});
|
|
|
|
if (
|
|
conversationModel.isPrivate() &&
|
|
messageModel.isIncoming() &&
|
|
expirationType === 'deleteAfterRead' &&
|
|
expireTimer > 0 &&
|
|
messageModel.get('unread') === READ_MESSAGE_STATE.read &&
|
|
messageExpirationFromRetrieve &&
|
|
messageExpirationFromRetrieve > 0
|
|
) {
|
|
/**
|
|
* Edge case: when we send a message before we poll for a message sent earlier, our convo volatile update will
|
|
* mark that incoming message as read right away (because it was sent earlier than our latest convo volatile lastRead).
|
|
* To take care of this case, we need to check if an incoming DaR message is in a read state but its expiration has not been updated yet.
|
|
* The way we do it, is by checking that the swarm expiration is before (now + expireTimer).
|
|
* If it looks like this expiration was not updated yet, we need to trigger a UpdateExpiryJob for that message.
|
|
*/
|
|
const now = NetworkTime.now();
|
|
const expirationNowPlusTimer = now + expireTimer * 1000;
|
|
const msgExpirationWasAlreadyUpdated = messageExpirationFromRetrieve <= expirationNowPlusTimer;
|
|
// Note: a message might be added even when it expired, but the periodic cleaning of expired message will pick it up and remove it soon enough
|
|
|
|
if (msgExpirationWasAlreadyUpdated) {
|
|
const expirationStartTimestamp = messageExpirationFromRetrieve - expireTimer * 1000;
|
|
window.log.debug(
|
|
`incoming DaR message already read by another device, forcing readAt ${
|
|
(Date.now() - expirationStartTimestamp) / 1000
|
|
}s ago, so with ${(messageExpirationFromRetrieve - Date.now()) / 1000}s left`
|
|
);
|
|
messageModel.set({
|
|
expirationStartTimestamp,
|
|
expires_at: messageExpirationFromRetrieve,
|
|
});
|
|
} else {
|
|
window.log.debug(
|
|
`incoming DaR message already read by another device but swarmExpiration seems NOT updated, forcing readAt NOW and triggering UpdateExpiryJob with ${expireTimer}s left`
|
|
);
|
|
messageModel.set({
|
|
expirationStartTimestamp: now,
|
|
expires_at: expirationNowPlusTimer,
|
|
});
|
|
// Ideally we would batch call those UpdateExpiry, but we can't currently and disappear v2 is already too complex as it is.
|
|
void UpdateMsgExpirySwarm.queueNewJobIfNeeded([messageModel.id]);
|
|
}
|
|
} else if (
|
|
expirationType === 'deleteAfterSend' &&
|
|
expireTimer > 0 &&
|
|
messageExpirationFromRetrieve &&
|
|
messageExpirationFromRetrieve > 0
|
|
) {
|
|
// Note: closed groups control message do not disappear
|
|
|
|
const expirationStartTimestamp = messageExpirationFromRetrieve - expireTimer * 1000;
|
|
const expires_at = messageExpirationFromRetrieve;
|
|
|
|
window.log.debug(
|
|
`incoming DaS message,\n\tforcing expirationStartTimestamp to ${expirationStartTimestamp} which is ${(Date.now() - expirationStartTimestamp) / 1000}s ago,\n\tand expires_at to ${expires_at} so with ${(messageExpirationFromRetrieve - Date.now()) / 1000}s left`
|
|
);
|
|
messageModel.set({
|
|
expirationStartTimestamp,
|
|
expires_at,
|
|
});
|
|
}
|
|
|
|
return messageModel;
|
|
}
|
|
|
|
async function updateMessageExpiriesOnSwarm(messages: Array<MessageModel>) {
|
|
const expiringDetails: ExpiringDetails = [];
|
|
|
|
messages.forEach(msg => {
|
|
const hash = msg.getMessageHash();
|
|
const timestampStarted = msg.getExpirationStartTimestamp();
|
|
const timerSeconds = msg.getExpireTimerSeconds();
|
|
const disappearingType = msg.getExpirationType();
|
|
if (
|
|
!hash ||
|
|
!timestampStarted ||
|
|
timestampStarted <= 0 ||
|
|
!timerSeconds ||
|
|
timerSeconds <= 0 ||
|
|
disappearingType !== 'deleteAfterRead' || // this is very important as a message not stored on the swarm will be assumed expired, and so deleted locally!
|
|
!msg.isIncoming() // this is very important as a message not stored on the swarm will be assumed expired, and so deleted locally!
|
|
) {
|
|
return;
|
|
}
|
|
expiringDetails.push({
|
|
messageHash: hash,
|
|
expireTimerMs: timerSeconds * 1000,
|
|
readAt: timestampStarted,
|
|
});
|
|
});
|
|
|
|
if (isEmpty(expiringDetails)) {
|
|
window.log.debug(`[updateMessageExpiriesOnSwarm] no expiringDetails to update`);
|
|
return;
|
|
}
|
|
window.log.debug('updateMessageExpiriesOnSwarm: expiringDetails', expiringDetails);
|
|
|
|
const newTTLs = await expireMessagesOnSnode(expiringDetails, { shortenOrExtend: 'shorten' });
|
|
const updatedMsgModels: Array<MessageModel> = [];
|
|
window.log.debug('updateMessageExpiriesOnSwarm newTTLs: ', newTTLs);
|
|
newTTLs.forEach(m => {
|
|
const message = messages.find(model => model.getMessageHash() === m.messageHash);
|
|
if (!message) {
|
|
return;
|
|
}
|
|
|
|
const newTTLms = m.updatedExpiryMs;
|
|
const realReadAt = newTTLms - message.getExpireTimerSeconds() * 1000;
|
|
if (
|
|
newTTLms &&
|
|
(newTTLms !== message.getExpiresAt() ||
|
|
message.get('expirationStartTimestamp') !== realReadAt) &&
|
|
message.getExpireTimerSeconds()
|
|
) {
|
|
window.log.debug(`updateMessageExpiriesOnSwarm: setting for msg hash ${m.messageHash}:`, {
|
|
expires_at: newTTLms,
|
|
expirationStartTimestamp: realReadAt,
|
|
unread: READ_MESSAGE_STATE.read,
|
|
});
|
|
message.set({
|
|
expires_at: newTTLms,
|
|
expirationStartTimestamp: realReadAt,
|
|
unread: READ_MESSAGE_STATE.read,
|
|
});
|
|
|
|
updatedMsgModels.push(message);
|
|
}
|
|
});
|
|
|
|
if (!isEmpty(updatedMsgModels)) {
|
|
await Promise.all(updatedMsgModels.map(m => m.commit()));
|
|
}
|
|
}
|
|
|
|
function getExpireDetailsForOutgoingMessage(
|
|
convo: ConversationModel,
|
|
createAtNetworkTimestamp: number
|
|
) {
|
|
const expireTimer = convo.getExpireTimer();
|
|
const expireDetails = {
|
|
expirationType: DisappearingMessages.changeToDisappearingMessageType(
|
|
convo,
|
|
expireTimer,
|
|
convo.getExpirationMode()
|
|
),
|
|
expireTimer,
|
|
expirationTimer: expireTimer,
|
|
messageExpirationFromRetrieve: expireTimer > 0 ? createAtNetworkTimestamp + expireTimer : null,
|
|
};
|
|
|
|
return expireDetails;
|
|
}
|
|
|
|
export const DisappearingMessages = {
|
|
destroyMessagesAndUpdateRedux,
|
|
initExpiringMessageListener,
|
|
updateExpiringMessagesCheck,
|
|
setExpirationStartTimestamp,
|
|
changeToDisappearingMessageType,
|
|
getExpireDetailsForOutgoingMessage,
|
|
changeToDisappearingConversationMode,
|
|
forcedDeleteAfterReadMsgSetting,
|
|
forcedDeleteAfterSendMsgSetting,
|
|
checkForExpireUpdateInContentMessage,
|
|
checkForExpiringOutgoingMessage,
|
|
getMessageReadyToDisappear,
|
|
updateMessageExpiriesOnSwarm,
|
|
destroyExpiredMessages,
|
|
};
|