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.
pull/1639/head
Warrick 4 years ago committed by GitHub
parent 9be6c6bb30
commit 3854d0e10d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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)"

@ -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();
}

@ -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));
}}
/>

@ -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<string, ECKeyPair>();
@ -885,7 +886,122 @@ export async function createClosedGroup(groupName: string, members: Array<string
convo.updateLastMessage();
// Send a closed group update message to all members individually
const promises = listOfMembers.map(async m => {
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<string>,
groupPublicKey: string,
groupName: string,
admins: Array<string>,
encryptionKeyPair: ECKeyPair,
dbMessage: MessageModel,
isRetry: boolean = false
): Promise<any> {
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<string> = new Array<string>();
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<string>,
groupPublicKey: string,
groupName: string,
admins: Array<string>,
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<string
expireTimer: 0,
};
const message = new ClosedGroupNewMessage(messageParams);
return getMessageQueue().sendToPubKey(PubKey.cast(m), message);
return getMessageQueue().sendToPubKeyNonDurably(PubKey.cast(m), message);
});
window.log.info(`Creating a new group and an encryptionKeyPair for group ${groupPublicKey}`);
// tslint:disable-next-line: no-non-null-assertion
await addClosedGroupEncryptionKeyPair(groupPublicKey, encryptionKeyPair.toHexKeyPair());
// Subscribe to this group id
SwarmPolling.getInstance().addGroupId(new PubKey(groupPublicKey));
await Promise.all(promises);
await forceSyncConfigurationNowIfNeeded();
window.inboxStore?.dispatch(conversationActions.openConversationExternal(groupPublicKey));
}

@ -1,5 +1,5 @@
import { PendingMessageCache } from './PendingMessageCache';
import { JobQueue, UserUtils } from '../utils';
import { JobQueue, MessageUtils, UserUtils } from '../utils';
import { PubKey, RawMessage } from '../types';
import { MessageSender } from '.';
import { ClosedGroupMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMessage';
@ -146,6 +146,29 @@ export class MessageQueue {
await this.process(PubKey.cast(ourPubKey), message, sentCb);
}
/**
* Sends a message that awaits until the message is completed sending
* @param user user pub key to send to
* @param message Message to be sent
*/
public async sendToPubKeyNonDurably(
user: PubKey,
message: ClosedGroupNewMessage
): Promise<boolean> {
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);

Loading…
Cancel
Save