Merge pull request #252 from BeaudanBrown/delete-contact

Delete contact + right click menu
pull/254/head
Beaudan Campbell-Brown 6 years ago committed by GitHub
commit bf76767ed8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -953,6 +953,16 @@
"description": "description":
"Confirmation dialog text that asks the user if they really wish to delete the conversation. Answer buttons use the strings 'ok' and 'cancel'. The deletion is permanent, i.e. it cannot be undone." "Confirmation dialog text that asks the user if they really wish to delete the conversation. Answer buttons use the strings 'ok' and 'cancel'. The deletion is permanent, i.e. it cannot be undone."
}, },
"deleteContact": {
"message": "Delete contact",
"description":
"Confirmation dialog title that asks the user if they really wish to delete the contact. Answer buttons use the strings 'ok' and 'cancel'. The deletion is permanent, i.e. it cannot be undone."
},
"deleteContactConfirmation": {
"message": "Permanently delete this contact and destroy all sessions?",
"description":
"Confirmation dialog text that tells the user what will happen if they delete the contact."
},
"sessionResetFailed": { "sessionResetFailed": {
"message": "Secure session reset failed", "message": "Secure session reset failed",
"description": "description":

@ -623,6 +623,12 @@
} }
}); });
Whisper.events.on('showConfirmationDialog', options => {
if (appView) {
appView.showConfirmationDialog(options);
}
});
Whisper.events.on('showNicknameDialog', options => { Whisper.events.on('showNicknameDialog', options => {
if (appView) { if (appView) {
appView.showNicknameDialog(options); appView.showNicknameDialog(options);

@ -1,4 +1,4 @@
/* global _, Whisper, Backbone, storage, lokiP2pAPI */ /* global _, Whisper, Backbone, storage, lokiP2pAPI, textsecure, libsignal */
/* eslint-disable more/no-then */ /* eslint-disable more/no-then */
@ -15,6 +15,7 @@
this.listenTo(conversations, 'add change:active_at', this.addActive); this.listenTo(conversations, 'add change:active_at', this.addActive);
this.listenTo(conversations, 'reset', () => this.reset([])); this.listenTo(conversations, 'reset', () => this.reset([]));
this.listenTo(conversations, 'remove', this.remove);
this.on( this.on(
'add remove change:unreadCount', 'add remove change:unreadCount',
@ -90,6 +91,7 @@
'add change:active_at change:friendRequestStatus', 'add change:active_at change:friendRequestStatus',
this.addActive this.addActive
); );
this.listenTo(conversations, 'remove', this.remove);
this.listenTo(conversations, 'reset', () => this.reset([])); this.listenTo(conversations, 'reset', () => this.reset([]));
this.collator = new Intl.Collator(); this.collator = new Intl.Collator();
@ -113,6 +115,9 @@
window.getContactCollection = () => contactCollection; window.getContactCollection = () => contactCollection;
window.ConversationController = { window.ConversationController = {
getCollection() {
return conversations;
},
markAsSelected(toSelect) { markAsSelected(toSelect) {
conversations.each(conversation => { conversations.each(conversation => {
const current = conversation.isSelected || false; const current = conversation.isSelected || false;
@ -207,6 +212,38 @@
return conversation; return conversation;
}, },
async deleteContact(id) {
if (typeof id !== 'string') {
throw new TypeError("'id' must be a string");
}
if (!this._initialFetchComplete) {
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
}
const conversation = conversations.get(id);
if (!conversation) {
return;
}
await conversation.destroyMessages();
const deviceIds = await textsecure.storage.protocol.getDeviceIds(id);
await Promise.all(
deviceIds.map(deviceId => {
const address = new libsignal.SignalProtocolAddress(id, deviceId);
const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
address
);
return sessionCipher.deleteAllSessionsForDevice();
})
);
await window.Signal.Data.removeConversation(id, {
Conversation: Whisper.Conversation,
});
conversations.remove(conversation);
},
getOrCreateAndWait(id, type) { getOrCreateAndWait(id, type) {
return this._initialPromise.then(() => { return this._initialPromise.then(() => {
const conversation = this.getOrCreate(id, type); const conversation = this.getOrCreate(id, type);

@ -417,6 +417,8 @@
isOnline: this.isOnline(), isOnline: this.isOnline(),
onClick: () => this.trigger('select', this), onClick: () => this.trigger('select', this),
onBlockContact: () => this.block(),
onUnblockContact: () => this.unblock(),
}; };
return result; return result;

@ -176,6 +176,15 @@
}); });
} }
}, },
showConfirmationDialog({ title, message, onOk, onCancel }) {
const dialog = new Whisper.ConfirmationDialogView({
title,
message,
resolve: onOk,
reject: onCancel,
});
this.el.append(dialog.el);
},
showNicknameDialog({ pubKey, title, message, nickname, onOk, onCancel }) { showNicknameDialog({ pubKey, title, message, nickname, onOk, onCancel }) {
const _title = title || `Change nickname for ${pubKey}`; const _title = title || `Change nickname for ${pubKey}`;
const dialog = new Whisper.NicknameDialogView({ const dialog = new Whisper.NicknameDialogView({

@ -1,4 +1,4 @@
/* global Whisper, Signal, Backbone */ /* global Whisper, Signal, Backbone, ConversationController, i18n */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function() { (function() {
@ -26,7 +26,23 @@
}, },
getProps() { getProps() {
return this.model.getPropsForListItem(); const modelProps = this.model.getPropsForListItem();
const props = {
...modelProps,
onDeleteContact: () => {
Whisper.events.trigger('showConfirmationDialog', {
message: i18n('deleteContactConfirmation'),
onOk: () => ConversationController.deleteContact(this.model.id),
});
},
onDeleteMessages: () => {
Whisper.events.trigger('showConfirmationDialog', {
message: i18n('deleteConversationConfirmation'),
onOk: () => this.model.destroyMessages(),
});
},
};
return props;
}, },
render() { render() {

@ -1,4 +1,4 @@
/* global Whisper, getInboxCollection, $ */ /* global Whisper, getInboxCollection, getContactCollection, $ */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function() { (function() {
@ -9,6 +9,9 @@
Whisper.ConversationListView = Whisper.ListView.extend({ Whisper.ConversationListView = Whisper.ListView.extend({
tagName: 'div', tagName: 'div',
itemView: Whisper.ConversationListItemView, itemView: Whisper.ConversationListItemView,
getCollection() {
return getInboxCollection();
},
updateLocation(conversation) { updateLocation(conversation) {
const $el = this.$(`.${conversation.cid}`); const $el = this.$(`.${conversation.cid}`);
@ -28,7 +31,7 @@
} }
const $allConversations = this.$('.conversation-list-item'); const $allConversations = this.$('.conversation-list-item');
const inboxCollection = getInboxCollection(); const inboxCollection = this.getCollection();
const index = inboxCollection.indexOf(conversation); const index = inboxCollection.indexOf(conversation);
const elIndex = $allConversations.index($el); const elIndex = $allConversations.index($el);
@ -66,7 +69,10 @@
}, },
}); });
Whisper.ConversationContactListView = Whisper.ListView.extend({ Whisper.ConversationContactListView = Whisper.ConversationListView.extend({
itemView: Whisper.ConversationContactListItemView, itemView: Whisper.ConversationContactListItemView,
getCollection() {
return getContactCollection();
},
}); });
})(); })();

@ -208,6 +208,7 @@
onSetDisappearingMessages: seconds => onSetDisappearingMessages: seconds =>
this.setDisappearingMessages(seconds), this.setDisappearingMessages(seconds),
onDeleteMessages: () => this.destroyMessages(), onDeleteMessages: () => this.destroyMessages(),
onDeleteContact: () => this.deleteContact(),
onResetSession: () => this.endSession(), onResetSession: () => this.endSession(),
// These are view only and don't update the Conversation model, so they // These are view only and don't update the Conversation model, so they
@ -1447,6 +1448,16 @@
} }
}, },
async deleteContact() {
Whisper.events.trigger('showConfirmationDialog', {
message: i18n('deleteContactConfirmation'),
onOk: () => {
ConversationController.deleteContact(this.model.id);
this.remove();
},
});
},
async destroyMessages() { async destroyMessages() {
try { try {
await this.confirm(i18n('deleteConversationConfirmation')); await this.confirm(i18n('deleteConversationConfirmation'));

@ -180,6 +180,12 @@
input: this.$('input.search'), input: this.$('input.search'),
}); });
this.searchView.listenTo(
ConversationController.getCollection(),
'remove',
this.searchView.filterContacts
);
this.searchView.$el.hide(); this.searchView.$el.hide();
this.listenTo(this.searchView, 'hide', function toggleVisibility() { this.listenTo(this.searchView, 'hide', function toggleVisibility() {

@ -8,6 +8,7 @@ import { ContactName } from './conversation/ContactName';
import { TypingAnimation } from './conversation/TypingAnimation'; import { TypingAnimation } from './conversation/TypingAnimation';
import { Colors, Localizer } from '../types/Util'; import { Colors, Localizer } from '../types/Util';
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
interface Props { interface Props {
phoneNumber: string; phoneNumber: string;
@ -32,6 +33,10 @@ interface Props {
i18n: Localizer; i18n: Localizer;
onClick?: () => void; onClick?: () => void;
onDeleteMessages?: () => void;
onDeleteContact?: () => void;
onBlockContact?: () => void;
onUnblockContact?: () => void;
} }
export class ConversationListItem extends React.Component<Props> { export class ConversationListItem extends React.Component<Props> {
@ -127,6 +132,29 @@ export class ConversationListItem extends React.Component<Props> {
); );
} }
public renderContextMenu(triggerId: string) {
const {
i18n,
isBlocked,
onDeleteContact,
onDeleteMessages,
onBlockContact,
onUnblockContact,
} = this.props;
return (
<ContextMenu id={triggerId}>
{isBlocked ? (
<MenuItem onClick={onUnblockContact}>{i18n('unblockUser')}</MenuItem>
) : (
<MenuItem onClick={onBlockContact}>{i18n('blockUser')}</MenuItem>
)}
<MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem>
<MenuItem onClick={onDeleteContact}>{i18n('deleteContact')}</MenuItem>
</ContextMenu>
);
}
public renderMessage() { public renderMessage() {
const { lastMessage, isTyping, unreadCount, i18n } = this.props; const { lastMessage, isTyping, unreadCount, i18n } = this.props;
@ -171,6 +199,7 @@ export class ConversationListItem extends React.Component<Props> {
public render() { public render() {
const { const {
phoneNumber,
unreadCount, unreadCount,
onClick, onClick,
isSelected, isSelected,
@ -178,25 +207,34 @@ export class ConversationListItem extends React.Component<Props> {
isBlocked, isBlocked,
} = this.props; } = this.props;
const triggerId = `${phoneNumber}-ctxmenu-${Date.now()}`;
return ( return (
<div <div>
role="button" <ContextMenuTrigger id={triggerId}>
onClick={onClick} <div
className={classNames( role="button"
'module-conversation-list-item', onClick={onClick}
unreadCount > 0 ? 'module-conversation-list-item--has-unread' : null, className={classNames(
isSelected ? 'module-conversation-list-item--is-selected' : null, 'module-conversation-list-item',
showFriendRequestIndicator unreadCount > 0
? 'module-conversation-list-item--has-friend-request' ? 'module-conversation-list-item--has-unread'
: null, : null,
isBlocked ? 'module-conversation-list-item--is-blocked' : null isSelected ? 'module-conversation-list-item--is-selected' : null,
)} showFriendRequestIndicator
> ? 'module-conversation-list-item--has-friend-request'
{this.renderAvatar()} : null,
<div className="module-conversation-list-item__content"> isBlocked ? 'module-conversation-list-item--is-blocked' : null
{this.renderHeader()} )}
{this.renderMessage()} >
</div> {this.renderAvatar()}
<div className="module-conversation-list-item__content">
{this.renderHeader()}
{this.renderMessage()}
</div>
</div>
</ContextMenuTrigger>
{this.renderContextMenu(triggerId)}
</div> </div>
); );
} }

@ -18,6 +18,7 @@ Note the five items in gear menu, and the second-level menu with disappearing me
console.log('onSetDisappearingMessages', seconds) console.log('onSetDisappearingMessages', seconds)
} }
onDeleteMessages={() => console.log('onDeleteMessages')} onDeleteMessages={() => console.log('onDeleteMessages')}
onDeleteContact={() => console.log('onDeleteContact')}
onResetSession={() => console.log('onResetSession')} onResetSession={() => console.log('onResetSession')}
onShowSafetyNumber={() => console.log('onShowSafetyNumber')} onShowSafetyNumber={() => console.log('onShowSafetyNumber')}
onShowAllMedia={() => console.log('onShowAllMedia')} onShowAllMedia={() => console.log('onShowAllMedia')}
@ -93,6 +94,7 @@ Note the five items in gear menu, and the second-level menu with disappearing me
console.log('onSetDisappearingMessages', seconds) console.log('onSetDisappearingMessages', seconds)
} }
onDeleteMessages={() => console.log('onDeleteMessages')} onDeleteMessages={() => console.log('onDeleteMessages')}
onDeleteContact={() => console.log('onDeleteContact')}
onResetSession={() => console.log('onResetSession')} onResetSession={() => console.log('onResetSession')}
onShowSafetyNumber={() => console.log('onShowSafetyNumber')} onShowSafetyNumber={() => console.log('onShowSafetyNumber')}
onShowAllMedia={() => console.log('onShowAllMedia')} onShowAllMedia={() => console.log('onShowAllMedia')}
@ -116,6 +118,7 @@ Note that the menu should includes 'Show Members' instead of 'Show Safety Number
console.log('onSetDisappearingMessages', seconds) console.log('onSetDisappearingMessages', seconds)
} }
onDeleteMessages={() => console.log('onDeleteMessages')} onDeleteMessages={() => console.log('onDeleteMessages')}
onDeleteContact={() => console.log('onDeleteContact')}
onResetSession={() => console.log('onResetSession')} onResetSession={() => console.log('onResetSession')}
onShowSafetyNumber={() => console.log('onShowSafetyNumber')} onShowSafetyNumber={() => console.log('onShowSafetyNumber')}
onShowAllMedia={() => console.log('onShowAllMedia')} onShowAllMedia={() => console.log('onShowAllMedia')}

@ -41,6 +41,7 @@ interface Props {
onSetDisappearingMessages: (seconds: number) => void; onSetDisappearingMessages: (seconds: number) => void;
onDeleteMessages: () => void; onDeleteMessages: () => void;
onDeleteContact: () => void;
onResetSession: () => void; onResetSession: () => void;
onShowSafetyNumber: () => void; onShowSafetyNumber: () => void;
@ -185,6 +186,7 @@ export class ConversationHeader extends React.Component<Props> {
isMe, isMe,
isGroup, isGroup,
onDeleteMessages, onDeleteMessages,
onDeleteContact,
onResetSession, onResetSession,
onSetDisappearingMessages, onSetDisappearingMessages,
onShowAllMedia, onShowAllMedia,
@ -246,6 +248,7 @@ export class ConversationHeader extends React.Component<Props> {
) : null} ) : null}
<MenuItem onClick={onCopyPublicKey}>{i18n('copyPublicKey')}</MenuItem> <MenuItem onClick={onCopyPublicKey}>{i18n('copyPublicKey')}</MenuItem>
<MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem> <MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem>
<MenuItem onClick={onDeleteContact}>{i18n('deleteContact')}</MenuItem>
</ContextMenu> </ContextMenu>
); );
} }

Loading…
Cancel
Save