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 diff --git a/js/models/messages.js b/js/models/messages.js index d9bd47690..00efaed58 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -1209,15 +1209,37 @@ 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) { - 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; + + // 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'); + + // 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 (!isOurDevice && !this.get('synced') && !this.get('sentSync')) { + if (shouldTriggerSyncMessage) { const contentDecoded = textsecure.protobuf.Content.decode( sentMessage.plainTextBuffer ); @@ -1225,14 +1247,18 @@ if (dataMessage) { this.sendSyncMessage(dataMessage); } - } else if (isOurDevice && this.get('sentSync')) { + } else if (shouldMarkMessageAsSynced) { 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/js/views/invite_contacts_dialog_view.js b/js/views/invite_contacts_dialog_view.js index 31f3fd086..0ba2486a5 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.get('active_at') ); if (!convo.isPublic()) { const members = convo.get('members') || []; 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; 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} /> 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; 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/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, diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index b07acdc97..ca21dd589 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -599,7 +599,27 @@ 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 +633,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, 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/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, }) ); 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); } /** 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 { 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]; + } }