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
yarn-error.log
# editor
.vscode/

@ -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"
},

@ -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": {

@ -141,6 +141,7 @@
<!-- DIALOGS-->
<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/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/admin_leave_closed_group_dialog_view.js'></script>

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

@ -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,

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

@ -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 {
.session-icon-button {
display: flex;

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

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

@ -34,7 +34,12 @@ class InviteContactsDialogInner extends React.Component<Props, State> {
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;

@ -47,7 +47,12 @@ export class UpdateGroupMembersDialog extends React.Component<Props, State> {
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);

@ -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,
onDeleteMessages: conversation.deleteMessages,
onDeleteSelectedMessages: this.deleteSelectedMessages,
onChangeNickname: conversation.changeNickname,
onClearNickname: conversation.clearNickname,
onCloseOverlay: () => {
this.setState({ selectedMessages: [] });
},

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

@ -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 (
<Menu id={triggerId} animation={animation.fade}>
{getBlockMenuItem(
@ -65,34 +70,23 @@ export const ConversationListItemContextMenu = (props: PropsContextConversationI
onUnblockContact,
window.i18n
)}
{/* {!isPublic && !isMe ? (
<Item onClick={onChangeNickname}>
{i18n('changeNickname')}
</Item>
) : 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)}
</Menu>
);
};

@ -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 <Item onClick={action}>{i18n('clearNickname')}</Item>;
}
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(
isPublic: boolean | undefined,
action: any,

@ -422,19 +422,18 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
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<ConversationAttributes> {
}
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');

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

1
ts/window.d.ts vendored

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

Loading…
Cancel
Save