From 3aa9ca785f230720ffc3ee90e6a0fa8fdf72e62d Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 3 May 2021 10:40:43 +1000 Subject: [PATCH] fix leave opengroup button right panel, and add memberCount --- js/background.js | 2 +- .../admin_leave_closed_group_dialog_view.js | 2 +- js/views/app_view.js | 2 +- test/models/conversations_test.js | 2 +- .../conversation/SessionConversation.tsx | 5 +- .../conversation/SessionRightPanel.tsx | 4 +- ts/models/conversation.ts | 4 +- ts/opengroup/opengroupV2/OpenGroupAPIV2.ts | 17 +-- .../opengroupV2/OpenGroupAPIV2CompactPoll.ts | 51 +++++++- .../opengroupV2/OpenGroupServerPoller.ts | 113 +++++++++++++++++- .../conversations/ConversationController.ts | 2 +- ts/session/snode_api/onions.ts | 1 + 12 files changed, 180 insertions(+), 25 deletions(-) diff --git a/js/background.js b/js/background.js index de74ed7e8..081ce55bc 100644 --- a/js/background.js +++ b/js/background.js @@ -601,7 +601,7 @@ } }); - Whisper.events.on('leaveGroup', async groupConvo => { + Whisper.events.on('leaveClosedGroup', async groupConvo => { if (appView) { appView.showLeaveGroupDialog(groupConvo); } diff --git a/js/views/admin_leave_closed_group_dialog_view.js b/js/views/admin_leave_closed_group_dialog_view.js index 00f9a14e5..b1c0e337a 100644 --- a/js/views/admin_leave_closed_group_dialog_view.js +++ b/js/views/admin_leave_closed_group_dialog_view.js @@ -37,7 +37,7 @@ this.remove(); }, submit() { - this.convo.leaveGroup(); + this.convo.leaveClosedGroup(); }, }); })(); diff --git a/js/views/app_view.js b/js/views/app_view.js index 0464dfe84..2f4934475 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -178,7 +178,7 @@ window.confirmationDialog({ title, message, - resolve: () => groupConvo.leaveGroup(), + resolve: () => groupConvo.leaveClosedGroup(), theme: this.getThemeObject(), }); } else { diff --git a/test/models/conversations_test.js b/test/models/conversations_test.js index 203c47749..b71c148a7 100644 --- a/test/models/conversations_test.js +++ b/test/models/conversations_test.js @@ -61,7 +61,7 @@ describe('ConversationCollection', () => { // type: 'group', // id: '052d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab', // }); - // await convo.leaveGroup(); + // await convo.leaveClosedGroup(); // assert.notEqual(convo.messageCollection.length, 0); // }); // it('has a title', () => { diff --git a/ts/components/session/conversation/SessionConversation.tsx b/ts/components/session/conversation/SessionConversation.tsx index 5bc87a1ec..fcd1ba1f6 100644 --- a/ts/components/session/conversation/SessionConversation.tsx +++ b/ts/components/session/conversation/SessionConversation.tsx @@ -384,7 +384,7 @@ export class SessionConversation extends React.Component { conversation.copyPublicKey(); }, onLeaveGroup: () => { - window.Whisper.events.trigger('leaveGroup', conversation); + window.Whisper.events.trigger('leaveClosedGroup', conversation); }, onInviteContacts: () => { window.Whisper.events.trigger('inviteContacts', conversation); @@ -492,8 +492,9 @@ export class SessionConversation extends React.Component { onInviteContacts: () => { window.Whisper.events.trigger('inviteContacts', conversation); }, + onDeleteContact: conversation.deleteContact, onLeaveGroup: () => { - window.Whisper.events.trigger('leaveGroup', conversation); + window.Whisper.events.trigger('leaveClosedGroup', conversation); }, onAddModerators: () => { window.Whisper.events.trigger('addModerators', conversation); diff --git a/ts/components/session/conversation/SessionRightPanel.tsx b/ts/components/session/conversation/SessionRightPanel.tsx index 27a883ef7..90be5504b 100644 --- a/ts/components/session/conversation/SessionRightPanel.tsx +++ b/ts/components/session/conversation/SessionRightPanel.tsx @@ -39,6 +39,7 @@ interface Props { onGoBack: () => void; onInviteContacts: () => void; onLeaveGroup: () => void; + onDeleteContact: () => void; onUpdateGroupName: () => void; onAddModerators: () => void; onRemoveModerators: () => void; @@ -218,6 +219,7 @@ class SessionRightPanel extends React.Component { name, timerOptions, onLeaveGroup, + onDeleteContact, isKickedFromGroup, left, isPublic, @@ -310,7 +312,7 @@ class SessionRightPanel extends React.Component { buttonColor={SessionButtonColor.Danger} disabled={isKickedFromGroup || left} buttonType={SessionButtonType.SquareOutline} - onClick={onLeaveGroup} + onClick={isPublic ? onDeleteContact : onLeaveGroup} /> )} diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 2b78ad2fa..b8c9b5be4 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -419,7 +419,7 @@ export class ConversationModel extends Backbone.Model { onCopyPublicKey: this.copyPublicKey, onDeleteContact: this.deleteContact, onLeaveGroup: () => { - window.Whisper.events.trigger('leaveGroup', this); + window.Whisper.events.trigger('leaveClosedGroup', this); }, onDeleteMessages: this.deleteMessages, onInviteContacts: () => { @@ -953,7 +953,7 @@ export class ConversationModel extends Backbone.Model { return model; } - public async leaveGroup() { + public async leaveClosedGroup() { if (this.isMediumGroup()) { await leaveClosedGroup(this.id); } else { diff --git a/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts b/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts index edcfb7ed1..a5b5db48e 100644 --- a/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts +++ b/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts @@ -419,7 +419,9 @@ export const getAllRoomInfos = async (roomInfos: OpenGroupV2Room) => { return parseRooms(result); }; -export const getMemberCount = async (roomInfos: OpenGroupRequestCommonType): Promise => { +export const getMemberCount = async ( + roomInfos: OpenGroupRequestCommonType +): Promise => { const request: OpenGroupV2Request = { method: 'GET', room: roomInfos.roomId, @@ -438,18 +440,7 @@ export const getMemberCount = async (roomInfos: OpenGroupRequestCommonType): Pro return; } - const conversationId = getOpenGroupV2ConversationId(roomInfos.serverUrl, roomInfos.roomId); - - const convo = ConversationController.getInstance().get(conversationId); - if (!convo) { - window.log.warn('cannot update conversation memberCount as it does not exist'); - return; - } - if (convo.get('subscriberCount') !== count) { - convo.set({ subscriberCount: count }); - // triggers the save to db and the refresh of the UI - await convo.commit(); - } + return count; }; /** diff --git a/ts/opengroup/opengroupV2/OpenGroupAPIV2CompactPoll.ts b/ts/opengroup/opengroupV2/OpenGroupAPIV2CompactPoll.ts index bb59a51de..8a353a6f0 100644 --- a/ts/opengroup/opengroupV2/OpenGroupAPIV2CompactPoll.ts +++ b/ts/opengroup/opengroupV2/OpenGroupAPIV2CompactPoll.ts @@ -8,7 +8,7 @@ import { parseStatusCodeFromOnionRequest } from './OpenGroupAPIV2Parser'; import _ from 'lodash'; import { sendViaOnion } from '../../session/onions/onionSend'; import { OpenGroupMessageV2 } from './OpenGroupMessageV2'; -import { downloadPreviewOpenGroupV2, getAuthToken } from './OpenGroupAPIV2'; +import { downloadPreviewOpenGroupV2, getAuthToken, getMemberCount } from './OpenGroupAPIV2'; const COMPACT_POLL_ENDPOINT = 'compact_poll'; @@ -72,6 +72,50 @@ export const getAllBase64AvatarForRooms = async ( return validPreviewBase64 ? validPreviewBase64 : null; }; +export const getAllMemberCount = async ( + serverUrl: string, + rooms: Set, + abortSignal: AbortSignal +): Promise | null> => { + // fetch all we need + const allValidRoomInfos = await getAllValidRoomInfos(serverUrl, rooms); + if (!allValidRoomInfos?.length) { + window.log.info('getAllMemberCount: no valid roominfos got.'); + return null; + } + if (abortSignal.aborted) { + window.log.info('memberCount aborted, returning null'); + return null; + } + // Currently this call will not abort if AbortSignal is aborted, + // but the call will return null. + const validMemberCount = _.compact( + await Promise.all( + allValidRoomInfos.map(async room => { + try { + const memberCount = await getMemberCount(room); + if (memberCount !== undefined) { + return { + roomId: room.roomId, + memberCount, + }; + } + } catch (e) { + window.log.warn('getPreview failed for room', room); + } + return null; + }) + ) + ); + + if (abortSignal.aborted) { + window.log.info('getMemberCount aborted, returning null'); + return null; + } + + return validMemberCount ? validMemberCount : null; +}; + /** * This function fetches the valid roomInfos from the database. * It also makes sure that the pubkey for all those rooms are the same, or returns null. @@ -258,6 +302,11 @@ export type ParsedBase64Avatar = { base64: string; }; +export type ParsedMemberCount = { + roomId: string; + memberCount: number; +}; + const parseCompactPollResult = async ( singleRoomResult: any, serverUrl: string diff --git a/ts/opengroup/opengroupV2/OpenGroupServerPoller.ts b/ts/opengroup/opengroupV2/OpenGroupServerPoller.ts index a063e7f15..3b5153212 100644 --- a/ts/opengroup/opengroupV2/OpenGroupServerPoller.ts +++ b/ts/opengroup/opengroupV2/OpenGroupServerPoller.ts @@ -5,8 +5,10 @@ import { OpenGroupRequestCommonType } from './ApiUtil'; import { compactFetchEverything, getAllBase64AvatarForRooms, + getAllMemberCount, ParsedBase64Avatar, ParsedDeletions, + ParsedMemberCount, ParsedRoomCompactPollResults, } from './OpenGroupAPIV2CompactPoll'; import _ from 'lodash'; @@ -15,13 +17,14 @@ import { getMessageIdsFromServerIds, removeMessage } from '../../data/data'; import { getV2OpenGroupRoom, saveV2OpenGroupRoom } from '../../data/opengroups'; import { OpenGroupMessageV2 } from './OpenGroupMessageV2'; import { handleOpenGroupV2Message } from '../../receiver/receiver'; -import { DAYS, SECONDS } from '../../session/utils/Number'; +import { DAYS, MINUTES, SECONDS } from '../../session/utils/Number'; import autoBind from 'auto-bind'; import { sha256 } from '../../session/crypto'; import { fromBase64ToArrayBuffer } from '../../session/utils/String'; const pollForEverythingInterval = SECONDS * 6; const pollForRoomAvatarInterval = DAYS * 1; +const pollForMemberCountInterval = MINUTES * 10; /** * An OpenGroupServerPollerV2 polls for everything for a particular server. We should @@ -31,10 +34,26 @@ const pollForRoomAvatarInterval = DAYS * 1; * for this server. */ export class OpenGroupServerPoller { + /** + * The server url to poll for this opengroup poller. + * Remember, we have one poller per opengroup poller, no matter how many rooms we have joined on this same server + */ private readonly serverUrl: string; + + /** + * The set of rooms to poll from. + * + */ private readonly roomIdsToPoll: Set = new Set(); + + /** + * This timer is used to tick for compact Polling for this opengroup server + * It ticks every `pollForEverythingInterval` except. + * If the last run is still in progress, the new one won't start and just return. + */ private pollForEverythingTimer?: NodeJS.Timeout; private pollForRoomAvatarTimer?: NodeJS.Timeout; + private pollForMemberCountTimer?: NodeJS.Timeout; private readonly abortController: AbortController; /** @@ -45,6 +64,7 @@ export class OpenGroupServerPoller { */ private isPolling = false; private isPreviewPolling = false; + private isMemberCountPolling = false; private wasStopped = false; constructor(roomInfos: Array) { @@ -71,6 +91,10 @@ export class OpenGroupServerPoller { this.previewPerRoomPoll, pollForRoomAvatarInterval ); + this.pollForMemberCountTimer = global.setInterval( + this.pollForAllMemberCount, + pollForMemberCountInterval + ); } /** @@ -90,6 +114,7 @@ export class OpenGroupServerPoller { // if we are not already polling right now, trigger a polling void this.compactPoll(); void this.previewPerRoomPoll(); + void this.pollForAllMemberCount(); } public removeRoomFromPoll(room: OpenGroupRequestCommonType) { @@ -120,6 +145,10 @@ export class OpenGroupServerPoller { if (this.pollForRoomAvatarTimer) { global.clearInterval(this.pollForRoomAvatarTimer); } + + if (this.pollForMemberCountTimer) { + global.clearInterval(this.pollForMemberCountTimer); + } if (this.pollForEverythingTimer) { // cancel next ticks for each timer global.clearInterval(this.pollForEverythingTimer); @@ -128,6 +157,7 @@ export class OpenGroupServerPoller { this.abortController?.abort(); this.pollForEverythingTimer = undefined; this.pollForRoomAvatarTimer = undefined; + this.pollForMemberCountTimer = undefined; this.wasStopped = true; } } @@ -162,6 +192,21 @@ export class OpenGroupServerPoller { return true; } + private shouldPollForMemberCount() { + if (this.wasStopped) { + window.log.error('Serverpoller was stopped. PolLForMemberCount should not happen'); + return false; + } + if (!this.roomIdsToPoll.size) { + return false; + } + // return early if a poll is already in progress + if (this.isMemberCountPolling) { + return false; + } + return true; + } + private async previewPerRoomPoll() { if (!this.shouldPollPreview()) { return; @@ -201,6 +246,46 @@ export class OpenGroupServerPoller { } } + private async pollForAllMemberCount() { + if (!this.shouldPollForMemberCount()) { + return; + } + // do everything with throwing so we can check only at one place + // what we have to clean + try { + this.isMemberCountPolling = true; + // don't try to make the request if we are aborted + if (this.abortController.signal.aborted) { + throw new Error('Poller aborted'); + } + + let memberCountGotResults = await getAllMemberCount( + this.serverUrl, + this.roomIdsToPoll, + this.abortController.signal + ); + + // check that we are still not aborted + if (this.abortController.signal.aborted) { + throw new Error('Abort controller was canceled. Dropping memberCount request'); + } + if (!memberCountGotResults) { + throw new Error('MemberCount: no results'); + } + // we were not aborted, make sure to filter out roomIds we are not polling for anymore + memberCountGotResults = memberCountGotResults.filter(result => + this.roomIdsToPoll.has(result.roomId) + ); + + // ==> At this point all those results need to trigger conversation updates, so update what we have to update + await handleAllMemberCount(this.serverUrl, memberCountGotResults); + } catch (e) { + window.log.warn('Got error while memberCount fetch:', e); + } finally { + this.isMemberCountPolling = false; + } + } + private async compactPoll() { if (!this.shouldPoll()) { return; @@ -396,3 +481,29 @@ const handleBase64AvatarUpdate = async ( }) ); }; + +async function handleAllMemberCount( + serverUrl: string, + memberCountGotResults: Array +) { + if (!memberCountGotResults.length) { + return; + } + + await Promise.all( + memberCountGotResults.map(async roomCount => { + const conversationId = getOpenGroupV2ConversationId(serverUrl, roomCount.roomId); + + const convo = ConversationController.getInstance().get(conversationId); + if (!convo) { + window.log.warn('cannot update conversation memberCount as it does not exist'); + return; + } + if (convo.get('subscriberCount') !== roomCount.memberCount) { + convo.set({ subscriberCount: roomCount.memberCount }); + // triggers the save to db and the refresh of the UI + await convo.commit(); + } + }) + ); +} diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index 8cfe27909..654b2568e 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -189,7 +189,7 @@ export class ConversationController { // Close group leaving if (conversation.isClosedGroup()) { - await conversation.leaveGroup(); + await conversation.leaveClosedGroup(); } else if (conversation.isPublic() && !conversation.isOpenGroupV2()) { const channelAPI = await conversation.getPublicSendData(); if (channelAPI === null) { diff --git a/ts/session/snode_api/onions.ts b/ts/session/snode_api/onions.ts index f35667b51..7b1abeba2 100644 --- a/ts/session/snode_api/onions.ts +++ b/ts/session/snode_api/onions.ts @@ -439,6 +439,7 @@ const sendOnionRequest = async ( const guardUrl = `https://${nodePath[0].ip}:${nodePath[0].port}${target}`; // no logs for that one as we do need to call insecureNodeFetch to our guardNode // window.log.info('insecureNodeFetch => plaintext for sendOnionRequest'); + console.warn('sendViaOnion payload: ', payload.length); const response = await insecureNodeFetch(guardUrl, guardFetchOptions); return processOnionResponse(reqIdx, response, destCtx.symmetricKey, false, abortSignal);