From d0d57ea8c789814e4632af2e228bd0d50aea8abf Mon Sep 17 00:00:00 2001 From: Beaudan Date: Fri, 8 Mar 2019 16:16:10 +1100 Subject: [PATCH] Add more functionality to the conversation right click menu, add right click for messages, change some of the ways toasts/confirmation dialogs are created, auto focus text input for nickname, clean up some stuff --- _locales/en/messages.json | 9 +++ js/background.js | 10 ++- js/models/conversations.js | 37 +++++++++ js/models/messages.js | 9 +++ js/views/app_view.js | 10 +-- js/views/conversation_list_item_view.js | 20 +---- js/views/conversation_view.js | 41 ++-------- js/views/inbox_view.js | 16 ++++ js/views/nickname_dialog_view.js | 4 +- ts/components/ConversationListItem.tsx | 34 ++++++-- .../conversation/ConversationHeader.tsx | 4 +- ts/components/conversation/Message.tsx | 81 +++++++++++-------- 12 files changed, 169 insertions(+), 106 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 74d2b9700..50543b22c 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1801,6 +1801,15 @@ "message": "Copied public key", "description": "A toast message telling the user that the key was copied" }, + "copyMessage": { + "message": "Copy message text", + "description": + "Button action that the user can click to copy their public keys" + }, + "copiedMessage": { + "message": "Copied message text", + "description": "A toast message telling the user that the message text was copied" + }, "editDisplayName": { "message": "Edit display name", "description": diff --git a/js/background.js b/js/background.js index 4e6c88c84..b3b95f36c 100644 --- a/js/background.js +++ b/js/background.js @@ -623,9 +623,15 @@ } }); + Whisper.events.on('showToast', options => { + if (appView && appView.inboxView && appView.inboxView.conversation_stack) { + appView.inboxView.conversation_stack.showToast(options); + } + }); + Whisper.events.on('showConfirmationDialog', options => { - if (appView) { - appView.showConfirmationDialog(options); + if (appView && appView.inboxView && appView.inboxView.conversation_stack) { + appView.inboxView.conversation_stack.showConfirmationDialog(options); } }); diff --git a/js/models/conversations.js b/js/models/conversations.js index 6c09c78de..f030df6ce 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -2,6 +2,7 @@ /* global Backbone: false */ /* global BlockedNumberController: false */ /* global ConversationController: false */ +/* global clipboard: false */ /* global i18n: false */ /* global profileImages: false */ /* global storage: false */ @@ -415,10 +416,17 @@ text: this.lastMessage, }, isOnline: this.isOnline(), + isMe: this.isMe(), + hasNickname: !!this.getNickname(), onClick: () => this.trigger('select', this), onBlockContact: () => this.block(), onUnblockContact: () => this.unblock(), + onChangeNickname: () => this.changeNickname(), + onClearNickname: async () => this.setNickname(null), + onCopyPublicKey: () => this.copyPublicKey(), + onDeleteContact: () => this.deleteContact(), + onDeleteMessages: () => this.deleteMessages(), }; return result; @@ -2044,6 +2052,35 @@ }); }, + copyPublicKey() { + clipboard.writeText(this.id); + window.Whisper.events.trigger('showToast', { + message: i18n('copiedPublicKey'), + }); + }, + + changeNickname() { + window.Whisper.events.trigger('showNicknameDialog', { + pubKey: this.id, + nickname: this.getNickname(), + onOk: newName => this.setNickname(newName), + }); + }, + + deleteContact() { + Whisper.events.trigger('showConfirmationDialog', { + message: i18n('deleteContactConfirmation'), + onOk: () => ConversationController.deleteContact(this.id), + }); + }, + + deleteMessages() { + Whisper.events.trigger('showConfirmationDialog', { + message: i18n('deleteConversationConfirmation'), + onOk: () => this.destroyMessages(), + }); + }, + async destroyMessages() { await window.Signal.Data.removeAllMessagesInConversation(this.id, { MessageCollection: Whisper.MessageCollection, diff --git a/js/models/messages.js b/js/models/messages.js index 77e6c1b12..a04587eb3 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -3,6 +3,7 @@ /* global storage: false */ /* global filesize: false */ /* global ConversationController: false */ +/* global clipboard: false */ /* global getAccountManager: false */ /* global i18n: false */ /* global Signal: false */ @@ -593,6 +594,7 @@ expirationTimestamp, isP2p: !!this.get('isP2p'), + onCopyText: () => this.copyText(), onReply: () => this.trigger('reply', this), onRetrySend: () => this.retrySend(), onShowDetail: () => this.trigger('show-message-detail', this), @@ -872,6 +874,13 @@ }; }, + copyText() { + clipboard.writeText(this.get('body')); + window.Whisper.events.trigger('showToast', { + message: i18n('copiedMessage'), + }); + }, + // One caller today: event handler for the 'Retry Send' entry in triple-dot menu async retrySend() { if (!textsecure.messaging) { diff --git a/js/views/app_view.js b/js/views/app_view.js index bf9584b73..0fe238ac5 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -176,15 +176,6 @@ }); } }, - 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 }) { const _title = title || `Change nickname for ${pubKey}`; const dialog = new Whisper.NicknameDialogView({ @@ -195,6 +186,7 @@ reject: onCancel, }); this.el.append(dialog.el); + dialog.focusInput(); }, showPasswordDialog({ type, resolve, reject }) { const dialog = Whisper.getPasswordDialogView(type, resolve, reject); diff --git a/js/views/conversation_list_item_view.js b/js/views/conversation_list_item_view.js index ec1164a66..06459a19e 100644 --- a/js/views/conversation_list_item_view.js +++ b/js/views/conversation_list_item_view.js @@ -1,4 +1,4 @@ -/* global Whisper, Signal, Backbone, ConversationController, i18n */ +/* global Whisper, Signal, Backbone */ // eslint-disable-next-line func-names (function() { @@ -26,23 +26,7 @@ }, getProps() { - 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; + return this.model.getPropsForListItem(); }, render() { diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index ddafd7add..35a94839b 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -10,7 +10,6 @@ textsecure, Whisper, ConversationController, - clipboard */ // eslint-disable-next-line func-names @@ -208,7 +207,7 @@ onSetDisappearingMessages: seconds => this.setDisappearingMessages(seconds), onDeleteMessages: () => this.destroyMessages(), - onDeleteContact: () => this.deleteContact(), + onDeleteContact: () => this.model.deleteContact(), onResetSession: () => this.endSession(), // These are view only and don't update the Conversation model, so they @@ -236,22 +235,13 @@ this.model.unblock(); }, onChangeNickname: () => { - window.Whisper.events.trigger('showNicknameDialog', { - pubKey: this.model.id, - nickname: this.model.getNickname(), - onOk: newName => this.model.setNickname(newName), - }); + this.model.changeNickname() }, onClearNickname: async () => { this.model.setNickname(null); }, onCopyPublicKey: () => { - clipboard.writeText(this.model.id); - const toast = new Whisper.MessageToastView({ - message: i18n('copiedPublicKey'), - }); - toast.$el.appendTo(this.$el); - toast.render(); + this.model.copyPublicKey() }, }; }; @@ -1448,33 +1438,16 @@ } }, - async deleteContact() { + destroyMessages() { Whisper.events.trigger('showConfirmationDialog', { - message: i18n('deleteContactConfirmation'), - onOk: () => { - ConversationController.deleteContact(this.model.id); + message: i18n('deleteConversationConfirmation'), + onOk: async () => { + await this.model.destroyMessages(); this.remove(); }, }); }, - async destroyMessages() { - try { - await this.confirm(i18n('deleteConversationConfirmation')); - try { - await this.model.destroyMessages(); - this.remove(); - } catch (error) { - window.log.error( - 'destroyMessages: Failed to successfully delete conversation', - error && error.stack ? error.stack : error - ); - } - } catch (error) { - // nothing to see here, user canceled out of dialog - } - }, - showSendConfirmationDialog(e, contacts) { let message; const isUnverified = this.model.isUnverified(); diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index c11fb79ed..54fd7009f 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -44,6 +44,22 @@ $el.remove(); } }, + showToast({ message }) { + const toast = new Whisper.MessageToastView({ + message, + }); + toast.$el.appendTo(this.$el); + toast.render(); + }, + showConfirmationDialog({ title, message, onOk, onCancel }) { + const dialog = new Whisper.ConfirmationDialogView({ + title, + message, + resolve: onOk, + reject: onCancel, + }); + this.el.append(dialog.el); + }, }); Whisper.FontSizeView = Whisper.View.extend({ diff --git a/js/views/nickname_dialog_view.js b/js/views/nickname_dialog_view.js index aa0af8143..2414de0d4 100644 --- a/js/views/nickname_dialog_view.js +++ b/js/views/nickname_dialog_view.js @@ -90,8 +90,8 @@ } event.preventDefault(); }, - focusCancel() { - this.$('.cancel').focus(); + focusInput() { + this.$input.focus(); }, }); })(); diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 4f4eb0fe7..e38cf6d54 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -30,12 +30,17 @@ interface Props { showFriendRequestIndicator?: boolean; isBlocked: boolean; isOnline: boolean; + isMe: boolean; + hasNickname: boolean; i18n: Localizer; onClick?: () => void; onDeleteMessages?: () => void; onDeleteContact?: () => void; onBlockContact?: () => void; + onChangeNickname?: () => void; + onClearNickname?: () => void; + onCopyPublicKey?: () => void; onUnblockContact?: () => void; } @@ -136,21 +141,38 @@ export class ConversationListItem extends React.Component { const { i18n, isBlocked, + isMe, + hasNickname, onDeleteContact, onDeleteMessages, onBlockContact, + onChangeNickname, + onClearNickname, + onCopyPublicKey, onUnblockContact, } = this.props; + const blockTitle = isBlocked ? i18n('unblockUser') : i18n('blockUser'); + const blockHandler = isBlocked ? onUnblockContact : onBlockContact; + return ( - {isBlocked ? ( - {i18n('unblockUser')} - ) : ( - {i18n('blockUser')} - )} + {!isMe ? ( + {blockTitle} + ) : null} + {!isMe ? ( + + {i18n('changeNickname')} + + ) : null} + {!isMe && hasNickname ? ( + {i18n('clearNickname')} + ) : null} + {i18n('copyPublicKey')} {i18n('deleteMessages')} - {i18n('deleteContact')} + {!isMe ? ( + {i18n('deleteContact')} + ) : null} ); } diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 147e2b188..e3a92dc14 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -248,7 +248,9 @@ export class ConversationHeader extends React.Component { ) : null} {i18n('copyPublicKey')} {i18n('deleteMessages')} - {i18n('deleteContact')} + {!isMe ? ( + {i18n('deleteContact')} + ) : null} ); } diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 502b0c985..043d342a5 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -83,6 +83,7 @@ export interface Props { onClickAttachment?: (attachment: AttachmentType) => void; onClickLinkPreview?: (url: string) => void; + onCopyText?: () => void; onReply?: () => void; onRetrySend?: () => void; onDownload?: (isDangerous: boolean) => void; @@ -785,6 +786,7 @@ export class Message extends React.Component { public renderContextMenu(triggerId: string) { const { attachments, + onCopyText, direction, status, onDelete, @@ -817,6 +819,11 @@ export class Message extends React.Component { {i18n('downloadAttachment')} ) : null} + + {i18n('copyMessage')} + { // This id is what connects our triple-dot click with our associated pop-up menu. // It needs to be unique. const triggerId = String(id || `${authorPhoneNumber}-${timestamp}`); + const rightClickTriggerId = `${authorPhoneNumber}-ctx-${timestamp}`; if (expired) { return null; @@ -942,40 +950,45 @@ export class Message extends React.Component { const isShowingImage = this.isShowingImage(); return ( -
- {this.renderError(direction === 'incoming')} - {this.renderMenu(direction === 'outgoing', triggerId)} -
- {this.renderAuthor()} - {this.renderQuote()} - {this.renderAttachment()} - {this.renderPreview()} - {this.renderEmbeddedContact()} - {this.renderText()} - {this.renderMetadata()} - {this.renderSendMessageButton()} - {this.renderAvatar()} -
- {this.renderError(direction === 'outgoing')} - {this.renderMenu(direction === 'incoming', triggerId)} - {this.renderContextMenu(triggerId)} +
+ +
+ {this.renderError(direction === 'incoming')} + {this.renderMenu(direction === 'outgoing', triggerId)} +
+ {this.renderAuthor()} + {this.renderQuote()} + {this.renderAttachment()} + {this.renderPreview()} + {this.renderEmbeddedContact()} + {this.renderText()} + {this.renderMetadata()} + {this.renderSendMessageButton()} + {this.renderAvatar()} +
+ {this.renderError(direction === 'outgoing')} + {this.renderMenu(direction === 'incoming', triggerId)} + {this.renderContextMenu(triggerId)} + {this.renderContextMenu(rightClickTriggerId)} +
+
); }