From 318a77be4b3dad4fbefa76084cda4ec49c7bb75a Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 4 May 2023 11:42:06 +1000 Subject: [PATCH] fix: mark messages sent before our lastreadts from the wrapper as read --- package.json | 2 +- ts/components/leftpane/ActionsPanel.tsx | 2 +- ts/models/conversation.ts | 22 +++++++----- ts/models/conversationAttributes.ts | 5 +++ ts/models/message.ts | 5 +-- ts/models/messageFactory.ts | 28 ++++++++++++--- ts/models/messageType.ts | 4 ++- ts/receiver/configMessage.ts | 34 +++++++------------ ts/receiver/contentMessage.ts | 4 +-- ts/receiver/queuedJob.ts | 6 ++-- ts/session/apis/snode_api/snodePool.ts | 9 ++--- ts/session/apis/snode_api/swarmPolling.ts | 2 +- ts/session/utils/calling/CallManager.ts | 5 +-- .../utils/libsession/libsession_utils.ts | 1 + yarn.lock | 6 ++-- 15 files changed, 81 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index 0619864a8..b8015b2e6 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "glob": "7.1.2", "image-type": "^4.1.0", "ip2country": "1.0.1", - "libsession_util_nodejs": "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.8/libsession_util_nodejs-v0.1.8.tar.gz", + "libsession_util_nodejs": "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.1.10/libsession_util_nodejs-v0.1.10.tar.gz", "libsodium-wrappers-sumo": "^0.7.9", "linkify-it": "3.0.2", "lodash": "^4.17.20", diff --git a/ts/components/leftpane/ActionsPanel.tsx b/ts/components/leftpane/ActionsPanel.tsx index bb3169e6d..d67994ffe 100644 --- a/ts/components/leftpane/ActionsPanel.tsx +++ b/ts/components/leftpane/ActionsPanel.tsx @@ -38,12 +38,12 @@ import { ActionPanelOnionStatusLight } from '../dialog/OnionStatusPathDialog'; import { SessionIconButton } from '../icon'; import { LeftPaneSectionContainer } from './LeftPaneSectionContainer'; +import { SettingsKey } from '../../data/settings-key'; import { getLatestReleaseFromFileServer } from '../../session/apis/file_server_api/FileServerApi'; import { forceRefreshRandomSnodePool } from '../../session/apis/snode_api/snodePool'; import { isDarkTheme } from '../../state/selectors/theme'; import { ThemeStateType } from '../../themes/constants/colors'; import { switchThemeTo } from '../../themes/switchTheme'; -import { SettingsKey } from '../../data/settings-key'; const Section = (props: { type: SectionType }) => { const ourNumber = useSelector(getOurNumber); diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index f2eba5dcc..81d0425e6 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -103,6 +103,7 @@ import { fillConvoAttributesWithDefaults, isDirectConversation, isOpenOrClosedGroup, + READ_MESSAGE_STATE, } from './conversationAttributes'; import { LibSessionUtil } from '../session/utils/libsession/libsession_utils'; @@ -113,6 +114,7 @@ import { getModeratorsOutsideRedux, getSubscriberCountOutsideRedux, } from '../state/selectors/sogsRoomInfo'; +import { markAttributesAsReadIfNeeded } from './messageFactory'; type InMemoryConvoInfos = { mentionedUs: boolean; @@ -345,10 +347,6 @@ export class ConversationModel extends Backbone.Model { if (this.get('expireTimer')) { toRet.expireTimer = this.get('expireTimer'); } - if (this.get('markedAsUnread')) { - toRet.isMarkedUnread = this.get('markedAsUnread'); - } - // those are values coming only from both the DB or the wrapper. Currently we display the data from the DB if (this.isClosedGroup()) { toRet.members = this.get('members') || []; @@ -682,7 +680,7 @@ export class ConversationModel extends Backbone.Model { messageRequestResponse: { isApproved: 1, }, - unread: 1, // 1 means unread + unread: READ_MESSAGE_STATE.unread, // 1 means unread expireTimer: 0, }); this.updateLastMessage(); @@ -829,7 +827,7 @@ export class ConversationModel extends Backbone.Model { ...commonAttributes, // Even though this isn't reflected to the user, we want to place the last seen // indicator above it. We set it to 'unread' to trigger that placement. - unread: 1, + unread: READ_MESSAGE_STATE.unread, source, sent_at: timestamp, received_at: timestamp, @@ -931,7 +929,7 @@ export class ConversationModel extends Backbone.Model { source: sender, type: 'outgoing', direction: 'outgoing', - unread: 0, // an outgoing message must be already read + unread: READ_MESSAGE_STATE.read, // an outgoing message must be already read received_at: messageAttributes.sent_at, // make sure to set a received_at timestamp for an outgoing message, so the order are right. }); } @@ -944,12 +942,18 @@ export class ConversationModel extends Backbone.Model { await this.setDidApproveMe(true); } - return this.addSingleMessage({ + const toBeAddedAttributes: MessageAttributesOptionals = { ...messageAttributes, conversationId: this.id, type: 'incoming', direction: 'outgoing', - }); + }; + + // if the message is trying to be added unread, make sure that it shouldn't be already read from our other devices + + markAttributesAsReadIfNeeded(toBeAddedAttributes); + + return this.addSingleMessage(toBeAddedAttributes); } /** diff --git a/ts/models/conversationAttributes.ts b/ts/models/conversationAttributes.ts index 250fbeaf2..be2a83477 100644 --- a/ts/models/conversationAttributes.ts +++ b/ts/models/conversationAttributes.ts @@ -153,3 +153,8 @@ export const CONVERSATION_PRIORITIES = { hidden: -1, pinned: 1, // anything over 0 means pinned, but when our local users pins a conversation, we set the priority to 1 }; + +export const READ_MESSAGE_STATE = { + unread: 1, + read: 0, +} as const; diff --git a/ts/models/message.ts b/ts/models/message.ts index e5936328b..69386ec1b 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -94,6 +94,7 @@ import { Notifications } from '../util/notifications'; import { Storage } from '../util/storage'; import { ConversationModel } from './conversation'; import { roomHasBlindEnabled } from '../types/sqlSharedTypes'; +import { READ_MESSAGE_STATE } from './conversationAttributes'; // tslint:disable: cyclomatic-complexity /** @@ -205,7 +206,7 @@ export class MessageModel extends Backbone.Model { const { unread } = attributes; if (unread === undefined) { - this.set({ unread: 0 }); + this.set({ unread: READ_MESSAGE_STATE.read }); } this.set(attributes); @@ -1100,7 +1101,7 @@ export class MessageModel extends Backbone.Model { } public markMessageReadNoCommit(readAt: number) { - this.set({ unread: 0 }); + this.set({ unread: READ_MESSAGE_STATE.read }); if ( this.get('expireTimer') && diff --git a/ts/models/messageFactory.ts b/ts/models/messageFactory.ts index 5a68fdd29..dd5a37450 100644 --- a/ts/models/messageFactory.ts +++ b/ts/models/messageFactory.ts @@ -1,4 +1,6 @@ import { UserUtils } from '../session/utils'; +import { SessionUtilConvoInfoVolatile } from '../session/utils/libsession/libsession_utils_convo_info_volatile'; +import { READ_MESSAGE_STATE } from './conversationAttributes'; import { MessageModel } from './message'; import { MessageAttributesOptionals, MessageModelType } from './messageType'; @@ -86,7 +88,7 @@ function getSharedAttributesForPublicMessage({ function getSharedAttributesForOutgoingMessage() { return { source: UserUtils.getOurPubKeyStrFromCache(), - unread: 0, + unread: READ_MESSAGE_STATE.read, sent_to: [], sent: true, type: 'outgoing' as MessageModelType, @@ -96,12 +98,29 @@ function getSharedAttributesForOutgoingMessage() { function getSharedAttributesForIncomingMessage() { return { - unread: 1, + unread: READ_MESSAGE_STATE.unread, type: 'incoming' as MessageModelType, direction: 'incoming' as MessageModelType, }; } +export function markAttributesAsReadIfNeeded(messageAttributes: MessageAttributesOptionals) { + // if the message is trying to be added unread, make sure that it shouldn't be already read from our other devices + if (messageAttributes.unread === READ_MESSAGE_STATE.unread) { + const latestUnreadForThisConvo = SessionUtilConvoInfoVolatile.getVolatileInfoCached( + messageAttributes.conversationId + ); + const sentAt = messageAttributes.serverTimestamp || messageAttributes.sent_at; + if ( + sentAt && + latestUnreadForThisConvo?.lastRead && + sentAt <= latestUnreadForThisConvo.lastRead + ) { + messageAttributes.unread = READ_MESSAGE_STATE.read; + } + } +} + /** * This function is only called when we get a message from ourself from an opengroup polling event */ @@ -128,11 +147,12 @@ export function createPublicMessageSentFromNotUs(args: { serverTimestamp: number; conversationId: string; }): MessageModel { - const messageData: MessageAttributesOptionals = { + const messageAttributes: MessageAttributesOptionals = { ...getSharedAttributesForPublicMessage(args), ...getSharedAttributesForIncomingMessage(), source: args.sender, }; + markAttributesAsReadIfNeeded(messageAttributes); - return new MessageModel(messageData); + return new MessageModel(messageAttributes); } diff --git a/ts/models/messageType.ts b/ts/models/messageType.ts index 6057ba5ca..74728ba2d 100644 --- a/ts/models/messageType.ts +++ b/ts/models/messageType.ts @@ -7,6 +7,7 @@ import { } from '../state/ducks/conversations'; import { AttachmentTypeWithPath } from '../types/Attachment'; import { Reaction, ReactionList, SortedReactionList } from '../types/Reaction'; +import { READ_MESSAGE_STATE } from './conversationAttributes'; export type MessageModelType = 'incoming' | 'outgoing'; @@ -44,6 +45,7 @@ export interface MessageAttributes { }; /** * 1 means unread, 0 or anything else is read. + * You can use the values from READ_MESSAGE_STATE.unread and READ_MESSAGE_STATE.read */ unread: number; group?: any; @@ -224,7 +226,7 @@ export const fillMessageAttributesWithDefaults = ( const defaulted = defaultsDeep(optAttributes, { expireTimer: 0, // disabled id: uuidv4(), - unread: 0, // if nothing is set, this message is considered read + unread: READ_MESSAGE_STATE.read, // if nothing is set, this message is considered read }); // this is just to cleanup a bit the db. delivered and delivered_to were removed, so everytime we load a message // we make sure to clean those fields in the json. diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index 9ac3451d8..dd4a3d065 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -1,6 +1,7 @@ import { compact, isEmpty, isNumber, toNumber } from 'lodash'; import { ConfigDumpData } from '../data/configDump/configDump'; import { Data } from '../data/data'; +import { SettingsKey } from '../data/settings-key'; import { ConversationInteraction } from '../interactions'; import { ConversationTypeEnum } from '../models/conversationAttributes'; import { SignalService } from '../protobuf'; @@ -27,6 +28,7 @@ import { configurationMessageReceived, trigger } from '../shims/events'; import { assertUnreachable } from '../types/sqlSharedTypes'; import { BlockedNumberController } from '../util'; import { Registration } from '../util/registration'; +import { ReleasedFeatures } from '../util/releaseFeature'; import { Storage, getLastProfileUpdateTimestamp, @@ -45,8 +47,6 @@ import { addKeyPairToCacheAndDBIfNeeded, handleNewClosedGroup } from './closedGr import { HexKeyPair } from './keypairs'; import { queueAllCachedFromSource } from './receiver'; import { EnvelopePlus } from './types'; -import { SettingsKey } from '../data/settings-key'; -import { ReleasedFeatures } from '../util/releaseFeature'; function groupByVariant( incomingConfigs: Array> @@ -92,13 +92,14 @@ async function mergeConfigsWithIncomingUpdates( data: msg.message.data, hash: msg.messageHash, })); - - await GenericWrapperActions.merge(variant, toMerge); + const mergedCount = await GenericWrapperActions.merge(variant, toMerge); const needsPush = await GenericWrapperActions.needsPush(variant); const needsDump = await GenericWrapperActions.needsDump(variant); const latestEnvelopeTimestamp = Math.max(...sameVariant.map(m => m.envelopeTimestamp)); - window.log.debug(`${variant}: "${publicKey}" needsPush:${needsPush} needsDump:${needsDump} `); + window.log.debug( + `${variant}: "${publicKey}" needsPush:${needsPush} needsDump:${needsDump}; mergedCount:${mergedCount} ` + ); const incomingConfResult: IncomingConfResult = { needsDump, @@ -118,9 +119,6 @@ async function mergeConfigsWithIncomingUpdates( } async function handleUserProfileUpdate(result: IncomingConfResult): Promise { - if (!result.needsDump) { - return result; - } const updateUserInfo = await UserConfigWrapperActions.getUserInfo(); if (!updateUserInfo) { return result; @@ -140,9 +138,6 @@ async function handleUserProfileUpdate(result: IncomingConfResult): Promise { - if (!result.needsDump) { - return result; - } const us = UserUtils.getOurPubKeyStrFromCache(); const allContacts = await ContactsWrapperActions.getAll(); @@ -432,10 +427,6 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) { } async function handleUserGroupsUpdate(result: IncomingConfResult): Promise { - if (!result.needsDump) { - return result; - } - const toHandle = SessionUtilUserGroups.getUserGroupTypes(); for (let index = 0; index < toHandle.length; index++) { const typeToHandle = toHandle[index]; @@ -494,10 +485,6 @@ async function applyConvoVolatileUpdateFromWrapper( async function handleConvoInfoVolatileUpdate( result: IncomingConfResult ): Promise { - if (!result.needsDump) { - return result; - } - const types = SessionUtilConvoInfoVolatile.getConvoInfoVolatileTypes(); for (let typeIndex = 0; typeIndex < types.length; typeIndex++) { const type = types[typeIndex]; @@ -508,6 +495,7 @@ async function handleConvoInfoVolatileUpdate( const wrapper1o1s = await ConvoInfoVolatileWrapperActions.getAll1o1(); for (let index = 0; index < wrapper1o1s.length; index++) { const fromWrapper = wrapper1o1s[index]; + await applyConvoVolatileUpdateFromWrapper( fromWrapper.pubkeyHex, fromWrapper.unread, @@ -650,8 +638,12 @@ async function handleConfigMessagesViaLibSession( return; } - window?.log?.info( - `Handling our sharedConfig message via libsession_util; count: ${configMessages.length}` + window?.log?.debug( + `Handling our sharedConfig message via libsession_util ${configMessages.map(m => ({ + variant: LibSessionUtil.kindToVariant(m.message.kind), + hash: m.messageHash, + seqno: (m.message.seqno as Long).toNumber(), + }))}` ); const incomingMergeResult = await mergeConfigsWithIncomingUpdates(configMessages); diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index 75cd4016a..3b8bd6dd7 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -12,7 +12,7 @@ import { deleteMessagesFromSwarmAndCompletelyLocally, deleteMessagesFromSwarmAndMarkAsDeletedLocally, } from '../interactions/conversations/unsendingInteractions'; -import { ConversationTypeEnum } from '../models/conversationAttributes'; +import { ConversationTypeEnum, READ_MESSAGE_STATE } from '../models/conversationAttributes'; import { findCachedBlindedMatchOrLookupOnAllServers } from '../session/apis/open_group_api/sogsv3/knownBlindedkeys'; import { getConversationController } from '../session/conversations'; import { concatUInt8Array, getSodiumRenderer } from '../session/crypto'; @@ -761,7 +761,7 @@ export async function handleDataExtractionNotification( referencedAttachmentTimestamp, // currently unused source, }, - unread: 1, // 1 means unread + unread: READ_MESSAGE_STATE.unread, // 1 means unread expireTimer: 0, }); convo.updateLastMessage(); diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index 40c9e5bf3..27a07cdb7 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -7,7 +7,7 @@ import { MessageModel, sliceQuoteText } from '../models/message'; import { getConversationController } from '../session/conversations'; import { Quote } from './types'; -import { ConversationTypeEnum } from '../models/conversationAttributes'; +import { ConversationTypeEnum, READ_MESSAGE_STATE } from '../models/conversationAttributes'; import { MessageDirection } from '../models/messageType'; import { SignalService } from '../protobuf'; import { ProfileManager } from '../session/profile_manager/ProfileManager'; @@ -144,7 +144,7 @@ async function processProfileKeyNoCommit( function updateReadStatus(message: MessageModel) { if (message.isExpirationTimerUpdate()) { - message.set({ unread: 0 }); + message.set({ unread: READ_MESSAGE_STATE.read }); } } @@ -307,7 +307,7 @@ async function handleExpirationTimerUpdateNoCommit( source, expireTimer, }, - unread: 0, // mark the message as read. + unread: READ_MESSAGE_STATE.read, // mark the message as read. }); conversation.set({ expireTimer }); diff --git a/ts/session/apis/snode_api/snodePool.ts b/ts/session/apis/snode_api/snodePool.ts index 8adcbff1d..ece4b4f60 100644 --- a/ts/session/apis/snode_api/snodePool.ts +++ b/ts/session/apis/snode_api/snodePool.ts @@ -1,4 +1,4 @@ -import _ from 'lodash'; +import _, { shuffle } from 'lodash'; import { Data, Snode } from '../../../data/data'; @@ -312,10 +312,11 @@ export async function getSwarmFor(pubkey: string): Promise> { } // Request new node list from the network - const freshNodes = _.shuffle(await requestSnodesForPubkeyFromNetwork(pubkey)); + const swarm = await requestSnodesForPubkeyFromNetwork(pubkey); + const mixedSwarm = shuffle(swarm); - const edkeys = freshNodes.map((n: Snode) => n.pubkey_ed25519); + const edkeys = mixedSwarm.map((n: Snode) => n.pubkey_ed25519); await internalUpdateSwarmFor(pubkey, edkeys); - return freshNodes; + return mixedSwarm; } diff --git a/ts/session/apis/snode_api/swarmPolling.ts b/ts/session/apis/snode_api/swarmPolling.ts index 90e54c442..87594098a 100644 --- a/ts/session/apis/snode_api/swarmPolling.ts +++ b/ts/session/apis/snode_api/swarmPolling.ts @@ -251,7 +251,7 @@ export class SwarmPolling { if (!isGroup && userConfigMessagesMerged.length) { window.log.info( - `received userConfigMessagesMerged: ${userConfigMessagesMerged.length} for key ${pubkey.key}` + `received userConfigMessages count: ${userConfigMessagesMerged.length} for key ${pubkey.key}` ); try { await this.handleSharedConfigMessages(userConfigMessagesMerged); diff --git a/ts/session/utils/calling/CallManager.ts b/ts/session/utils/calling/CallManager.ts index ae50d3fbe..98d3bf77b 100644 --- a/ts/session/utils/calling/CallManager.ts +++ b/ts/session/utils/calling/CallManager.ts @@ -29,6 +29,7 @@ import { PnServer } from '../../apis/push_notification_api'; import { approveConvoAndSendResponse } from '../../../interactions/conversationInteractions'; import { GetNetworkTime } from '../../apis/snode_api/getNetworkTime'; import { SnodeNamespaces } from '../../apis/snode_api/namespaces'; +import { READ_MESSAGE_STATE } from '../../../models/conversationAttributes'; // tslint:disable: function-name @@ -860,7 +861,7 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) { received_at: networkTimestamp, expireTimer: 0, callNotificationType: 'answered-a-call', - unread: 0, + unread: READ_MESSAGE_STATE.read, }); await buildAnswerAndSendIt(fromSender); @@ -1199,7 +1200,7 @@ async function addMissedCallMessage(callerPubkey: string, sentAt: number) { received_at: GetNetworkTime.getNowWithNetworkOffset(), expireTimer: 0, callNotificationType: 'missed-call', - unread: 1, + unread: READ_MESSAGE_STATE.unread, }); } diff --git a/ts/session/utils/libsession/libsession_utils.ts b/ts/session/utils/libsession/libsession_utils.ts index 07d71f712..79cb65977 100644 --- a/ts/session/utils/libsession/libsession_utils.ts +++ b/ts/session/utils/libsession/libsession_utils.ts @@ -126,6 +126,7 @@ async function pendingChangesForPubkey(pubkey: string): Promise