WIP: User nicknames (#1618)

* WIP Adding change nickname dialog.

* WIP adding nickname change dialog.

* WIP nickname dialog.

* WIP: Able to set conversation nicknames. Next step cleaning and adding to conversation list menu.

* Fix message capitilisations.

* Add change nickname to conversation list menu.

* Enable clear nickname menu item.

* Added messages for changing nicknames.

* Clearing nicknames working from header and message list.

* Adding modal styling to nickname modal.

* Reorder nickname menu item positions.

* Add group based conditional nickname menu options to conversation header menu.

* minor tidying.

* Remove unused error causing el option.

* Formatting.

* Linting fixes.

* Made PR fixes

* Prioritise displaying nicknames for inviting new closed group members
and updating closed group members.
pull/1622/head
Warrick 4 years ago committed by GitHub
parent e41d182972
commit cb307790f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

3
.gitignore vendored

@ -47,3 +47,6 @@ proxy.pub
*.tsbuildinfo *.tsbuildinfo
yarn-error.log yarn-error.log
# editor
.vscode/

@ -1026,8 +1026,16 @@
"message": "Change Nickname", "message": "Change Nickname",
"description": "Conversation menu option to change user 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": { "clearNickname": {
"message": "Clear nickname", "message": "Clear Nickname",
"description": "Conversation menu option to clear user nickname" "description": "Conversation menu option to clear user nickname"
}, },
"timerOption_0_seconds_abbreviated": { "timerOption_0_seconds_abbreviated": {
@ -1620,7 +1628,9 @@
"noModeratorsToRemove": { "noModeratorsToRemove": {
"message": "no moderators to remove" "message": "no moderators to remove"
}, },
"onlyAdminCanRemoveMembers": { "message": "You are not the creator" }, "onlyAdminCanRemoveMembers": {
"message": "You are not the creator"
},
"onlyAdminCanRemoveMembersDesc": { "onlyAdminCanRemoveMembersDesc": {
"message": "Only the creator of the group can remove users" "message": "Only the creator of the group can remove users"
}, },

@ -1156,8 +1156,16 @@
"message": "Change Nickname", "message": "Change Nickname",
"description": "Conversation menu option to change user 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": { "clearNickname": {
"message": "Clear nickname", "message": "Clear Nickname",
"description": "Conversation menu option to clear user nickname" "description": "Conversation menu option to clear user nickname"
}, },
"hideMenuBarTitle": { "hideMenuBarTitle": {

@ -141,6 +141,7 @@
<!-- DIALOGS--> <!-- DIALOGS-->
<script type='text/javascript' src='js/views/update_group_dialog_view.js'></script> <script type='text/javascript' src='js/views/update_group_dialog_view.js'></script>
<script type='text/javascript' src='js/views/edit_profile_dialog_view.js'></script> <script type='text/javascript' src='js/views/edit_profile_dialog_view.js'></script>
<script type='text/javascript' src='js/views/session_change_nickname_dialog_view.js'></script>
<script type='text/javascript' src='js/views/invite_contacts_dialog_view.js'></script> <script type='text/javascript' src='js/views/invite_contacts_dialog_view.js'></script>
<script type='text/javascript' src='js/views/admin_leave_closed_group_dialog_view.js'></script> <script type='text/javascript' src='js/views/admin_leave_closed_group_dialog_view.js'></script>

@ -383,6 +383,19 @@
confirmDialog.render(); 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 = () => { window.showResetSessionIdDialog = () => {
appView.showResetSessionIdDialog(); appView.showResetSessionIdDialog();
}; };

@ -15,6 +15,7 @@ const { Message } = require('../../ts/components/conversation/Message');
const { EditProfileDialog } = require('../../ts/components/EditProfileDialog'); const { EditProfileDialog } = require('../../ts/components/EditProfileDialog');
const { UserDetailsDialog } = require('../../ts/components/UserDetailsDialog'); const { UserDetailsDialog } = require('../../ts/components/UserDetailsDialog');
const { SessionSeedModal } = require('../../ts/components/session/SessionSeedModal'); const { SessionSeedModal } = require('../../ts/components/session/SessionSeedModal');
const { SessionNicknameDialog } = require('../../ts/components/session/SessionNicknameDialog');
const { SessionIDResetDialog } = require('../../ts/components/session/SessionIDResetDialog'); const { SessionIDResetDialog } = require('../../ts/components/session/SessionIDResetDialog');
const { SessionRegistrationView } = require('../../ts/components/session/SessionRegistrationView'); const { SessionRegistrationView } = require('../../ts/components/session/SessionRegistrationView');
@ -151,6 +152,7 @@ exports.setup = (options = {}) => {
SessionConfirm, SessionConfirm,
SessionSeedModal, SessionSeedModal,
SessionIDResetDialog, SessionIDResetDialog,
SessionNicknameDialog,
SessionPasswordModal, SessionPasswordModal,
SessionRegistrationView, SessionRegistrationView,
Message, Message,

@ -119,6 +119,13 @@
const dialog = new Whisper.EditProfileDialogView(options); const dialog = new Whisper.EditProfileDialogView(options);
this.el.prepend(dialog.el); 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() { showResetSessionIdDialog() {
const theme = this.getThemeObject(); const theme = this.getThemeObject();
const resetSessionIDDialog = new Whisper.SessionIDResetDialog({ theme }); const resetSessionIDDialog = new Whisper.SessionIDResetDialog({ theme });

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

@ -1198,6 +1198,17 @@ input {
} }
} }
.session-nickname-wrapper {
position: absolute;
height: 100%;
width: 100%;
display: flex;
.session-modal {
margin: auto auto;
}
}
.messages-container { .messages-container {
.session-icon-button { .session-icon-button {
display: flex; display: flex;

@ -42,6 +42,7 @@ type PropsHousekeeping = {
onUnblockContact?: () => void; onUnblockContact?: () => void;
onInviteContacts?: () => void; onInviteContacts?: () => void;
onClearNickname?: () => void; onClearNickname?: () => void;
onChangeNickname?: () => void;
onMarkAllRead: () => void; onMarkAllRead: () => void;
theme: DefaultTheme; theme: DefaultTheme;
}; };

@ -58,6 +58,8 @@ interface Props {
onSetDisappearingMessages: (seconds: number) => void; onSetDisappearingMessages: (seconds: number) => void;
onDeleteMessages: () => void; onDeleteMessages: () => void;
onDeleteContact: () => void; onDeleteContact: () => void;
onChangeNickname?: () => void;
onClearNickname?: () => void;
onCloseOverlay: () => void; onCloseOverlay: () => void;
onDeleteSelectedMessages: () => void; onDeleteSelectedMessages: () => void;

@ -34,7 +34,12 @@ class InviteContactsDialogInner extends React.Component<Props, State> {
contacts = contacts.map(d => { contacts = contacts.map(d => {
const lokiProfile = d.getLokiProfile(); 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 // TODO: should take existing members into account
const existingMember = false; const existingMember = false;

@ -47,7 +47,12 @@ export class UpdateGroupMembersDialog extends React.Component<Props, State> {
let contacts = this.props.contactList; let contacts = this.props.contactList;
contacts = contacts.map(d => { contacts = contacts.map(d => {
const lokiProfile = d.getLokiProfile(); 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); const existingMember = this.props.existingMembers.includes(d.id);

@ -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 (
<SessionModal
title={title}
onClose={onClickClose}
showExitIcon={false}
showHeader={showHeader}
theme={props.theme}
>
{!showHeader && <div className="spacer-lg" />}
<div className="session-modal__centered">
<span className="subtle">{message}</span>
<div className="spacer-lg" />
</div>
<input
type="nickname"
id="nickname-modal-input"
placeholder={placeholder}
onKeyUp={onNicknameInput}
/>
<div className="session-modal__button-group">
<SessionButton text={window.i18n('ok')} onClick={saveNickname} />
<SessionButton text={window.i18n('cancel')} onClick={onClickClose} />
</div>
</SessionModal>
);
};
export const SessionNicknameDialog = withTheme(SessionNicknameInner);

@ -359,6 +359,8 @@ export class SessionConversation extends React.Component<Props, State> {
onSetDisappearingMessages: conversation.updateExpirationTimer, onSetDisappearingMessages: conversation.updateExpirationTimer,
onDeleteMessages: conversation.deleteMessages, onDeleteMessages: conversation.deleteMessages,
onDeleteSelectedMessages: this.deleteSelectedMessages, onDeleteSelectedMessages: this.deleteSelectedMessages,
onChangeNickname: conversation.changeNickname,
onClearNickname: conversation.clearNickname,
onCloseOverlay: () => { onCloseOverlay: () => {
this.setState({ selectedMessages: [] }); this.setState({ selectedMessages: [] });
}, },

@ -3,6 +3,8 @@ import { animation, Menu } from 'react-contexify';
import { import {
getAddModeratorsMenuItem, getAddModeratorsMenuItem,
getBlockMenuItem, getBlockMenuItem,
getChangeNicknameMenuItem,
getClearNicknameMenuItem,
getCopyMenuItem, getCopyMenuItem,
getDeleteContactMenuItem, getDeleteContactMenuItem,
getDeleteMessagesMenuItem, getDeleteMessagesMenuItem,
@ -26,10 +28,14 @@ export type PropsConversationHeaderMenu = {
timerOptions: Array<TimerOption>; timerOptions: Array<TimerOption>;
isPrivate: boolean; isPrivate: boolean;
isBlocked: boolean; isBlocked: boolean;
hasNickname?: boolean;
onDeleteMessages?: () => void; onDeleteMessages?: () => void;
onDeleteContact?: () => void; onDeleteContact?: () => void;
onCopyPublicKey?: () => void; onCopyPublicKey?: () => void;
onInviteContacts?: () => void; onInviteContacts?: () => void;
onChangeNickname?: () => void;
onClearNickname?: () => void;
onLeaveGroup: () => void; onLeaveGroup: () => void;
onMarkAllRead: () => void; onMarkAllRead: () => void;
@ -53,7 +59,10 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => {
isBlocked, isBlocked,
isPrivate, isPrivate,
left, left,
hasNickname,
onClearNickname,
onChangeNickname,
onDeleteMessages, onDeleteMessages,
onDeleteContact, onDeleteContact,
onCopyPublicKey, onCopyPublicKey,
@ -83,6 +92,8 @@ export const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => {
{getCopyMenuItem(isPublic, isGroup, onCopyPublicKey, window.i18n)} {getCopyMenuItem(isPublic, isGroup, onCopyPublicKey, window.i18n)}
{getMarkAllReadMenuItem(onMarkAllRead, window.i18n)} {getMarkAllReadMenuItem(onMarkAllRead, window.i18n)}
{getChangeNicknameMenuItem(isMe, onChangeNickname, isGroup, window.i18n)}
{getClearNicknameMenuItem(isMe, hasNickname, onClearNickname, isGroup, window.i18n)}
{getDeleteMessagesMenuItem(isPublic, onDeleteMessages, window.i18n)} {getDeleteMessagesMenuItem(isPublic, onDeleteMessages, window.i18n)}
{getAddModeratorsMenuItem(isAdmin, isKickedFromGroup, onAddModerators, window.i18n)} {getAddModeratorsMenuItem(isAdmin, isKickedFromGroup, onAddModerators, window.i18n)}
{getRemoveModeratorsMenuItem(isAdmin, isKickedFromGroup, onRemoveModerators, window.i18n)} {getRemoveModeratorsMenuItem(isAdmin, isKickedFromGroup, onRemoveModerators, window.i18n)}

@ -4,6 +4,7 @@ import { ConversationTypeEnum } from '../../../models/conversation';
import { import {
getBlockMenuItem, getBlockMenuItem,
getChangeNicknameMenuItem,
getClearNicknameMenuItem, getClearNicknameMenuItem,
getCopyMenuItem, getCopyMenuItem,
getDeleteContactMenuItem, getDeleteContactMenuItem,
@ -32,6 +33,7 @@ export type PropsContextConversationItem = {
onUnblockContact?: () => void; onUnblockContact?: () => void;
onInviteContacts?: () => void; onInviteContacts?: () => void;
onClearNickname?: () => void; onClearNickname?: () => void;
onChangeNickname?: () => void;
}; };
export const ConversationListItemContextMenu = (props: PropsContextConversationItem) => { export const ConversationListItemContextMenu = (props: PropsContextConversationItem) => {
@ -53,8 +55,11 @@ export const ConversationListItemContextMenu = (props: PropsContextConversationI
onUnblockContact, onUnblockContact,
onInviteContacts, onInviteContacts,
onLeaveGroup, onLeaveGroup,
onChangeNickname,
} = props; } = props;
const isGroup = type === 'group';
return ( return (
<Menu id={triggerId} animation={animation.fade}> <Menu id={triggerId} animation={animation.fade}>
{getBlockMenuItem( {getBlockMenuItem(
@ -65,34 +70,23 @@ export const ConversationListItemContextMenu = (props: PropsContextConversationI
onUnblockContact, onUnblockContact,
window.i18n window.i18n
)} )}
{/* {!isPublic && !isMe ? ( {getCopyMenuItem(isPublic, isGroup, onCopyPublicKey, window.i18n)}
<Item onClick={onChangeNickname}>
{i18n('changeNickname')}
</Item>
) : null} */}
{getClearNicknameMenuItem(isPublic, isMe, hasNickname, onClearNickname, window.i18n)}
{getCopyMenuItem(isPublic, type === 'group', onCopyPublicKey, window.i18n)}
{getMarkAllReadMenuItem(onMarkAllRead, window.i18n)} {getMarkAllReadMenuItem(onMarkAllRead, window.i18n)}
{getChangeNicknameMenuItem(isMe, onChangeNickname, isGroup, window.i18n)}
{getClearNicknameMenuItem(isMe, hasNickname, onClearNickname, isGroup, window.i18n)}
{getDeleteMessagesMenuItem(isPublic, onDeleteMessages, window.i18n)} {getDeleteMessagesMenuItem(isPublic, onDeleteMessages, window.i18n)}
{getInviteContactMenuItem(type === 'group', isPublic, onInviteContacts, window.i18n)} {getInviteContactMenuItem(isGroup, isPublic, onInviteContacts, window.i18n)}
{getDeleteContactMenuItem( {getDeleteContactMenuItem(
isMe, isMe,
type === 'group', isGroup,
isPublic, isPublic,
left, left,
isKickedFromGroup, isKickedFromGroup,
onDeleteContact, onDeleteContact,
window.i18n window.i18n
)} )}
{getLeaveGroupMenuItem( {getLeaveGroupMenuItem(isKickedFromGroup, left, isGroup, isPublic, onLeaveGroup, window.i18n)}
isKickedFromGroup,
left,
type === 'group',
isPublic,
onLeaveGroup,
window.i18n
)}
</Menu> </Menu>
); );
}; };

@ -20,8 +20,12 @@ function showBlock(isMe: boolean, isPrivate: boolean): boolean {
return !isMe && isPrivate; return !isMe && isPrivate;
} }
function showClearNickname(isPublic: boolean, isMe: boolean, hasNickname: boolean): boolean { function showClearNickname(isMe: boolean, hasNickname: boolean, isGroup: boolean): boolean {
return !isPublic && !isMe && hasNickname; return !isMe && hasNickname && !isGroup;
}
function showChangeNickname(isMe: boolean, isGroup: boolean) {
return !isMe && !isGroup;
} }
function showDeleteMessages(isPublic: boolean): boolean { function showDeleteMessages(isPublic: boolean): boolean {
@ -252,18 +256,30 @@ export function getBlockMenuItem(
} }
export function getClearNicknameMenuItem( export function getClearNicknameMenuItem(
isPublic: boolean | undefined,
isMe: boolean | undefined, isMe: boolean | undefined,
hasNickname: boolean | undefined, hasNickname: boolean | undefined,
action: any, action: any,
isGroup: boolean | undefined,
i18n: LocalizerType i18n: LocalizerType
): JSX.Element | null { ): JSX.Element | null {
if (showClearNickname(Boolean(isPublic), Boolean(isMe), Boolean(hasNickname))) { if (showClearNickname(Boolean(isMe), Boolean(hasNickname), Boolean(isGroup))) {
return <Item onClick={action}>{i18n('clearNickname')}</Item>; return <Item onClick={action}>{i18n('clearNickname')}</Item>;
} }
return null; 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 <Item onClick={action}>{i18n('changeNickname')}</Item>;
}
return null;
}
export function getDeleteMessagesMenuItem( export function getDeleteMessagesMenuItem(
isPublic: boolean | undefined, isPublic: boolean | undefined,
action: any, action: any,

@ -422,19 +422,18 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
onUnblockContact: this.unblock, onUnblockContact: this.unblock,
onCopyPublicKey: this.copyPublicKey, onCopyPublicKey: this.copyPublicKey,
onDeleteContact: this.deleteContact, onDeleteContact: this.deleteContact,
onChangeNickname: this.changeNickname,
onClearNickname: this.clearNickname,
onDeleteMessages: this.deleteMessages,
onLeaveGroup: () => { onLeaveGroup: () => {
window.Whisper.events.trigger('leaveClosedGroup', this); window.Whisper.events.trigger('leaveClosedGroup', this);
}, },
onDeleteMessages: this.deleteMessages,
onInviteContacts: () => { onInviteContacts: () => {
window.Whisper.events.trigger('inviteContacts', this); window.Whisper.events.trigger('inviteContacts', this);
}, },
onMarkAllRead: () => { onMarkAllRead: () => {
void this.markReadBouncy(Date.now()); void this.markReadBouncy(Date.now());
}, },
onClearNickname: () => {
void this.setLokiProfile({ displayName: null });
},
}; };
} }
@ -1308,9 +1307,23 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
} }
public changeNickname() { 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() { public deleteContact() {
let title = window.i18n('delete'); let title = window.i18n('delete');
let message = window.i18n('deleteContactConfirmation'); let message = window.i18n('deleteContactConfirmation');

@ -92,6 +92,7 @@ export interface ConversationType {
onInviteContacts?: () => void; onInviteContacts?: () => void;
onMarkAllRead?: () => void; onMarkAllRead?: () => void;
onClearNickname?: () => void; onClearNickname?: () => void;
onChangeNickname?: () => void;
} }
export type ConversationLookupType = { export type ConversationLookupType = {

1
ts/window.d.ts vendored

@ -68,6 +68,7 @@ declare global {
setPassword: any; setPassword: any;
setSettingValue: any; setSettingValue: any;
showEditProfileDialog: any; showEditProfileDialog: any;
showNicknameDialog: any;
showResetSessionIdDialog: any; showResetSessionIdDialog: any;
storage: any; storage: any;
textsecure: LibTextsecure; textsecure: LibTextsecure;

Loading…
Cancel
Save