From 3854d0e10d984677a0f030e2dfe83864ef0f1a61 Mon Sep 17 00:00:00 2001 From: Warrick Date: Tue, 18 May 2021 13:12:51 +1000 Subject: [PATCH] WIP: Closed group reliability (#1630) * WIP: added non-durable messaging function. * WIP: Non-durable sending * WIP: adding dialog box. * Creating dialog if group invite message promises don't return true. * removed console log * applied PR changes, linting and formatting. * WIP: allowing resend invite to failures. * using lookup. * WIP: recursively opening dialog. * WIP: debugging reject triggering on confirmation modal. * register events fix. * Closed group invite retry dialog working. * Added english text to messages. * Prevent saving of hexkey pair if it already exists. * Fixed nickname edit input trimming end letter. * Don't show closed group invite dialog unless it has failed at least once. * Fix linting error. * Fix plurality. * Ensure admin members are included in all invite reattempts, mixed plurality. --- _locales/en/messages.json | 32 +++++ js/views/session_confirm_view.js | 13 +- .../session/SessionNicknameDialog.tsx | 2 +- ts/receiver/closedGroups.ts | 133 ++++++++++++++++-- ts/session/sending/MessageQueue.ts | 25 +++- 5 files changed, 185 insertions(+), 20 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 39e629741..4f4796c53 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1387,6 +1387,38 @@ "description": "Button action that the user can click to edit a group name (open)", "androidKey": "conversation__menu_edit_group" }, + "closedGroupInviteFailTitle": { + "message": "Group Invitation Failed", + "description": "Title for the dialog of a failed group invite modal" + }, + "closedGroupInviteFailTitlePlural": { + "message": "Group Invitations Failed", + "description": "Title for the dialog of a failed group invite modal plural" + }, + "closedGroupInviteFailMessage": { + "message": "Unable to successfully invite a group member", + "description": "Message for the dialog of a failed group invite modal" + }, + "closedGroupInviteFailMessagePlural": { + "message": "Unable to successfully invite all group members", + "description": "Message for the dialog of a failed group invite modal plural" + }, + "closedGroupInviteOkText": { + "message": "Retry invitations", + "description": "Text for the OK button of a closed group invite failure" + }, + "closedGroupInviteSuccessTitlePlural": { + "message": "Group Invitations Completed", + "description": "The title for the modal dialog when a closed group invite retry succeeds" + }, + "closedGroupInviteSuccessTitle": { + "message": "Group Invitation Succeeded", + "description": "The title for the modal dialog when a closed group invite retry succeeds" + }, + "closedGroupInviteSuccessMessage": { + "message": "Successfully invited closed group members", + "description": "The message for the modal dialog when a closed group invite retry succeeds" + }, "editGroupName": { "message": "Edit group name", "description": "Button action that the user can click to edit a group name (closed)" diff --git a/js/views/session_confirm_view.js b/js/views/session_confirm_view.js index 1d5a6b8a8..8d29a14d2 100644 --- a/js/views/session_confirm_view.js +++ b/js/views/session_confirm_view.js @@ -33,32 +33,39 @@ unregisterEvents() { document.removeEventListener('keyup', this.props.onClickClose, false); + if (this.confirmView && this.confirmView.el) { + window.ReactDOM.unmountComponentAtNode(this.confirmView.el); + } + this.$('.session-confirm-wrapper').remove(); }, render() { this.$('.session-confirm-wrapper').remove(); + this.registerEvents(); this.confirmView = new Whisper.ReactWrapperView({ className: 'loki-dialog modal session-confirm-wrapper', Component: window.Signal.Components.SessionConfirm, props: this.props, }); - this.registerEvents(); this.$el.prepend(this.confirmView.el); }, ok() { - this.$('.session-confirm-wrapper').remove(); this.unregisterEvents(); + + this.$('.session-confirm-wrapper').remove(); if (this.props.resolve) { this.props.resolve(); } }, cancel() { - this.$('.session-confirm-wrapper').remove(); this.unregisterEvents(); + + this.$('.session-confirm-wrapper').remove(); + if (this.props.reject) { this.props.reject(); } diff --git a/ts/components/session/SessionNicknameDialog.tsx b/ts/components/session/SessionNicknameDialog.tsx index aaf1882e3..d68143f6e 100644 --- a/ts/components/session/SessionNicknameDialog.tsx +++ b/ts/components/session/SessionNicknameDialog.tsx @@ -59,7 +59,7 @@ const SessionNicknameInner = (props: Props) => { type="nickname" id="nickname-modal-input" placeholder={window.i18n('nicknamePlaceholder')} - onKeyPress={e => { + onKeyUp={e => { void onNicknameInput(_.cloneDeep(e)); }} /> diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index 7c89425a4..6f9fd0039 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -34,6 +34,7 @@ import { ClosedGroupEncryptionPairReplyMessage } from '../session/messages/outgo import { queueAllCachedFromSource } from './receiver'; import { actions as conversationActions } from '../state/ducks/conversations'; import { SwarmPolling } from '../session/snode_api/swarmPolling'; +import { MessageModel } from '../models/message'; export const distributingClosedGroupEncryptionKeyPairs = new Map(); @@ -885,7 +886,122 @@ export async function createClosedGroup(groupName: string, members: Array { + const allInvitesSent = await sendToGroupMembers( + listOfMembers, + groupPublicKey, + groupName, + admins, + encryptionKeyPair, + dbMessage + ); + + if (allInvitesSent) { + const newHexKeypair = encryptionKeyPair.toHexKeyPair(); + + const isHexKeyPairSaved = await isKeyPairAlreadySaved(groupPublicKey, newHexKeypair); + + if (!isHexKeyPairSaved) { + // tslint:disable-next-line: no-non-null-assertion + await addClosedGroupEncryptionKeyPair(groupPublicKey, encryptionKeyPair.toHexKeyPair()); + } else { + window.log.info('Dropping already saved keypair for group', groupPublicKey); + } + + // Subscribe to this group id + SwarmPolling.getInstance().addGroupId(new PubKey(groupPublicKey)); + } + + await forceSyncConfigurationNowIfNeeded(); + + window.inboxStore?.dispatch(conversationActions.openConversationExternal(groupPublicKey)); +} + +/** + * Sends a group invite message to each member of the group. + * @returns Array of promises for group invite messages sent to group members + */ +async function sendToGroupMembers( + listOfMembers: Array, + groupPublicKey: string, + groupName: string, + admins: Array, + encryptionKeyPair: ECKeyPair, + dbMessage: MessageModel, + isRetry: boolean = false +): Promise { + const promises = createInvitePromises( + listOfMembers, + groupPublicKey, + groupName, + admins, + encryptionKeyPair, + dbMessage + ); + window.log.info(`Creating a new group and an encryptionKeyPair for group ${groupPublicKey}`); + // evaluating if all invites sent, if failed give the option to retry failed invites via modal dialog + const inviteResults = await Promise.all(promises); + const allInvitesSent = _.every(inviteResults, Boolean); + + if (allInvitesSent) { + if (isRetry) { + const invitesTitle = + inviteResults.length > 1 + ? window.i18n('closedGroupInviteSuccessTitlePlural') + : window.i18n('closedGroupInviteSuccessTitle'); + window.confirmationDialog({ + title: invitesTitle, + message: window.i18n('closedGroupInviteSuccessMessage'), + }); + } + return allInvitesSent; + } else { + // Confirmation dialog that recursively calls sendToGroupMembers on resolve + window.confirmationDialog({ + title: + inviteResults.length > 1 + ? window.i18n('closedGroupInviteFailTitlePlural') + : window.i18n('closedGroupInviteFailTitle'), + message: + inviteResults.length > 1 + ? window.i18n('closedGroupInviteFailMessagePlural') + : window.i18n('closedGroupInviteFailMessage'), + okText: window.i18n('closedGroupInviteOkText'), + resolve: async () => { + const membersToResend: Array = new Array(); + inviteResults.forEach((result, index) => { + const member = listOfMembers[index]; + // group invite must always contain the admin member. + if (result !== true || admins.includes(member)) { + membersToResend.push(member); + } + }); + if (membersToResend.length > 0) { + const isRetrySend = true; + await sendToGroupMembers( + membersToResend, + groupPublicKey, + groupName, + admins, + encryptionKeyPair, + dbMessage, + isRetrySend + ); + } + }, + }); + } + return allInvitesSent; +} + +function createInvitePromises( + listOfMembers: Array, + groupPublicKey: string, + groupName: string, + admins: Array, + encryptionKeyPair: ECKeyPair, + dbMessage: MessageModel +) { + return listOfMembers.map(async m => { const messageParams: ClosedGroupNewMessageParams = { groupId: groupPublicKey, name: groupName, @@ -897,19 +1013,6 @@ export async function createClosedGroup(groupName: string, members: Array { + let rawMessage; + try { + rawMessage = await MessageUtils.toRawMessage(user, message); + const wrappedEnvelope = await MessageSender.send(rawMessage); + await MessageSentHandler.handleMessageSentSuccess(rawMessage, wrappedEnvelope); + return !!wrappedEnvelope; + } catch (error) { + if (rawMessage) { + await MessageSentHandler.handleMessageSentFailure(rawMessage, error); + } + return false; + } + } + public async processPending(device: PubKey) { const messages = await this.pendingMessageCache.getForDevice(device);