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

This commit is contained in:
Beaudan 2019-03-08 16:16:10 +11:00
parent bf76767ed8
commit d0d57ea8c7
12 changed files with 169 additions and 106 deletions

View file

@ -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":

View file

@ -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);
}
});

View file

@ -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,

View file

@ -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) {

View file

@ -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);

View file

@ -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() {

View file

@ -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();

View file

@ -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({

View file

@ -90,8 +90,8 @@
}
event.preventDefault();
},
focusCancel() {
this.$('.cancel').focus();
focusInput() {
this.$input.focus();
},
});
})();

View file

@ -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<Props> {
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 (
<ContextMenu id={triggerId}>
{isBlocked ? (
<MenuItem onClick={onUnblockContact}>{i18n('unblockUser')}</MenuItem>
) : (
<MenuItem onClick={onBlockContact}>{i18n('blockUser')}</MenuItem>
)}
{!isMe ? (
<MenuItem onClick={blockHandler}>{blockTitle}</MenuItem>
) : null}
{!isMe ? (
<MenuItem onClick={onChangeNickname}>
{i18n('changeNickname')}
</MenuItem>
) : null}
{!isMe && hasNickname ? (
<MenuItem onClick={onClearNickname}>{i18n('clearNickname')}</MenuItem>
) : null}
<MenuItem onClick={onCopyPublicKey}>{i18n('copyPublicKey')}</MenuItem>
<MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem>
<MenuItem onClick={onDeleteContact}>{i18n('deleteContact')}</MenuItem>
{!isMe ? (
<MenuItem onClick={onDeleteContact}>{i18n('deleteContact')}</MenuItem>
) : null}
</ContextMenu>
);
}

View file

@ -248,7 +248,9 @@ export class ConversationHeader extends React.Component<Props> {
) : null}
<MenuItem onClick={onCopyPublicKey}>{i18n('copyPublicKey')}</MenuItem>
<MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem>
<MenuItem onClick={onDeleteContact}>{i18n('deleteContact')}</MenuItem>
{!isMe ? (
<MenuItem onClick={onDeleteContact}>{i18n('deleteContact')}</MenuItem>
) : null}
</ContextMenu>
);
}

View file

@ -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<Props, State> {
public renderContextMenu(triggerId: string) {
const {
attachments,
onCopyText,
direction,
status,
onDelete,
@ -817,6 +819,11 @@ export class Message extends React.Component<Props, State> {
{i18n('downloadAttachment')}
</MenuItem>
) : null}
<MenuItem
onClick={onCopyText}
>
{i18n('copyMessage')}
</MenuItem>
<MenuItem
attributes={{
className: 'module-message__context__reply',
@ -933,6 +940,7 @@ export class Message extends React.Component<Props, State> {
// 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<Props, State> {
const isShowingImage = this.isShowingImage();
return (
<div
className={classNames(
'module-message',
`module-message--${direction}`,
expiring ? 'module-message--expired' : null
)}
>
{this.renderError(direction === 'incoming')}
{this.renderMenu(direction === 'outgoing', triggerId)}
<div
className={classNames(
'module-message__container',
`module-message__container--${direction}`,
direction === 'incoming'
? `module-message__container--incoming-${authorColor}`
: null
)}
style={{
width: isShowingImage ? width : undefined,
}}
>
{this.renderAuthor()}
{this.renderQuote()}
{this.renderAttachment()}
{this.renderPreview()}
{this.renderEmbeddedContact()}
{this.renderText()}
{this.renderMetadata()}
{this.renderSendMessageButton()}
{this.renderAvatar()}
</div>
{this.renderError(direction === 'outgoing')}
{this.renderMenu(direction === 'incoming', triggerId)}
{this.renderContextMenu(triggerId)}
<div>
<ContextMenuTrigger id={rightClickTriggerId}>
<div
className={classNames(
'module-message',
`module-message--${direction}`,
expiring ? 'module-message--expired' : null
)}
>
{this.renderError(direction === 'incoming')}
{this.renderMenu(direction === 'outgoing', triggerId)}
<div
className={classNames(
'module-message__container',
`module-message__container--${direction}`,
direction === 'incoming'
? `module-message__container--incoming-${authorColor}`
: null
)}
style={{
width: isShowingImage ? width : undefined,
}}
>
{this.renderAuthor()}
{this.renderQuote()}
{this.renderAttachment()}
{this.renderPreview()}
{this.renderEmbeddedContact()}
{this.renderText()}
{this.renderMetadata()}
{this.renderSendMessageButton()}
{this.renderAvatar()}
</div>
{this.renderError(direction === 'outgoing')}
{this.renderMenu(direction === 'incoming', triggerId)}
{this.renderContextMenu(triggerId)}
{this.renderContextMenu(rightClickTriggerId)}
</div>
</ContextMenuTrigger>
</div>
);
}