diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 5bb5056b2..6ff8d244f 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -928,6 +928,9 @@ "ok": { "message": "OK" }, + "yes": { + "message": "Yes" + }, "cancel": { "message": "Cancel" }, @@ -994,6 +997,9 @@ "description": "Placeholder text in the message entry field when it is the first message sent to that contact" }, + "sendMessageLeftGroup": { + "message": "You left this group" + }, "groupMembers": { "message": "Group members" }, @@ -1917,6 +1923,17 @@ "Button action that the user can click to rename the group or add a new member" }, + "leaveGroup": { + "message": "Leave Group", + "description": "Button action that the user can click to leave the group" + }, + + "leaveGroupDialogTitle": { + "message": "Are you sure you want to leave this group?", + "description": + "Title shown to the user to confirm they want to leave the group" + }, + "copiedPublicKey": { "message": "Copied public key", "description": "A toast message telling the user that the key was copied" diff --git a/js/background.js b/js/background.js index ef707a912..4bb9ece96 100644 --- a/js/background.js +++ b/js/background.js @@ -763,6 +763,12 @@ } }); + Whisper.events.on('leaveGroup', async groupConvo => { + if (appView) { + appView.showLeaveGroupDialog(groupConvo); + } + }); + Whisper.events.on('deleteConversation', async conversation => { await conversation.destroyMessages(); await window.Signal.Data.removeConversation(conversation.id, { diff --git a/js/models/conversations.js b/js/models/conversations.js index cf0eebcbd..b36b2ce77 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -671,6 +671,11 @@ this.trigger('disable:input', true); return; } + if (!this.isPrivate() && this.get('left')) { + this.trigger('disable:input', true); + this.trigger('change:placeholder', 'left-group'); + return; + } switch (this.get('friendRequestStatus')) { case FriendRequestStatusEnum.none: case FriendRequestStatusEnum.requestExpired: @@ -1962,6 +1967,8 @@ textsecure.messaging.leaveGroup(this.id, groupNumbers, options) ) ); + + this.updateTextInputState(); } }, diff --git a/js/models/messages.js b/js/models/messages.js index de66e2200..b869fa88a 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -1816,6 +1816,8 @@ groupUpdate.joined = difference; } if (conversation.get('left')) { + // TODO: Maybe we shouldn't assume this message adds us: + // we could maybe still get this message by mistake window.log.warn('re-added to a left group'); attributes.left = false; } @@ -1885,6 +1887,9 @@ attributes.active_at = now; conversation.set(attributes); + // Re-enable typing if re-joined the group + conversation.updateTextInputState(); + if (message.isExpirationTimerUpdate()) { message.set({ expirationTimerUpdate: { diff --git a/js/modules/signal.js b/js/modules/signal.js index a33373f10..94162c9c9 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -50,6 +50,7 @@ const { const { UpdateGroupDialog, } = require('../../ts/components/conversation/UpdateGroupDialog'); +const { ConfirmDialog } = require('../../ts/components/ConfirmDialog'); const { MediaGallery, } = require('../../ts/components/conversation/media-gallery/MediaGallery'); @@ -226,6 +227,7 @@ exports.setup = (options = {}) => { MainHeader, MemberList, CreateGroupDialog, + ConfirmDialog, UpdateGroupDialog, MediaGallery, Message, diff --git a/js/views/app_view.js b/js/views/app_view.js index f40e1af1f..81ddb94d7 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -215,5 +215,9 @@ const dialog = new Whisper.UpdateGroupDialogView(groupConvo); this.el.append(dialog.el); }, + showLeaveGroupDialog(groupConvo) { + const dialog = new Whisper.LeaveGroupDialogView(groupConvo); + this.el.append(dialog.el); + }, }); })(); diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index cbe28b2ee..137016358 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -272,6 +272,10 @@ onUpdateGroup: () => { window.Whisper.events.trigger('updateGroup', this.model); }, + + onLeaveGroup: () => { + window.Whisper.events.trigger('leaveGroup', this.model); + }, }; }; this.titleView = new Whisper.ReactWrapperView({ @@ -440,6 +444,9 @@ case 'disabled': placeholder = i18n('sendMessageDisabled'); break; + case 'left-group': + placeholder = i18n('sendMessageLeftGroup'); + break; default: placeholder = i18n('sendMessage'); break; diff --git a/js/views/create_group_dialog_view.js b/js/views/create_group_dialog_view.js index f34d03844..28a89c757 100644 --- a/js/views/create_group_dialog_view.js +++ b/js/views/create_group_dialog_view.js @@ -46,8 +46,48 @@ }, }); + Whisper.LeaveGroupDialogView = Whisper.View.extend({ + className: 'loki-dialog modal', + initialize(groupConvo) { + this.groupConvo = groupConvo; + this.titleText = groupConvo.get('name'); + this.messageText = i18n('leaveGroupDialogTitle'); + this.okText = i18n('yes'); + this.cancelText = i18n('cancel'); + + this.close = this.close.bind(this); + this.confirm = this.confirm.bind(this); + + this.$el.focus(); + this.render(); + }, + render() { + this.dialogView = new Whisper.ReactWrapperView({ + className: 'leave-group-dialog', + Component: window.Signal.Components.ConfirmDialog, + props: { + titleText: this.titleText, + messageText: this.messageText, + okText: this.okText, + cancelText: this.cancelText, + onConfirm: this.confirm, + onClose: this.close, + }, + }); + + this.$el.append(this.dialogView.el); + return this; + }, + async confirm() { + await this.groupConvo.leaveGroup(); + this.close(); + }, + close() { + this.remove(); + }, + }); + Whisper.UpdateGroupDialogView = Whisper.View.extend({ - templateName: 'group-creation-template', className: 'loki-dialog modal', initialize(groupConvo) { this.groupName = groupConvo.get('name'); diff --git a/stylesheets/_mentions.scss b/stylesheets/_mentions.scss index ceb0a91af..22e7d727f 100644 --- a/stylesheets/_mentions.scss +++ b/stylesheets/_mentions.scss @@ -1,3 +1,30 @@ +.leave-group-dialog { + .content { + max-width: 100% !important; + } + + .titleText { + font-size: large; + text-align: center; + margin: 2px; + } + + .ok { + background-color: orangered; + min-width: 70px; + border: none; + + &:hover { + background-color: red; + } + } + + .cancel { + border: none; + min-width: 70px; + } +} + .create-group-dialog { .content { max-width: 100% !important; diff --git a/ts/components/ConfirmDialog.tsx b/ts/components/ConfirmDialog.tsx new file mode 100644 index 000000000..0a1cb510e --- /dev/null +++ b/ts/components/ConfirmDialog.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +interface Props { + titleText: string; + messageText: string; + okText: string; + cancelText: string; + onConfirm: any; + onClose: any; +} + +export class ConfirmDialog extends React.Component { + constructor(props: any) { + super(props); + } + + public render() { + return ( +
+

{this.props.titleText}

+

{this.props.messageText}

+
+ + +
+
+ ); + } +} diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index f97c42ffe..4a7223b5c 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -63,6 +63,7 @@ interface Props { onCopyPublicKey: () => void; onUpdateGroup: () => void; + onLeaveGroup: () => void; i18n: LocalizerType; } @@ -225,6 +226,7 @@ export class ConversationHeader extends React.Component { onDeleteContact, onCopyPublicKey, onUpdateGroup, + onLeaveGroup, } = this.props; return ( @@ -233,6 +235,7 @@ export class ConversationHeader extends React.Component { {i18n('copyPublicKey')} {i18n('deleteMessages')} {i18n('updateGroup')} + {i18n('leaveGroup')} {!isMe && isClosable ? ( !isPublic ? (