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

pull/253/head
Beaudan 6 years ago
parent bf76767ed8
commit d0d57ea8c7

@ -1801,6 +1801,15 @@
"message": "Copied public key", "message": "Copied public key",
"description": "A toast message telling the user that the key was copied" "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": { "editDisplayName": {
"message": "Edit display name", "message": "Edit display name",
"description": "description":

@ -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 => { Whisper.events.on('showConfirmationDialog', options => {
if (appView) { if (appView && appView.inboxView && appView.inboxView.conversation_stack) {
appView.showConfirmationDialog(options); appView.inboxView.conversation_stack.showConfirmationDialog(options);
} }
}); });

@ -2,6 +2,7 @@
/* global Backbone: false */ /* global Backbone: false */
/* global BlockedNumberController: false */ /* global BlockedNumberController: false */
/* global ConversationController: false */ /* global ConversationController: false */
/* global clipboard: false */
/* global i18n: false */ /* global i18n: false */
/* global profileImages: false */ /* global profileImages: false */
/* global storage: false */ /* global storage: false */
@ -415,10 +416,17 @@
text: this.lastMessage, text: this.lastMessage,
}, },
isOnline: this.isOnline(), isOnline: this.isOnline(),
isMe: this.isMe(),
hasNickname: !!this.getNickname(),
onClick: () => this.trigger('select', this), onClick: () => this.trigger('select', this),
onBlockContact: () => this.block(), onBlockContact: () => this.block(),
onUnblockContact: () => this.unblock(), onUnblockContact: () => this.unblock(),
onChangeNickname: () => this.changeNickname(),
onClearNickname: async () => this.setNickname(null),
onCopyPublicKey: () => this.copyPublicKey(),
onDeleteContact: () => this.deleteContact(),
onDeleteMessages: () => this.deleteMessages(),
}; };
return result; 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() { async destroyMessages() {
await window.Signal.Data.removeAllMessagesInConversation(this.id, { await window.Signal.Data.removeAllMessagesInConversation(this.id, {
MessageCollection: Whisper.MessageCollection, MessageCollection: Whisper.MessageCollection,

@ -3,6 +3,7 @@
/* global storage: false */ /* global storage: false */
/* global filesize: false */ /* global filesize: false */
/* global ConversationController: false */ /* global ConversationController: false */
/* global clipboard: false */
/* global getAccountManager: false */ /* global getAccountManager: false */
/* global i18n: false */ /* global i18n: false */
/* global Signal: false */ /* global Signal: false */
@ -593,6 +594,7 @@
expirationTimestamp, expirationTimestamp,
isP2p: !!this.get('isP2p'), isP2p: !!this.get('isP2p'),
onCopyText: () => this.copyText(),
onReply: () => this.trigger('reply', this), onReply: () => this.trigger('reply', this),
onRetrySend: () => this.retrySend(), onRetrySend: () => this.retrySend(),
onShowDetail: () => this.trigger('show-message-detail', this), 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 // One caller today: event handler for the 'Retry Send' entry in triple-dot menu
async retrySend() { async retrySend() {
if (!textsecure.messaging) { if (!textsecure.messaging) {

@ -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 }) { 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({
@ -195,6 +186,7 @@
reject: onCancel, reject: onCancel,
}); });
this.el.append(dialog.el); this.el.append(dialog.el);
dialog.focusInput();
}, },
showPasswordDialog({ type, resolve, reject }) { showPasswordDialog({ type, resolve, reject }) {
const dialog = Whisper.getPasswordDialogView(type, resolve, reject); const dialog = Whisper.getPasswordDialogView(type, resolve, reject);

@ -1,4 +1,4 @@
/* global Whisper, Signal, Backbone, ConversationController, i18n */ /* global Whisper, Signal, Backbone */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
(function() { (function() {
@ -26,23 +26,7 @@
}, },
getProps() { getProps() {
const modelProps = this.model.getPropsForListItem(); return 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() {

@ -10,7 +10,6 @@
textsecure, textsecure,
Whisper, Whisper,
ConversationController, ConversationController,
clipboard
*/ */
// eslint-disable-next-line func-names // eslint-disable-next-line func-names
@ -208,7 +207,7 @@
onSetDisappearingMessages: seconds => onSetDisappearingMessages: seconds =>
this.setDisappearingMessages(seconds), this.setDisappearingMessages(seconds),
onDeleteMessages: () => this.destroyMessages(), onDeleteMessages: () => this.destroyMessages(),
onDeleteContact: () => this.deleteContact(), onDeleteContact: () => this.model.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
@ -236,22 +235,13 @@
this.model.unblock(); this.model.unblock();
}, },
onChangeNickname: () => { onChangeNickname: () => {
window.Whisper.events.trigger('showNicknameDialog', { this.model.changeNickname()
pubKey: this.model.id,
nickname: this.model.getNickname(),
onOk: newName => this.model.setNickname(newName),
});
}, },
onClearNickname: async () => { onClearNickname: async () => {
this.model.setNickname(null); this.model.setNickname(null);
}, },
onCopyPublicKey: () => { onCopyPublicKey: () => {
clipboard.writeText(this.model.id); this.model.copyPublicKey()
const toast = new Whisper.MessageToastView({
message: i18n('copiedPublicKey'),
});
toast.$el.appendTo(this.$el);
toast.render();
}, },
}; };
}; };
@ -1448,33 +1438,16 @@
} }
}, },
async deleteContact() { destroyMessages() {
Whisper.events.trigger('showConfirmationDialog', { Whisper.events.trigger('showConfirmationDialog', {
message: i18n('deleteContactConfirmation'), message: i18n('deleteConversationConfirmation'),
onOk: () => { onOk: async () => {
ConversationController.deleteContact(this.model.id); await this.model.destroyMessages();
this.remove(); 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) { showSendConfirmationDialog(e, contacts) {
let message; let message;
const isUnverified = this.model.isUnverified(); const isUnverified = this.model.isUnverified();

@ -44,6 +44,22 @@
$el.remove(); $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({ Whisper.FontSizeView = Whisper.View.extend({

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

@ -30,12 +30,17 @@ interface Props {
showFriendRequestIndicator?: boolean; showFriendRequestIndicator?: boolean;
isBlocked: boolean; isBlocked: boolean;
isOnline: boolean; isOnline: boolean;
isMe: boolean;
hasNickname: boolean;
i18n: Localizer; i18n: Localizer;
onClick?: () => void; onClick?: () => void;
onDeleteMessages?: () => void; onDeleteMessages?: () => void;
onDeleteContact?: () => void; onDeleteContact?: () => void;
onBlockContact?: () => void; onBlockContact?: () => void;
onChangeNickname?: () => void;
onClearNickname?: () => void;
onCopyPublicKey?: () => void;
onUnblockContact?: () => void; onUnblockContact?: () => void;
} }
@ -136,21 +141,38 @@ export class ConversationListItem extends React.Component<Props> {
const { const {
i18n, i18n,
isBlocked, isBlocked,
isMe,
hasNickname,
onDeleteContact, onDeleteContact,
onDeleteMessages, onDeleteMessages,
onBlockContact, onBlockContact,
onChangeNickname,
onClearNickname,
onCopyPublicKey,
onUnblockContact, onUnblockContact,
} = this.props; } = this.props;
const blockTitle = isBlocked ? i18n('unblockUser') : i18n('blockUser');
const blockHandler = isBlocked ? onUnblockContact : onBlockContact;
return ( return (
<ContextMenu id={triggerId}> <ContextMenu id={triggerId}>
{isBlocked ? ( {!isMe ? (
<MenuItem onClick={onUnblockContact}>{i18n('unblockUser')}</MenuItem> <MenuItem onClick={blockHandler}>{blockTitle}</MenuItem>
) : ( ) : null}
<MenuItem onClick={onBlockContact}>{i18n('blockUser')}</MenuItem> {!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={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem>
<MenuItem onClick={onDeleteContact}>{i18n('deleteContact')}</MenuItem> {!isMe ? (
<MenuItem onClick={onDeleteContact}>{i18n('deleteContact')}</MenuItem>
) : null}
</ContextMenu> </ContextMenu>
); );
} }

@ -248,7 +248,9 @@ 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> {!isMe ? (
<MenuItem onClick={onDeleteContact}>{i18n('deleteContact')}</MenuItem>
) : null}
</ContextMenu> </ContextMenu>
); );
} }

@ -83,6 +83,7 @@ export interface Props {
onClickAttachment?: (attachment: AttachmentType) => void; onClickAttachment?: (attachment: AttachmentType) => void;
onClickLinkPreview?: (url: string) => void; onClickLinkPreview?: (url: string) => void;
onCopyText?: () => void;
onReply?: () => void; onReply?: () => void;
onRetrySend?: () => void; onRetrySend?: () => void;
onDownload?: (isDangerous: boolean) => void; onDownload?: (isDangerous: boolean) => void;
@ -785,6 +786,7 @@ export class Message extends React.Component<Props, State> {
public renderContextMenu(triggerId: string) { public renderContextMenu(triggerId: string) {
const { const {
attachments, attachments,
onCopyText,
direction, direction,
status, status,
onDelete, onDelete,
@ -817,6 +819,11 @@ export class Message extends React.Component<Props, State> {
{i18n('downloadAttachment')} {i18n('downloadAttachment')}
</MenuItem> </MenuItem>
) : null} ) : null}
<MenuItem
onClick={onCopyText}
>
{i18n('copyMessage')}
</MenuItem>
<MenuItem <MenuItem
attributes={{ attributes={{
className: 'module-message__context__reply', 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. // This id is what connects our triple-dot click with our associated pop-up menu.
// It needs to be unique. // It needs to be unique.
const triggerId = String(id || `${authorPhoneNumber}-${timestamp}`); const triggerId = String(id || `${authorPhoneNumber}-${timestamp}`);
const rightClickTriggerId = `${authorPhoneNumber}-ctx-${timestamp}`;
if (expired) { if (expired) {
return null; return null;
@ -942,40 +950,45 @@ export class Message extends React.Component<Props, State> {
const isShowingImage = this.isShowingImage(); const isShowingImage = this.isShowingImage();
return ( return (
<div <div>
className={classNames( <ContextMenuTrigger id={rightClickTriggerId}>
'module-message', <div
`module-message--${direction}`, className={classNames(
expiring ? 'module-message--expired' : null 'module-message',
)} `module-message--${direction}`,
> expiring ? 'module-message--expired' : null
{this.renderError(direction === 'incoming')} )}
{this.renderMenu(direction === 'outgoing', triggerId)} >
<div {this.renderError(direction === 'incoming')}
className={classNames( {this.renderMenu(direction === 'outgoing', triggerId)}
'module-message__container', <div
`module-message__container--${direction}`, className={classNames(
direction === 'incoming' 'module-message__container',
? `module-message__container--incoming-${authorColor}` `module-message__container--${direction}`,
: null direction === 'incoming'
)} ? `module-message__container--incoming-${authorColor}`
style={{ : null
width: isShowingImage ? width : undefined, )}
}} style={{
> width: isShowingImage ? width : undefined,
{this.renderAuthor()} }}
{this.renderQuote()} >
{this.renderAttachment()} {this.renderAuthor()}
{this.renderPreview()} {this.renderQuote()}
{this.renderEmbeddedContact()} {this.renderAttachment()}
{this.renderText()} {this.renderPreview()}
{this.renderMetadata()} {this.renderEmbeddedContact()}
{this.renderSendMessageButton()} {this.renderText()}
{this.renderAvatar()} {this.renderMetadata()}
</div> {this.renderSendMessageButton()}
{this.renderError(direction === 'outgoing')} {this.renderAvatar()}
{this.renderMenu(direction === 'incoming', triggerId)} </div>
{this.renderContextMenu(triggerId)} {this.renderError(direction === 'outgoing')}
{this.renderMenu(direction === 'incoming', triggerId)}
{this.renderContextMenu(triggerId)}
{this.renderContextMenu(rightClickTriggerId)}
</div>
</ContextMenuTrigger>
</div> </div>
); );
} }

Loading…
Cancel
Save