From d6d9bec5bae0704388004d15e6253a3ff7e2d8f7 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 12 Mar 2024 10:52:41 +1100 Subject: [PATCH] fix: fixed a bunch of groupv2 chunk2 issues --- ts/components/MemberListItem.tsx | 8 +- .../conversation/MessageRequestButtons.tsx | 4 +- .../message-item/GroupUpdateMessage.tsx | 16 +- .../overlay/OverlayRightPanelSettings.tsx | 9 +- .../dialog/UpdateGroupMembersDialog.tsx | 2 + .../overlay/OverlayMessageRequest.tsx | 23 ++- ts/components/menu/Menu.tsx | 1 - ts/hooks/useParamSelector.ts | 3 + ts/interactions/conversationInteractions.ts | 44 ++++-- ts/models/conversation.ts | 139 ++++++++---------- ts/models/message.ts | 24 ++- ts/react.d.ts | 11 +- ts/receiver/contentMessage.ts | 34 +++-- ts/session/apis/snode_api/retrieveRequest.ts | 8 +- ts/session/apis/snode_api/swarmPolling.ts | 8 +- .../GroupUpdateMemberChangeMessage.ts | 4 + ts/session/profile_manager/ProfileManager.ts | 5 +- ts/session/sending/MessageQueue.ts | 8 +- ts/session/sending/MessageSender.ts | 29 +++- ts/session/sending/MessageSentHandler.ts | 19 +-- ts/session/utils/calling/CallManager.ts | 3 +- ts/state/ducks/metaGroups.ts | 35 ++++- 22 files changed, 263 insertions(+), 174 deletions(-) diff --git a/ts/components/MemberListItem.tsx b/ts/components/MemberListItem.tsx index aad823b9f..4cc5e1d59 100644 --- a/ts/components/MemberListItem.tsx +++ b/ts/components/MemberListItem.tsx @@ -163,7 +163,7 @@ const GroupStatusText = ({ groupPk, pubkey }: { pubkey: PubkeyType; groupPk: Gro } return ( {statusText} @@ -197,7 +197,7 @@ const ResendInviteButton = ({ }) => { return ( { return ( - {memberName} + {memberName} { { - await handleAcceptConversationRequest({ convoId: selectedConvoId, sendResponse: true }); + onClick={() => { + void handleAcceptConversationRequest({ convoId: selectedConvoId }); }} text={window.i18n('accept')} dataTestId="accept-message-request" diff --git a/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx b/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx index 507ae6a75..d6c25980a 100644 --- a/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx +++ b/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { PubkeyType } from 'libsession_util_nodejs'; +import { cloneDeep } from 'lodash'; import { useConversationsUsernameWithQuoteOrShortPk } from '../../../../hooks/useParamSelector'; import { arrayContainsUsOnly } from '../../../../models/message'; import { PreConditionFailed } from '../../../../session/utils/errors'; @@ -49,7 +50,10 @@ function moveUsToStart( if (!usItem) { throw new PreConditionFailed('"we" should have been there'); } - return { sortedWithUsFirst: [usItem, ...changed.slice(usAt, 1)] }; + // deepClone because splice mutates the array + const changedCopy = cloneDeep(changed); + changedCopy.splice(usAt, 1); + return { sortedWithUsFirst: [usItem, ...changedCopy] }; } function changeOfMembersV2({ @@ -84,10 +88,12 @@ function changeOfMembersV2({ : ('Removed' as const); const key = `group${subject}${action}` as const; - return window.i18n( - key, - sortedWithUsFirst.map(m => m.name) - ); + const sortedWithUsOrCount = + subject === 'Others' + ? [sortedWithUsFirst[0].name, (sortedWithUsFirst.length - 1).toString()] + : sortedWithUsFirst.map(m => m.name); + + return window.i18n(key, sortedWithUsOrCount); } // TODO those lookups might need to be memoized diff --git a/ts/components/conversation/right-panel/overlay/OverlayRightPanelSettings.tsx b/ts/components/conversation/right-panel/overlay/OverlayRightPanelSettings.tsx index 3c1076dbc..e6e68abb7 100644 --- a/ts/components/conversation/right-panel/overlay/OverlayRightPanelSettings.tsx +++ b/ts/components/conversation/right-panel/overlay/OverlayRightPanelSettings.tsx @@ -31,6 +31,7 @@ import { useSelectedIsActive, useSelectedIsBlocked, useSelectedIsGroupOrCommunity, + useSelectedIsGroupV2, useSelectedIsKickedFromGroup, useSelectedIsPublic, useSelectedLastMessage, @@ -125,13 +126,19 @@ const HeaderItem = () => { const isBlocked = useSelectedIsBlocked(); const isKickedFromGroup = useSelectedIsKickedFromGroup(); const isGroup = useSelectedIsGroupOrCommunity(); + const isGroupV2 = useSelectedIsGroupV2(); + const isPublic = useSelectedIsPublic(); const subscriberCount = useSelectedSubscriberCount(); + const weAreAdmin = useSelectedWeAreAdmin(); if (!selectedConvoKey) { return null; } - const showInviteContacts = isGroup && !isKickedFromGroup && !isBlocked; + const showInviteLegacyGroup = + !isPublic && !isGroupV2 && isGroup && !isKickedFromGroup && !isBlocked; + const showInviteGroupV2 = isGroupV2 && !isKickedFromGroup && !isBlocked && weAreAdmin; + const showInviteContacts = isPublic || showInviteLegacyGroup || showInviteGroupV2; const showMemberCount = !!(subscriberCount && subscriberCount > 0); return ( diff --git a/ts/components/dialog/UpdateGroupMembersDialog.tsx b/ts/components/dialog/UpdateGroupMembersDialog.tsx index 6e88916ef..4181105f4 100644 --- a/ts/components/dialog/UpdateGroupMembersDialog.tsx +++ b/ts/components/dialog/UpdateGroupMembersDialog.tsx @@ -293,6 +293,7 @@ export const UpdateGroupMembersDialog = (props: Props) => { onClick={onClickOK} buttonType={SessionButtonType.Simple} disabled={isProcessingUIChange} + dataTestId="session-confirm-ok-button" /> )} { buttonType={SessionButtonType.Simple} onClick={closeDialog} disabled={isProcessingUIChange} + dataTestId="session-confirm-cancel-button" /> diff --git a/ts/components/leftpane/overlay/OverlayMessageRequest.tsx b/ts/components/leftpane/overlay/OverlayMessageRequest.tsx index 0da5a616e..2a724afcc 100644 --- a/ts/components/leftpane/overlay/OverlayMessageRequest.tsx +++ b/ts/components/leftpane/overlay/OverlayMessageRequest.tsx @@ -4,6 +4,7 @@ import { useDispatch, useSelector } from 'react-redux'; import useKey from 'react-use/lib/useKey'; import styled from 'styled-components'; import { declineConversationWithoutConfirm } from '../../../interactions/conversationInteractions'; +import { ed25519Str } from '../../../session/onions/onionPath'; import { forceSyncConfigurationNowIfNeeded } from '../../../session/utils/sync/syncUtils'; import { updateConfirmModal } from '../../../state/ducks/modalDialog'; import { resetLeftOverlayMode } from '../../../state/ducks/section'; @@ -76,14 +77,20 @@ export const OverlayMessageRequest = () => { for (let index = 0; index < messageRequests.length; index++) { const convoId = messageRequests[index]; - // eslint-disable-next-line no-await-in-loop - await declineConversationWithoutConfirm({ - alsoBlock: false, - conversationId: convoId, - currentlySelectedConvo, - syncToDevices: false, - conversationIdOrigin: null, // block is false, no need for conversationIdOrigin - }); + try { + // eslint-disable-next-line no-await-in-loop + await declineConversationWithoutConfirm({ + alsoBlock: false, + conversationId: convoId, + currentlySelectedConvo, + syncToDevices: false, + conversationIdOrigin: null, // block is false, no need for conversationIdOrigin + }); + } catch (e) { + window.log.warn( + `failed to decline msg request ${ed25519Str(convoId)} with error: ${e.message}` + ); + } } await forceSyncConfigurationNowIfNeeded(); diff --git a/ts/components/menu/Menu.tsx b/ts/components/menu/Menu.tsx index cc935600f..c37feee4f 100644 --- a/ts/components/menu/Menu.tsx +++ b/ts/components/menu/Menu.tsx @@ -450,7 +450,6 @@ export const AcceptMsgRequestMenuItem = () => { onClick={async () => { await handleAcceptConversationRequest({ convoId, - sendResponse: true, }); }} > diff --git a/ts/hooks/useParamSelector.ts b/ts/hooks/useParamSelector.ts index 48b23cfbe..6b6984876 100644 --- a/ts/hooks/useParamSelector.ts +++ b/ts/hooks/useParamSelector.ts @@ -47,6 +47,9 @@ export function useConversationUsername(convoId?: string) { // So let's keep falling back to convoProps?.displayNameInProfile if groupName is not set yet (it comes later through the groupInfos namespace) return groupName; } + if (convoId && (PubKey.is03Pubkey(convoId) || PubKey.is05Pubkey(convoId))) { + return convoProps?.nickname || convoProps?.displayNameInProfile || PubKey.shorten(convoId); + } return convoProps?.nickname || convoProps?.displayNameInProfile || convoId; } diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index e80a7124f..6482373e6 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -4,7 +4,7 @@ import { ConversationTypeEnum, READ_MESSAGE_STATE, } from '../models/conversationAttributes'; -import { CallManager, SyncUtils, ToastUtils, UserUtils } from '../session/utils'; +import { CallManager, PromiseUtils, SyncUtils, ToastUtils, UserUtils } from '../session/utils'; import { SessionButtonColor } from '../components/basic/SessionButton'; import { getCallMediaPermissionsSettings } from '../components/settings/SessionSettings'; @@ -113,26 +113,25 @@ export async function unblockConvoById(conversationId: string) { await conversation.commit(); } -export const handleAcceptConversationRequest = async ({ - convoId, - sendResponse, -}: { - convoId: string; - sendResponse: boolean; -}) => { +export const handleAcceptConversationRequest = async ({ convoId }: { convoId: string }) => { const convo = ConvoHub.use().get(convoId); - if (!convo) { + if (!convo || (!convo.isPrivate() && !convo.isClosedGroupV2())) { return null; } - await convo.setDidApproveMe(true, false); + const previousIsApproved = convo.isApproved(); + const previousDidApprovedMe = convo.didApproveMe(); + // Note: we don't mark as approvedMe = true, as we do not know if they did send us a message yet. await convo.setIsApproved(true, false); await convo.commit(); + void forceSyncConfigurationNowIfNeeded(); if (convo.isPrivate()) { - await convo.addOutgoingApprovalMessage(Date.now()); - if (sendResponse) { + // we only need the approval message (and sending a reply) when we are accepting a message request. i.e. someone sent us a message already and we didn't accept it yet. + if (!previousIsApproved && previousDidApprovedMe) { + await convo.addOutgoingApprovalMessage(Date.now()); await convo.sendMessageRequestResponse(); } + return null; } if (PubKey.is03Pubkey(convoId)) { @@ -143,12 +142,17 @@ export const handleAcceptConversationRequest = async ({ } // this updates the wrapper and refresh the redux slice await UserGroupsWrapperActions.setGroup({ ...found, invitePending: false }); - const acceptedPromise = new Promise(resolve => { + + // nothing else to do (and especially not wait for first poll) when the convo was already approved + if (previousIsApproved) { + return null; + } + const pollAndSendResponsePromise = new Promise(resolve => { getSwarmPollingInstance().addGroupId(convoId, async () => { // we need to do a first poll to fetch the keys etc before we can send our invite response // this is pretty hacky, but also an admin seeing a message from that user in the group will mark it as not pending anymore await sleepFor(2000); - if (sendResponse) { + if (!previousIsApproved) { await GroupV2Receiver.sendInviteResponseToGroup({ groupPk: convoId }); } window.log.info( @@ -157,7 +161,17 @@ export const handleAcceptConversationRequest = async ({ return resolve(true); }); }); - await acceptedPromise; + + // try at most 10s for the keys, and everything to come before continuing processing. + // Note: this is important as otherwise the polling just hangs when sending a message to a group (as the cb in addGroupId() is never called back) + const timeout = 10000; + try { + await PromiseUtils.timeout(pollAndSendResponsePromise, timeout); + } catch (e) { + window.log.warn( + `handleAcceptConversationRequest: waited ${timeout}ms for first poll of group ${ed25519Str(convoId)} to happen, but timedout with: ${e.message}` + ); + } } return null; }; diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 4db8bf55e..1ba4cdfae 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -17,6 +17,7 @@ import { xor, } from 'lodash'; +import { DisappearingMessageConversationModeType } from 'libsession_util_nodejs'; import { v4 } from 'uuid'; import { SignalService } from '../protobuf'; import { getMessageQueue } from '../session'; @@ -29,7 +30,7 @@ import { PubKey } from '../session/types'; import { ToastUtils, UserUtils } from '../session/utils'; import { BlockedNumberController } from '../util'; import { MessageModel } from './message'; -import { MessageAttributesOptionals, MessageDirection } from './messageType'; +import { MessageAttributesOptionals } from './messageType'; import { Data } from '../data/data'; import { OpenGroupRequestCommonType } from '../session/apis/open_group_api/opengroupV2/ApiUtil'; @@ -81,7 +82,6 @@ import { UserSync } from '../session/utils/job_runners/jobs/UserSyncJob'; import { SessionUtilContact } from '../session/utils/libsession/libsession_utils_contacts'; import { SessionUtilConvoInfoVolatile } from '../session/utils/libsession/libsession_utils_convo_info_volatile'; import { SessionUtilUserGroups } from '../session/utils/libsession/libsession_utils_user_groups'; -import { forceSyncConfigurationNowIfNeeded } from '../session/utils/sync/syncUtils'; import { getOurProfile } from '../session/utils/User'; import { deleteExternalFilesOfConversation, @@ -129,7 +129,6 @@ import { import { handleAcceptConversationRequest } from '../interactions/conversationInteractions'; import { DisappearingMessages } from '../session/disappearing_messages'; -import { DisappearingMessageConversationModeType } from '../session/disappearing_messages/types'; import { GroupUpdateInfoChangeMessage } from '../session/messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateInfoChangeMessage'; import { FetchMsgExpirySwarm } from '../session/utils/job_runners/jobs/FetchMsgExpirySwarmJob'; import { UpdateMsgExpirySwarm } from '../session/utils/job_runners/jobs/UpdateMsgExpirySwarmJob'; @@ -150,7 +149,7 @@ type InMemoryConvoInfos = { const inMemoryConvoInfos: Map = new Map(); export class ConversationModel extends Backbone.Model { - public updateLastMessage: () => unknown; // unknown because it is a Promise that we do not wait to await + public updateLastMessage: () => unknown; // unknown because it is a Promise that we do not want to await public throttledBumpTyping: () => void; public throttledNotify: (message: MessageModel) => void; public markConversationRead: (opts: { @@ -237,7 +236,7 @@ export class ConversationModel extends Backbone.Model { public isClosedGroup(): boolean { return Boolean( - (this.get('type') === ConversationTypeEnum.GROUP && this.id.startsWith('05')) || + (this.get('type') === ConversationTypeEnum.GROUP && PubKey.is05Pubkey(this.id)) || this.isClosedGroupV2() ); } @@ -254,7 +253,9 @@ export class ConversationModel extends Backbone.Model { return this.isPrivate() && PubKey.isBlinded(this.id); } - // returns true if this is a closed/medium or open group + /** + * @returns true if this is a legacy, closed or community + */ public isGroup() { return isOpenOrClosedGroup(this.get('type')); } @@ -298,6 +299,14 @@ export class ConversationModel extends Backbone.Model { } public getPriority() { + if (PubKey.is05Pubkey(this.id) && this.isPrivate()) { + // TODO once we have a libsession state, we can make this used accross the app without repeating as much + // if a private chat, trust the value from the Libsession wrapper cached first + const contact = SessionUtilContact.getContactCached(this.id); + if (contact) { + return contact.priority; + } + } return this.get('priority') || CONVERSATION_PRIORITIES.default; } @@ -325,8 +334,8 @@ export class ConversationModel extends Backbone.Model { toRet.priority = priorityFromDb; } - if (this.get('markedAsUnread')) { - toRet.isMarkedUnread = !!this.get('markedAsUnread'); + if (this.isMarkedUnread()) { + toRet.isMarkedUnread = this.isMarkedUnread(); } const blocksSogsMsgReqsTimestamp = this.get('blocksSogsMsgReqsTimestamp'); @@ -380,17 +389,17 @@ export class ConversationModel extends Backbone.Model { if (this.getRealSessionUsername()) { toRet.displayNameInProfile = this.getRealSessionUsername(); } - if (this.get('nickname')) { - toRet.nickname = this.get('nickname'); + if (this.getNickname()) { + toRet.nickname = this.getNickname(); } if (BlockedNumberController.isBlocked(this.id)) { toRet.isBlocked = true; } - if (this.get('didApproveMe')) { - toRet.didApproveMe = this.get('didApproveMe'); + if (this.didApproveMe()) { + toRet.didApproveMe = this.didApproveMe(); } - if (this.get('isApproved')) { - toRet.isApproved = this.get('isApproved'); + if (this.isApproved()) { + toRet.isApproved = this.isApproved(); } if (this.getExpireTimer()) { toRet.expireTimer = this.getExpireTimer(); @@ -601,32 +610,16 @@ export class ConversationModel extends Backbone.Model { expireTimer, }; - const shouldApprove = !this.isApproved() && this.isPrivate(); - const incomingMessageCount = await Data.getMessageCountByType( - this.id, - MessageDirection.incoming - ); - const hasIncomingMessages = incomingMessageCount > 0; - if (PubKey.isBlinded(this.id)) { window.log.info('Sending a blinded message react to this user: ', this.id); await this.sendBlindedMessageRequest(chatMessageParams); return; } - if (shouldApprove) { - await this.setIsApproved(true); - if (hasIncomingMessages) { - // have to manually add approval for local client here as DB conditional approval check in config msg handling will prevent this from running - await this.addOutgoingApprovalMessage(Date.now()); - if (!this.didApproveMe()) { - await this.setDidApproveMe(true); - } - // should only send once - await this.sendMessageRequestResponse(); - void forceSyncConfigurationNowIfNeeded(); - } - } + // handleAcceptConversationRequest will take care of sending response depending on the type of conversation, if needed + await handleAcceptConversationRequest({ + convoId: this.id, + }); if (this.isOpenGroupV2()) { // communities have no expiration timer support, so enforce it here. @@ -739,7 +732,7 @@ export class ConversationModel extends Backbone.Model { /** * When you have accepted another users message request - * @param timestamp for determining the order for this message to appear like a regular message + * Note: you shouldn't need to use this directly. Instead use `handleAcceptConversationRequest()` */ public async addOutgoingApprovalMessage(timestamp: number) { await this.addSingleOutgoingMessage({ @@ -772,8 +765,9 @@ export class ConversationModel extends Backbone.Model { } /** - * Sends an accepted message request response. + * Sends an accepted message request response to a private chat * Currently, we never send anything for denied message requests. + * Note: you souldn't to use this directly. Instead use `handleAcceptConversationRequest()` */ public async sendMessageRequestResponse() { if (!this.isPrivate()) { @@ -1547,7 +1541,7 @@ export class ConversationModel extends Backbone.Model { public async setIsApproved(value: boolean, shouldCommit: boolean = true) { const valueForced = Boolean(value); - if (!this.isPrivate()) { + if (!this.isPrivate() && !this.isClosedGroupV2()) { return; } @@ -1752,11 +1746,20 @@ export class ConversationModel extends Backbone.Model { } public didApproveMe() { - return Boolean(this.get('didApproveMe')); + if (PubKey.is05Pubkey(this.id) && this.isPrivate()) { + // if a private chat, trust the value from the Libsession wrapper cached first + // TODO once we have a libsession state, we can make this used accross the app without repeating as much + return SessionUtilContact.getContactCached(this.id)?.approvedMe ?? !!this.get('didApproveMe'); + } + return !!this.get('didApproveMe'); } public isApproved() { - return Boolean(this.get('isApproved')); + if (PubKey.is05Pubkey(this.id) && this.isPrivate()) { + // if a private chat, trust the value from the Libsession wrapper cached first + return SessionUtilContact.getContactCached(this.id)?.approved ?? !!this.get('isApproved'); + } + return !!this.get('isApproved'); } /** @@ -2035,37 +2038,16 @@ export class ConversationModel extends Backbone.Model { lokiProfile: UserUtils.getOurProfile(), }; - const shouldApprove = !this.isApproved() && (this.isPrivate() || this.isClosedGroupV2()); - - const incomingMessageCount = await Data.getMessageCountByType( - this.id, - MessageDirection.incoming - ); - const hasIncomingMessages = incomingMessageCount > 0; - if (PubKey.isBlinded(this.id)) { window.log.info('Sending a blinded message to this user: ', this.id); await this.sendBlindedMessageRequest(chatMessageParams); return; } - if (shouldApprove) { - await handleAcceptConversationRequest({ - convoId: this.id, - sendResponse: !message, - }); - await this.setIsApproved(true); - if (hasIncomingMessages) { - // have to manually add approval for local client here as DB conditional approval check in config msg handling will prevent this from running - await this.addOutgoingApprovalMessage(Date.now()); - if (!this.didApproveMe()) { - await this.setDidApproveMe(true); - } - // should only send once - await this.sendMessageRequestResponse(); - void forceSyncConfigurationNowIfNeeded(); - } - } + // handleAcceptConversationRequest will take care of sending response depending on the type of conversation + await handleAcceptConversationRequest({ + convoId: this.id, + }); if (this.isOpenGroupV2()) { const chatMessageOpenGroupV2 = new OpenGroupVisibleMessage(chatMessageParams); @@ -2262,20 +2244,19 @@ export class ConversationModel extends Backbone.Model { const lastMessageStatus = lastMessageModel.getMessagePropStatus() || undefined; const lastMessageNotificationText = lastMessageModel.getNotificationText() || undefined; // we just want to set the `status` to `undefined` if there are no `lastMessageNotificationText` - const lastMessageUpdate = - !!lastMessageNotificationText && !isEmpty(lastMessageNotificationText) - ? { - lastMessage: lastMessageNotificationText || '', - lastMessageStatus, - lastMessageInteractionType, - lastMessageInteractionStatus, - } - : { - lastMessage: '', - lastMessageStatus: undefined, - lastMessageInteractionType: undefined, - lastMessageInteractionStatus: undefined, - }; + const lastMessageUpdate = !isEmpty(lastMessageNotificationText) + ? { + lastMessage: lastMessageNotificationText || '', + lastMessageStatus, + lastMessageInteractionType, + lastMessageInteractionStatus, + } + : { + lastMessage: '', + lastMessageStatus: undefined, + lastMessageInteractionType: undefined, + lastMessageInteractionStatus: undefined, + }; const existingLastMessageInteractionType = this.get('lastMessageInteractionType'); const existingLastMessageInteractionStatus = this.get('lastMessageInteractionStatus'); @@ -2444,7 +2425,7 @@ export class ConversationModel extends Backbone.Model { ) { return false; } - return Boolean(this.get('isApproved')); + return this.isApproved(); } private async bumpTyping() { diff --git a/ts/models/message.ts b/ts/models/message.ts index a0d72d543..27845722e 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -120,6 +120,18 @@ export function arrayContainsOneItemOnly(arrayToCheck: Array | undefined return arrayToCheck && arrayToCheck.length === 1; } +function formatJoined(joined: Array) { + const names = joined.map(ConvoHub.use().getContactProfileNameOrShortenedPubKey); + const messages = []; + + if (names.length > 1) { + messages.push(window.i18n('multipleJoinedTheGroup', [names.join(', ')])); + } else { + messages.push(window.i18n('joinedTheGroup', names)); + } + return messages.join(' '); +} + export class MessageModel extends Backbone.Model { constructor(attributes: MessageAttributesOptionals & { skipTimerInit?: boolean }) { const filledAttrs = fillMessageAttributesWithDefaults(attributes); @@ -1328,15 +1340,11 @@ export class MessageModel extends Backbone.Model { } if (groupUpdate.joined && groupUpdate.joined.length) { - const names = groupUpdate.joined.map(ConvoHub.use().getContactProfileNameOrShortenedPubKey); - const messages = []; + return formatJoined(groupUpdate.joined); + } - if (names.length > 1) { - messages.push(window.i18n('multipleJoinedTheGroup', [names.join(', ')])); - } else { - messages.push(window.i18n('joinedTheGroup', names)); - } - return messages.join(' '); + if (groupUpdate.joinedWithHistory && groupUpdate.joinedWithHistory.length) { + return formatJoined(groupUpdate.joinedWithHistory); } if (groupUpdate.kicked && groupUpdate.kicked.length) { diff --git a/ts/react.d.ts b/ts/react.d.ts index 9adacdf0a..9bac24028 100644 --- a/ts/react.d.ts +++ b/ts/react.d.ts @@ -7,8 +7,7 @@ import 'react'; declare module 'react' { type SessionDataTestId = - | 'group_member_status_text' - | 'group_member_name' + | 'group-member-status-text' | 'loading-spinner' | 'session-toast' | 'loading-animation' @@ -17,7 +16,6 @@ declare module 'react' { | 'chooser-new-group' | 'chooser-new-conversation-button' | 'new-conversation-button' - | 'module-conversation__user__profile-name' | 'message-request-banner' | 'leftpane-section-container' | 'group-name-input' @@ -164,14 +162,13 @@ declare module 'react' { | 'continue-session-button' | 'next-new-conversation-button' | 'reveal-recovery-phrase' - | 'resend_invite_button' + | 'resend-invite-button' | 'session-confirm-cancel-button' | 'session-confirm-ok-button' | 'confirm-nickname' | 'path-light-svg' - | 'group_member_status_text' - | 'group_member_name' - | 'resend_promote_button' + | 'group-member-name' + | 'resend-promote-button' | 'next-button' | 'save-button-profile-update' | 'save-button-profile-update' diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index e538c59e7..e520ed68c 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -20,6 +20,7 @@ import { concatUInt8Array, getSodiumRenderer } from '../session/crypto'; import { removeMessagePadding } from '../session/crypto/BufferPadding'; import { DisappearingMessages } from '../session/disappearing_messages'; import { ReadyToDisappearMsgUpdate } from '../session/disappearing_messages/types'; +import { ed25519Str } from '../session/onions/onionPath'; import { ProfileManager } from '../session/profile_manager/ProfileManager'; import { GroupUtils, UserUtils } from '../session/utils'; import { perfEnd, perfStart } from '../session/utils/Performance'; @@ -722,12 +723,7 @@ async function handleMessageRequestResponse( envelope: EnvelopePlus, messageRequestResponse: SignalService.MessageRequestResponse ) { - const { isApproved } = messageRequestResponse; - if (!isApproved) { - await IncomingMessageCache.removeFromCache(envelope); - return; - } - if (!messageRequestResponse) { + if (!messageRequestResponse || !messageRequestResponse.isApproved) { window?.log?.error('handleMessageRequestResponse: Invalid parameters -- dropping message.'); await IncomingMessageCache.removeFromCache(envelope); return; @@ -738,6 +734,14 @@ async function handleMessageRequestResponse( const convosToMerge = findCachedBlindedMatchOrLookupOnAllServers(envelope.source, sodium); const unblindedConvoId = envelope.source; + if (!PubKey.is05Pubkey(unblindedConvoId)) { + window?.log?.warn( + 'handleMessageRequestResponse: Invalid unblindedConvoId -- dropping message.' + ); + await IncomingMessageCache.removeFromCache(envelope); + return; + } + const conversationToApprove = await ConvoHub.use().getOrCreateAndWait( unblindedConvoId, ConversationTypeEnum.PRIVATE @@ -747,12 +751,14 @@ async function handleMessageRequestResponse( mostRecentActiveAt = toNumber(envelope.timestamp); } + const previousApprovedMe = conversationToApprove.didApproveMe(); + await conversationToApprove.setDidApproveMe(true, false); + conversationToApprove.set({ active_at: mostRecentActiveAt, - isApproved: true, - didApproveMe: true, }); await conversationToApprove.unhideIfNeeded(false); + await conversationToApprove.commit(); if (convosToMerge.length) { // merge fields we care by hand @@ -809,23 +815,21 @@ async function handleMessageRequestResponse( ); } - if (!conversationToApprove || conversationToApprove.didApproveMe()) { - await conversationToApprove?.commit(); - window?.log?.info( - 'Conversation already contains the correct value for the didApproveMe field.' + if (previousApprovedMe) { + await conversationToApprove.commit(); + + window.log.inf( + `convo ${ed25519Str(conversationToApprove.id)} previousApprovedMe is already true. Nothing to do ` ); await IncomingMessageCache.removeFromCache(envelope); - return; } - await conversationToApprove.setDidApproveMe(true, true); // Conversation was not approved before so a sync is needed await conversationToApprove.addIncomingApprovalMessage( toNumber(envelope.timestamp), unblindedConvoId ); - await IncomingMessageCache.removeFromCache(envelope); } diff --git a/ts/session/apis/snode_api/retrieveRequest.ts b/ts/session/apis/snode_api/retrieveRequest.ts index 39a9fcafe..6ff1dca0a 100644 --- a/ts/session/apis/snode_api/retrieveRequest.ts +++ b/ts/session/apis/snode_api/retrieveRequest.ts @@ -253,10 +253,10 @@ async function retrieveNextMessagesNoRetries( GetNetworkTime.handleTimestampOffsetFromNetwork('retrieve', bodyFirstResult.t); // merge results with their corresponding namespaces - return results.map((result, index) => ({ - code: result.code, - messages: result.body as RetrieveMessagesResultsContent, - namespace: namespacesAndLastHashes[index].namespace, + return namespacesAndLastHashes.map((n, index) => ({ + code: results[index].code, + messages: results[index].body as RetrieveMessagesResultsContent, + namespace: n.namespace, })); } catch (e) { window?.log?.warn('exception while parsing json of nextMessage:', e); diff --git a/ts/session/apis/snode_api/swarmPolling.ts b/ts/session/apis/snode_api/swarmPolling.ts index 469272bfe..f8ad0ab56 100644 --- a/ts/session/apis/snode_api/swarmPolling.ts +++ b/ts/session/apis/snode_api/swarmPolling.ts @@ -144,6 +144,11 @@ export class SwarmPolling { if (this.groupPolling.findIndex(m => m.pubkey.key === pk.key) === -1) { window?.log?.info('Swarm addGroupId: adding pubkey to polling', pk.key); this.groupPolling.push({ pubkey: pk, lastPolledTimestamp: 0, callbackFirstPoll }); + } else if (callbackFirstPoll) { + // group is already polled. Hopefully we already have keys for it to decrypt messages? + void sleepFor(2000).then(() => { + void callbackFirstPoll(); + }); } } @@ -547,7 +552,7 @@ export class SwarmPolling { } results = results.slice(0, results.length - 1); } - console.warn('results what when we get kicked out?: ', results); + // console.warn('results what when we get kicked out?: ', results); // debugger const lastMessages = results.map(r => { return last(r.messages.messages); }); @@ -845,7 +850,6 @@ async function handleMessagesForGroupV2( throw new Error('decryptForGroupV2 returned empty envelope'); } - console.warn('envelopePlus', envelopePlus); // this is the processing of the message itself, which can be long. // We allow 1 minute per message at most, which should be plenty await Receiver.handleSwarmContentDecryptedWithTimeout({ diff --git a/ts/session/messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberChangeMessage.ts b/ts/session/messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberChangeMessage.ts index f3d6692c4..0292898f1 100644 --- a/ts/session/messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberChangeMessage.ts +++ b/ts/session/messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberChangeMessage.ts @@ -111,6 +111,10 @@ export class GroupUpdateMemberChangeMessage extends GroupUpdateMessage { ), }); + if (type === Type.ADDED && this.typeOfChange === 'addedWithHistory') { + memberChangeMessage.historyShared = true; + } + return new SignalService.DataMessage({ groupUpdateMessage: { memberChangeMessage } }); } diff --git a/ts/session/profile_manager/ProfileManager.ts b/ts/session/profile_manager/ProfileManager.ts index cfe3c778e..15dfc8615 100644 --- a/ts/session/profile_manager/ProfileManager.ts +++ b/ts/session/profile_manager/ProfileManager.ts @@ -23,9 +23,8 @@ async function updateOurProfileSync({ displayName, profileUrl, profileKey, prior } await updateProfileOfContact(us, displayName, profileUrl, profileKey); - if (priority !== null && ourConvo.getPriority() !== priority) { - ourConvo.set('priority', priority); - await ourConvo.commit(); + if (priority !== null) { + await ourConvo.setPriorityFromWrapper(priority, true); } } diff --git a/ts/session/sending/MessageQueue.ts b/ts/session/sending/MessageQueue.ts index 2c4d2983e..8df9411a2 100644 --- a/ts/session/sending/MessageQueue.ts +++ b/ts/session/sending/MessageQueue.ts @@ -368,9 +368,11 @@ export class MessageQueue { 'sendSingleMessageAndHandleResult: failed to send message with: ', error.message ); - if (rawMessage) { - await MessageSentHandler.handleSwarmMessageSentFailure(rawMessage, error); - } + await MessageSentHandler.handleSwarmMessageSentFailure( + { device: rawMessage.device, identifier: rawMessage.identifier }, + error + ); + return null; } finally { // Remove from the cache because retrying is done in the sender diff --git a/ts/session/sending/MessageSender.ts b/ts/session/sending/MessageSender.ts index 2c71c1df9..731c9742b 100644 --- a/ts/session/sending/MessageSender.ts +++ b/ts/session/sending/MessageSender.ts @@ -376,14 +376,13 @@ async function sendMessagesDataToSnode( revokeSubRequest, unrevokeSubRequest, ]); - const targetNode = await SnodePool.getNodeFromSwarmOrThrow(asssociatedWith); try { const storeResults = await BatchRequests.doUnsignedSnodeBatchRequestNoRetries( rawRequests, targetNode, - 4000, + 6000, asssociatedWith, method ); @@ -395,6 +394,11 @@ async function sendMessagesDataToSnode( ); throw new Error('doUnsignedSnodeBatchRequestNoRetries: Invalid result'); } + await handleBatchResultWithSubRequests({ + batchResult: storeResults, + subRequests: rawRequests, + destination: asssociatedWith, + }); const firstResult = storeResults[0]; @@ -545,6 +549,9 @@ async function encryptMessagesAndWrap( /** * Send an array of preencrypted data to the corresponding swarm. + * Warning: + * This does not handle result of messages and marking messages as read, syncing them currently. + * For this, use the `MessageQueue.sendSingleMessage()` for now. * * @param params the data to deposit * @param destination the pubkey we should deposit those message to @@ -700,6 +707,10 @@ export const MessageSender = { signSubRequests, }; +/** + * Note: this function does not handle the syncing logic of messages yet. + * Use it to push message to group, to note to self, or with user messages which do not require a syncing logic + */ async function handleBatchResultWithSubRequests({ batchResult, destination, @@ -756,7 +767,21 @@ async function handleBatchResultWithSubRequests({ const foundMessage = await Data.getMessageById(subRequest.dbMessageIdentifier); if (foundMessage) { await foundMessage.updateMessageHash(storedHash); + // - a message pushed to a group is always synced + // - a message sent to ourself when it was a marked as sentSync is a synced message to ourself + if ( + isDestinationClosedGroup || + (subRequest.destination === us && foundMessage.get('sentSync')) + ) { + foundMessage.set({ synced: true }); + } + foundMessage.set({ + sent_to: [subRequest.destination], + sent: true, + sent_at: storedAt, + }); await foundMessage.commit(); + await foundMessage.getConversation()?.updateLastMessage(); window?.log?.info(`updated message ${foundMessage.get('id')} with hash: ${storedHash}`); } /* eslint-enable no-await-in-loop */ diff --git a/ts/session/sending/MessageSentHandler.ts b/ts/session/sending/MessageSentHandler.ts index 2683d1246..7bd3aa415 100644 --- a/ts/session/sending/MessageSentHandler.ts +++ b/ts/session/sending/MessageSentHandler.ts @@ -147,7 +147,10 @@ async function handleSwarmMessageSentSuccess( fetchedMessage.getConversation()?.updateLastMessage(); } -async function handleSwarmMessageSentFailure(sentMessage: OutgoingRawMessage, error: any) { +async function handleSwarmMessageSentFailure( + sentMessage: Pick, + error: any +) { const fetchedMessage = await fetchHandleMessageSentData(sentMessage.identifier); if (!fetchedMessage) { return; @@ -157,14 +160,12 @@ async function handleSwarmMessageSentFailure(sentMessage: OutgoingRawMessage, er await fetchedMessage.saveErrors(error); } - if (!(sentMessage instanceof OpenGroupVisibleMessage)) { - const isOurDevice = UserUtils.isUsFromCache(sentMessage.device); - // if this message was for ourself, and it was not already synced, - // it means that we failed to sync it. - // so just remove the flag saying that we are currently sending the sync message - if (isOurDevice && !fetchedMessage.get('sync')) { - fetchedMessage.set({ sentSync: false }); - } + const isOurDevice = UserUtils.isUsFromCache(sentMessage.device); + // if this message was for ourself, and it was not already synced, + // it means that we failed to sync it. + // so just remove the flag saying that we are currently sending the sync message + if (isOurDevice && !fetchedMessage.get('sync')) { + fetchedMessage.set({ sentSync: false }); } // always mark the message as sent. diff --git a/ts/session/utils/calling/CallManager.ts b/ts/session/utils/calling/CallManager.ts index bccf2b122..936dc2052 100644 --- a/ts/session/utils/calling/CallManager.ts +++ b/ts/session/utils/calling/CallManager.ts @@ -533,7 +533,7 @@ export async function USER_callRecipient(recipient: string) { weAreCallerOnCurrentCall = true; // initiating a call is analogous to sending a message request - await handleAcceptConversationRequest({ convoId: recipient, sendResponse: false }); + await handleAcceptConversationRequest({ convoId: recipient }); // Note: we do the sending of the preoffer manually as the sendTo1o1NonDurably rely on having a message saved to the db for MessageSentSuccess // which is not the case for a pre offer message (the message only exists in memory) @@ -934,7 +934,6 @@ export async function USER_acceptIncomingCallRequest(fromSender: string) { // consider the conversation completely approved await handleAcceptConversationRequest({ convoId: fromSender, - sendResponse: true, }); } diff --git a/ts/state/ducks/metaGroups.ts b/ts/state/ducks/metaGroups.ts index 7f403f64b..04f184dee 100644 --- a/ts/state/ducks/metaGroups.ts +++ b/ts/state/ducks/metaGroups.ts @@ -136,7 +136,6 @@ const initNewGroupInWrapper = createAsyncThunk( throw new Error('groupSecretKey was empty just after creation.'); } newGroup.name = groupName; // this will be used by the linked devices until they fetch the info from the groups swarm - // the `GroupSync` below will need the secretKey of the group to be saved in the wrapper. So save it! await UserGroupsWrapperActions.setGroup(newGroup); const ourEd25519KeypairBytes = await UserUtils.getUserED25519KeyPairBytes(); @@ -183,8 +182,7 @@ const initNewGroupInWrapper = createAsyncThunk( const convo = await ConvoHub.use().getOrCreateAndWait(groupPk, ConversationTypeEnum.GROUPV2); await convo.setIsApproved(true, false); - - console.warn('updateMessages for new group might need an update message?'); + await convo.commit(); // commit here too, as the poll needs it to be approved const result = await GroupSync.pushChangesToGroupSwarmIfNeeded({ groupPk, @@ -196,6 +194,36 @@ const initNewGroupInWrapper = createAsyncThunk( window.log.warn('GroupSync.pushChangesToGroupSwarmIfNeeded during create failed'); throw new Error('failed to pushChangesToGroupSwarmIfNeeded'); } + + // push one group change message were initial members are added to the group + if (membersFromWrapper.length) { + const membersHex = uniq(membersFromWrapper.map(m => m.pubkeyHex)); + const sentAt = GetNetworkTime.now(); + const msgModel = await ClosedGroup.addUpdateMessage({ + diff: { type: 'add', added: membersHex, withHistory: false }, + expireUpdate: null, + sender: us, + sentAt, + convo, + }); + const groupChange = await getWithoutHistoryControlMessage({ + adminSecretKey: groupSecretKey, + convo, + groupPk, + withoutHistory: membersHex, + createAtNetworkTimestamp: sentAt, + dbMsgIdentifier: msgModel.id, + }); + if (groupChange) { + await GroupSync.storeGroupUpdateMessages({ + groupPk, + updateMessages: [groupChange], + }); + } + } + + await convo.commit(); + getSwarmPollingInstance().addGroupId(new PubKey(groupPk)); await convo.unhideIfNeeded(); @@ -838,7 +866,6 @@ async function handleMemberAddedFromUI({ if (groupChange) { updateMessagesToPush.push(groupChange); } - console.warn(`diff: { type: ' should add case for addWithHistory here ?`); } await convo.commit();