diff --git a/js/background.js b/js/background.js
index f193f266d..5791381e7 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;
@@ -1517,31 +1514,18 @@
);
window.lokiPublicChatAPI = null;
window.feeds = [];
- messageReceiver = new textsecure.MessageReceiver(
- USERNAME,
- PASSWORD,
- mySignalingKey,
- options
- );
+ messageReceiver = new textsecure.MessageReceiver(mySignalingKey, options);
messageReceiver.addEventListener(
'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
- );
+ messageReceiver = new textsecure.MessageReceiver(mySignalingKey, options);
messageReceiver.addEventListener(
'message',
window.NewReceiver.handleMessageEvent
@@ -1560,10 +1544,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/js/models/conversations.js b/js/models/conversations.js
index ba9a8bbf7..cb3aa8370 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;
@@ -616,6 +608,7 @@
},
isOnline: this.isOnline(),
hasNickname: !!this.getNickname(),
+ isKickedFromGroup: !!this.get('isKickedFromGroup'),
selectedMessages: this.selectedMessages,
@@ -628,6 +621,9 @@
onDeleteContact: () => this.deleteContact(),
onDeleteMessages: () => this.deleteMessages(),
onCloseOverlay: () => this.resetMessageSelection(),
+ onInviteContacts: () => {
+ window.Whisper.events.trigger('inviteContacts', this);
+ },
};
return result;
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/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/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 f31114f99..4cfb70640 100644
--- a/libtextsecure/sendmessage.js
+++ b/libtextsecure/sendmessage.js
@@ -453,6 +453,26 @@ 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 currentlyBlockedNumbers = window.BlockedNumberController.getBlockedNumbers();
+
+ // currently we only sync user blocked, not groups
+ const blockedSyncMessage = new libsession.Messages.Outgoing.BlockedListSyncMessage(
+ {
+ timestamp: Date.now(),
+ numbers: currentlyBlockedNumbers,
+ groups: [],
+ }
+ );
+ return libsession.getMessageQueue().sendSyncMessage(blockedSyncMessage);
+ },
syncReadMessages(reads) {
const myDevice = textsecure.storage.user.getDeviceId();
// FIXME currently not in used
@@ -509,8 +529,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(
@@ -521,6 +541,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/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);
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'
- );
- });
- });
- });
-});
diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx
index 72254409c..43f4b1c10 100644
--- a/ts/components/ConversationListItem.tsx
+++ b/ts/components/ConversationListItem.tsx
@@ -11,6 +11,14 @@ import { ContactName } from './conversation/ContactName';
import { TypingAnimation } from './conversation/TypingAnimation';
import { Colors, LocalizerType } from '../types/Util';
+import {
+ getBlockMenuItem,
+ getClearNicknameMenuItem,
+ getCopyIdMenuItem,
+ getDeleteContactMenuItem,
+ getInviteContactMenuItem,
+ getLeaveGroupMenuItem,
+} from '../session/utils/Menu';
export type PropsData = {
id: string;
@@ -43,6 +51,7 @@ export type PropsData = {
hasNickname?: boolean;
isSecondary?: boolean;
isGroupInvitation?: boolean;
+ isKickedFromGroup?: boolean;
};
type PropsHousekeeping = {
@@ -56,6 +65,7 @@ type PropsHousekeeping = {
onClearNickname?: () => void;
onCopyPublicKey?: () => void;
onUnblockContact?: () => void;
+ onInviteContacts?: () => void;
};
type Props = PropsData & PropsHousekeeping;
@@ -163,46 +173,73 @@ export class ConversationListItem extends React.PureComponent {
isRss,
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 (
- {!isPublic && !isRss && !isMe ? (
-
- ) : null}
+ {getBlockMenuItem(
+ isMe,
+ isPrivate,
+ isBlocked,
+ onBlockContact,
+ onUnblockContact,
+ i18n
+ )}
{/* {!isPublic && !isRss && !isMe ? (
) : null} */}
- {!isPublic && !isRss && !isMe && hasNickname ? (
-
- ) : null}
- {!isPublic && !isRss ? (
-
- ) : null}
+ {getClearNicknameMenuItem(
+ isPublic,
+ isRss,
+ isMe,
+ hasNickname,
+ onClearNickname,
+ i18n
+ )}
+ {getCopyIdMenuItem(
+ isPublic,
+ isRss,
+ type === 'group',
+ onCopyPublicKey,
+ i18n
+ )}
- {!isMe && isClosable ? (
- !isPublic ? (
-
- ) : (
-
- )
- ) : null}
+ {getInviteContactMenuItem(
+ type === 'group',
+ isPublic,
+ onInviteContacts,
+ i18n
+ )}
+ {getDeleteContactMenuItem(
+ isMe,
+ isClosable,
+ type === 'group',
+ isPublic,
+ 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 8b1cb57ed..69355ea29 100644
--- a/ts/components/conversation/ConversationHeader.tsx
+++ b/ts/components/conversation/ConversationHeader.tsx
@@ -20,6 +20,7 @@ import {
SessionButtonColor,
SessionButtonType,
} from '../session/SessionButton';
+import * as Menu from '../../session/utils/Menu';
export interface TimerOption {
name: string;
@@ -38,6 +39,7 @@ interface Props {
isMe: boolean;
isClosable?: boolean;
isGroup: boolean;
+ isPrivate: boolean;
isArchived: boolean;
isPublic: boolean;
isRss: boolean;
@@ -304,50 +306,59 @@ 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 ? (
-
- ) : null}
+ {Menu.getCopyIdMenuItem(
+ isPublic,
+ isRss,
+ isGroup,
+ onCopyPublicKey,
+ i18n
+ )}
- {amMod && !isKickedFromGroup ? (
-
- ) : null}
- {amMod && !isKickedFromGroup ? (
-
- ) : null}
- {amMod && !isKickedFromGroup ? (
-
- ) : null}
- {isPrivateGroup && !isKickedFromGroup ? (
-
- ) : 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 */}
- {isGroup && isPublic ? (
-
- ) : null}
- {!isMe && isClosable && !isPrivateGroup ? (
- !isPublic ? (
-
- ) : (
-
- )
- ) : null}
+ {Menu.getInviteContactMenuItem(
+ isGroup,
+ isPublic,
+ onInviteContacts,
+ i18n
+ )}
+ {Menu.getDeleteContactMenuItem(
+ isMe,
+ isClosable,
+ isGroup,
+ isPublic,
+ isRss,
+ onDeleteContact,
+ i18n
+ )}
);
}
@@ -433,6 +444,7 @@ export class ConversationHeader extends React.Component {
isBlocked,
isMe,
isGroup,
+ isPrivate,
isKickedFromGroup,
isPublic,
isRss,
@@ -445,43 +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 = !isKickedFromGroup && !isBlocked && (
-
- {(timerOptions || []).map(item => (
-
- ))}
-
+ const disappearingMessagesMenuItem = Menu.getDisappearingMenuItem(
+ isPublic,
+ isRss,
+ isKickedFromGroup,
+ isBlocked,
+ timerOptions,
+ onSetDisappearingMessages,
+ i18n
);
- const showMembersMenuItem = isGroup && (
-
+ const showMembersMenuItem = Menu.getShowMemberMenuItem(
+ isPublic,
+ isRss,
+ isGroup,
+ onShowGroupMembers,
+ i18n
);
- const showSafetyNumberMenuItem = !isGroup && !isMe && (
-
+ const showSafetyNumberMenuItem = Menu.getShowSafetyNumberMenuItem(
+ isPublic,
+ isRss,
+ isGroup,
+ isMe,
+ onShowSafetyNumber,
+ i18n
);
- const resetSessionMenuItem = !isGroup && (
-
+ const resetSessionMenuItem = Menu.getResetSessionMenuItem(
+ isPublic,
+ isRss,
+ isGroup,
+ onResetSession,
+ i18n
);
- const blockHandlerMenuItem = !isMe && !isRss && (
-
+ const blockHandlerMenuItem = Menu.getBlockMenuItem(
+ isMe,
+ isPrivate,
+ isBlocked,
+ onBlockUser,
+ onUnblockUser,
+ i18n
);
return (
diff --git a/ts/receiver/syncMessages.ts b/ts/receiver/syncMessages.ts
index 94b56e432..c3955cca6 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,42 @@ 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);
+ // blocked.numbers contains numbers
+ if (blocked.numbers) {
+ const currentlyBlockedNumbers = BlockedNumberController.getBlockedNumbers();
+ const toRemoveFromBlocked = _.difference(
+ currentlyBlockedNumbers,
+ blocked.numbers
+ );
+ const toAddToBlocked = _.difference(
+ blocked.numbers,
+ currentlyBlockedNumbers
+ );
+
+ 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 => markConvoBlocked(true, n)));
+
+ await Promise.all(
+ toRemoveFromBlocked.map(async n => markConvoBlocked(false, 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..9c988a9ba
--- /dev/null
+++ b/ts/session/messages/outgoing/content/sync/BlockedListSyncMessage.ts
@@ -0,0 +1,43 @@
+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();
+ // currently we do not handle the closed group blocked
+ syncMessage.blocked = new SignalService.SyncMessage.Blocked({
+ numbers: this.numbers,
+ groupIds: this.groups,
+ });
+
+ 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/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 ;
+ }
+ 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 (
+
+ );
+ }
+ return ;
+ }
+ 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 ;
+ }
+ return null;
+}
+
+export function getUpdateGroupNameMenuItem(
+ amMod: boolean | undefined,
+ isKickedFromGroup: boolean | undefined,
+ action: any,
+ i18n: LocalizerType
+): JSX.Element | null {
+ if (showUpdateGroupName(amMod, isKickedFromGroup)) {
+ return (
+
+ );
+ }
+ return null;
+}
+
+export function getRemoveModeratorsMenuItem(
+ amMod: boolean | undefined,
+ isKickedFromGroup: boolean | undefined,
+ action: any,
+ i18n: LocalizerType
+): JSX.Element | null {
+ if (showRemoveModerators(amMod, isKickedFromGroup)) {
+ return ;
+ }
+ return null;
+}
+
+export function getAddModeratorsMenuItem(
+ amMod: boolean | undefined,
+ isKickedFromGroup: boolean | undefined,
+ action: any,
+ i18n: LocalizerType
+): JSX.Element | null {
+ if (showAddModerators(amMod, isKickedFromGroup)) {
+ return ;
+ }
+ 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 ;
+ }
+ 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 => (
+
+ ))}
+
+ );
+ }
+ 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 ;
+ }
+ 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 ;
+ }
+ 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 ;
+ }
+ 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 ;
+ }
+ 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 ;
+ }
+ return null;
+}
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,
}
);
}
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,
};