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 && }
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+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 (
);
};
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;