feat: add cleanup of old expire update in history

we only keep one from each sender
pull/2940/head
Audric Ackermann 1 year ago
parent 543c80bbe3
commit 19e9e0311e

@ -192,10 +192,10 @@
"followSetting": "Follow Setting", "followSetting": "Follow Setting",
"followSettingDisabled": "Messages you send will no longer disappear. Are you sure you want to turn off disappearing messages?", "followSettingDisabled": "Messages you send will no longer disappear. Are you sure you want to turn off disappearing messages?",
"followSettingTimeAndType": "Set your messages to disappear $time$ after they have been $type$?", "followSettingTimeAndType": "Set your messages to disappear $time$ after they have been $type$?",
"youChangedTheTimer": "You have set messages to disappear $time$ after they have been $mode$", "youChangedTheTimer": "<b>You</b> have set messages to disappear <b>$time$</b> after they have been <b>$mode$</b>",
"youChangedTheTimerLegacy": "You set the disappearing message timer to $time$", "youChangedTheTimerLegacy": "<b>You<b> set the disappearing message timer to <b>$time$</b>",
"theyChangedTheTimer": "$name$ has set messages to disappear $time$ after they have been $mode$", "theyChangedTheTimer": "<b>$name$</b> has set messages to disappear <b>$time$</b> after they have been <b>$mode$</b>",
"theyChangedTheTimerLegacy": "$name$ set the disappearing message timer to $time$", "theyChangedTheTimerLegacy": "<b>$name$</b> set the disappearing message timer to <b>$time$</b>",
"timerOption_0_seconds": "Off", "timerOption_0_seconds": "Off",
"timerOption_5_seconds": "5 seconds", "timerOption_5_seconds": "5 seconds",
"timerOption_10_seconds": "10 seconds", "timerOption_10_seconds": "10 seconds",
@ -233,12 +233,12 @@
"disappearingMessagesModeLegacy": "Legacy", "disappearingMessagesModeLegacy": "Legacy",
"disappearingMessagesModeLegacySubtitle": "Original version of disappearing messages.", "disappearingMessagesModeLegacySubtitle": "Original version of disappearing messages.",
"disappearingMessagesDisabled": "Disappearing messages disabled", "disappearingMessagesDisabled": "Disappearing messages disabled",
"disabledDisappearingMessages": "$name$ has turned off disappearing messages.", "disabledDisappearingMessages": "<b>$name$</b> has turned <b>off</b> disappearing messages.",
"youDisabledDisappearingMessages": "You have turned off disappearing messages.", "youDisabledDisappearingMessages": "<b>You</b> have turned <b>off</b> disappearing messages.",
"youDisabledYourDisappearingMessages": "You turned off disappearing messages. Messages you send will no longer disappear.", "youDisabledYourDisappearingMessages": "<b>You</b> turned <b>off</b> disappearing messages. Messages you send will no longer disappear.",
"youSetYourDisappearingMessages": "You set your messages to disappear $time$ after they have been $type$.", "youSetYourDisappearingMessages": "<b>You</b> set your messages to disappear <b>$time$</b> after they have been <b>$type$</b>.",
"theyDisabledTheirDisappearingMessages": "$name$ has turned off disappearing messages. Messages they send will no longer disappear.", "theyDisabledTheirDisappearingMessages": "<b>$name$</b> has turned <b>off</b> disappearing messages. Messages they send will no longer disappear.",
"theySetTheirDisappearingMessages": "$name$ has set their messages to disappear $time$ after they have been $type$.", "theySetTheirDisappearingMessages": "<b>$name$</b> has set their messages to disappear <b>$time$</b> after they have been <b>$type$</b>.",
"timerSetTo": "Disappearing message time set to $time$", "timerSetTo": "Disappearing message time set to $time$",
"set": "Set", "set": "Set",
"changeNickname": "Change Nickname", "changeNickname": "Change Nickname",

@ -95,7 +95,7 @@
"glob": "7.1.2", "glob": "7.1.2",
"image-type": "^4.1.0", "image-type": "^4.1.0",
"ip2country": "1.0.1", "ip2country": "1.0.1",
"libsession_util_nodejs": "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.3.0/libsession_util_nodejs-v0.3.0.tar.gz", "libsession_util_nodejs": "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.3.1/libsession_util_nodejs-v0.3.1.tar.gz",
"libsodium-wrappers-sumo": "^0.7.9", "libsodium-wrappers-sumo": "^0.7.9",
"linkify-it": "^4.0.1", "linkify-it": "^4.0.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",

@ -10,7 +10,7 @@ type TextProps = {
ellipsisOverflow?: boolean; ellipsisOverflow?: boolean;
}; };
const StyledDefaultText = styled.div<TextProps>` const StyledDefaultText = styled.div<Omit<TextProps, 'text'>>`
transition: var(--default-duration); transition: var(--default-duration);
max-width: ${props => (props.maxWidth ? props.maxWidth : '')}; max-width: ${props => (props.maxWidth ? props.maxWidth : '')};
padding: ${props => (props.padding ? props.padding : '')}; padding: ${props => (props.padding ? props.padding : '')};
@ -26,6 +26,12 @@ export const Text = (props: TextProps) => {
return <StyledDefaultText {...props}>{props.text}</StyledDefaultText>; return <StyledDefaultText {...props}>{props.text}</StyledDefaultText>;
}; };
export const TextWithChildren = (
props: Omit<TextProps, 'text'> & { children: React.ReactNode }
) => {
return <StyledDefaultText {...props}>{props.children}</StyledDefaultText>;
};
type SpacerProps = { type SpacerProps = {
size: 'xl' | 'lg' | 'md' | 'sm' | 'xs'; size: 'xl' | 'lg' | 'md' | 'sm' | 'xs';
style?: CSSProperties; style?: CSSProperties;

@ -17,7 +17,7 @@ import {
} from '../../state/selectors/selectedConversation'; } from '../../state/selectors/selectedConversation';
import { ReleasedFeatures } from '../../util/releaseFeature'; import { ReleasedFeatures } from '../../util/releaseFeature';
import { Flex } from '../basic/Flex'; import { Flex } from '../basic/Flex';
import { Text } from '../basic/Text'; import { TextWithChildren } from '../basic/Text';
import { ExpirableReadableMessage } from './message/message-item/ExpirableReadableMessage'; import { ExpirableReadableMessage } from './message/message-item/ExpirableReadableMessage';
// eslint-disable-next-line import/order // eslint-disable-next-line import/order
import { pick } from 'lodash'; import { pick } from 'lodash';
@ -25,6 +25,7 @@ import { ConversationInteraction } from '../../interactions';
import { getConversationController } from '../../session/conversations'; import { getConversationController } from '../../session/conversations';
import { updateConfirmModal } from '../../state/ducks/modalDialog'; import { updateConfirmModal } from '../../state/ducks/modalDialog';
import { SessionButtonColor } from '../basic/SessionButton'; import { SessionButtonColor } from '../basic/SessionButton';
import { SessionHtmlRenderer } from '../basic/SessionHTMLRenderer';
const FollowSettingButton = styled.button` const FollowSettingButton = styled.button`
color: var(--primary-color); color: var(--primary-color);
@ -208,7 +209,9 @@ export const TimerNotification = (props: PropsForExpirationTimer) => {
padding="5px 10px" padding="5px 10px"
style={{ textAlign: 'center' }} style={{ textAlign: 'center' }}
> >
<Text text={textToRender} subtle={true} /> <TextWithChildren subtle={true}>
<SessionHtmlRenderer html={textToRender} />
</TextWithChildren>
<FollowSettingsButton {...props} /> <FollowSettingsButton {...props} />
</Flex> </Flex>
</ExpirableReadableMessage> </ExpirableReadableMessage>

@ -6,6 +6,7 @@ import { useMessageExpirationPropsById } from '../../../../hooks/useParamSelecto
import { useMessageStatus } from '../../../../state/selectors'; import { useMessageStatus } from '../../../../state/selectors';
import { getMostRecentMessageId } from '../../../../state/selectors/conversations'; import { getMostRecentMessageId } from '../../../../state/selectors/conversations';
import { useSelectedIsGroup } from '../../../../state/selectors/selectedConversation';
import { SpacerXS } from '../../../basic/Text'; import { SpacerXS } from '../../../basic/Text';
import { SessionIcon, SessionIconType } from '../../../icon'; import { SessionIcon, SessionIconType } from '../../../icon';
import { ExpireTimer } from '../../ExpireTimer'; import { ExpireTimer } from '../../ExpireTimer';
@ -60,7 +61,7 @@ export const MessageStatus = (props: Props) => {
} }
}; };
const MessageStatusContainer = styled.div<{ isIncoming: boolean }>` const MessageStatusContainer = styled.div<{ isIncoming: boolean; isGroup: boolean }>`
display: inline-block; display: inline-block;
align-self: ${props => (props.isIncoming ? 'flex-start' : 'flex-end')}; align-self: ${props => (props.isIncoming ? 'flex-start' : 'flex-end')};
flex-direction: ${props => flex-direction: ${props =>
@ -73,6 +74,8 @@ const MessageStatusContainer = styled.div<{ isIncoming: boolean }>`
cursor: pointer; cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
margin-inline-start: ${props =>
props.isGroup || !props.isIncoming ? 'var(--width-avatar-group-msg-list)' : 0};
`; `;
const StyledStatusText = styled.div` const StyledStatusText = styled.div`
@ -144,7 +147,12 @@ function MessageStatusExpireTimer(props: Props) {
const MessageStatusSending = ({ dataTestId }: Props) => { const MessageStatusSending = ({ dataTestId }: Props) => {
// while sending, we do not display the expire timer at all. // while sending, we do not display the expire timer at all.
return ( return (
<MessageStatusContainer data-testid={dataTestId} data-testtype="sending" isIncoming={false}> <MessageStatusContainer
data-testid={dataTestId}
data-testtype="sending"
isIncoming={false}
isGroup={false}
>
<TextDetails text={window.i18n('sending')} /> <TextDetails text={window.i18n('sending')} />
<IconNormal rotateDuration={2} iconType="sending" /> <IconNormal rotateDuration={2} iconType="sending" />
</MessageStatusContainer> </MessageStatusContainer>
@ -171,13 +179,19 @@ function IconForExpiringMessageId({
const MessageStatusSent = ({ dataTestId, messageId }: Props) => { const MessageStatusSent = ({ dataTestId, messageId }: Props) => {
const isExpiring = useIsExpiring(messageId); const isExpiring = useIsExpiring(messageId);
const isMostRecentMessage = useIsMostRecentMessage(messageId); const isMostRecentMessage = useIsMostRecentMessage(messageId);
const isGroup = useSelectedIsGroup();
// we hide a "sent" message status which is not expiring except for the most recent message // we hide a "sent" message status which is not expiring except for the most recent message
if (!isExpiring && !isMostRecentMessage) { if (!isExpiring && !isMostRecentMessage) {
return null; return null;
} }
return ( return (
<MessageStatusContainer data-testid={dataTestId} data-testtype="sent" isIncoming={false}> <MessageStatusContainer
data-testid={dataTestId}
data-testtype="sent"
isIncoming={false}
isGroup={isGroup}
>
<TextDetails text={window.i18n('sent')} /> <TextDetails text={window.i18n('sent')} />
<IconForExpiringMessageId messageId={messageId} iconType="circleCheck" /> <IconForExpiringMessageId messageId={messageId} iconType="circleCheck" />
</MessageStatusContainer> </MessageStatusContainer>
@ -190,6 +204,7 @@ const MessageStatusRead = ({
isIncoming, isIncoming,
}: Props & { isIncoming: boolean }) => { }: Props & { isIncoming: boolean }) => {
const isExpiring = useIsExpiring(messageId); const isExpiring = useIsExpiring(messageId);
const isGroup = useSelectedIsGroup();
const isMostRecentMessage = useIsMostRecentMessage(messageId); const isMostRecentMessage = useIsMostRecentMessage(messageId);
@ -199,7 +214,12 @@ const MessageStatusRead = ({
} }
return ( return (
<MessageStatusContainer data-testid={dataTestId} data-testtype="read" isIncoming={isIncoming}> <MessageStatusContainer
data-testid={dataTestId}
data-testtype="read"
isIncoming={isIncoming}
isGroup={isGroup}
>
<TextDetails text={window.i18n('read')} /> <TextDetails text={window.i18n('read')} />
<IconForExpiringMessageId messageId={messageId} iconType="doubleCheckCircleFilled" /> <IconForExpiringMessageId messageId={messageId} iconType="doubleCheckCircleFilled" />
</MessageStatusContainer> </MessageStatusContainer>
@ -211,6 +231,7 @@ const MessageStatusError = ({ dataTestId }: Props) => {
ipcRenderer.send('show-debug-log'); ipcRenderer.send('show-debug-log');
}, []); }, []);
// when on error, we do not display the expire timer at all. // when on error, we do not display the expire timer at all.
const isGroup = useSelectedIsGroup();
return ( return (
<MessageStatusContainer <MessageStatusContainer
@ -219,6 +240,7 @@ const MessageStatusError = ({ dataTestId }: Props) => {
onClick={showDebugLog} onClick={showDebugLog}
title={window.i18n('sendFailed')} title={window.i18n('sendFailed')}
isIncoming={false} isIncoming={false}
isGroup={isGroup}
> >
<TextDetails text={window.i18n('failed')} /> <TextDetails text={window.i18n('failed')} />
<IconDanger iconType="error" /> <IconDanger iconType="error" />

@ -252,6 +252,19 @@ async function saveMessages(arrayOfMessages: Array<MessageAttributes>): Promise<
await channels.saveMessages(cleanData(arrayOfMessages)); await channels.saveMessages(cleanData(arrayOfMessages));
} }
/**
*
* @param conversationId the conversation from which to remove all but the most recent disappear timer update
* @param isPrivate if that conversation is private, we keep a expiration timer update for each sender
* @returns the array of messageIds removed, or [] if none were removed
*/
async function cleanUpExpirationTimerUpdateHistory(
conversationId: string,
isPrivate: boolean
): Promise<Array<string>> {
return channels.cleanUpExpirationTimerUpdateHistory(conversationId, isPrivate);
}
async function removeMessage(id: string): Promise<void> { async function removeMessage(id: string): Promise<void> {
const message = await getMessageById(id, true); const message = await getMessageById(id, true);
@ -800,6 +813,7 @@ export const Data = {
saveMessages, saveMessages,
removeMessage, removeMessage,
removeMessagesByIds, removeMessagesByIds,
cleanUpExpirationTimerUpdateHistory,
getMessageIdsFromServerIds, getMessageIdsFromServerIds,
getMessageById, getMessageById,
getMessagesBySenderAndSentAt, getMessagesBySenderAndSentAt,

@ -41,6 +41,7 @@ const channelsToMake = new Set([
'saveMessages', 'saveMessages',
'removeMessage', 'removeMessage',
'removeMessagesByIds', 'removeMessagesByIds',
'cleanUpExpirationTimerUpdateHistory',
'getUnreadByConversation', 'getUnreadByConversation',
'getUnreadDisappearingByConversation', 'getUnreadDisappearingByConversation',
'markAllAsReadByConversationNoExpiration', 'markAllAsReadByConversationNoExpiration',

@ -48,6 +48,7 @@ import {
conversationsChanged, conversationsChanged,
markConversationFullyRead, markConversationFullyRead,
MessageModelPropsWithoutConvoProps, MessageModelPropsWithoutConvoProps,
messagesDeleted,
ReduxConversationType, ReduxConversationType,
} from '../state/ducks/conversations'; } from '../state/ducks/conversations';
@ -823,7 +824,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
fromSync, // if the update comes from a config or sync message fromSync, // if the update comes from a config or sync message
fromCurrentDevice, fromCurrentDevice,
shouldCommitConvo = true, shouldCommitConvo = true,
shouldCommitMessage = true,
existingMessage, existingMessage,
}: { }: {
providedDisappearingMode?: DisappearingMessageConversationModeType; providedDisappearingMode?: DisappearingMessageConversationModeType;
@ -833,7 +833,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
fromSync: boolean; fromSync: boolean;
fromCurrentDevice: boolean; fromCurrentDevice: boolean;
shouldCommitConvo?: boolean; shouldCommitConvo?: boolean;
shouldCommitMessage?: boolean;
existingMessage?: MessageModel; existingMessage?: MessageModel;
}): Promise<boolean> { }): Promise<boolean> {
const isRemoteChange = Boolean((receivedAt || fromSync) && !fromCurrentDevice); const isRemoteChange = Boolean((receivedAt || fromSync) && !fromCurrentDevice);
@ -864,7 +863,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
); );
const isV2DisappearReleased = ReleasedFeatures.isDisappearMessageV2FeatureReleasedCached(); const isV2DisappearReleased = ReleasedFeatures.isDisappearMessageV2FeatureReleasedCached();
// when the v2 disappear is released, the changes we make are only for our outgoing messages, not shared with a contact anymore // when the v2 disappear is released, the changes we make are only for our outgoing messages, not shared with a contact anymore
if (isV2DisappearReleased) { if (isV2DisappearReleased) {
if (!this.isPrivate()) { if (!this.isPrivate()) {
@ -873,6 +871,10 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
expireTimer, expireTimer,
}); });
} else if (fromSync || fromCurrentDevice) { } else if (fromSync || fromCurrentDevice) {
if (expirationMode === 'legacy') {
// TODO legacy messages support will be removed in a future release
return false;
}
// v2 is live, this is a private chat and a change we made, set the setting to what was given, otherwise discard it // v2 is live, this is a private chat and a change we made, set the setting to what was given, otherwise discard it
this.set({ this.set({
expirationMode, expirationMode,
@ -960,16 +962,16 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
message.get('id') message.get('id')
), ),
}); });
if (shouldCommitMessage) {
await message.commit();
}
} }
} }
return true;
}
if (shouldCommitMessage) {
await message.commit(); await message.commit();
await cleanUpExpireHistoryFromConvo(this.id, this.isPrivate());
return true;
} }
await message.commit();
await cleanUpExpireHistoryFromConvo(this.id, this.isPrivate());
// //
// Below is the "sending the update to the conversation" part. // Below is the "sending the update to the conversation" part.
// We would have returned if that message sending part was not needed // We would have returned if that message sending part was not needed
@ -1158,18 +1160,18 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
const sendReceipt = const sendReceipt =
settingsReadReceiptEnabled && !this.isBlocked() && !this.isIncomingRequest(); settingsReadReceiptEnabled && !this.isBlocked() && !this.isIncomingRequest();
if (sendReceipt) { if (!sendReceipt) {
window?.log?.info(`Sending ${timestamps.length} read receipts.`); return;
// we should probably stack read receipts and send them every 5 seconds for instance per conversation }
window?.log?.info(`Sending ${timestamps.length} read receipts.`);
const receiptMessage = new ReadReceiptMessage({ const receiptMessage = new ReadReceiptMessage({
timestamp: Date.now(), timestamp: Date.now(),
timestamps, timestamps,
}); });
const device = new PubKey(this.id); const device = new PubKey(this.id);
await getMessageQueue().sendToPubKey(device, receiptMessage, SnodeNamespaces.UserMessages); await getMessageQueue().sendToPubKey(device, receiptMessage, SnodeNamespaces.UserMessages);
}
} }
public async setNickname(nickname: string | null, shouldCommit = false) { public async setNickname(nickname: string | null, shouldCommit = false) {
@ -2519,3 +2521,14 @@ export function hasValidIncomingRequestValues({
const isActive = activeAt && isFinite(activeAt) && activeAt > 0; const isActive = activeAt && isFinite(activeAt) && activeAt > 0;
return Boolean(isPrivate && !isMe && !isApproved && !isBlocked && isActive && didApproveMe); return Boolean(isPrivate && !isMe && !isApproved && !isBlocked && isActive && didApproveMe);
} }
async function cleanUpExpireHistoryFromConvo(conversationId: string, isPrivate: boolean) {
const updateIdsRemoved = await Data.cleanUpExpirationTimerUpdateHistory(
conversationId,
isPrivate
);
window.inboxStore.dispatch(
messagesDeleted(updateIdsRemoved.map(m => ({ conversationKey: conversationId, messageId: m })))
);
}

@ -1677,6 +1677,8 @@ function updateToSessionSchemaVersion34(currentVersion: number, db: BetterSqlite
// Message changes // Message changes
db.prepare(`ALTER TABLE ${MESSAGES_TABLE} ADD COLUMN expirationType TEXT;`).run(); db.prepare(`ALTER TABLE ${MESSAGES_TABLE} ADD COLUMN expirationType TEXT;`).run();
db.prepare(`ALTER TABLE ${MESSAGES_TABLE} ADD COLUMN flags INTEGER;`).run();
db.prepare(`UPDATE ${MESSAGES_TABLE} SET flags = json_extract(json, '$.flags');`);
// #endregion // #endregion

@ -61,6 +61,8 @@ import {
} from '../types/sqlSharedTypes'; } from '../types/sqlSharedTypes';
import { KNOWN_BLINDED_KEYS_ITEM, SettingsKey } from '../data/settings-key'; import { KNOWN_BLINDED_KEYS_ITEM, SettingsKey } from '../data/settings-key';
import { MessageAttributes } from '../models/messageType';
import { SignalService } from '../protobuf';
import { Quote } from '../receiver/types'; import { Quote } from '../receiver/types';
import { import {
getSQLCipherIntegrityCheck, getSQLCipherIntegrityCheck,
@ -780,11 +782,10 @@ function getMessageCount() {
return row['count(*)']; return row['count(*)'];
} }
function saveMessage(data: any) { function saveMessage(data: MessageAttributes) {
const { const {
body, body,
conversationId, conversationId,
// eslint-disable-next-line camelcase
expires_at, expires_at,
hasAttachments, hasAttachments,
hasFileAttachments, hasFileAttachments,
@ -792,10 +793,8 @@ function saveMessage(data: any) {
id, id,
serverId, serverId,
serverTimestamp, serverTimestamp,
// eslint-disable-next-line camelcase
received_at, received_at,
sent, sent,
// eslint-disable-next-line camelcase
sent_at, sent_at,
source, source,
type, type,
@ -803,6 +802,7 @@ function saveMessage(data: any) {
expirationType, expirationType,
expireTimer, expireTimer,
expirationStartTimestamp, expirationStartTimestamp,
flags,
} = data; } = data;
if (!id) { if (!id) {
@ -834,6 +834,7 @@ function saveMessage(data: any) {
source, source,
type: type || '', type: type || '',
unread, unread,
flags: flags ?? 0,
}; };
assertGlobalInstance() assertGlobalInstance()
@ -857,7 +858,8 @@ function saveMessage(data: any) {
sent_at, sent_at,
source, source,
type, type,
unread unread,
flags
) values ( ) values (
$id, $id,
$json, $json,
@ -877,7 +879,8 @@ function saveMessage(data: any) {
$sent_at, $sent_at,
$source, $source,
$type, $type,
$unread $unread,
$flags
);` );`
) )
.run(payload); .run(payload);
@ -959,8 +962,8 @@ function cleanSeenMessages() {
}); });
} }
function saveMessages(arrayOfMessages: Array<any>) { function saveMessages(arrayOfMessages: Array<MessageAttributes>) {
console.info('saveMessages of length: ', arrayOfMessages.length); console.info('saveMessages count: ', arrayOfMessages.length);
assertGlobalInstance().transaction(() => { assertGlobalInstance().transaction(() => {
map(arrayOfMessages, saveMessage); map(arrayOfMessages, saveMessage);
})(); })();
@ -969,8 +972,6 @@ function saveMessages(arrayOfMessages: Array<any>) {
function removeMessage(id: string, instance?: BetterSqlite3.Database) { function removeMessage(id: string, instance?: BetterSqlite3.Database) {
if (!isString(id)) { if (!isString(id)) {
throw new Error('removeMessage: only takes single message to delete!'); throw new Error('removeMessage: only takes single message to delete!');
return;
} }
assertGlobalInstanceOrInstance(instance) assertGlobalInstanceOrInstance(instance)
@ -1008,6 +1009,48 @@ function removeAllMessagesInConversation(
.run({ conversationId }); .run({ conversationId });
} }
function cleanUpExpirationTimerUpdateHistory(
conversationId: string,
isPrivate: boolean,
db?: BetterSqlite3.Database
) {
if (isEmpty(conversationId)) {
return [];
}
const rows = assertGlobalInstanceOrInstance(db)
.prepare(
`SELECT id, source FROM ${MESSAGES_TABLE} WHERE conversationId = $conversationId and flags = ${SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE} ${orderByClause}`
)
.all({ conversationId });
if (rows.length <= 1) {
return [];
}
// we want to allow 1 message at most per sender for private chats only
const bySender: Record<string, Array<string>> = {};
// we keep the order, so the first message of each array should be kept, the other ones discarded
rows.forEach(r => {
const groupedById = isPrivate ? r.source : conversationId;
if (!bySender[groupedById]) {
bySender[groupedById] = [];
}
bySender[groupedById].push(r.id);
});
const allMsgIdsRemoved: Array<string> = [];
Object.keys(bySender).forEach(k => {
const idsToRemove = bySender[k].slice(1); // we keep the first one
if (isEmpty(idsToRemove)) {
return;
}
removeMessagesByIds(idsToRemove, db);
allMsgIdsRemoved.push(...idsToRemove);
});
return allMsgIdsRemoved;
}
function getMessageIdsFromServerIds(serverIds: Array<string | number>, conversationId: string) { function getMessageIdsFromServerIds(serverIds: Array<string | number>, conversationId: string) {
if (!Array.isArray(serverIds)) { if (!Array.isArray(serverIds)) {
return []; return [];
@ -2414,6 +2457,7 @@ export const sqlNode = {
saveMessages, saveMessages,
removeMessage, removeMessage,
removeMessagesByIds, removeMessagesByIds,
cleanUpExpirationTimerUpdateHistory,
removeAllMessagesInConversation, removeAllMessagesInConversation,
getUnreadByConversation, getUnreadByConversation,
getUnreadDisappearingByConversation, getUnreadDisappearingByConversation,

@ -279,7 +279,6 @@ export async function handleNewClosedGroup(
const envelopeTimestamp = toNumber(envelope.timestamp); const envelopeTimestamp = toNumber(envelope.timestamp);
// a type new is sent and received on one to one so do not use envelope.senderIdentity here // a type new is sent and received on one to one so do not use envelope.senderIdentity here
const sender = envelope.source; const sender = envelope.source;
if ( if (
(await sentAtMoreRecentThanWrapper(envelopeTimestamp, 'UserGroupsConfig')) === (await sentAtMoreRecentThanWrapper(envelopeTimestamp, 'UserGroupsConfig')) ===
'wrapper_more_recent' 'wrapper_more_recent'

@ -230,6 +230,7 @@ async function handleUserProfileUpdate(result: IncomingConfResult): Promise<Inco
let changes = false; let changes = false;
const expireTimer = ourConvo.getExpireTimer(); const expireTimer = ourConvo.getExpireTimer();
const wrapperNoteToSelfExpirySeconds = await UserConfigWrapperActions.getNoteToSelfExpiry(); const wrapperNoteToSelfExpirySeconds = await UserConfigWrapperActions.getNoteToSelfExpiry();
if (wrapperNoteToSelfExpirySeconds !== expireTimer) { if (wrapperNoteToSelfExpirySeconds !== expireTimer) {

@ -474,7 +474,6 @@ export async function innerHandleSwarmContentMessage(
content, content,
conversationModelForUIUpdate conversationModelForUIUpdate
); );
// TODO legacy messages support will be removed in a future release // TODO legacy messages support will be removed in a future release
if (expireUpdate?.isDisappearingMessagesV2Released) { if (expireUpdate?.isDisappearingMessagesV2Released) {
await DisappearingMessages.checkHasOutdatedDisappearingMessageClient( await DisappearingMessages.checkHasOutdatedDisappearingMessageClient(

@ -1,4 +1,4 @@
import { throttle, uniq } from 'lodash'; import { isNumber, throttle, uniq } from 'lodash';
import { messagesExpired } from '../../state/ducks/conversations'; import { messagesExpired } from '../../state/ducks/conversations';
import { initWallClockListener } from '../../util/wallClockListener'; import { initWallClockListener } from '../../util/wallClockListener';
@ -102,7 +102,7 @@ async function checkExpiringMessages() {
} }
const expiresAt = next.getExpiresAt(); const expiresAt = next.getExpiresAt();
if (!expiresAt) { if (!expiresAt || !isNumber(expiresAt)) {
return; return;
} }
window.log.info('next message expires', new Date(expiresAt).toISOString()); window.log.info('next message expires', new Date(expiresAt).toISOString());

@ -1,5 +1,4 @@
// TODO legacy messages support will be removed in a future release // TODO legacy messages support will be removed in a future release
import { isNumber } from 'lodash';
import { ProtobufUtils, SignalService } from '../../protobuf'; import { ProtobufUtils, SignalService } from '../../protobuf';
import { ReleasedFeatures } from '../../util/releaseFeature'; import { ReleasedFeatures } from '../../util/releaseFeature';
import { DisappearingMessageConversationModeType } from './types'; import { DisappearingMessageConversationModeType } from './types';
@ -26,16 +25,10 @@ export function checkIsLegacyDisappearingDataMessage(
} }
function contentHasTimerProp(contentMessage: SignalService.Content) { function contentHasTimerProp(contentMessage: SignalService.Content) {
return ( return ProtobufUtils.hasDefinedProperty(contentMessage, 'expirationTimer');
ProtobufUtils.hasDefinedProperty(contentMessage, 'expirationTimer') ||
isNumber(contentMessage.expirationTimer)
);
} }
function contentHasTypeProp(contentMessage: SignalService.Content) { function contentHasTypeProp(contentMessage: SignalService.Content) {
return ( return ProtobufUtils.hasDefinedProperty(contentMessage, 'expirationType');
ProtobufUtils.hasDefinedProperty(contentMessage, 'expirationType') ||
isNumber(contentMessage.expirationType)
);
} }
/** Use this to check for legacy disappearing messages where the expirationType and expireTimer should be undefined on the ContentMessage */ /** Use this to check for legacy disappearing messages where the expirationType and expireTimer should be undefined on the ContentMessage */
@ -44,8 +37,8 @@ export function couldBeLegacyDisappearingMessageContent(
): boolean { ): boolean {
const couldBe = const couldBe =
(contentMessage.expirationType === SignalService.Content.ExpirationType.UNKNOWN || (contentMessage.expirationType === SignalService.Content.ExpirationType.UNKNOWN ||
(ReleasedFeatures.isDisappearMessageV2FeatureReleasedCached() && ReleasedFeatures.isDisappearMessageV2FeatureReleasedCached()) &&
!contentHasTypeProp(contentMessage))) && !contentHasTypeProp(contentMessage) &&
!contentHasTimerProp(contentMessage); !contentHasTimerProp(contentMessage);
return couldBe; return couldBe;

@ -148,10 +148,7 @@ async function send(
const storedAt = batchResult?.[0]?.body?.t; const storedAt = batchResult?.[0]?.body?.t;
const storedHash = batchResult?.[0]?.body?.hash; const storedHash = batchResult?.[0]?.body?.hash;
// If message also has a sync message, save that hash. Otherwise save the hash from the regular message send i.e. only closed groups in this case.
if ( if (
encryptedAndWrapped.identifier &&
(encryptedAndWrapped.isSyncMessage || isDestinationClosedGroup) &&
batchResult && batchResult &&
!isEmpty(batchResult) && !isEmpty(batchResult) &&
batchResult[0].code === 200 && batchResult[0].code === 200 &&
@ -159,39 +156,48 @@ async function send(
isString(storedHash) && isString(storedHash) &&
isNumber(storedAt) isNumber(storedAt)
) { ) {
// TODO: the expiration is due to be returned by the storage server on "store" soon, we will then be able to use it instead of doing the storedAt + ttl logic below
// if we have a hash and a storedAt, mark it as seen so we don't reprocess it on the next retrieve
await Data.saveSeenMessageHashes([{ expiresAt: storedAt + ttl, hash: storedHash }]); await Data.saveSeenMessageHashes([{ expiresAt: storedAt + ttl, hash: storedHash }]);
const foundMessage = await Data.getMessageById(encryptedAndWrapped.identifier); // If message also has a sync message, save that hash. Otherwise save the hash from the regular message send i.e. only closed groups in this case.
if (foundMessage) {
await foundMessage.updateMessageHash(storedHash);
const convo = foundMessage.getConversation();
const expireTimer = foundMessage.getExpireTimer();
const expirationType = foundMessage.getExpirationType();
if (
convo &&
expirationType &&
expireTimer > 0 &&
// a message has started to disappear
foundMessage.getExpirationStartTimestamp()
) {
const expirationMode = DisappearingMessages.changeToDisappearingConversationMode(
convo,
expirationType,
expireTimer
);
const canBeDeleteAfterRead = convo && !convo.isMe() && convo.isPrivate(); if (
encryptedAndWrapped.identifier &&
(encryptedAndWrapped.isSyncMessage || isDestinationClosedGroup)
) {
const foundMessage = await Data.getMessageById(encryptedAndWrapped.identifier);
if (foundMessage) {
await foundMessage.updateMessageHash(storedHash);
const convo = foundMessage.getConversation();
const expireTimer = foundMessage.getExpireTimer();
const expirationType = foundMessage.getExpirationType();
// TODO legacy messages support will be removed in a future release
if ( if (
canBeDeleteAfterRead && convo &&
(expirationMode === 'legacy' || expirationMode === 'deleteAfterRead') expirationType &&
expireTimer > 0 &&
// a message has started to disappear
foundMessage.getExpirationStartTimestamp()
) { ) {
await DisappearingMessages.updateMessageExpiryOnSwarm(foundMessage, 'send()'); const expirationMode = DisappearingMessages.changeToDisappearingConversationMode(
convo,
expirationType,
expireTimer
);
const canBeDeleteAfterRead = convo && !convo.isMe() && convo.isPrivate();
// TODO legacy messages support will be removed in a future release
if (
canBeDeleteAfterRead &&
(expirationMode === 'legacy' || expirationMode === 'deleteAfterRead')
) {
await DisappearingMessages.updateMessageExpiryOnSwarm(foundMessage, 'send()');
}
} }
}
await foundMessage.commit(); await foundMessage.commit();
}
} }
} }

@ -26,7 +26,6 @@ async function insertUserProfileIntoWrapper(convoId: string) {
const areBlindedMsgRequestEnabled = !!Storage.get(SettingsKey.hasBlindedMsgRequestsEnabled); const areBlindedMsgRequestEnabled = !!Storage.get(SettingsKey.hasBlindedMsgRequestsEnabled);
const expirySeconds = ourConvo.getExpireTimer() || 0; const expirySeconds = ourConvo.getExpireTimer() || 0;
window.log.debug( window.log.debug(
`inserting into userprofile wrapper: username:"${dbName}", priority:${priority} image:${JSON.stringify( `inserting into userprofile wrapper: username:"${dbName}", priority:${priority} image:${JSON.stringify(
{ {

@ -600,7 +600,6 @@ describe('DisappearingMessage', () => {
receivedAt: GetNetworkTime.getNowWithNetworkOffset(), receivedAt: GetNetworkTime.getNowWithNetworkOffset(),
fromSync: true, fromSync: true,
shouldCommitConvo: false, shouldCommitConvo: false,
shouldCommitMessage: false,
existingMessage: undefined, existingMessage: undefined,
fromCurrentDevice: false, fromCurrentDevice: false,
}); });

@ -1039,21 +1039,21 @@
"@types/prop-types" "*" "@types/prop-types" "*"
"@types/react" "*" "@types/react" "*"
"@types/react@*", "@types/react@^17", "@types/react@^17.0.2": "@types/react@*", "@types/react@17.0.2", "@types/react@^17":
version "17.0.65" version "17.0.2"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.65.tgz#95f6a2ab61145ffb69129d07982d047f9e0870cd" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.2.tgz#3de24c4efef902dd9795a49c75f760cbe4f7a5a8"
integrity sha512-oxur785xZYHvnI7TRS61dXbkIhDPnGfsXKv0cNXR/0ml4SipRIFpSMzA7HMEfOywFwJ5AOnPrXYTEiTRUQeGlQ== integrity sha512-Xt40xQsrkdvjn1EyWe1Bc0dJLcil/9x2vAuW7ya+PuQip4UYUaXyhzWmAbwRsdMgwOFHpfp7/FFZebDU6Y8VHA==
dependencies: dependencies:
"@types/prop-types" "*" "@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2" csstype "^3.0.2"
"@types/react@17.0.2": "@types/react@^17.0.2":
version "17.0.2" version "17.0.65"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.2.tgz#3de24c4efef902dd9795a49c75f760cbe4f7a5a8" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.65.tgz#95f6a2ab61145ffb69129d07982d047f9e0870cd"
integrity sha512-Xt40xQsrkdvjn1EyWe1Bc0dJLcil/9x2vAuW7ya+PuQip4UYUaXyhzWmAbwRsdMgwOFHpfp7/FFZebDU6Y8VHA== integrity sha512-oxur785xZYHvnI7TRS61dXbkIhDPnGfsXKv0cNXR/0ml4SipRIFpSMzA7HMEfOywFwJ5AOnPrXYTEiTRUQeGlQ==
dependencies: dependencies:
"@types/prop-types" "*" "@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2" csstype "^3.0.2"
"@types/redux-logger@3.0.7": "@types/redux-logger@3.0.7":
@ -1817,9 +1817,9 @@ available-typed-arrays@^1.0.5:
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
axios@^1.3.2: axios@^1.3.2:
version "1.5.1" version "1.6.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.5.1.tgz#11fbaa11fc35f431193a9564109c88c1f27b585f" resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.2.tgz#de67d42c755b571d3e698df1b6504cde9b0ee9f2"
integrity sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A== integrity sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==
dependencies: dependencies:
follow-redirects "^1.15.0" follow-redirects "^1.15.0"
form-data "^4.0.0" form-data "^4.0.0"
@ -4896,9 +4896,9 @@ levn@~0.3.0:
prelude-ls "~1.1.2" prelude-ls "~1.1.2"
type-check "~0.3.2" type-check "~0.3.2"
"libsession_util_nodejs@https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.3.0/libsession_util_nodejs-v0.3.0.tar.gz": "libsession_util_nodejs@https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.3.1/libsession_util_nodejs-v0.3.1.tar.gz":
version "0.3.0" version "0.3.1"
resolved "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.3.0/libsession_util_nodejs-v0.3.0.tar.gz#83b733c8fdede577651881de239a8fd2843c929f" resolved "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.3.1/libsession_util_nodejs-v0.3.1.tar.gz#d2c94bfaae6e3ef594609abb08cf8be485fa5d39"
dependencies: dependencies:
cmake-js "^7.2.1" cmake-js "^7.2.1"
node-addon-api "^6.1.0" node-addon-api "^6.1.0"

Loading…
Cancel
Save