From 7eb1440263110108993be12e67207e9bdabdc8e1 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 9 Jul 2020 08:57:20 +1000 Subject: [PATCH 01/11] use db identifier for opengroup message so we handle sent events --- js/models/conversations.js | 1 + 1 file changed, 1 insertion(+) diff --git a/js/models/conversations.js b/js/models/conversations.js index b64e18387..9d97f4793 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1359,6 +1359,7 @@ attachments: uploads.attachments, preview: uploads.preview, quote: uploads.quote, + identifier: id, }; const openGroupMessage = new libsession.Messages.Outgoing.OpenGroupMessage( openGroupParams From 126f18278f5e38b8023514eef4582b11b6731fd1 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 9 Jul 2020 08:57:45 +1000 Subject: [PATCH 02/11] handle message sent success for opengroups events correctly --- js/models/messages.js | 25 ++++++++++++++----- .../session/LeftPaneMessageSection.tsx | 2 +- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/js/models/messages.js b/js/models/messages.js index d9bd47690..5f714f83a 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -1210,14 +1210,23 @@ }, async handleMessageSentSuccess(sentMessage) { - const sentTo = this.get('sent_to') || []; + let sentTo = this.get('sent_to') || []; const isOurDevice = await window.libsession.Protocols.MultiDeviceProtocol.isOurDevice( sentMessage.device ); + const isOpenGroupMessage = + sentMessage.group && + sentMessage.group instanceof libsession.Types.OpenGroup; + // Handle the sync logic here - if (!isOurDevice && !this.get('synced') && !this.get('sentSync')) { + if ( + !isOurDevice && + !isOpenGroupMessage && + !this.get('synced') && + !this.get('sentSync') + ) { const contentDecoded = textsecure.protobuf.Content.decode( sentMessage.plainTextBuffer ); @@ -1228,11 +1237,15 @@ } else if (isOurDevice && this.get('sentSync')) { this.set({ synced: true }); } - const primaryPubKey = await libsession.Protocols.MultiDeviceProtocol.getPrimaryDevice( - sentMessage.device - ); + if (!isOpenGroupMessage) { + const primaryPubKey = await libsession.Protocols.MultiDeviceProtocol.getPrimaryDevice( + sentMessage.device + ); + sentTo = _.union(sentTo, [primaryPubKey.key]); + } + this.set({ - sent_to: _.union(sentTo, [primaryPubKey.key]), + sent_to: sentTo, sent: true, expirationStartTimestamp: Date.now(), // unidentifiedDeliveries: result.unidentifiedDeliveries, diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index 26f92bfd6..31a46fdc3 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -496,7 +496,7 @@ export class LeftPaneMessageSection extends React.Component { } finally { this.setState({ loading: false, - }); + }); } } From d8df91fa154a49e4c510e390a006b0254c4e1b3d Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 9 Jul 2020 08:58:32 +1000 Subject: [PATCH 03/11] use the groupId as conversation id to create a conversation when this is a group --- ts/receiver/dataMessage.ts | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index 293103cb8..575534f10 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -599,7 +599,24 @@ export async function handleMessageEvent(event: MessageEvent): Promise { sendDeliveryReceipt(source, data.timestamp); } - await window.ConversationController.getOrCreateAndWait(id, type); + // Conversation Id is: + // - primarySource if it is an incoming DM message, + // - destination if it is an outgoing message, + // - group.id if it is a group message + let conversationId = id; + if (isGroupMessage) { + /* handle one part of the group logic here: + handle requesting info of a new group, + dropping an admin only update from a non admin, ... + */ + conversationId = message.group.id; + } + + if (!conversationId) { + window.console.warn('Invalid conversation id for incoming message', conversationId); + } + + await window.ConversationController.getOrCreateAndWait(conversationId, type); // =========== Process flags ============= @@ -613,20 +630,12 @@ export async function handleMessageEvent(event: MessageEvent): Promise { // ========================================= - // Conversation Id is: - // - primarySource if it is an incoming DM message, - // - destination if it is an outgoing message, - // - group.id if it is a group message - let conversationId = id; - const primarySource = await MultiDeviceProtocol.getPrimaryDevice(source); if (isGroupMessage) { /* handle one part of the group logic here: handle requesting info of a new group, dropping an admin only update from a non admin, ... */ - conversationId = message.group.id; - const shouldReturn = await preprocessGroupMessage( source, message.group, From 880e0396529f8f3149ce0ffaaf0897bd4b5354eb Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 9 Jul 2020 10:28:10 +1000 Subject: [PATCH 04/11] handle grantSignature undefined --- ts/components/session/LeftPaneMessageSection.tsx | 7 ++++--- ts/receiver/dataMessage.ts | 5 ++++- ts/session/protocols/MultiDeviceProtocol.ts | 4 +++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index 31a46fdc3..b366a922c 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -456,7 +456,7 @@ export class LeftPaneMessageSection extends React.Component { if (!OpenGroup.validate(serverUrl)) { window.pushToast({ title: window.i18n('noServerURL'), - id: 'connectToServerFail', + id: 'connectToServer', type: 'error', }); @@ -480,7 +480,7 @@ export class LeftPaneMessageSection extends React.Component { if (await OpenGroup.serverExists(serverUrl)) { window.pushToast({ title: window.i18n('connectingToServer'), - id: 'connectToServerSuccess', + id: 'connectToServer', type: 'success', }); @@ -488,9 +488,10 @@ export class LeftPaneMessageSection extends React.Component { } }); } catch (e) { + window.console.error('Failed to connect to server:', e); window.pushToast({ title: window.i18n('connectToServerFail'), - id: 'connectToServerFail', + id: 'connectToServer', type: 'error', }); } finally { diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index 575534f10..f14d5642e 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -613,7 +613,10 @@ export async function handleMessageEvent(event: MessageEvent): Promise { } if (!conversationId) { - window.console.warn('Invalid conversation id for incoming message', conversationId); + window.console.warn( + 'Invalid conversation id for incoming message', + conversationId + ); } await window.ConversationController.getOrCreateAndWait(conversationId, type); diff --git a/ts/session/protocols/MultiDeviceProtocol.ts b/ts/session/protocols/MultiDeviceProtocol.ts index 140e394b8..2ae330040 100644 --- a/ts/session/protocols/MultiDeviceProtocol.ts +++ b/ts/session/protocols/MultiDeviceProtocol.ts @@ -98,7 +98,9 @@ export class MultiDeviceProtocol { primaryDevicePubKey, secondaryDevicePubKey, requestSignature: StringUtils.encode(requestSignature, 'base64'), - grantSignature: StringUtils.encode(grantSignature, 'base64'), + grantSignature: grantSignature + ? StringUtils.encode(grantSignature, 'base64') + : undefined, }) ); From 4aa066768606c37c4ae9909a62104c4925aab37b Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 9 Jul 2020 10:33:26 +1000 Subject: [PATCH 05/11] OpenGroup: get conversation from conversationController rather than appDotNetApi --- ts/session/types/OpenGroup.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/ts/session/types/OpenGroup.ts b/ts/session/types/OpenGroup.ts index 462a21589..290e1c7f9 100644 --- a/ts/session/types/OpenGroup.ts +++ b/ts/session/types/OpenGroup.ts @@ -152,17 +152,14 @@ export class OpenGroup { if (!OpenGroup.validate(server)) { return; } - - const prefixedServer = this.prefixify(server); - const serverInfo = (await window.lokiPublicChatAPI.findOrCreateServer( - prefixedServer - )) as any; - - if (!serverInfo?.channels?.length) { - return; - } - - return serverInfo.channels[0].conversation; + const rawServerURL = server + .replace(/^https?:\/\//i, '') + .replace(/[/\\]+$/i, ''); + const channelId = 1; + const conversationId = `publicChat:${channelId}@${rawServerURL}`; + + // Quickly peak to make sure we don't already have it + return window.ConversationController.get(conversationId); } /** From c8241f47e6adeb08c71b5c8233e76c75d56e5b79 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 9 Jul 2020 10:58:05 +1000 Subject: [PATCH 06/11] CreateClosedGroup: only show direct conversations Also use the conversation display name when available --- .../session/SessionClosableOverlay.tsx | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index f2085948c..b70853766 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -66,18 +66,34 @@ export class SessionClosableOverlay extends React.Component { } public getContacts() { + const { overlayMode } = this.props; const contactsList = this.props.contacts ?? []; + // Depending on the rendered overlay type we have to filter the contact list. + let filteredContactsList = contactsList; + const isClosedGroupView = + overlayMode === SessionClosableOverlayType.ClosedGroup; + if (isClosedGroupView) { + filteredContactsList = filteredContactsList.filter( + c => c.type === 'direct' + ); + } - return contactsList.map((d: any) => { - const name = d.name ?? window.i18n('anonymous'); - + return filteredContactsList.map((d: any) => { // TODO: should take existing members into account const existingMember = false; + // if it has a profilename, use it and the shortened pubkey will be added automatically + // if no profile name, Anonymous and the shortened pubkey will be added automatically + let title; + if (d.profileName) { + title = `${d.profileName}`; + } else { + title = `${window.i18n('anonymous')}`; + } return { id: d.id, authorPhoneNumber: d.id, - authorProfileName: name, + authorProfileName: title, selected: false, authorName: name, authorColor: d.color, From adb7234b432c2720a1f23e226f2e25e5534e93c0 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 9 Jul 2020 11:07:16 +1000 Subject: [PATCH 07/11] exclude empty timestamp conversation from contacts list --- js/views/invite_contacts_dialog_view.js | 7 ++++++- ts/components/conversation/InviteContactsDialog.tsx | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/js/views/invite_contacts_dialog_view.js b/js/views/invite_contacts_dialog_view.js index 31f3fd086..5d5c4172b 100644 --- a/js/views/invite_contacts_dialog_view.js +++ b/js/views/invite_contacts_dialog_view.js @@ -15,7 +15,12 @@ const convos = window.getConversations().models; this.contacts = convos.filter( - d => !!d && !d.isBlocked() && d.isPrivate() && !d.isMe() + d => + !!d && + !d.isBlocked() && + d.isPrivate() && + !d.isMe() && + !!d.attributes.timestamp ); if (!convo.isPublic()) { const members = convo.get('members') || []; diff --git a/ts/components/conversation/InviteContactsDialog.tsx b/ts/components/conversation/InviteContactsDialog.tsx index 417ae66e0..1741978fd 100644 --- a/ts/components/conversation/InviteContactsDialog.tsx +++ b/ts/components/conversation/InviteContactsDialog.tsx @@ -31,7 +31,9 @@ export class InviteContactsDialog extends React.Component { contacts = contacts.map(d => { const lokiProfile = d.getLokiProfile(); - const name = lokiProfile ? lokiProfile.displayName : 'Anonymous'; + const name = lokiProfile + ? lokiProfile.displayName + : window.i18n('anonymous'); // TODO: should take existing members into account const existingMember = false; From f03d66de386c346b354a6c1d77a7e76bc5e351af Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 9 Jul 2020 16:09:30 +1000 Subject: [PATCH 08/11] use active_at to show and sync real contacts from contact list --- js/views/invite_contacts_dialog_view.js | 2 +- ts/receiver/multidevice.ts | 3 ++- ts/session/utils/SyncMessage.ts | 6 ++++-- ts/state/selectors/conversations.ts | 6 +----- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/js/views/invite_contacts_dialog_view.js b/js/views/invite_contacts_dialog_view.js index 5d5c4172b..0ba2486a5 100644 --- a/js/views/invite_contacts_dialog_view.js +++ b/js/views/invite_contacts_dialog_view.js @@ -20,7 +20,7 @@ !d.isBlocked() && d.isPrivate() && !d.isMe() && - !!d.attributes.timestamp + !!d.get('active_at') ); if (!convo.isPublic()) { const members = convo.get('members') || []; diff --git a/ts/receiver/multidevice.ts b/ts/receiver/multidevice.ts index 4b75cceeb..5cf786f64 100644 --- a/ts/receiver/multidevice.ts +++ b/ts/receiver/multidevice.ts @@ -334,6 +334,7 @@ async function onContactReceived(details: any) { // activeAt is null, then this contact has been purposefully hidden. if (activeAt !== null) { activeAt = activeAt || Date.now(); + conversation.set('active_at', activeAt); } const ourPrimaryKey = window.storage.get('primaryDevicePubKey'); if (ourPrimaryKey) { @@ -375,7 +376,6 @@ async function onContactReceived(details: any) { conversation.set({ // name: details.name, color: details.color, - active_at: activeAt, }); await conversation.setLokiProfile({ displayName: details.name }); @@ -427,6 +427,7 @@ async function onContactReceived(details: any) { verifiedEvent.viaContactSync = true; await onVerified(verifiedEvent); } + await conversation.trigger('change'); } catch (error) { window.log.error('onContactReceived error:', Errors.toLogFormat(error)); } diff --git a/ts/session/utils/SyncMessage.ts b/ts/session/utils/SyncMessage.ts index ffa5b8bdb..85df60a86 100644 --- a/ts/session/utils/SyncMessage.ts +++ b/ts/session/utils/SyncMessage.ts @@ -50,7 +50,8 @@ export async function getSyncContacts(): Promise | undefined> { c.isPrivate() && !c.isOurLocalDevice() && !c.isBlocked() && - !c.attributes.secondaryStatus + !c.attributes.secondaryStatus && + !!c.get('active_at') ) || []; const secondaryContactsPartial = conversations.filter( @@ -58,7 +59,8 @@ export async function getSyncContacts(): Promise | undefined> { c.isPrivate() && !c.isOurLocalDevice() && !c.isBlocked() && - c.attributes.secondaryStatus + c.attributes.secondaryStatus && + !!c.get('active_at') ); const secondaryContactsPromise = secondaryContactsPartial.map(async c => diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index 3f28c7851..786340d27 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -139,7 +139,7 @@ export const _getLeftPaneLists = ( // Remove all invalid conversations and conversatons of devices associated // with cancelled attempted links - if (!conversation.isPublic && !conversation.timestamp) { + if (!conversation.isPublic && !conversation.activeAt) { continue; } @@ -151,10 +151,6 @@ export const _getLeftPaneLists = ( unreadCount += conversation.unreadCount; } - if (!conversation.isPublic && !conversation.activeAt) { - continue; - } - if (conversation.isArchived) { archivedConversations.push(conversation); } else { From 935d26a41b89f552d01a03388ec537bce8f0a466 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 9 Jul 2020 16:09:38 +1000 Subject: [PATCH 09/11] add missing window import to decode group sync message --- libtextsecure/contacts_parser.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libtextsecure/contacts_parser.js b/libtextsecure/contacts_parser.js index 4a4c38b69..e7f1a7e21 100644 --- a/libtextsecure/contacts_parser.js +++ b/libtextsecure/contacts_parser.js @@ -55,5 +55,7 @@ GroupBuffer.prototype.constructor = GroupBuffer; const ContactBuffer = function Constructor(arrayBuffer) { ProtoParser.call(this, arrayBuffer, textsecure.protobuf.ContactDetails); }; + +window.GroupBuffer = GroupBuffer; ContactBuffer.prototype = Object.create(ProtoParser.prototype); ContactBuffer.prototype.constructor = ContactBuffer; From 8019bce372a53c21fa9f82e8e8db60a04b95e252 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 9 Jul 2020 16:09:59 +1000 Subject: [PATCH 10/11] make the 'OK' button green on putting an alias for linked device --- ts/components/DevicePairingDialog.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ts/components/DevicePairingDialog.tsx b/ts/components/DevicePairingDialog.tsx index 05407f441..e56bba982 100644 --- a/ts/components/DevicePairingDialog.tsx +++ b/ts/components/DevicePairingDialog.tsx @@ -98,6 +98,7 @@ export class DevicePairingDialog extends React.Component { text={window.i18n('ok')} onClick={this.validateSecondaryDevice} disabled={!deviceAlias} + buttonColor={SessionButtonColor.Green} /> From b502fcc3f9206b08299d24e1b408a3f5f02e1812 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 9 Jul 2020 16:26:25 +1000 Subject: [PATCH 11/11] clean code and add comments --- js/models/messages.js | 23 ++++++++++++++++++----- ts/test/test-utils/utils/message.ts | 4 ++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/js/models/messages.js b/js/models/messages.js index 5f714f83a..00efaed58 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -1209,6 +1209,10 @@ return errors[0][0]; }, + /** + * This function is called by inbox_view.js when a message was successfully sent for one device. + * So it might be called several times for the same message + */ async handleMessageSentSuccess(sentMessage) { let sentTo = this.get('sent_to') || []; @@ -1220,13 +1224,22 @@ sentMessage.group && sentMessage.group instanceof libsession.Types.OpenGroup; - // Handle the sync logic here - if ( + // We trigger a sync message only when the message is not to one of our devices, AND + // the message is not for an open group (there is no sync for opengroups, each device pulls all messages), AND + // if we did not sync or trigger a sync message for this specific message already + const shouldTriggerSyncMessage = !isOurDevice && !isOpenGroupMessage && !this.get('synced') && - !this.get('sentSync') - ) { + !this.get('sentSync'); + + // A message is synced if we triggered a sync message (sentSync) + // and the current message was sent to our device (so a sync message) + const shouldMarkMessageAsSynced = + isOurDevice && !isOpenGroupMessage && this.get('sentSync'); + + // Handle the sync logic here + if (shouldTriggerSyncMessage) { const contentDecoded = textsecure.protobuf.Content.decode( sentMessage.plainTextBuffer ); @@ -1234,7 +1247,7 @@ if (dataMessage) { this.sendSyncMessage(dataMessage); } - } else if (isOurDevice && this.get('sentSync')) { + } else if (shouldMarkMessageAsSynced) { this.set({ synced: true }); } if (!isOpenGroupMessage) { diff --git a/ts/test/test-utils/utils/message.ts b/ts/test/test-utils/utils/message.ts index 4aef39cd0..4d904b02b 100644 --- a/ts/test/test-utils/utils/message.ts +++ b/ts/test/test-utils/utils/message.ts @@ -111,4 +111,8 @@ export class MockConversation { return this.isPrimary ? this.id : generateFakePubKey().key; } + + public get(obj: string) { + return (this.attributes as any)[obj]; + } }