From d2a47570617a408cc15543d34e7d296fe7081d6f Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 23 Jul 2020 10:53:24 +1000 Subject: [PATCH 01/11] trigger and handle blocked contacts/groups as a list on block/unblock --- js/models/conversations.js | 12 +-- libtextsecure/sendmessage.js | 30 ++++++++ ts/receiver/syncMessages.ts | 76 ++++++++++++++++--- .../content/sync/BlockedListSyncMessage.ts | 42 ++++++++++ .../messages/outgoing/content/sync/index.ts | 1 + ts/session/utils/SyncMessage.ts | 22 ++++++ 6 files changed, 164 insertions(+), 19 deletions(-) create mode 100644 ts/session/messages/outgoing/content/sync/BlockedListSyncMessage.ts diff --git a/js/models/conversations.js b/js/models/conversations.js index ba9a8bbf7..7cb29d34c 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -253,11 +253,7 @@ this.trigger('change', this); this.messageCollection.forEach(m => m.trigger('change')); this.updateTextInputState(); - if (this.isPrivate()) { - await textsecure.messaging.sendContactSyncMessage([this]); - } else { - await textsecure.messaging.sendGroupSyncMessage([this]); - } + await textsecure.messaging.sendBlockedListSyncMessage(); }, async unblock() { if (!this.id || this.isPublic() || this.isRss()) { @@ -270,11 +266,7 @@ this.trigger('change', this); this.messageCollection.forEach(m => m.trigger('change')); this.updateTextInputState(); - if (this.isPrivate()) { - await textsecure.messaging.sendContactSyncMessage([this]); - } else { - await textsecure.messaging.sendGroupSyncMessage([this]); - } + await textsecure.messaging.sendBlockedListSyncMessage(); }, setMessageSelectionBackdrop() { const messageSelected = this.selectedMessages.size > 0; diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index f31114f99..aac069dec 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -453,6 +453,33 @@ MessageSender.prototype = { return libsession.getMessageQueue().sendSyncMessage(openGroupsSyncMessage); }, + async sendBlockedListSyncMessage() { + // If we havn't got a primaryDeviceKey then we are in the middle of pairing + // primaryDevicePubKey is set to our own number if we are the master device + const primaryDeviceKey = window.storage.get('primaryDevicePubKey'); + if (!primaryDeviceKey) { + return Promise.resolve(); + } + const convos = window.getConversations().models; + + const conversations = Array.isArray(convos) ? convos : [convos]; + + const { + blockedNumbersConvos, + blockedGroupsConvos, + } = await libsession.Utils.SyncMessageUtils.filterBlockedNumbers( + conversations + ); + + const blockedSyncMessage = new libsession.Messages.Outgoing.BlockedListSyncMessage( + { + timestamp: Date.now(), + numbers: blockedNumbersConvos.map(n => n.id), + groups: blockedGroupsConvos.map(g => g.id), + } + ); + return libsession.getMessageQueue().sendSyncMessage(blockedSyncMessage); + }, syncReadMessages(reads) { const myDevice = textsecure.storage.user.getDeviceId(); // FIXME currently not in used @@ -521,6 +548,9 @@ textsecure.MessageSender = function MessageSenderWrapper(username, password) { this.syncVerification = sender.syncVerification.bind(sender); this.makeProxiedRequest = sender.makeProxiedRequest.bind(sender); this.getProxiedSize = sender.getProxiedSize.bind(sender); + this.sendBlockedListSyncMessage = sender.sendBlockedListSyncMessage.bind( + sender + ); }; textsecure.MessageSender.prototype = { diff --git a/ts/receiver/syncMessages.ts b/ts/receiver/syncMessages.ts index 94b56e432..85164ffbd 100644 --- a/ts/receiver/syncMessages.ts +++ b/ts/receiver/syncMessages.ts @@ -17,6 +17,8 @@ import { handleContacts } from './multidevice'; import { onGroupReceived } from './groups'; import { MultiDeviceProtocol } from '../session/protocols'; import { DataMessage } from '../session/messages/outgoing'; +import { BlockedNumberController } from '../util'; +import { StringUtils } from '../session/utils'; export async function handleSyncMessage( envelope: EnvelopePlus, @@ -36,6 +38,14 @@ export async function handleSyncMessage( ); } + // remove empty fields (generated by ts even if they should be null) + if (syncMessage.openGroups && !syncMessage.openGroups.length) { + syncMessage.openGroups = null; + } + if (syncMessage.read && !syncMessage.read.length) { + syncMessage.read = null; + } + if (syncMessage.sent) { const sentMessage = syncMessage.sent; const message = sentMessage.message as SignalService.IDataMessage; @@ -166,16 +176,64 @@ async function handleBlocked( blocked: SignalService.SyncMessage.IBlocked ) { window.log.info('Setting these numbers as blocked:', blocked.numbers); - window.textsecure.storage.put('blocked', blocked.numbers); - const groupIds = _.map(blocked.groupIds, (groupId: any) => - groupId.toBinary() - ); - window.log.info( - 'Setting these groups as blocked:', - groupIds.map((groupId: any) => `group(${groupId})`) - ); - window.textsecure.storage.put('blocked-groups', groupIds); + async function fetchAndRefreshConversation(n: string) { + const conv = await window.ConversationController.get(n); + if (conv) { + conv.trigger('change', conv); + } + } + + if (blocked.numbers) { + const currentlyBlockedNumbers = BlockedNumberController.getBlockedNumbers(); + const toRemoveFromBlocked = _.difference( + currentlyBlockedNumbers, + blocked.numbers + ); + const toAddToBlocked = _.difference( + blocked.numbers, + currentlyBlockedNumbers + ); + + await Promise.all( + toAddToBlocked.map(async n => { + await BlockedNumberController.block(n); + await fetchAndRefreshConversation(n); + }) + ); + await Promise.all( + toRemoveFromBlocked.map(async n => { + await BlockedNumberController.unblock(n); + await fetchAndRefreshConversation(n); + }) + ); + } + + if (blocked.groupIds) { + const groupIds = _.map(blocked.groupIds, (groupId: any) => + StringUtils.decode(groupId, 'utf8') + ); + window.log.info( + 'Setting these groups as blocked:', + groupIds.map((groupId: any) => `group(${groupId})`) + ); + const currentlyBlockedGroups = BlockedNumberController.getBlockedGroups(); + const toRemoveFromBlocked = _.difference(currentlyBlockedGroups, groupIds); + const toAddToBlocked = _.difference(groupIds, currentlyBlockedGroups); + + await Promise.all( + toAddToBlocked.map(async n => { + await BlockedNumberController.blockGroup(n); + await fetchAndRefreshConversation(n); + }) + ); + await Promise.all( + toRemoveFromBlocked.map(async n => { + await BlockedNumberController.unblockGroup(n); + await fetchAndRefreshConversation(n); + }) + ); + } await removeFromCache(envelope); } diff --git a/ts/session/messages/outgoing/content/sync/BlockedListSyncMessage.ts b/ts/session/messages/outgoing/content/sync/BlockedListSyncMessage.ts new file mode 100644 index 000000000..d116c39aa --- /dev/null +++ b/ts/session/messages/outgoing/content/sync/BlockedListSyncMessage.ts @@ -0,0 +1,42 @@ +import { SyncMessage } from './SyncMessage'; +import { SignalService } from '../../../../../protobuf'; +import { MessageParams } from '../../Message'; +import { StringUtils } from '../../../../utils'; + +interface BlockedListSyncMessageParams extends MessageParams { + groups: Array; + numbers: Array; +} + +export abstract class BlockedListSyncMessage extends SyncMessage { + public readonly groups: Array; + public readonly numbers: Array; + + constructor(params: BlockedListSyncMessageParams) { + super({ timestamp: params.timestamp, identifier: params.identifier }); + this.groups = params.groups.map(g => { + if (typeof g !== 'string') { + throw new TypeError( + `invalid group id (expected string) found:${typeof g}` + ); + } + return new Uint8Array(StringUtils.encode(g, 'utf8')); + }); + if (params.numbers.length && typeof params.numbers[0] !== 'string') { + throw new TypeError( + `invalid number (expected string) found:${typeof params.numbers[0]}` + ); + } + this.numbers = params.numbers; + } + + protected syncProto(): SignalService.SyncMessage { + const syncMessage = super.syncProto(); + syncMessage.blocked = new SignalService.SyncMessage.Blocked({ + groupIds: this.groups, + numbers: this.numbers, + }); + + return syncMessage; + } +} diff --git a/ts/session/messages/outgoing/content/sync/index.ts b/ts/session/messages/outgoing/content/sync/index.ts index a2aea70b9..bfb24b2a9 100644 --- a/ts/session/messages/outgoing/content/sync/index.ts +++ b/ts/session/messages/outgoing/content/sync/index.ts @@ -6,3 +6,4 @@ export * from './SyncMessage'; export * from './SentSyncMessage'; export * from './SyncReadMessage'; export * from './VerifiedSyncMessage'; +export * from './BlockedListSyncMessage'; diff --git a/ts/session/utils/SyncMessage.ts b/ts/session/utils/SyncMessage.ts index 85df60a86..8df5b5f41 100644 --- a/ts/session/utils/SyncMessage.ts +++ b/ts/session/utils/SyncMessage.ts @@ -95,6 +95,28 @@ export async function filterOpenGroupsConvos( ); } +export async function filterBlockedNumbers( + conversations: Array +): Promise { + // If we haven't got a primaryDeviceKey then we are in the middle of pairing + // primaryDevicePubKey is set to our own number if we are the master device + const thisDevice = await UserUtil.getCurrentDevicePubKey(); + + if (!thisDevice) { + return { blockedNumbers: [], blockedGroupsIds: [] }; + } + + const blockedNumbersConvos = conversations.filter( + c => c.isPrivate() && c.isBlocked() + ); + + const blockedGroupsConvos = conversations.filter( + c => c.isClosedGroup() && c.isBlocked() + ); + + return { blockedNumbersConvos, blockedGroupsConvos }; +} + // Serialise as ... // This is an implementation of the reciprocal of contacts_parser.js export function serialiseByteBuffers(buffers: Array): ByteBuffer { From 3e23039adb3c412315e9b10a4176b0579caf0304 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 23 Jul 2020 16:15:34 +1000 Subject: [PATCH 02/11] do not sync blocked closed group as mobile is not ready yet --- libtextsecure/sendmessage.js | 11 ++-- ts/receiver/syncMessages.ts | 56 ++++++------------- .../content/sync/BlockedListSyncMessage.ts | 3 +- ts/session/utils/SyncMessage.ts | 12 +--- 4 files changed, 26 insertions(+), 56 deletions(-) diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index aac069dec..fba096558 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -464,18 +464,15 @@ MessageSender.prototype = { const conversations = Array.isArray(convos) ? convos : [convos]; - const { - blockedNumbersConvos, - blockedGroupsConvos, - } = await libsession.Utils.SyncMessageUtils.filterBlockedNumbers( + const blockedConvos = await libsession.Utils.SyncMessageUtils.filterBlockedNumbers( conversations ); - + // currently we only sync user blocked, not groups const blockedSyncMessage = new libsession.Messages.Outgoing.BlockedListSyncMessage( { timestamp: Date.now(), - numbers: blockedNumbersConvos.map(n => n.id), - groups: blockedGroupsConvos.map(g => g.id), + numbers: blockedConvos.map(n => n.id), + groups: [], } ); return libsession.getMessageQueue().sendSyncMessage(blockedSyncMessage); diff --git a/ts/receiver/syncMessages.ts b/ts/receiver/syncMessages.ts index 85164ffbd..f6bd2a686 100644 --- a/ts/receiver/syncMessages.ts +++ b/ts/receiver/syncMessages.ts @@ -177,13 +177,8 @@ async function handleBlocked( ) { window.log.info('Setting these numbers as blocked:', blocked.numbers); - async function fetchAndRefreshConversation(n: string) { - const conv = await window.ConversationController.get(n); - if (conv) { - conv.trigger('change', conv); - } - } + // blocked.numbers contains numbers if (blocked.numbers) { const currentlyBlockedNumbers = BlockedNumberController.getBlockedNumbers(); const toRemoveFromBlocked = _.difference( @@ -195,43 +190,26 @@ async function handleBlocked( currentlyBlockedNumbers ); - await Promise.all( - toAddToBlocked.map(async n => { - await BlockedNumberController.block(n); - await fetchAndRefreshConversation(n); - }) - ); - await Promise.all( - toRemoveFromBlocked.map(async n => { - await BlockedNumberController.unblock(n); - await fetchAndRefreshConversation(n); - }) - ); - } - - if (blocked.groupIds) { - const groupIds = _.map(blocked.groupIds, (groupId: any) => - StringUtils.decode(groupId, 'utf8') - ); - window.log.info( - 'Setting these groups as blocked:', - groupIds.map((groupId: any) => `group(${groupId})`) - ); - const currentlyBlockedGroups = BlockedNumberController.getBlockedGroups(); - const toRemoveFromBlocked = _.difference(currentlyBlockedGroups, groupIds); - const toAddToBlocked = _.difference(groupIds, currentlyBlockedGroups); + async function markConvoBlocked(block: boolean, n: string) { + const conv = await window.ConversationController.get(n); + if (conv) { + if (conv.isPrivate()) { + await BlockedNumberController.setBlocked(n, block); + } else { + window.console.warn('Ignoring block/unblock for group:', n); + } + conv.trigger('change', conv); + } else { + window.console.warn('Did not find corresponding conversation to block', n); + } + } await Promise.all( - toAddToBlocked.map(async n => { - await BlockedNumberController.blockGroup(n); - await fetchAndRefreshConversation(n); - }) + toAddToBlocked.map(async n => markConvoBlocked(true, n)) ); + await Promise.all( - toRemoveFromBlocked.map(async n => { - await BlockedNumberController.unblockGroup(n); - await fetchAndRefreshConversation(n); - }) + toRemoveFromBlocked.map(async n => markConvoBlocked(false, n)) ); } diff --git a/ts/session/messages/outgoing/content/sync/BlockedListSyncMessage.ts b/ts/session/messages/outgoing/content/sync/BlockedListSyncMessage.ts index d116c39aa..9c988a9ba 100644 --- a/ts/session/messages/outgoing/content/sync/BlockedListSyncMessage.ts +++ b/ts/session/messages/outgoing/content/sync/BlockedListSyncMessage.ts @@ -32,9 +32,10 @@ export abstract class BlockedListSyncMessage extends SyncMessage { protected syncProto(): SignalService.SyncMessage { const syncMessage = super.syncProto(); + // currently we do not handle the closed group blocked syncMessage.blocked = new SignalService.SyncMessage.Blocked({ - groupIds: this.groups, numbers: this.numbers, + groupIds: this.groups, }); return syncMessage; diff --git a/ts/session/utils/SyncMessage.ts b/ts/session/utils/SyncMessage.ts index 8df5b5f41..e78073133 100644 --- a/ts/session/utils/SyncMessage.ts +++ b/ts/session/utils/SyncMessage.ts @@ -103,18 +103,12 @@ export async function filterBlockedNumbers( const thisDevice = await UserUtil.getCurrentDevicePubKey(); if (!thisDevice) { - return { blockedNumbers: [], blockedGroupsIds: [] }; + return []; } - const blockedNumbersConvos = conversations.filter( - c => c.isPrivate() && c.isBlocked() - ); - - const blockedGroupsConvos = conversations.filter( - c => c.isClosedGroup() && c.isBlocked() + return conversations.filter( + c => c.isBlocked() && c.isPrivate() ); - - return { blockedNumbersConvos, blockedGroupsConvos }; } // Serialise as ... From ffbba2480f2bb47d2a14b3eb30be03c96d887cb5 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 23 Jul 2020 16:16:52 +1000 Subject: [PATCH 03/11] hide blocking option from UI for blocking groups --- ts/components/ConversationListItem.tsx | 4 +++- ts/components/conversation/ConversationHeader.tsx | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 72254409c..686fb4a36 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -163,6 +163,7 @@ export class ConversationListItem extends React.PureComponent { isRss, isPublic, hasNickname, + type, onDeleteContact, onDeleteMessages, onBlockContact, @@ -174,10 +175,11 @@ export class ConversationListItem extends React.PureComponent { const blockTitle = isBlocked ? i18n('unblockUser') : i18n('blockUser'); const blockHandler = isBlocked ? onUnblockContact : onBlockContact; + const isPrivate = type === 'direct'; return ( - {!isPublic && !isRss && !isMe ? ( + {!isPublic && !isRss && !isMe && isPrivate ? ( {blockTitle} ) : null} {/* {!isPublic && !isRss && !isMe ? ( diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 8b1cb57ed..8a66cf64a 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -480,7 +480,7 @@ export class ConversationHeader extends React.Component { const resetSessionMenuItem = !isGroup && ( {i18n('resetSession')} ); - const blockHandlerMenuItem = !isMe && !isRss && ( + const blockHandlerMenuItem = !isMe && !isRss && !isGroup && ( {blockTitle} ); From 5d7ecc690d3ebb95965bae42e8bb9572e1d73c6e Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 23 Jul 2020 16:28:04 +1000 Subject: [PATCH 04/11] remove unused USERNAME and PASSWORD from MessageReceiver & MessageSender --- js/background.js | 17 ++--------------- libtextsecure/message_receiver.js | 7 ------- libtextsecure/sendmessage.js | 4 ++-- libtextsecure/test/message_receiver_test.js | 2 -- test/models/conversations_test.js | 2 +- 5 files changed, 5 insertions(+), 27 deletions(-) diff --git a/js/background.js b/js/background.js index f193f266d..881ab9c34 100644 --- a/js/background.js +++ b/js/background.js @@ -1486,9 +1486,6 @@ if (messageReceiver) { await messageReceiver.close(); } - - const USERNAME = storage.get('number_id'); - const PASSWORD = storage.get('password'); const mySignalingKey = storage.get('signaling_key'); connectCount += 1; @@ -1518,8 +1515,6 @@ window.lokiPublicChatAPI = null; window.feeds = []; messageReceiver = new textsecure.MessageReceiver( - USERNAME, - PASSWORD, mySignalingKey, options ); @@ -1527,18 +1522,13 @@ 'message', window.NewReceiver.handleMessageEvent ); - window.textsecure.messaging = new textsecure.MessageSender( - USERNAME, - PASSWORD - ); + window.textsecure.messaging = new textsecure.MessageSender(); return; } initAPIs(); await initSpecialConversations(); messageReceiver = new textsecure.MessageReceiver( - USERNAME, - PASSWORD, mySignalingKey, options ); @@ -1560,10 +1550,7 @@ logger: window.log, }); - window.textsecure.messaging = new textsecure.MessageSender( - USERNAME, - PASSWORD - ); + window.textsecure.messaging = new textsecure.MessageSender(); // On startup after upgrading to a new version, request a contact sync // (but only if we're not the primary device) diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 4c0f198b6..66dfc6833 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -1,7 +1,6 @@ /* global window: false */ /* global callWorker: false */ /* global textsecure: false */ -/* global libsignal: false */ /* global WebSocket: false */ /* global Event: false */ /* global dcodeIO: false */ @@ -18,14 +17,8 @@ function MessageReceiver(username, password, signalingKey) { this.count = 0; this.signalingKey = signalingKey; - this.username = username; - this.password = password; this.server = WebAPI.connect(); - const address = libsignal.SignalProtocolAddress.fromString(username); - this.number = address.getName(); - this.deviceId = address.getDeviceId(); - this.pending = Promise.resolve(); // only do this once to prevent duplicates diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index fba096558..c7da3911f 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -533,8 +533,8 @@ MessageSender.prototype = { window.textsecure = window.textsecure || {}; -textsecure.MessageSender = function MessageSenderWrapper(username, password) { - const sender = new MessageSender(username, password); +textsecure.MessageSender = function MessageSenderWrapper() { + const sender = new MessageSender(); this.sendContactSyncMessage = sender.sendContactSyncMessage.bind(sender); this.sendGroupSyncMessage = sender.sendGroupSyncMessage.bind(sender); this.sendOpenGroupsSyncMessage = sender.sendOpenGroupsSyncMessage.bind( diff --git a/libtextsecure/test/message_receiver_test.js b/libtextsecure/test/message_receiver_test.js index 0715b173d..cff0f3b0c 100644 --- a/libtextsecure/test/message_receiver_test.js +++ b/libtextsecure/test/message_receiver_test.js @@ -95,8 +95,6 @@ describe('MessageReceiver', () => { }); window.messageReceiver = new textsecure.MessageReceiver( - 'username', - 'password', 'signalingKey' // 'ws://localhost:8080', // window, diff --git a/test/models/conversations_test.js b/test/models/conversations_test.js index b4aadef27..e8a7b1548 100644 --- a/test/models/conversations_test.js +++ b/test/models/conversations_test.js @@ -3,7 +3,7 @@ 'use strict'; describe('ConversationCollection', () => { - textsecure.messaging = new textsecure.MessageSender(''); + textsecure.messaging = new textsecure.MessageSender(); before(clearDatabase); after(clearDatabase); From cd516aab64cfc853e1f1db08a7984029fd46c500 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 23 Jul 2020 16:29:19 +1000 Subject: [PATCH 05/11] lint --- js/background.js | 10 ++-------- ts/receiver/syncMessages.ts | 10 +++++----- ts/session/utils/SyncMessage.ts | 4 +--- 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/js/background.js b/js/background.js index 881ab9c34..5791381e7 100644 --- a/js/background.js +++ b/js/background.js @@ -1514,10 +1514,7 @@ ); window.lokiPublicChatAPI = null; window.feeds = []; - messageReceiver = new textsecure.MessageReceiver( - mySignalingKey, - options - ); + messageReceiver = new textsecure.MessageReceiver(mySignalingKey, options); messageReceiver.addEventListener( 'message', window.NewReceiver.handleMessageEvent @@ -1528,10 +1525,7 @@ initAPIs(); await initSpecialConversations(); - messageReceiver = new textsecure.MessageReceiver( - mySignalingKey, - options - ); + messageReceiver = new textsecure.MessageReceiver(mySignalingKey, options); messageReceiver.addEventListener( 'message', window.NewReceiver.handleMessageEvent diff --git a/ts/receiver/syncMessages.ts b/ts/receiver/syncMessages.ts index f6bd2a686..c3955cca6 100644 --- a/ts/receiver/syncMessages.ts +++ b/ts/receiver/syncMessages.ts @@ -177,7 +177,6 @@ async function handleBlocked( ) { window.log.info('Setting these numbers as blocked:', blocked.numbers); - // blocked.numbers contains numbers if (blocked.numbers) { const currentlyBlockedNumbers = BlockedNumberController.getBlockedNumbers(); @@ -200,13 +199,14 @@ async function handleBlocked( } conv.trigger('change', conv); } else { - window.console.warn('Did not find corresponding conversation to block', n); + window.console.warn( + 'Did not find corresponding conversation to block', + n + ); } } - await Promise.all( - toAddToBlocked.map(async n => markConvoBlocked(true, n)) - ); + await Promise.all(toAddToBlocked.map(async n => markConvoBlocked(true, n))); await Promise.all( toRemoveFromBlocked.map(async n => markConvoBlocked(false, n)) diff --git a/ts/session/utils/SyncMessage.ts b/ts/session/utils/SyncMessage.ts index e78073133..3967095c9 100644 --- a/ts/session/utils/SyncMessage.ts +++ b/ts/session/utils/SyncMessage.ts @@ -106,9 +106,7 @@ export async function filterBlockedNumbers( return []; } - return conversations.filter( - c => c.isBlocked() && c.isPrivate() - ); + return conversations.filter(c => c.isBlocked() && c.isPrivate()); } // Serialise as ... From 8105f0647aa2040c47082bc023925e317fd3ccc7 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 23 Jul 2020 16:49:53 +1000 Subject: [PATCH 06/11] make a few unit tests quicker by polling more aggressively on results --- ts/session/utils/Promise.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ts/session/utils/Promise.ts b/ts/session/utils/Promise.ts index 2ce92a534..9019ff282 100644 --- a/ts/session/utils/Promise.ts +++ b/ts/session/utils/Promise.ts @@ -52,7 +52,7 @@ export async function poll( ): Promise { const defaults: PollOptions = { timeout: 2000, - interval: 1000, + interval: 100, }; const { timeout, interval } = { @@ -113,6 +113,7 @@ export async function waitUntil( }, { timeout, + interval: timeout / 20, } ); } From ce6263dc21b311d64202a38623fbfe300387d68e Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 24 Jul 2020 11:24:37 +1000 Subject: [PATCH 07/11] share ContextMenu item tests for ConversationListeItem and ConversationHeader --- js/views/conversation_view.js | 1 + ts/components/ConversationListItem.tsx | 20 +++- .../conversation/ConversationHeader.tsx | 52 +++++--- ts/session/utils/Menu.ts | 111 ++++++++++++++++++ ts/session/utils/index.ts | 2 + 5 files changed, 168 insertions(+), 18 deletions(-) create mode 100644 ts/session/utils/Menu.ts diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index e49d28380..2a7fa5e2b 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -172,6 +172,7 @@ isClosable: this.model.isClosable(), isBlocked: this.model.isBlocked(), isGroup: !this.model.isPrivate(), + isPrivate: this.model.isPrivate(), isOnline: this.model.isOnline(), isArchived: this.model.get('isArchived'), isPublic: this.model.isPublic(), diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 686fb4a36..75b48e443 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -11,6 +11,12 @@ import { ContactName } from './conversation/ContactName'; import { TypingAnimation } from './conversation/TypingAnimation'; import { Colors, LocalizerType } from '../types/Util'; +import { + showClearNickname, + showBlock, + showCopyId, + showDeleteContact, +} from '../session/utils/Menu'; export type PropsData = { id: string; @@ -179,7 +185,7 @@ export class ConversationListItem extends React.PureComponent { return ( - {!isPublic && !isRss && !isMe && isPrivate ? ( + {showBlock(isMe, isPrivate) ? ( {blockTitle} ) : null} {/* {!isPublic && !isRss && !isMe ? ( @@ -187,14 +193,20 @@ export class ConversationListItem extends React.PureComponent { {i18n('changeNickname')} ) : null} */} - {!isPublic && !isRss && !isMe && hasNickname ? ( + {showClearNickname(isPublic, isRss, isMe, hasNickname) ? ( {i18n('clearNickname')} ) : null} - {!isPublic && !isRss ? ( + {showCopyId(isPublic, isRss) ? ( {i18n('copyPublicKey')} ) : null} {i18n('deleteMessages')} - {!isMe && isClosable ? ( + {showDeleteContact( + isMe, + isClosable, + type === 'group', + isPublic, + isRss + ) ? ( !isPublic ? ( {i18n('deleteContact')} diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 8a66cf64a..0e530554d 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -20,6 +20,20 @@ import { SessionButtonColor, SessionButtonType, } from '../session/SessionButton'; +import { + showAddModerators, + showBlock, + showCopyId, + showDeleteContact, + showInviteContact, + showLeaveGroup, + showMemberMenu, + showRemoveModerators, + showResetSession, + showSafetyNumber, + showTimerOptions, + showUpdateGroupName, +} from '../../session/utils/Menu'; export interface TimerOption { name: string; @@ -38,6 +52,7 @@ interface Props { isMe: boolean; isClosable?: boolean; isGroup: boolean; + isPrivate: boolean; isArchived: boolean; isPublic: boolean; isRss: boolean; @@ -304,40 +319,38 @@ export class ConversationHeader extends React.Component { onUpdateGroupName, } = this.props; - const isPrivateGroup = isGroup && !isPublic && !isRss; - const copyIdLabel = isGroup ? i18n('copyChatId') : i18n('copyPublicKey'); return ( {this.renderPublicMenuItems()} - {!isRss ? ( + {showCopyId(isPublic, isRss) ? ( {copyIdLabel} ) : null} {i18n('deleteMessages')} - {amMod && !isKickedFromGroup ? ( + {showAddModerators(amMod, isKickedFromGroup) ? ( {i18n('addModerators')} ) : null} - {amMod && !isKickedFromGroup ? ( + {showRemoveModerators(amMod, isKickedFromGroup) ? ( {i18n('removeModerators')} ) : null} - {amMod && !isKickedFromGroup ? ( + {showUpdateGroupName(amMod, isKickedFromGroup) ? ( {i18n('editGroupNameOrPicture')} ) : null} - {isPrivateGroup && !isKickedFromGroup ? ( + {showLeaveGroup(isKickedFromGroup, isGroup, isPublic, isRss) ? ( {i18n('leaveGroup')} ) : null} {/* TODO: add delete group */} - {isGroup && isPublic ? ( + {showInviteContact(isGroup, isPublic) ? ( {i18n('inviteContacts')} ) : null} - {!isMe && isClosable && !isPrivateGroup ? ( + {showDeleteContact(isMe, isClosable, isGroup, isPublic, isRss) ? ( !isPublic ? ( {i18n('deleteContact')} @@ -433,6 +446,7 @@ export class ConversationHeader extends React.Component { isBlocked, isMe, isGroup, + isPrivate, isKickedFromGroup, isPublic, isRss, @@ -454,7 +468,12 @@ export class ConversationHeader extends React.Component { const blockTitle = isBlocked ? i18n('unblockUser') : i18n('blockUser'); const blockHandler = isBlocked ? onUnblockUser : onBlockUser; - const disappearingMessagesMenuItem = !isKickedFromGroup && !isBlocked && ( + const disappearingMessagesMenuItem = showTimerOptions( + isPublic, + isRss, + isKickedFromGroup, + isBlocked + ) && ( {(timerOptions || []).map(item => ( { ))} ); - const showMembersMenuItem = isGroup && ( + const showMembersMenuItem = showMemberMenu(isPublic, isRss, isGroup) && ( {i18n('showMembers')} ); - const showSafetyNumberMenuItem = !isGroup && !isMe && ( + const showSafetyNumberMenuItem = showSafetyNumber( + isPublic, + isRss, + isGroup, + isMe + ) && ( {i18n('showSafetyNumber')} ); - const resetSessionMenuItem = !isGroup && ( + const resetSessionMenuItem = showResetSession(isPublic, isRss, isGroup) && ( {i18n('resetSession')} ); - const blockHandlerMenuItem = !isMe && !isRss && !isGroup && ( + const blockHandlerMenuItem = showBlock(isMe, isPrivate) && ( {blockTitle} ); diff --git a/ts/session/utils/Menu.ts b/ts/session/utils/Menu.ts new file mode 100644 index 000000000..63418c5b0 --- /dev/null +++ b/ts/session/utils/Menu.ts @@ -0,0 +1,111 @@ +export function showTimerOptions( + isPublic: boolean, + isRss: boolean, + isKickedFromGroup: boolean, + isBlocked: boolean +): boolean { + return ( + Boolean(!isPublic) && Boolean(!isRss) && !isKickedFromGroup && !isBlocked + ); +} + +export function showMemberMenu( + isPublic: boolean, + isRss: boolean, + isGroup: boolean +): boolean { + return Boolean(!isPublic) && Boolean(!isRss) && isGroup; +} + +export function showSafetyNumber( + isPublic: boolean, + isRss: boolean, + isGroup: boolean, + isMe: boolean +): boolean { + return Boolean(!isPublic) && Boolean(!isRss) && !isGroup && !isMe; +} + +export function showResetSession( + isPublic: boolean, + isRss: boolean, + isGroup: boolean +): boolean { + return Boolean(!isPublic) && Boolean(!isRss) && Boolean(!isGroup); +} + +export function showBlock( + isMe: boolean | undefined, + isPrivate: boolean | undefined +): boolean { + return Boolean(!isMe) && Boolean(isPrivate); +} + +export function showClearNickname( + isPublic: boolean | undefined, + isRss: boolean | undefined, + isMe: boolean | undefined, + hasNickname: boolean | undefined +): boolean { + return ( + Boolean(!isPublic) && + Boolean(!isRss) && + Boolean(!isMe) && + Boolean(hasNickname) + ); +} + +export function showCopyId( + isPublic: boolean | undefined, + isRss: boolean | undefined +): boolean { + return Boolean(!isPublic) && Boolean(!isRss); +} + +export function showDeleteContact( + isMe: boolean | undefined, + isClosable: boolean | undefined, + isGroup: boolean | undefined, + isPublic: boolean | undefined, + isRss: boolean | undefined +): boolean { + return ( + Boolean(!isMe) && Boolean(isClosable) && !!(!isGroup || isPublic || isRss) + ); +} + +export function showAddModerators( + amMod: boolean | undefined, + isKickedFromGroup: boolean | undefined +): boolean { + return Boolean(!isKickedFromGroup) && Boolean(amMod); +} + +export function showRemoveModerators( + amMod: boolean | undefined, + isKickedFromGroup: boolean | undefined +): boolean { + return Boolean(!isKickedFromGroup) && Boolean(amMod); +} +export function showUpdateGroupName( + amMod: boolean | undefined, + isKickedFromGroup: boolean | undefined +): boolean { + return Boolean(!isKickedFromGroup) && Boolean(amMod); +} + +export function showLeaveGroup( + isKickedFromGroup: boolean | undefined, + isGroup: boolean | undefined, + isPublic: boolean | undefined, + isRss: boolean | undefined +): boolean { + return Boolean(!isKickedFromGroup) && !!(!isGroup || isPublic || isRss); +} + +export function showInviteContact( + isGroup: boolean | undefined, + isPublic: boolean | undefined +): boolean { + return Boolean(isGroup) && Boolean(isPublic); +} diff --git a/ts/session/utils/index.ts b/ts/session/utils/index.ts index d9cafc0fa..b38705d0e 100644 --- a/ts/session/utils/index.ts +++ b/ts/session/utils/index.ts @@ -5,6 +5,7 @@ import * as StringUtils from './String'; import * as NumberUtils from './Number'; import * as PromiseUtils from './Promise'; import * as ProtobufUtils from './Protobuf'; +import * as MenuUtils from './Menu'; export * from './Attachments'; export * from './TypedEmitter'; @@ -18,4 +19,5 @@ export { NumberUtils, PromiseUtils, ProtobufUtils, + MenuUtils, }; From 8910be2ceb07762c72b8d6674b68f30e0f2f8e12 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 24 Jul 2020 15:54:40 +1000 Subject: [PATCH 08/11] remove unused class and files --- js/views/conversation_list_item_view.js | 69 -------- js/views/conversation_list_view.js | 71 -------- js/views/conversation_search_view.js | 177 -------------------- test/views/conversation_search_view_test.js | 67 -------- 4 files changed, 384 deletions(-) delete mode 100644 js/views/conversation_list_item_view.js delete mode 100644 js/views/conversation_list_view.js delete mode 100644 js/views/conversation_search_view.js delete mode 100644 test/views/conversation_search_view_test.js diff --git a/js/views/conversation_list_item_view.js b/js/views/conversation_list_item_view.js deleted file mode 100644 index 06459a19e..000000000 --- a/js/views/conversation_list_item_view.js +++ /dev/null @@ -1,69 +0,0 @@ -/* global Whisper, Signal, Backbone */ - -// eslint-disable-next-line func-names -(function() { - 'use strict'; - - window.Whisper = window.Whisper || {}; - - // list of conversations, showing user/group and last message sent - Whisper.ConversationListItemView = Whisper.View.extend({ - tagName: 'div', - className() { - return `conversation-list-item contact ${this.model.cid}`; - }, - templateName: 'conversation-preview', - initialize() { - this.listenTo(this.model, 'destroy', this.remove); - }, - - remove() { - if (this.childView) { - this.childView.remove(); - this.childView = null; - } - Backbone.View.prototype.remove.call(this); - }, - - getProps() { - return this.model.getPropsForListItem(); - }, - - render() { - if (this.childView) { - this.childView.remove(); - this.childView = null; - } - - const props = this.getProps(); - this.childView = new Whisper.ReactWrapperView({ - className: 'list-item-wrapper', - Component: Signal.Components.ConversationListItem, - props, - }); - - const update = () => this.childView.update(this.getProps()); - - this.listenTo(this.model, 'change', update); - - this.$el.append(this.childView.el); - - return this; - }, - }); - - // list of conversations, showing user/group and last message sent - Whisper.ConversationContactListItemView = Whisper.ConversationListItemView.extend( - { - getProps() { - // We don't want to show a timestamp or a message - const props = this.model.getPropsForListItem(); - delete props.lastMessage; - delete props.lastUpdated; - delete props.isSelected; - - return props; - }, - } - ); -})(); diff --git a/js/views/conversation_list_view.js b/js/views/conversation_list_view.js deleted file mode 100644 index 6d49ce854..000000000 --- a/js/views/conversation_list_view.js +++ /dev/null @@ -1,71 +0,0 @@ -/* global Whisper, getInboxCollection, $ */ - -// eslint-disable-next-line func-names -(function() { - 'use strict'; - - window.Whisper = window.Whisper || {}; - - Whisper.ConversationListView = Whisper.ListView.extend({ - tagName: 'div', - itemView: Whisper.ConversationListItemView, - getCollection() { - return getInboxCollection(); - }, - updateLocation(conversation) { - const $el = this.$(`.${conversation.cid}`); - - if (!$el || !$el.length) { - window.log.warn( - 'updateLocation: did not find element for conversation', - conversation.idForLogging() - ); - return; - } - if ($el.length > 1) { - window.log.warn( - 'updateLocation: found more than one element for conversation', - conversation.idForLogging() - ); - return; - } - - const $allConversations = this.$('.conversation-list-item'); - const inboxCollection = this.getCollection(); - const index = inboxCollection.indexOf(conversation); - - const elIndex = $allConversations.index($el); - if (elIndex < 0) { - window.log.warn( - 'updateLocation: did not find index for conversation', - conversation.idForLogging() - ); - } - - if (index === elIndex) { - return; - } - if (index === 0) { - this.$el.prepend($el); - } else if (index === this.collection.length - 1) { - this.$el.append($el); - } else { - const targetConversation = inboxCollection.at(index - 1); - const target = this.$(`.${targetConversation.cid}`); - $el.insertAfter(target); - } - - if ($('.selected').length) { - $('.selected')[0].scrollIntoView({ - block: 'nearest', - }); - } - }, - removeItem(conversation) { - const $el = this.$(`.${conversation.cid}`); - if ($el && $el.length > 0) { - $el.remove(); - } - }, - }); -})(); diff --git a/js/views/conversation_search_view.js b/js/views/conversation_search_view.js deleted file mode 100644 index e1920e099..000000000 --- a/js/views/conversation_search_view.js +++ /dev/null @@ -1,177 +0,0 @@ -/* global ConversationController, i18n, textsecure, Whisper */ - -// eslint-disable-next-line func-names -(function() { - 'use strict'; - - window.Whisper = window.Whisper || {}; - - const isSearchable = conversation => conversation.isSearchable(); - - Whisper.NewContactView = Whisper.View.extend({ - templateName: 'new-contact', - className: 'conversation-list-item contact', - events: { - click: 'validate', - }, - initialize() { - this.listenTo(this.model, 'change', this.render); // auto update - }, - render_attributes() { - // Show the appropriate message based on model validity - const message = - this.model && this.model.isValid() - ? i18n('startConversation') - : i18n('invalidNumberError'); - return { - number: message, - title: this.model.getNumber(), - avatar: this.model.getAvatar(), - }; - }, - validate() { - if (this.model.isValid()) { - this.$el.addClass('valid'); - } else { - this.$el.removeClass('valid'); - } - }, - }); - - Whisper.ConversationSearchView = Whisper.View.extend({ - className: 'conversation-search', - initialize(options) { - this.$input = options.input; - this.$new_contact = this.$('.new-contact'); - - this.typeahead = new Whisper.ConversationCollection(); - this.collection = new Whisper.ConversationCollection([], { - comparator(m) { - return m.getTitle().toLowerCase(); - }, - }); - this.listenTo(this.collection, 'select', conversation => { - this.resetTypeahead(); - this.trigger('open', conversation); - }); - - // View to display the matched contacts from typeahead - this.typeahead_view = new Whisper.ConversationListView({ - collection: this.collection, - }); - this.$el.append(this.typeahead_view.el); - this.initNewContact(); - this.pending = Promise.resolve(); - }, - - events: { - 'click .new-contact': 'createConversation', - }, - - filterContacts() { - const query = this.$input.val().trim(); - if (query.length) { - // Update the contact model - this.new_contact_view.model.set('id', query); - this.new_contact_view.render().$el.hide(); - this.new_contact_view.validate(); - this.hideHints(); - - // NOTE: Temporarily allow `then` until we convert the entire file - // to `async` / `await`: - /* eslint-disable more/no-then */ - this.pending = this.pending.then(() => - this.typeahead.search(query).then(() => { - let results = this.typeahead.filter(isSearchable); - const noteToSelf = i18n('noteToSelf'); - if (noteToSelf.toLowerCase().indexOf(query.toLowerCase()) !== -1) { - const ourNumber = textsecure.storage.user.getNumber(); - const conversation = ConversationController.get(ourNumber); - if (conversation) { - // ensure that we don't have duplicates in our results - results = results.filter(item => item.id !== ourNumber); - results.unshift(conversation); - } - } - - this.typeahead_view.collection.reset(results); - - // This will allow us to show the last message when searching - this.typeahead_view.collection.forEach(c => c.updateLastMessage()); - - // Show the new contact view if we already have results - if (this.typeahead_view.collection.length === 0) { - this.new_contact_view.$el.show(); - } - }) - ); - /* eslint-enable more/no-then */ - this.trigger('show'); - } else { - this.resetTypeahead(); - } - }, - - initNewContact() { - if (this.new_contact_view) { - this.new_contact_view.undelegateEvents(); - this.new_contact_view.$el.hide(); - } - const model = new Whisper.Conversation({ type: 'private' }); - this.new_contact_view = new Whisper.NewContactView({ - el: this.$new_contact, - model, - }).render(); - }, - - async createConversation() { - const isValidNumber = this.new_contact_view.model.isValid(); - if (!isValidNumber) { - this.$input.focus(); - return; - } - - const newConversationId = this.new_contact_view.model.id; - const conversation = await ConversationController.getOrCreateAndWait( - newConversationId, - 'private' - ); - this.trigger('open', conversation); - this.initNewContact(); - this.resetTypeahead(); - }, - - reset() { - this.delegateEvents(); - this.typeahead_view.delegateEvents(); - this.new_contact_view.delegateEvents(); - this.resetTypeahead(); - }, - - resetTypeahead() { - this.hideHints(); - this.new_contact_view.$el.hide(); - this.$input.val('').focus(); - this.typeahead_view.collection.reset([]); - this.trigger('hide'); - }, - - showHints() { - if (!this.hintView) { - this.hintView = new Whisper.HintView({ - className: 'contact placeholder', - content: i18n('newPhoneNumber'), - }).render(); - this.hintView.$el.insertAfter(this.$input); - } - this.hintView.$el.show(); - }, - - hideHints() { - if (this.hintView) { - this.hintView.remove(); - this.hintView = null; - } - }, - }); -})(); diff --git a/test/views/conversation_search_view_test.js b/test/views/conversation_search_view_test.js deleted file mode 100644 index 93eda548b..000000000 --- a/test/views/conversation_search_view_test.js +++ /dev/null @@ -1,67 +0,0 @@ -/* global $, Whisper */ - -describe('ConversationSearchView', () => { - describe('Searching for left groups', () => { - let convo; - - before(() => { - convo = new Whisper.ConversationCollection().add({ - id: '1-search-view', - name: 'i left this group', - members: [], - type: 'group', - left: true, - }); - - return window.Signal.Data.saveConversation(convo.attributes, { - Conversation: Whisper.Conversation, - }); - }); - describe('with no messages', () => { - let input; - let view; - - before(done => { - input = $(''); - view = new Whisper.ConversationSearchView({ input }).render(); - view.$input.val('left'); - view.filterContacts(); - view.typeahead_view.collection.on('reset', () => { - done(); - }); - }); - it('should not surface left groups with no messages', () => { - assert.isUndefined( - view.typeahead_view.collection.get(convo.id), - 'got left group' - ); - }); - }); - describe('with messages', () => { - let input; - let view; - before(async () => { - input = $(''); - view = new Whisper.ConversationSearchView({ input }).render(); - convo.set({ id: '2-search-view', left: false }); - - await window.Signal.Data.saveConversation(convo.attributes, { - Conversation: Whisper.Conversation, - }); - - view.$input.val('left'); - view.filterContacts(); - - return new Promise(resolve => { - view.typeahead_view.collection.on('reset', resolve); - }); - }); - it('should surface left groups with messages', () => { - assert.isDefined( - view.typeahead_view.collection.get(convo.id), - 'got left group' - ); - }); - }); - }); -}); From 401c37c39edbcceca7df7929c7773c6f8db2cca4 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 24 Jul 2020 16:38:03 +1000 Subject: [PATCH 09/11] make standardized menu, use them in ConversationHeader and ConversationListItem --- js/models/conversations.js | 4 + ts/components/ConversationListItem.tsx | 81 +++-- .../conversation/ConversationHeader.tsx | 164 ++++----- ts/session/utils/Menu.ts | 111 ------ ts/session/utils/Menu.tsx | 336 ++++++++++++++++++ 5 files changed, 469 insertions(+), 227 deletions(-) delete mode 100644 ts/session/utils/Menu.ts create mode 100644 ts/session/utils/Menu.tsx diff --git a/js/models/conversations.js b/js/models/conversations.js index 7cb29d34c..cb3aa8370 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -608,6 +608,7 @@ }, isOnline: this.isOnline(), hasNickname: !!this.getNickname(), + isKickedFromGroup: !!this.get('isKickedFromGroup'), selectedMessages: this.selectedMessages, @@ -620,6 +621,9 @@ onDeleteContact: () => this.deleteContact(), onDeleteMessages: () => this.deleteMessages(), onCloseOverlay: () => this.resetMessageSelection(), + onInviteContacts: () => { + window.Whisper.events.trigger('inviteContacts', this); + }, }; return result; diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 75b48e443..43f4b1c10 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -12,10 +12,12 @@ import { TypingAnimation } from './conversation/TypingAnimation'; import { Colors, LocalizerType } from '../types/Util'; import { - showClearNickname, - showBlock, - showCopyId, - showDeleteContact, + getBlockMenuItem, + getClearNicknameMenuItem, + getCopyIdMenuItem, + getDeleteContactMenuItem, + getInviteContactMenuItem, + getLeaveGroupMenuItem, } from '../session/utils/Menu'; export type PropsData = { @@ -49,6 +51,7 @@ export type PropsData = { hasNickname?: boolean; isSecondary?: boolean; isGroupInvitation?: boolean; + isKickedFromGroup?: boolean; }; type PropsHousekeeping = { @@ -62,6 +65,7 @@ type PropsHousekeeping = { onClearNickname?: () => void; onCopyPublicKey?: () => void; onUnblockContact?: () => void; + onInviteContacts?: () => void; }; type Props = PropsData & PropsHousekeeping; @@ -170,53 +174,72 @@ export class ConversationListItem extends React.PureComponent { isPublic, hasNickname, type, + isKickedFromGroup, onDeleteContact, onDeleteMessages, onBlockContact, - onChangeNickname, onClearNickname, onCopyPublicKey, onUnblockContact, + onInviteContacts, } = this.props; - const blockTitle = isBlocked ? i18n('unblockUser') : i18n('blockUser'); - const blockHandler = isBlocked ? onUnblockContact : onBlockContact; const isPrivate = type === 'direct'; return ( - {showBlock(isMe, isPrivate) ? ( - {blockTitle} - ) : null} + {getBlockMenuItem( + isMe, + isPrivate, + isBlocked, + onBlockContact, + onUnblockContact, + i18n + )} {/* {!isPublic && !isRss && !isMe ? ( {i18n('changeNickname')} ) : null} */} - {showClearNickname(isPublic, isRss, isMe, hasNickname) ? ( - {i18n('clearNickname')} - ) : null} - {showCopyId(isPublic, isRss) ? ( - {i18n('copyPublicKey')} - ) : null} + {getClearNicknameMenuItem( + isPublic, + isRss, + isMe, + hasNickname, + onClearNickname, + i18n + )} + {getCopyIdMenuItem( + isPublic, + isRss, + type === 'group', + onCopyPublicKey, + i18n + )} {i18n('deleteMessages')} - {showDeleteContact( + {getInviteContactMenuItem( + type === 'group', + isPublic, + onInviteContacts, + i18n + )} + {getDeleteContactMenuItem( isMe, isClosable, type === 'group', isPublic, - isRss - ) ? ( - !isPublic ? ( - - {i18n('deleteContact')} - - ) : ( - - {i18n('deletePublicChannel')} - - ) - ) : null} + isRss, + onDeleteContact, + i18n + )} + {getLeaveGroupMenuItem( + isKickedFromGroup, + type === 'group', + isPublic, + isRss, + onDeleteContact, + i18n + )} ); } diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 0e530554d..69355ea29 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -20,20 +20,7 @@ import { SessionButtonColor, SessionButtonType, } from '../session/SessionButton'; -import { - showAddModerators, - showBlock, - showCopyId, - showDeleteContact, - showInviteContact, - showLeaveGroup, - showMemberMenu, - showRemoveModerators, - showResetSession, - showSafetyNumber, - showTimerOptions, - showUpdateGroupName, -} from '../../session/utils/Menu'; +import * as Menu from '../../session/utils/Menu'; export interface TimerOption { name: string; @@ -319,48 +306,59 @@ export class ConversationHeader extends React.Component { onUpdateGroupName, } = this.props; - const copyIdLabel = isGroup ? i18n('copyChatId') : i18n('copyPublicKey'); - return ( {this.renderPublicMenuItems()} - {showCopyId(isPublic, isRss) ? ( - {copyIdLabel} - ) : null} + {Menu.getCopyIdMenuItem( + isPublic, + isRss, + isGroup, + onCopyPublicKey, + i18n + )} {i18n('deleteMessages')} - {showAddModerators(amMod, isKickedFromGroup) ? ( - {i18n('addModerators')} - ) : null} - {showRemoveModerators(amMod, isKickedFromGroup) ? ( - - {i18n('removeModerators')} - - ) : null} - {showUpdateGroupName(amMod, isKickedFromGroup) ? ( - - {i18n('editGroupNameOrPicture')} - - ) : null} - {showLeaveGroup(isKickedFromGroup, isGroup, isPublic, isRss) ? ( - {i18n('leaveGroup')} - ) : null} + {Menu.getAddModeratorsMenuItem( + amMod, + isKickedFromGroup, + onAddModerators, + i18n + )} + {Menu.getRemoveModeratorsMenuItem( + amMod, + isKickedFromGroup, + onRemoveModerators, + i18n + )} + {Menu.getUpdateGroupNameMenuItem( + amMod, + isKickedFromGroup, + onUpdateGroupName, + i18n + )} + {Menu.getLeaveGroupMenuItem( + isKickedFromGroup, + isGroup, + isPublic, + isRss, + onLeaveGroup, + i18n + )} {/* TODO: add delete group */} - {showInviteContact(isGroup, isPublic) ? ( - - {i18n('inviteContacts')} - - ) : null} - {showDeleteContact(isMe, isClosable, isGroup, isPublic, isRss) ? ( - !isPublic ? ( - - {i18n('deleteContact')} - - ) : ( - - {i18n('deletePublicChannel')} - - ) - ) : null} + {Menu.getInviteContactMenuItem( + isGroup, + isPublic, + onInviteContacts, + i18n + )} + {Menu.getDeleteContactMenuItem( + isMe, + isClosable, + isGroup, + isPublic, + isRss, + onDeleteContact, + i18n + )} ); } @@ -459,53 +457,45 @@ export class ConversationHeader extends React.Component { onUnblockUser, } = this.props; - if (isPublic || isRss) { - return null; - } - - const disappearingTitle = i18n('disappearingMessages') as any; - - const blockTitle = isBlocked ? i18n('unblockUser') : i18n('blockUser'); - const blockHandler = isBlocked ? onUnblockUser : onBlockUser; - - const disappearingMessagesMenuItem = showTimerOptions( + const disappearingMessagesMenuItem = Menu.getDisappearingMenuItem( isPublic, isRss, isKickedFromGroup, - isBlocked - ) && ( - - {(timerOptions || []).map(item => ( - { - onSetDisappearingMessages(item.value); - }} - > - {item.name} - - ))} - + isBlocked, + timerOptions, + onSetDisappearingMessages, + i18n ); - const showMembersMenuItem = showMemberMenu(isPublic, isRss, isGroup) && ( - {i18n('showMembers')} + const showMembersMenuItem = Menu.getShowMemberMenuItem( + isPublic, + isRss, + isGroup, + onShowGroupMembers, + i18n ); - const showSafetyNumberMenuItem = showSafetyNumber( + const showSafetyNumberMenuItem = Menu.getShowSafetyNumberMenuItem( isPublic, isRss, isGroup, - isMe - ) && ( - - {i18n('showSafetyNumber')} - + isMe, + onShowSafetyNumber, + i18n ); - const resetSessionMenuItem = showResetSession(isPublic, isRss, isGroup) && ( - {i18n('resetSession')} + const resetSessionMenuItem = Menu.getResetSessionMenuItem( + isPublic, + isRss, + isGroup, + onResetSession, + i18n ); - const blockHandlerMenuItem = showBlock(isMe, isPrivate) && ( - {blockTitle} + const blockHandlerMenuItem = Menu.getBlockMenuItem( + isMe, + isPrivate, + isBlocked, + onBlockUser, + onUnblockUser, + i18n ); return ( diff --git a/ts/session/utils/Menu.ts b/ts/session/utils/Menu.ts deleted file mode 100644 index 63418c5b0..000000000 --- a/ts/session/utils/Menu.ts +++ /dev/null @@ -1,111 +0,0 @@ -export function showTimerOptions( - isPublic: boolean, - isRss: boolean, - isKickedFromGroup: boolean, - isBlocked: boolean -): boolean { - return ( - Boolean(!isPublic) && Boolean(!isRss) && !isKickedFromGroup && !isBlocked - ); -} - -export function showMemberMenu( - isPublic: boolean, - isRss: boolean, - isGroup: boolean -): boolean { - return Boolean(!isPublic) && Boolean(!isRss) && isGroup; -} - -export function showSafetyNumber( - isPublic: boolean, - isRss: boolean, - isGroup: boolean, - isMe: boolean -): boolean { - return Boolean(!isPublic) && Boolean(!isRss) && !isGroup && !isMe; -} - -export function showResetSession( - isPublic: boolean, - isRss: boolean, - isGroup: boolean -): boolean { - return Boolean(!isPublic) && Boolean(!isRss) && Boolean(!isGroup); -} - -export function showBlock( - isMe: boolean | undefined, - isPrivate: boolean | undefined -): boolean { - return Boolean(!isMe) && Boolean(isPrivate); -} - -export function showClearNickname( - isPublic: boolean | undefined, - isRss: boolean | undefined, - isMe: boolean | undefined, - hasNickname: boolean | undefined -): boolean { - return ( - Boolean(!isPublic) && - Boolean(!isRss) && - Boolean(!isMe) && - Boolean(hasNickname) - ); -} - -export function showCopyId( - isPublic: boolean | undefined, - isRss: boolean | undefined -): boolean { - return Boolean(!isPublic) && Boolean(!isRss); -} - -export function showDeleteContact( - isMe: boolean | undefined, - isClosable: boolean | undefined, - isGroup: boolean | undefined, - isPublic: boolean | undefined, - isRss: boolean | undefined -): boolean { - return ( - Boolean(!isMe) && Boolean(isClosable) && !!(!isGroup || isPublic || isRss) - ); -} - -export function showAddModerators( - amMod: boolean | undefined, - isKickedFromGroup: boolean | undefined -): boolean { - return Boolean(!isKickedFromGroup) && Boolean(amMod); -} - -export function showRemoveModerators( - amMod: boolean | undefined, - isKickedFromGroup: boolean | undefined -): boolean { - return Boolean(!isKickedFromGroup) && Boolean(amMod); -} -export function showUpdateGroupName( - amMod: boolean | undefined, - isKickedFromGroup: boolean | undefined -): boolean { - return Boolean(!isKickedFromGroup) && Boolean(amMod); -} - -export function showLeaveGroup( - isKickedFromGroup: boolean | undefined, - isGroup: boolean | undefined, - isPublic: boolean | undefined, - isRss: boolean | undefined -): boolean { - return Boolean(!isKickedFromGroup) && !!(!isGroup || isPublic || isRss); -} - -export function showInviteContact( - isGroup: boolean | undefined, - isPublic: boolean | undefined -): boolean { - return Boolean(isGroup) && Boolean(isPublic); -} diff --git a/ts/session/utils/Menu.tsx b/ts/session/utils/Menu.tsx new file mode 100644 index 000000000..e6c47d372 --- /dev/null +++ b/ts/session/utils/Menu.tsx @@ -0,0 +1,336 @@ +import React from 'react'; +import { MenuItem, SubMenu } from 'react-contextmenu'; +import { LocalizerType } from '../../types/Util'; +import { TimerOption } from '../../components/conversation/ConversationHeader'; + +function showTimerOptions( + isPublic: boolean, + isRss: boolean, + isKickedFromGroup: boolean, + isBlocked: boolean +): boolean { + return ( + Boolean(!isPublic) && Boolean(!isRss) && !isKickedFromGroup && !isBlocked + ); +} + +function showMemberMenu( + isPublic: boolean, + isRss: boolean, + isGroup: boolean +): boolean { + return Boolean(!isPublic) && Boolean(!isRss) && isGroup; +} + +function showSafetyNumber( + isPublic: boolean, + isRss: boolean, + isGroup: boolean, + isMe: boolean +): boolean { + return Boolean(!isPublic) && Boolean(!isRss) && !isGroup && !isMe; +} + +function showResetSession( + isPublic: boolean, + isRss: boolean, + isGroup: boolean +): boolean { + return Boolean(!isPublic) && Boolean(!isRss) && Boolean(!isGroup); +} + +function showBlock( + isMe: boolean | undefined, + isPrivate: boolean | undefined +): boolean { + return Boolean(!isMe) && Boolean(isPrivate); +} + +function showClearNickname( + isPublic: boolean | undefined, + isRss: boolean | undefined, + isMe: boolean | undefined, + hasNickname: boolean | undefined +): boolean { + return ( + Boolean(!isPublic) && + Boolean(!isRss) && + Boolean(!isMe) && + Boolean(hasNickname) + ); +} + +function showCopyId( + isPublic: boolean | undefined, + isRss: boolean | undefined +): boolean { + return Boolean(!isPublic) && Boolean(!isRss); +} + +function showDeleteContact( + isMe: boolean | undefined, + isClosable: boolean | undefined, + isGroup: boolean | undefined, + isPublic: boolean | undefined, + isRss: boolean | undefined +): boolean { + return ( + Boolean(!isMe) && Boolean(isClosable) && !!(!isGroup || isPublic || isRss) + ); +} + +function showAddModerators( + amMod: boolean | undefined, + isKickedFromGroup: boolean | undefined +): boolean { + return Boolean(!isKickedFromGroup) && Boolean(amMod); +} + +function showRemoveModerators( + amMod: boolean | undefined, + isKickedFromGroup: boolean | undefined +): boolean { + return Boolean(!isKickedFromGroup) && Boolean(amMod); +} + +function showUpdateGroupName( + amMod: boolean | undefined, + isKickedFromGroup: boolean | undefined +): boolean { + return Boolean(!isKickedFromGroup) && Boolean(amMod); +} + +function showLeaveGroup( + isKickedFromGroup: boolean | undefined, + isGroup: boolean | undefined, + isPublic: boolean | undefined, + isRss: boolean | undefined +): boolean { + return Boolean(!isKickedFromGroup) && Boolean(isGroup) && !isPublic && !isRss; +} + +function showInviteContact( + isGroup: boolean | undefined, + isPublic: boolean | undefined +): boolean { + return Boolean(isGroup) && Boolean(isPublic); +} + +/** Menu items standardized */ + +export function getInviteContactMenuItem( + isGroup: boolean | undefined, + isPublic: boolean | undefined, + action: any, + i18n: LocalizerType +): JSX.Element | null { + if (showInviteContact(isGroup, isPublic)) { + return {i18n('inviteContacts')}; + } + return null; +} + +export function getDeleteContactMenuItem( + isMe: boolean | undefined, + isClosable: boolean | undefined, + isGroup: boolean | undefined, + isPublic: boolean | undefined, + isRss: boolean | undefined, + action: any, + i18n: LocalizerType +): JSX.Element | null { + if (showDeleteContact(isMe, isClosable, isGroup, isPublic, isRss)) { + if (isPublic) { + return ( + {i18n('deletePublicChannel')} + ); + } + return {i18n('deleteContact')}; + } + return null; +} + +export function getLeaveGroupMenuItem( + isKickedFromGroup: boolean | undefined, + isGroup: boolean | undefined, + isPublic: boolean | undefined, + isRss: boolean | undefined, + action: any, + i18n: LocalizerType +): JSX.Element | null { + if (showLeaveGroup(isKickedFromGroup, isGroup, isPublic, isRss)) { + return {i18n('leaveGroup')}; + } + return null; +} + +export function getUpdateGroupNameMenuItem( + amMod: boolean | undefined, + isKickedFromGroup: boolean | undefined, + action: any, + i18n: LocalizerType +): JSX.Element | null { + if (showUpdateGroupName(amMod, isKickedFromGroup)) { + return ( + {i18n('editGroupNameOrPicture')} + ); + } + return null; +} + +export function getRemoveModeratorsMenuItem( + amMod: boolean | undefined, + isKickedFromGroup: boolean | undefined, + action: any, + i18n: LocalizerType +): JSX.Element | null { + if (showRemoveModerators(amMod, isKickedFromGroup)) { + return {i18n('removeModerators')}; + } + return null; +} + +export function getAddModeratorsMenuItem( + amMod: boolean | undefined, + isKickedFromGroup: boolean | undefined, + action: any, + i18n: LocalizerType +): JSX.Element | null { + if (showAddModerators(amMod, isKickedFromGroup)) { + return {i18n('addModerators')}; + } + return null; +} + +export function getCopyIdMenuItem( + isPublic: boolean | undefined, + isRss: boolean | undefined, + isGroup: boolean | undefined, + action: any, + i18n: LocalizerType +): JSX.Element | null { + if (showCopyId(isPublic, isRss)) { + const copyIdLabel = isGroup ? i18n('copyChatId') : i18n('copyPublicKey'); + return {copyIdLabel}; + } + return null; +} + +export function getDisappearingMenuItem( + isPublic: boolean | undefined, + isRss: boolean | undefined, + isKickedFromGroup: boolean | undefined, + isBlocked: boolean | undefined, + timerOptions: Array, + action: any, + i18n: LocalizerType +): JSX.Element | null { + if ( + showTimerOptions( + Boolean(isPublic), + Boolean(isRss), + Boolean(isKickedFromGroup), + Boolean(isBlocked) + ) + ) { + return ( + + {(timerOptions || []).map(item => ( + { + action(item.value); + }} + > + {item.name} + + ))} + + ); + } + return null; +} + +export function getShowMemberMenuItem( + isPublic: boolean | undefined, + isRss: boolean | undefined, + isGroup: boolean | undefined, + action: any, + i18n: LocalizerType +): JSX.Element | null { + if (showMemberMenu(Boolean(isPublic), Boolean(isRss), Boolean(isGroup))) { + return {i18n('showMembers')}; + } + return null; +} + +export function getShowSafetyNumberMenuItem( + isPublic: boolean | undefined, + isRss: boolean | undefined, + isGroup: boolean | undefined, + isMe: boolean | undefined, + action: any, + i18n: LocalizerType +): JSX.Element | null { + if ( + showSafetyNumber( + Boolean(isPublic), + Boolean(isRss), + Boolean(isGroup), + Boolean(isMe) + ) + ) { + return {i18n('showSafetyNumber')}; + } + return null; +} + +export function getResetSessionMenuItem( + isPublic: boolean | undefined, + isRss: boolean | undefined, + isGroup: boolean | undefined, + action: any, + i18n: LocalizerType +): JSX.Element | null { + if (showResetSession(Boolean(isPublic), Boolean(isRss), Boolean(isGroup))) { + return {i18n('resetSession')}; + } + return null; +} + +export function getBlockMenuItem( + isMe: boolean | undefined, + isPrivate: boolean | undefined, + isBlocked: boolean | undefined, + actionBlock: any, + actionUnblock: any, + i18n: LocalizerType +): JSX.Element | null { + if (showBlock(Boolean(isMe), Boolean(isPrivate))) { + const blockTitle = isBlocked ? i18n('unblockUser') : i18n('blockUser'); + const blockHandler = isBlocked ? actionUnblock : actionBlock; + return {blockTitle}; + } + return null; +} + +export function getClearNicknameMenuItem( + isPublic: boolean | undefined, + isRss: boolean | undefined, + isMe: boolean | undefined, + hasNickname: boolean | undefined, + action: any, + i18n: LocalizerType +): JSX.Element | null { + if ( + showClearNickname( + Boolean(isPublic), + Boolean(isRss), + Boolean(isMe), + Boolean(hasNickname) + ) + ) { + return {i18n('clearNickname')}; + } + return null; +} From 38724d0992d582edfbe52bee8a3f7de75ab5ae1c Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 27 Jul 2020 08:57:44 +1000 Subject: [PATCH 10/11] filter convos to block sync by getting list from BlockedNumberController --- libtextsecure/sendmessage.js | 5 +++-- ts/session/utils/SyncMessage.ts | 14 -------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index c7da3911f..d0f1309fc 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -463,9 +463,10 @@ MessageSender.prototype = { const convos = window.getConversations().models; const conversations = Array.isArray(convos) ? convos : [convos]; + const currentlyBlockedNumbers = window.BlockedNumberController.getBlockedNumbers(); - const blockedConvos = await libsession.Utils.SyncMessageUtils.filterBlockedNumbers( - conversations + const blockedConvos = conversations.filter( + c => c.isPrivate() && currentlyBlockedNumbers.includes(c.id) ); // currently we only sync user blocked, not groups const blockedSyncMessage = new libsession.Messages.Outgoing.BlockedListSyncMessage( diff --git a/ts/session/utils/SyncMessage.ts b/ts/session/utils/SyncMessage.ts index 3967095c9..85df60a86 100644 --- a/ts/session/utils/SyncMessage.ts +++ b/ts/session/utils/SyncMessage.ts @@ -95,20 +95,6 @@ export async function filterOpenGroupsConvos( ); } -export async function filterBlockedNumbers( - conversations: Array -): Promise { - // If we haven't got a primaryDeviceKey then we are in the middle of pairing - // primaryDevicePubKey is set to our own number if we are the master device - const thisDevice = await UserUtil.getCurrentDevicePubKey(); - - if (!thisDevice) { - return []; - } - - return conversations.filter(c => c.isBlocked() && c.isPrivate()); -} - // Serialise as ... // This is an implementation of the reciprocal of contacts_parser.js export function serialiseByteBuffers(buffers: Array): ByteBuffer { From 4fe0705f3f5c4db23332ab0aa323923ccc6f13f8 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 27 Jul 2020 10:26:15 +1000 Subject: [PATCH 11/11] do not filter by existing conversation when syncing blocked ids --- libtextsecure/sendmessage.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index d0f1309fc..4cfb70640 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -460,19 +460,14 @@ MessageSender.prototype = { if (!primaryDeviceKey) { return Promise.resolve(); } - const convos = window.getConversations().models; - const conversations = Array.isArray(convos) ? convos : [convos]; const currentlyBlockedNumbers = window.BlockedNumberController.getBlockedNumbers(); - const blockedConvos = conversations.filter( - c => c.isPrivate() && currentlyBlockedNumbers.includes(c.id) - ); // currently we only sync user blocked, not groups const blockedSyncMessage = new libsession.Messages.Outgoing.BlockedListSyncMessage( { timestamp: Date.now(), - numbers: blockedConvos.map(n => n.id), + numbers: currentlyBlockedNumbers, groups: [], } );