From 37562e11f8d7664381f90bec8554f50b09ae7f9e Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 28 Apr 2021 13:56:33 +1000 Subject: [PATCH] add back invite contacts to opengroupv2 --- js/views/invite_contacts_dialog_view.js | 53 +---------- .../conversation/InviteContactsDialog.tsx | 95 ++++++++++++++++++- .../session/LeftPaneMessageSection.tsx | 34 ++----- ts/interactions/conversation.ts | 20 +++- ts/interactions/message.ts | 73 +++++++++++++- ts/models/conversation.ts | 9 +- ts/models/message.ts | 12 +-- ts/opengroup/opengroupV2/JoinOpenGroupV2.ts | 76 ++++++++++++++- 8 files changed, 268 insertions(+), 104 deletions(-) diff --git a/js/views/invite_contacts_dialog_view.js b/js/views/invite_contacts_dialog_view.js index 1d5826197..db57a61b9 100644 --- a/js/views/invite_contacts_dialog_view.js +++ b/js/views/invite_contacts_dialog_view.js @@ -1,4 +1,4 @@ -/* global Whisper, _ */ +/* global Whisper */ // eslint-disable-next-line func-names (function() { @@ -10,7 +10,6 @@ className: 'loki-dialog modal', initialize(convo) { this.close = this.close.bind(this); - this.submit = this.submit.bind(this); this.theme = convo.theme; const convos = window.getConversationController().getConversations(); @@ -37,10 +36,10 @@ Component: window.Signal.Components.InviteContactsDialog, props: { contactList: this.contacts, - onSubmit: this.submit, onClose: this.close, chatName: this.chatName, theme: this.theme, + convo: this.convo, }, }); @@ -50,53 +49,5 @@ close() { this.remove(); }, - submit(pubkeys) { - // public group chats - if (this.isPublic) { - const serverInfos = { - address: this.chatServer, - name: this.chatName, - channelId: this.channelId, - }; - pubkeys.forEach(async pubkeyStr => { - const convo = await window - .getConversationController() - .getOrCreateAndWait(pubkeyStr, 'private'); - - if (convo) { - convo.sendMessage('', null, null, null, serverInfos); - } - }); - } else { - // private group chats - const ourPK = window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache(); - let existingMembers = this.convo.get('members') || []; - // at least make sure it's an array - if (!Array.isArray(existingMembers)) { - existingMembers = []; - } - existingMembers = existingMembers.filter(d => !!d); - const newMembers = pubkeys.filter(d => !existingMembers.includes(d)); - - if (newMembers.length > 0) { - // Do not trigger an update if there is too many members - if ( - newMembers.length + existingMembers.length > - window.CONSTANTS.CLOSED_GROUP_SIZE_LIMIT - ) { - window.libsession.Utils.ToastUtils.pushTooManyMembers(); - return; - } - - const allMembers = window.Lodash.concat(existingMembers, newMembers, [ourPK]); - const uniqMembers = _.uniq(allMembers, true, d => d); - - const groupId = this.convo.get('id'); - const groupName = this.convo.get('name'); - - window.libsession.ClosedGroup.initiateGroupUpdate(groupId, groupName, uniqMembers); - } - } - }, }); })(); diff --git a/ts/components/conversation/InviteContactsDialog.tsx b/ts/components/conversation/InviteContactsDialog.tsx index 3727f479e..d8e4ef2a2 100644 --- a/ts/components/conversation/InviteContactsDialog.tsx +++ b/ts/components/conversation/InviteContactsDialog.tsx @@ -3,14 +3,19 @@ import React from 'react'; import { SessionModal } from '../session/SessionModal'; import { SessionButton, SessionButtonColor } from '../session/SessionButton'; import { ContactType, SessionMemberListItem } from '../session/SessionMemberListItem'; -import { DefaultTheme, withTheme } from 'styled-components'; - +import { DefaultTheme } from 'styled-components'; +import { ConversationController } from '../../session/conversations'; +import { ToastUtils, UserUtils } from '../../session/utils'; +import { initiateGroupUpdate } from '../../session/group'; +import { ConversationModel, ConversationType } from '../../models/conversation'; +import { getCompleteUrlForV2ConvoId } from '../../interactions/conversation'; +import _ from 'lodash'; interface Props { contactList: Array; chatName: string; - onSubmit: any; onClose: any; theme: DefaultTheme; + convo: ConversationModel; } interface State { @@ -25,6 +30,8 @@ class InviteContactsDialogInner extends React.Component { this.closeDialog = this.closeDialog.bind(this); this.onClickOK = this.onClickOK.bind(this); this.onKeyUp = this.onKeyUp.bind(this); + this.submitForOpenGroup = this.submitForOpenGroup.bind(this); + this.submitForClosedGroup = this.submitForClosedGroup.bind(this); let contacts = this.props.contactList; @@ -89,11 +96,91 @@ class InviteContactsDialogInner extends React.Component { ); } + private async submitForOpenGroup(pubkeys: Array) { + const { convo } = this.props; + if (convo.isOpenGroupV1()) { + const v1 = convo.toOpenGroupV1(); + const groupInvitation = { + serverAddress: v1.server, + serverName: convo.getName(), + channelId: 1, // always 1 + }; + pubkeys.forEach(async pubkeyStr => { + const privateConvo = await ConversationController.getInstance().getOrCreateAndWait( + pubkeyStr, + ConversationType.PRIVATE + ); + + if (privateConvo) { + void privateConvo.sendMessage('', null, null, null, groupInvitation); + } + }); + } else if (convo.isOpenGroupV2()) { + const v2 = convo.toOpenGroupV2(); + const completeUrl = await getCompleteUrlForV2ConvoId(convo.id); + const groupInvitation = { + serverAddress: completeUrl, + serverName: convo.getName(), + }; + pubkeys.forEach(async pubkeyStr => { + const privateConvo = await ConversationController.getInstance().getOrCreateAndWait( + pubkeyStr, + ConversationType.PRIVATE + ); + + if (privateConvo) { + void privateConvo.sendMessage('', null, null, null, groupInvitation); + } + }); + } + } + + private async submitForClosedGroup(pubkeys: Array) { + const { convo } = this.props; + // FIXME audric is this dialog still used for closed groups? I think + // public group chats + + // private group chats + const ourPK = UserUtils.getOurPubKeyStrFromCache(); + let existingMembers = convo.get('members') || []; + // at least make sure it's an array + if (!Array.isArray(existingMembers)) { + existingMembers = []; + } + existingMembers = _.compact(existingMembers); + const newMembers = pubkeys.filter(d => !existingMembers.includes(d)); + + if (newMembers.length > 0) { + // Do not trigger an update if there is too many members + if (newMembers.length + existingMembers.length > window.CONSTANTS.CLOSED_GROUP_SIZE_LIMIT) { + ToastUtils.pushTooManyMembers(); + return; + } + + const allMembers = _.concat(existingMembers, newMembers, [ourPK]); + const uniqMembers = _.uniq(allMembers); + + const groupId = convo.get('id'); + const groupName = convo.get('name'); + + await initiateGroupUpdate( + groupId, + groupName || window.i18n('unknown'), + uniqMembers, + undefined + ); + } + } + private onClickOK() { const selectedContacts = this.state.contactList.filter(d => d.checkmarked).map(d => d.id); if (selectedContacts.length > 0) { - this.props.onSubmit(selectedContacts); + if (this.props.convo.isPublic()) { + void this.submitForOpenGroup(selectedContacts); + } else { + void this.submitForClosedGroup(selectedContacts); + } } this.closeDialog(); diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index bfb3218e6..c8d85c369 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -29,7 +29,7 @@ import { getOpenGroupV2ConversationId, openGroupV2CompleteURLRegex, } from '../../opengroup/utils/OpenGroupUtils'; -import { joinOpenGroupV2, parseOpenGroupV2 } from '../../opengroup/opengroupV2/JoinOpenGroupV2'; +import { joinOpenGroupV2WithUIEvents } from '../../opengroup/opengroupV2/JoinOpenGroupV2'; export interface Props { searchTerm: string; @@ -423,34 +423,12 @@ export class LeftPaneMessageSection extends React.Component { } private async handleOpenGroupJoinV2(serverUrlV2: string) { - const parsedRoom = parseOpenGroupV2(serverUrlV2); - if (!parsedRoom) { - ToastUtils.pushToastError('connectToServer', window.i18n('invalidOpenGroupUrl')); - return; - } - try { - const conversationID = getOpenGroupV2ConversationId(parsedRoom.serverUrl, parsedRoom.roomId); - ToastUtils.pushToastInfo('connectingToServer', window.i18n('connectingToServer')); - this.setState({ loading: true }); - await joinOpenGroupV2(parsedRoom, false); + const loadingCallback = (loading: boolean) => { + this.setState({ loading }); + }; + const joinSuccess = await joinOpenGroupV2WithUIEvents(serverUrlV2, true, loadingCallback); - const isConvoCreated = ConversationController.getInstance().get(conversationID); - if (isConvoCreated) { - ToastUtils.pushToastSuccess( - 'connectToServerSuccess', - window.i18n('connectToServerSuccess') - ); - return true; - } else { - ToastUtils.pushToastError('connectToServerFail', window.i18n('connectToServerFail')); - } - } catch (error) { - window.log.warn('got error while joining open group:', error); - ToastUtils.pushToastError('connectToServerFail', window.i18n('connectToServerFail')); - } finally { - this.setState({ loading: false }); - } - return false; + return joinSuccess; } private async handleJoinChannelButtonClick(serverUrl: string) { diff --git a/ts/interactions/conversation.ts b/ts/interactions/conversation.ts index 03ba45c82..0f11a9933 100644 --- a/ts/interactions/conversation.ts +++ b/ts/interactions/conversation.ts @@ -11,15 +11,27 @@ import { ApiV2 } from '../opengroup/opengroupV2'; import _ from 'lodash'; +export const getCompleteUrlForV2ConvoId = async (convoId: string) => { + if (convoId.match(openGroupV2ConversationIdRegex)) { + // this is a v2 group, just build the url + const roomInfos = await getV2OpenGroupRoom(convoId); + if (roomInfos) { + const fullUrl = getCompleteUrlFromRoom(roomInfos); + + return fullUrl; + } + } + return undefined; +}; + export async function copyPublicKey(convoId: string) { if (convoId.match(openGroupPrefixRegex)) { // open group v1 or v2 if (convoId.match(openGroupV2ConversationIdRegex)) { // this is a v2 group, just build the url - const roomInfos = await getV2OpenGroupRoom(convoId); - if (roomInfos) { - const fullUrl = getCompleteUrlFromRoom(roomInfos); - window.clipboard.writeText(fullUrl); + const completeUrl = await getCompleteUrlForV2ConvoId(convoId); + if (completeUrl) { + window.clipboard.writeText(completeUrl); ToastUtils.pushCopiedToClipBoard(); return; diff --git a/ts/interactions/message.ts b/ts/interactions/message.ts index 1b06c0641..8bc18d432 100644 --- a/ts/interactions/message.ts +++ b/ts/interactions/message.ts @@ -1,11 +1,17 @@ import _ from 'lodash'; import { getV2OpenGroupRoom } from '../data/opengroups'; -import { ConversationModel } from '../models/conversation'; +import { ConversationModel, ConversationType } from '../models/conversation'; +import { OpenGroup } from '../opengroup/opengroupV1/OpenGroup'; import { ApiV2 } from '../opengroup/opengroupV2'; -import { isOpenGroupV2 } from '../opengroup/utils/OpenGroupUtils'; +import { + joinOpenGroupV2WithUIEvents, + parseOpenGroupV2, +} from '../opengroup/opengroupV2/JoinOpenGroupV2'; +import { isOpenGroupV2, openGroupV2CompleteURLRegex } from '../opengroup/utils/OpenGroupUtils'; import { ConversationController } from '../session/conversations'; import { PubKey } from '../session/types'; import { ToastUtils } from '../session/utils'; +import { openConversationExternal } from '../state/ducks/conversations'; export function banUser(userToBan: string, conversation?: ConversationModel) { let pubKeyToBan: PubKey; @@ -190,3 +196,66 @@ export async function addSenderAsModerator(sender: string, convoId: string) { window.log.error('Got error while adding moderator:', e); } } + +async function acceptOpenGroupInvitationV1(serverAddress: string) { + try { + if (serverAddress.length === 0 || !OpenGroup.validate(serverAddress)) { + ToastUtils.pushToastError('connectToServer', window.i18n('invalidOpenGroupUrl')); + return; + } + + // Already connected? + if (OpenGroup.getConversation(serverAddress)) { + ToastUtils.pushToastError('publicChatExists', window.i18n('publicChatExists')); + return; + } + // To some degree this has been copy-pasted from LeftPaneMessageSection + const rawServerUrl = serverAddress.replace(/^https?:\/\//i, '').replace(/[/\\]+$/i, ''); + const sslServerUrl = `https://${rawServerUrl}`; + const conversationId = `publicChat:1@${rawServerUrl}`; + + const conversationExists = ConversationController.getInstance().get(conversationId); + if (conversationExists) { + window.log.warn('We are already a member of this public chat'); + ToastUtils.pushAlreadyMemberOpenGroup(); + + return; + } + + const conversation = await ConversationController.getInstance().getOrCreateAndWait( + conversationId, + ConversationType.GROUP + ); + await conversation.setPublicSource(sslServerUrl, 1); + + const channelAPI = await window.lokiPublicChatAPI.findOrCreateChannel( + sslServerUrl, + 1, + conversationId + ); + if (!channelAPI) { + window.log.warn(`Could not connect to ${serverAddress}`); + return; + } + openConversationExternal(conversationId); + } catch (e) { + window.log.warn('failed to join opengroupv1 from invitation', e); + ToastUtils.pushToastError('connectToServerFail', window.i18n('connectToServerFail')); + } +} + +const acceptOpenGroupInvitationV2 = async (completeUrl: string) => { + // this function does not throw, and will showToasts if anything happens + await joinOpenGroupV2WithUIEvents(completeUrl, true); +}; + +/** + * Accepts a v1 (channelid defaults to 1) url or a v2 url (with pubkey) + */ +export const acceptOpenGroupInvitation = async (completeUrl: string) => { + if (completeUrl.match(openGroupV2CompleteURLRegex)) { + await acceptOpenGroupInvitationV2(completeUrl); + } else { + await acceptOpenGroupInvitationV1(completeUrl); + } +}; diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 2fdf70701..7eb4eb49d 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -664,9 +664,9 @@ export class ConversationModel extends Backbone.Model { const groupInvitMessage = new GroupInvitationMessage({ identifier: id, timestamp: sentAt, - serverName: groupInvitation.name, - channelId: groupInvitation.channelId, - serverAddress: groupInvitation.address, + serverName: groupInvitation.serverName, + channelId: 1, + serverAddress: groupInvitation.serverAddress, expireTimer: this.get('expireTimer'), }); // we need the return await so that errors are caught in the catch {} @@ -706,7 +706,7 @@ export class ConversationModel extends Backbone.Model { attachments: any, quote: any, preview: any, - groupInvitation = null + groupInvitation: any = null ) { this.clearTypingTimers(); @@ -770,6 +770,7 @@ export class ConversationModel extends Backbone.Model { return null; } this.queueJob(async () => { + console.warn('sending groupinvi', messageModel); await this.sendMessageJob(messageModel, expireTimer); }); return null; diff --git a/ts/models/message.ts b/ts/models/message.ts index ff7f254b9..fb194fbeb 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -30,6 +30,7 @@ import { uploadLinkPreviewsV2, uploadQuoteThumbnailsV2, } from '../session/utils/AttachmentsV2'; +import { acceptOpenGroupInvitation } from '../interactions/message'; export class MessageModel extends Backbone.Model { public propsForTimerNotification: any; public propsForGroupNotification: any; @@ -280,17 +281,16 @@ export class MessageModel extends Backbone.Model { if (!direction) { direction = this.get('type') === 'outgoing' ? 'outgoing' : 'incoming'; } + const serverAddress = invitation.serverAddress?.length + ? `${invitation.serverAddress.slice(0, 30)}...` + : ''; return { serverName: invitation.serverName, - serverAddress: invitation.serverAddress, + serverAddress, direction, onClick: () => { - window.Whisper.events.trigger( - 'publicChatInvitationAccepted', - invitation.serverAddress, - invitation.channelId - ); + void acceptOpenGroupInvitation(invitation.serverAddress); }, }; } diff --git a/ts/opengroup/opengroupV2/JoinOpenGroupV2.ts b/ts/opengroup/opengroupV2/JoinOpenGroupV2.ts index 9973f6a0c..376b137e0 100644 --- a/ts/opengroup/opengroupV2/JoinOpenGroupV2.ts +++ b/ts/opengroup/opengroupV2/JoinOpenGroupV2.ts @@ -4,7 +4,7 @@ import { removeV2OpenGroupRoom, } from '../../data/opengroups'; import { ConversationController } from '../../session/conversations'; -import { PromiseUtils } from '../../session/utils'; +import { PromiseUtils, ToastUtils } from '../../session/utils'; import { forceSyncConfigurationNowIfNeeded } from '../../session/utils/syncUtils'; import { getOpenGroupV2ConversationId, @@ -58,10 +58,7 @@ export function parseOpenGroupV2(urlWithPubkey: string): OpenGroupV2Room | undef * @param room The room id to join * @param publicKey The server publicKey. It comes from the joining link. (or is already here for the default open group server) */ -export async function joinOpenGroupV2( - room: OpenGroupV2Room, - fromSyncMessage: boolean -): Promise { +async function joinOpenGroupV2(room: OpenGroupV2Room, fromSyncMessage: boolean): Promise { if (!room.serverUrl || !room.roomId || room.roomId.length < 2 || !room.serverPublicKey) { return; } @@ -110,3 +107,72 @@ export async function joinOpenGroupV2( throw new Error(e); } } + +/** + * This function does not throw + * This function can be used to join an opengroupv2 server, from a user initiated click or from a syncMessage. + * If the user made the request, the UI callback needs to be set. + * the callback will be called on loading events (start and stop joining). Also, this callback being set defines if we will trigger a sync message or not. + * + * Basically, + * - user invitation click => uicallback set + * - user join manually from the join open group field => uicallback set + * - joining from a sync message => no uicallback + * + * + * return true if the room did not exist before, and we join it correctly + */ +export async function joinOpenGroupV2WithUIEvents( + completeUrl: string, + showToasts: boolean, + uiCallback?: (loading: boolean) => void +): Promise { + const parsedRoom = parseOpenGroupV2(completeUrl); + if (!parsedRoom) { + if (showToasts) { + ToastUtils.pushToastError('connectToServer', window.i18n('invalidOpenGroupUrl')); + } + return false; + } + try { + const conversationID = getOpenGroupV2ConversationId(parsedRoom.serverUrl, parsedRoom.roomId); + if (ConversationController.getInstance().get(conversationID)) { + if (showToasts) { + ToastUtils.pushToastError('publicChatExists', window.i18n('publicChatExists')); + } + return false; + } + if (showToasts) { + ToastUtils.pushToastInfo('connectingToServer', window.i18n('connectingToServer')); + } + if (uiCallback) { + uiCallback(true); + } + await joinOpenGroupV2(parsedRoom, showToasts); + + const isConvoCreated = ConversationController.getInstance().get(conversationID); + if (isConvoCreated) { + if (showToasts) { + ToastUtils.pushToastSuccess( + 'connectToServerSuccess', + window.i18n('connectToServerSuccess') + ); + } + return true; + } else { + if (showToasts) { + ToastUtils.pushToastError('connectToServerFail', window.i18n('connectToServerFail')); + } + } + } catch (error) { + window.log.warn('got error while joining open group:', error); + if (showToasts) { + ToastUtils.pushToastError('connectToServerFail', window.i18n('connectToServerFail')); + } + } finally { + if (uiCallback) { + uiCallback(false); + } + } + return false; +}