diff --git a/.gitignore b/.gitignore index a9c1554bd..222adda6a 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ proxy.pub *.tsbuildinfo yarn-error.log + +# editor +.vscode/ \ No newline at end of file diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d63759ba6..39e629741 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1026,8 +1026,16 @@ "message": "Change Nickname", "description": "Conversation menu option to change user nickname" }, + "nicknamePlaceholder": { + "message": "New Nickname", + "description": "A placeholder value for entering a new nickname" + }, + "changeNicknameMessage": { + "message": "Enter a nickname for this user", + "description": "A short message describing what the nickname modal input changes" + }, "clearNickname": { - "message": "Clear nickname", + "message": "Clear Nickname", "description": "Conversation menu option to clear user nickname" }, "timerOption_0_seconds_abbreviated": { @@ -1620,7 +1628,9 @@ "noModeratorsToRemove": { "message": "no moderators to remove" }, - "onlyAdminCanRemoveMembers": { "message": "You are not the creator" }, + "onlyAdminCanRemoveMembers": { + "message": "You are not the creator" + }, "onlyAdminCanRemoveMembersDesc": { "message": "Only the creator of the group can remove users" }, diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index b5fe92ed7..423da8102 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -1156,8 +1156,16 @@ "message": "Change Nickname", "description": "Conversation menu option to change user nickname" }, + "nicknamePlaceholder": { + "message": "New Nickname", + "description": "A placeholder value for entering a new nickname" + }, + "changeNicknameMessage": { + "message": "Enter a nickname for this user", + "description": "A short message describing what the nickname modal input changes" + }, "clearNickname": { - "message": "Clear nickname", + "message": "Clear Nickname", "description": "Conversation menu option to clear user nickname" }, "hideMenuBarTitle": { diff --git a/background.html b/background.html index 74629f9b4..7d02dfdb6 100644 --- a/background.html +++ b/background.html @@ -141,6 +141,7 @@ + diff --git a/js/background.js b/js/background.js index 78dd264b0..b4296c50f 100644 --- a/js/background.js +++ b/js/background.js @@ -383,6 +383,19 @@ confirmDialog.render(); }; + window.showNicknameDialog = params => { + const options = { + title: params.title || undefined, + message: params.message, + placeholder: params.placeholder, + convoId: params.convoId || undefined, + }; + + if (appView) { + appView.showNicknameDialog(options); + } + }; + window.showResetSessionIdDialog = () => { appView.showResetSessionIdDialog(); }; diff --git a/js/modules/signal.js b/js/modules/signal.js index 8dc240578..c7ddabfa3 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -15,6 +15,7 @@ const { Message } = require('../../ts/components/conversation/Message'); const { EditProfileDialog } = require('../../ts/components/EditProfileDialog'); const { UserDetailsDialog } = require('../../ts/components/UserDetailsDialog'); const { SessionSeedModal } = require('../../ts/components/session/SessionSeedModal'); +const { SessionNicknameDialog } = require('../../ts/components/session/SessionNicknameDialog'); const { SessionIDResetDialog } = require('../../ts/components/session/SessionIDResetDialog'); const { SessionRegistrationView } = require('../../ts/components/session/SessionRegistrationView'); @@ -151,6 +152,7 @@ exports.setup = (options = {}) => { SessionConfirm, SessionSeedModal, SessionIDResetDialog, + SessionNicknameDialog, SessionPasswordModal, SessionRegistrationView, Message, diff --git a/js/views/app_view.js b/js/views/app_view.js index 2f4934475..bd23dac0e 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -119,6 +119,13 @@ const dialog = new Whisper.EditProfileDialogView(options); this.el.prepend(dialog.el); }, + showNicknameDialog(options) { + // // eslint-disable-next-line no-param-reassign + const modifiedOptions = { ...options }; + modifiedOptions.theme = this.getThemeObject(); + const dialog = new Whisper.SessionNicknameDialog(modifiedOptions); + this.el.prepend(dialog.el); + }, showResetSessionIdDialog() { const theme = this.getThemeObject(); const resetSessionIDDialog = new Whisper.SessionIDResetDialog({ theme }); diff --git a/js/views/session_change_nickname_dialog_view.js b/js/views/session_change_nickname_dialog_view.js new file mode 100644 index 000000000..a4ddd088c --- /dev/null +++ b/js/views/session_change_nickname_dialog_view.js @@ -0,0 +1,54 @@ +/* global Whisper */ + +// eslint-disable-next-line func-names +(function() { + 'use strict'; + + window.Whisper = window.Whisper || {}; + + Whisper.SessionNicknameDialog = Whisper.View.extend({ + className: 'loki-dialog session-nickname-wrapper modal', + initialize(options) { + this.props = { + title: options.title, + message: options.message, + onClickOk: this.ok.bind(this), + onClickClose: this.cancel.bind(this), + convoId: options.convoId, + placeholder: options.placeholder, + }; + this.render(); + }, + registerEvents() { + this.unregisterEvents(); + document.addEventListener('keyup', this.props.onClickClose, false); + }, + + unregisterEvents() { + document.removeEventListener('keyup', this.props.onClickClose, false); + this.$('session-nickname-wrapper').remove(); + }, + render() { + this.dialogView = new Whisper.ReactWrapperView({ + className: 'session-nickname-wrapper', + Component: window.Signal.Components.SessionNicknameDialog, + props: this.props, + }); + + this.$el.append(this.dialogView.el); + return this; + }, + + close() { + this.remove(); + }, + cancel() { + this.remove(); + this.unregisterEvents(); + }, + ok() { + this.remove(); + this.unregisterEvents(); + }, + }); +})(); diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss index 303f0a013..d3ec7dde5 100644 --- a/stylesheets/_session.scss +++ b/stylesheets/_session.scss @@ -1198,6 +1198,17 @@ input { } } +.session-nickname-wrapper { + position: absolute; + height: 100%; + width: 100%; + display: flex; + + .session-modal { + margin: auto auto; + } +} + .messages-container { .session-icon-button { display: flex; diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index bbf0eb86f..a2404c912 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -42,6 +42,7 @@ type PropsHousekeeping = { onUnblockContact?: () => void; onInviteContacts?: () => void; onClearNickname?: () => void; + onChangeNickname?: () => void; onMarkAllRead: () => void; theme: DefaultTheme; }; diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 38270bf40..7e0f433cf 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -58,6 +58,8 @@ interface Props { onSetDisappearingMessages: (seconds: number) => void; onDeleteMessages: () => void; onDeleteContact: () => void; + onChangeNickname?: () => void; + onClearNickname?: () => void; onCloseOverlay: () => void; onDeleteSelectedMessages: () => void; diff --git a/ts/components/conversation/InviteContactsDialog.tsx b/ts/components/conversation/InviteContactsDialog.tsx index 684b9aee6..287e34520 100644 --- a/ts/components/conversation/InviteContactsDialog.tsx +++ b/ts/components/conversation/InviteContactsDialog.tsx @@ -34,7 +34,12 @@ class InviteContactsDialogInner extends React.Component { contacts = contacts.map(d => { const lokiProfile = d.getLokiProfile(); - const name = lokiProfile ? lokiProfile.displayName : window.i18n('anonymous'); + const nickname = d.getNickname(); + const name = nickname + ? nickname + : lokiProfile + ? lokiProfile.displayName + : window.i18n('anonymous'); // TODO: should take existing members into account const existingMember = false; diff --git a/ts/components/conversation/UpdateGroupMembersDialog.tsx b/ts/components/conversation/UpdateGroupMembersDialog.tsx index f6873e6fc..53c79e434 100644 --- a/ts/components/conversation/UpdateGroupMembersDialog.tsx +++ b/ts/components/conversation/UpdateGroupMembersDialog.tsx @@ -47,7 +47,12 @@ export class UpdateGroupMembersDialog extends React.Component { let contacts = this.props.contactList; contacts = contacts.map(d => { const lokiProfile = d.getLokiProfile(); - const name = lokiProfile ? lokiProfile.displayName : window.i18n('anonymous'); + const nickname = d.getNickname(); + const name = nickname + ? nickname + : lokiProfile + ? lokiProfile.displayName + : window.i18n('anonymous'); const existingMember = this.props.existingMembers.includes(d.id); diff --git a/ts/components/session/SessionNicknameDialog.tsx b/ts/components/session/SessionNicknameDialog.tsx new file mode 100644 index 000000000..d41d824dd --- /dev/null +++ b/ts/components/session/SessionNicknameDialog.tsx @@ -0,0 +1,80 @@ +import React, { useState } from 'react'; +import { ConversationController } from '../../session/conversations/ConversationController'; +import { SessionModal } from './SessionModal'; +import { SessionButton, SessionButtonColor } from './SessionButton'; +import { DefaultTheme, withTheme } from 'styled-components'; + +type Props = { + message: string; + title: string; + placeholder?: string; + onOk?: any; + onClose?: any; + onClickOk: any; + onClickClose: any; + hideCancel: boolean; + okTheme: SessionButtonColor; + theme: DefaultTheme; + convoId?: string; +}; + +const SessionNicknameInner = (props: Props) => { + const { title = '', message, onClickOk, onClickClose, convoId, placeholder } = props; + const showHeader = true; + const [nickname, setNickname] = useState(''); + + /** + * Changes the state of nickname variable. If enter is pressed, saves the current + * entered nickname value as the nickname. + */ + const onNicknameInput = async (event: any) => { + if (event.key === 'Enter') { + await saveNickname(); + } + const currentNicknameEntered = event.target.value; + setNickname(currentNicknameEntered); + }; + + /** + * Saves the currently entered nickname. + */ + const saveNickname = async () => { + if (!convoId) { + return; + } + const convo = ConversationController.getInstance().get(convoId); + onClickOk(nickname); + await convo.setNickname(nickname); + }; + + return ( + + {!showHeader &&
} + +
+ {message} +
+
+ + + +
+ + +
+ + ); +}; + +export const SessionNicknameDialog = withTheme(SessionNicknameInner); diff --git a/ts/components/session/conversation/SessionConversation.tsx b/ts/components/session/conversation/SessionConversation.tsx index 4df91edcc..f1f0bf848 100644 --- a/ts/components/session/conversation/SessionConversation.tsx +++ b/ts/components/session/conversation/SessionConversation.tsx @@ -359,6 +359,8 @@ export class SessionConversation extends React.Component { onSetDisappearingMessages: conversation.updateExpirationTimer, onDeleteMessages: conversation.deleteMessages, onDeleteSelectedMessages: this.deleteSelectedMessages, + onChangeNickname: conversation.changeNickname, + onClearNickname: conversation.clearNickname, onCloseOverlay: () => { this.setState({ selectedMessages: [] }); }, diff --git a/ts/components/session/menu/ConversationHeaderMenu.tsx b/ts/components/session/menu/ConversationHeaderMenu.tsx index 5ace0b1d7..8e3dd71a5 100644 --- a/ts/components/session/menu/ConversationHeaderMenu.tsx +++ b/ts/components/session/menu/ConversationHeaderMenu.tsx @@ -3,6 +3,8 @@ import { animation, Menu } from 'react-contexify'; import { getAddModeratorsMenuItem, getBlockMenuItem, + getChangeNicknameMenuItem, + getClearNicknameMenuItem, getCopyMenuItem, getDeleteContactMenuItem, getDeleteMessagesMenuItem, @@ -26,10 +28,14 @@ export type PropsConversationHeaderMenu = { timerOptions: Array; isPrivate: boolean; isBlocked: boolean; + hasNickname?: boolean; + onDeleteMessages?: () => void; onDeleteContact?: () => void; onCopyPublicKey?: () => void; onInviteContacts?: () => void; + onChangeNickname?: () => void; + onClearNickname?: () => void; onLeaveGroup: () => void; onMarkAllRead: () => void; @@ -53,7 +59,10 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => { isBlocked, isPrivate, left, + hasNickname, + onClearNickname, + onChangeNickname, onDeleteMessages, onDeleteContact, onCopyPublicKey, @@ -83,6 +92,8 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => { {getCopyMenuItem(isPublic, isGroup, onCopyPublicKey, window.i18n)} {getMarkAllReadMenuItem(onMarkAllRead, window.i18n)} + {getChangeNicknameMenuItem(isMe, onChangeNickname, isGroup, window.i18n)} + {getClearNicknameMenuItem(isMe, hasNickname, onClearNickname, isGroup, window.i18n)} {getDeleteMessagesMenuItem(isPublic, onDeleteMessages, window.i18n)} {getAddModeratorsMenuItem(isAdmin, isKickedFromGroup, onAddModerators, window.i18n)} {getRemoveModeratorsMenuItem(isAdmin, isKickedFromGroup, onRemoveModerators, window.i18n)} diff --git a/ts/components/session/menu/ConversationListItemContextMenu.tsx b/ts/components/session/menu/ConversationListItemContextMenu.tsx index 0acf21c1c..baa098893 100644 --- a/ts/components/session/menu/ConversationListItemContextMenu.tsx +++ b/ts/components/session/menu/ConversationListItemContextMenu.tsx @@ -4,6 +4,7 @@ import { ConversationTypeEnum } from '../../../models/conversation'; import { getBlockMenuItem, + getChangeNicknameMenuItem, getClearNicknameMenuItem, getCopyMenuItem, getDeleteContactMenuItem, @@ -32,6 +33,7 @@ export type PropsContextConversationItem = { onUnblockContact?: () => void; onInviteContacts?: () => void; onClearNickname?: () => void; + onChangeNickname?: () => void; }; export const ConversationListItemContextMenu = (props: PropsContextConversationItem) => { @@ -53,8 +55,11 @@ export const ConversationListItemContextMenu = (props: PropsContextConversationI onUnblockContact, onInviteContacts, onLeaveGroup, + onChangeNickname, } = props; + const isGroup = type === 'group'; + return ( {getBlockMenuItem( @@ -65,34 +70,23 @@ export const ConversationListItemContextMenu = (props: PropsContextConversationI onUnblockContact, window.i18n )} - {/* {!isPublic && !isMe ? ( - - {i18n('changeNickname')} - - ) : null} */} - {getClearNicknameMenuItem(isPublic, isMe, hasNickname, onClearNickname, window.i18n)} - {getCopyMenuItem(isPublic, type === 'group', onCopyPublicKey, window.i18n)} + {getCopyMenuItem(isPublic, isGroup, onCopyPublicKey, window.i18n)} {getMarkAllReadMenuItem(onMarkAllRead, window.i18n)} + {getChangeNicknameMenuItem(isMe, onChangeNickname, isGroup, window.i18n)} + {getClearNicknameMenuItem(isMe, hasNickname, onClearNickname, isGroup, window.i18n)} {getDeleteMessagesMenuItem(isPublic, onDeleteMessages, window.i18n)} - {getInviteContactMenuItem(type === 'group', isPublic, onInviteContacts, window.i18n)} + {getInviteContactMenuItem(isGroup, isPublic, onInviteContacts, window.i18n)} {getDeleteContactMenuItem( isMe, - type === 'group', + isGroup, isPublic, left, isKickedFromGroup, onDeleteContact, window.i18n )} - {getLeaveGroupMenuItem( - isKickedFromGroup, - left, - type === 'group', - isPublic, - onLeaveGroup, - window.i18n - )} + {getLeaveGroupMenuItem(isKickedFromGroup, left, isGroup, isPublic, onLeaveGroup, window.i18n)} ); }; diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 077da45d5..99bc6a4ba 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -20,8 +20,12 @@ function showBlock(isMe: boolean, isPrivate: boolean): boolean { return !isMe && isPrivate; } -function showClearNickname(isPublic: boolean, isMe: boolean, hasNickname: boolean): boolean { - return !isPublic && !isMe && hasNickname; +function showClearNickname(isMe: boolean, hasNickname: boolean, isGroup: boolean): boolean { + return !isMe && hasNickname && !isGroup; +} + +function showChangeNickname(isMe: boolean, isGroup: boolean) { + return !isMe && !isGroup; } function showDeleteMessages(isPublic: boolean): boolean { @@ -252,18 +256,30 @@ export function getBlockMenuItem( } export function getClearNicknameMenuItem( - isPublic: boolean | undefined, isMe: boolean | undefined, hasNickname: boolean | undefined, action: any, + isGroup: boolean | undefined, i18n: LocalizerType ): JSX.Element | null { - if (showClearNickname(Boolean(isPublic), Boolean(isMe), Boolean(hasNickname))) { + if (showClearNickname(Boolean(isMe), Boolean(hasNickname), Boolean(isGroup))) { return {i18n('clearNickname')}; } return null; } +export function getChangeNicknameMenuItem( + isMe: boolean | undefined, + action: any, + isGroup: boolean | undefined, + i18n: LocalizerType +): JSX.Element | null { + if (showChangeNickname(Boolean(isMe), Boolean(isGroup))) { + return {i18n('changeNickname')}; + } + return null; +} + export function getDeleteMessagesMenuItem( isPublic: boolean | undefined, action: any, diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 31691e856..49900c782 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -422,19 +422,18 @@ export class ConversationModel extends Backbone.Model { onUnblockContact: this.unblock, onCopyPublicKey: this.copyPublicKey, onDeleteContact: this.deleteContact, + onChangeNickname: this.changeNickname, + onClearNickname: this.clearNickname, + onDeleteMessages: this.deleteMessages, onLeaveGroup: () => { window.Whisper.events.trigger('leaveClosedGroup', this); }, - onDeleteMessages: this.deleteMessages, onInviteContacts: () => { window.Whisper.events.trigger('inviteContacts', this); }, onMarkAllRead: () => { void this.markReadBouncy(Date.now()); }, - onClearNickname: () => { - void this.setLokiProfile({ displayName: null }); - }, }; } @@ -1308,9 +1307,23 @@ export class ConversationModel extends Backbone.Model { } public changeNickname() { - throw new Error('changeNickname todo'); + if (this.isGroup()) { + throw new Error( + 'Called changeNickname() on a group. This is only supported in 1-on-1 conversation items and 1-on-1 conversation headers' + ); + } + window.showNicknameDialog({ + title: window.i18n('changeNickname') || 'Change Nickname', + message: window.i18n('changeNicknameMessage') || '', + placeholder: window.i18n('nicknamePlaceholder') || '', + convoId: this.id, + }); } + public clearNickname = () => { + void this.setNickname(''); + }; + public deleteContact() { let title = window.i18n('delete'); let message = window.i18n('deleteContactConfirmation'); diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index b6a2e03c6..fa7b3ac6f 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -92,6 +92,7 @@ export interface ConversationType { onInviteContacts?: () => void; onMarkAllRead?: () => void; onClearNickname?: () => void; + onChangeNickname?: () => void; } export type ConversationLookupType = { diff --git a/ts/window.d.ts b/ts/window.d.ts index 638b82297..26b33771f 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -68,6 +68,7 @@ declare global { setPassword: any; setSettingValue: any; showEditProfileDialog: any; + showNicknameDialog: any; showResetSessionIdDialog: any; storage: any; textsecure: LibTextsecure;