refactoring of account settings into multiple screens

This commit is contained in:
or-else
2020-04-13 16:01:24 +03:00
parent 5507ccd728
commit ea8ac3c293
25 changed files with 1494 additions and 823 deletions
+27 -17
View File
@@ -1,26 +1,32 @@
{
"validate_credential_action": "confirm",
"title_tag_manager": "Tags (user discovery)",
"label_user_contacts": "Contacts:",
"button_add_another": "Add another",
"label_message_sound": "Message sound:",
"label_push_notifications": "Notification alerts:",
"label_push_notifications_disabled": "Notification alerts (requires HTTPS):",
"label_incognito_mode": "Incognito mode:",
"delete_account": "Delete account",
"delete_account_arning": "Are you sure you want to delete your account? It cannot be undone.",
"label_password": "Password",
"password_unchanged_prompt": "Unchanged",
"button_logout": "Logout",
"button_delete_account": "Delete account",
"label_default_access_mode": "Default access mode:",
"link_contact_us": "Contact Us",
"link_terms_of_service": "Terms of Service",
"link_privacy_policy": "Privacy Policy",
"label_sdk": "SDK:",
"label_server_address": "Server address:",
"archived_contacts": "Archived contacts ({count})",
"contacts_not_found": "You have no chats<br />¯∖_(ツ)_/¯",
"full_name_prompt": "Full name, e.g. John Doe",
"email_prompt": "Email, e.g. jdoe@example.com",
"button_sign_up": "Sign up",
"validate_credential_action": "confirm",
"label_your_name": "Your name",
"label_password": "Password",
"password_unchanged_prompt": "Unchanged",
"label_message_sound": "Message sound:",
"label_push_notifications": "Notification alerts:",
"label_push_notifications_disabled": "Notification alerts (requires HTTPS):",
"label_incognito_mode": "Incognito mode:",
"title_tag_manager": "Tags (user discovery)",
"label_user_contacts": "Contacts:",
"label_user_id": "Address:",
"label_default_access_mode": "Default access mode:",
"button_logout": "Logout",
"label_legal": "Legal",
"link_contact_us": "Contact Us",
"link_terms_of_service": "Terms of Service",
"link_privacy_policy": "Privacy Policy",
"label_user_id": "ID:",
"button_edit": "Edit",
"requested_permissions": "Requested",
"granted_permissions": "Granted",
"menu_item_edit_permissions": "Edit permissions",
@@ -78,7 +84,11 @@
"sidepanel_title_login": "Sign In",
"sidepanel_title_register": "Create Account",
"sidepanel_title_settings": "Settings",
"sidepanel_title_edit_account": "Edit Account",
"sidepanel_title_account_settings": "Account Settings",
"sidepanel_title_acc_general": "General",
"sidepanel_title_acc_security": "Security",
"sidepanel_title_acc_notifications": "Notifications",
"sidepanel_title_acc_support": "Support",
"sidepanel_title_newtpk": "Start New Chat",
"sidepanel_title_cred": "Confirm Credentials",
"sidepanel_title_reset": "Reset Password",
+14 -4
View File
@@ -6,7 +6,7 @@
"label_your_name": "Ваше имя",
"label_password": "Пароль",
"password_unchanged_prompt": "Не изменен",
"label_user_id": "Адрес:",
"label_user_id": "ID:",
"label_default_access_mode": "Доступ по умолчанию:",
"label_message_sound": "Звук нового сообщения:",
"label_push_notifications": "Уведомления:",
@@ -34,7 +34,7 @@
"sidepanel_title_login": "Авторизация",
"sidepanel_title_register": "Зарегистрироваться",
"sidepanel_title_settings": "Настройки",
"sidepanel_title_edit_account": "Редактировать аккаунт",
"sidepanel_title_account_settings": "Настройки аккаунта",
"sidepanel_title_newtpk": "Новый чат",
"sidepanel_title_cred": "Подтвердить",
"sidepanel_title_reset": "Сменить пароль",
@@ -135,7 +135,6 @@
"more_online_members": "+еще {overflow}",
"label_user_contacts": "Конакты:",
"validate_credential_action": "подтвердить",
"label_legal": "Прочее",
"link_contact_us": "Связаться с нами",
"link_terms_of_service": "Условия сервиса",
"link_privacy_policy": "Политика конфиденциальности",
@@ -153,5 +152,16 @@
"search_no_results": "Ничего не найдено",
"deleted_content": "удалено",
"image_caption_prompt": "Подпись к фото",
"label_incognito_mode": "Режим инкогнито:"
"label_incognito_mode": "Режим инкогнито:",
"delete_account": "Удалить аккаунт",
"delete_account_arning": "Вы уверены, что ходите удалить свой аккаунт? Его невозможно будет восстановить.",
"button_delete_account": "Удалить аккаунт",
"sidepanel_title_acc_security": "Безопасность",
"sidepanel_title_acc_notifications": "Уведомления",
"sidepanel_title_acc_support": "Поддержка",
"label_sdk": "SDK:",
"label_server_address": "Адрес сервера:",
"button_edit": "Редактировать",
"button_add_another": "Добавить",
"sidepanel_title_acc_general": "Общие настройки"
}
+13 -3
View File
@@ -61,7 +61,7 @@
"sidepanel_title_login": "登录",
"sidepanel_title_register": "创建账户",
"sidepanel_title_settings": "设置",
"sidepanel_title_edit_account": "编辑账户",
"sidepanel_title_account_settings": "帐号设定",
"sidepanel_title_newtpk": "开始新会话",
"sidepanel_title_cred": "确认凭据",
"sidepanel_title_reset": "重置密码",
@@ -134,7 +134,6 @@
"more_online_members": "还有{overflow}个",
"label_user_contacts": "往来:",
"validate_credential_action": "确认",
"label_legal": "法律东西",
"link_contact_us": "联系我们",
"link_terms_of_service": "条款和条件",
"link_privacy_policy": "隐私政策",
@@ -153,5 +152,16 @@
"deleted_content": "内容已删除",
"image_caption_prompt": "图片标题",
"menu_item_send_retry": "重试",
"label_incognito_mode": "无痕模式:"
"label_incognito_mode": "无痕模式:",
"delete_account": "删除帐户",
"delete_account_arning": "您确定要删除您的帐户吗?无法撤消。",
"button_delete_account": "删除帐户",
"sidepanel_title_acc_security": "安全",
"sidepanel_title_acc_notifications": "通知",
"sidepanel_title_acc_support": "支持",
"label_sdk": "开发包:",
"label_server_address": "服务器地址:",
"button_edit": "编辑",
"button_add_another": "加上另一个",
"sidepanel_title_acc_general": "常用设定"
}
+54 -24
View File
@@ -1,27 +1,33 @@
{
"en": {
"validate_credential_action": "confirm",
"title_tag_manager": "Tags (user discovery)",
"label_user_contacts": "Contacts:",
"button_add_another": "Add another",
"label_message_sound": "Message sound:",
"label_push_notifications": "Notification alerts:",
"label_push_notifications_disabled": "Notification alerts (requires HTTPS):",
"label_incognito_mode": "Incognito mode:",
"delete_account": "Delete account",
"delete_account_arning": "Are you sure you want to delete your account? It cannot be undone.",
"label_password": "Password",
"password_unchanged_prompt": "Unchanged",
"button_logout": "Logout",
"button_delete_account": "Delete account",
"label_default_access_mode": "Default access mode:",
"link_contact_us": "Contact Us",
"link_terms_of_service": "Terms of Service",
"link_privacy_policy": "Privacy Policy",
"label_sdk": "SDK:",
"label_server_address": "Server address:",
"archived_contacts": "Archived contacts ({count})",
"contacts_not_found": "You have no chats<br />¯∖_(ツ)_/¯",
"full_name_prompt": "Full name, e.g. John Doe",
"email_prompt": "Email, e.g. jdoe@example.com",
"button_sign_up": "Sign up",
"validate_credential_action": "confirm",
"label_your_name": "Your name",
"label_password": "Password",
"password_unchanged_prompt": "Unchanged",
"label_message_sound": "Message sound:",
"label_push_notifications": "Notification alerts:",
"label_push_notifications_disabled": "Notification alerts (requires HTTPS):",
"label_incognito_mode": "Incognito mode:",
"title_tag_manager": "Tags (user discovery)",
"label_user_contacts": "Contacts:",
"label_user_id": "Address:",
"label_default_access_mode": "Default access mode:",
"button_logout": "Logout",
"label_legal": "Legal",
"link_contact_us": "Contact Us",
"link_terms_of_service": "Terms of Service",
"link_privacy_policy": "Privacy Policy",
"label_user_id": "ID:",
"button_edit": "Edit",
"requested_permissions": "Requested",
"granted_permissions": "Granted",
"menu_item_edit_permissions": "Edit permissions",
@@ -79,7 +85,11 @@
"sidepanel_title_login": "Sign In",
"sidepanel_title_register": "Create Account",
"sidepanel_title_settings": "Settings",
"sidepanel_title_edit_account": "Edit Account",
"sidepanel_title_account_settings": "Account Settings",
"sidepanel_title_acc_general": "General",
"sidepanel_title_acc_security": "Security",
"sidepanel_title_acc_notifications": "Notifications",
"sidepanel_title_acc_support": "Support",
"sidepanel_title_newtpk": "Start New Chat",
"sidepanel_title_cred": "Confirm Credentials",
"sidepanel_title_reset": "Reset Password",
@@ -164,7 +174,7 @@
"label_your_name": "Ваше имя",
"label_password": "Пароль",
"password_unchanged_prompt": "Не изменен",
"label_user_id": "Адрес:",
"label_user_id": "ID:",
"label_default_access_mode": "Доступ по умолчанию:",
"label_message_sound": "Звук нового сообщения:",
"label_push_notifications": "Уведомления:",
@@ -192,7 +202,7 @@
"sidepanel_title_login": "Авторизация",
"sidepanel_title_register": "Зарегистрироваться",
"sidepanel_title_settings": "Настройки",
"sidepanel_title_edit_account": "Редактировать аккаунт",
"sidepanel_title_account_settings": "Настройки аккаунта",
"sidepanel_title_newtpk": "Новый чат",
"sidepanel_title_cred": "Подтвердить",
"sidepanel_title_reset": "Сменить пароль",
@@ -293,7 +303,6 @@
"more_online_members": "+еще {overflow}",
"label_user_contacts": "Конакты:",
"validate_credential_action": "подтвердить",
"label_legal": "Прочее",
"link_contact_us": "Связаться с нами",
"link_terms_of_service": "Условия сервиса",
"link_privacy_policy": "Политика конфиденциальности",
@@ -311,7 +320,18 @@
"search_no_results": "Ничего не найдено",
"deleted_content": "удалено",
"image_caption_prompt": "Подпись к фото",
"label_incognito_mode": "Режим инкогнито:"
"label_incognito_mode": "Режим инкогнито:",
"delete_account": "Удалить аккаунт",
"delete_account_arning": "Вы уверены, что ходите удалить свой аккаунт? Его невозможно будет восстановить.",
"button_delete_account": "Удалить аккаунт",
"sidepanel_title_acc_security": "Безопасность",
"sidepanel_title_acc_notifications": "Уведомления",
"sidepanel_title_acc_support": "Поддержка",
"label_sdk": "SDK:",
"label_server_address": "Адрес сервера:",
"button_edit": "Редактировать",
"button_add_another": "Добавить",
"sidepanel_title_acc_general": "Общие настройки"
},
"zh": {
"archived_contacts": "已归档联系人 ({count})",
@@ -376,7 +396,7 @@
"sidepanel_title_login": "登录",
"sidepanel_title_register": "创建账户",
"sidepanel_title_settings": "设置",
"sidepanel_title_edit_account": "编辑账户",
"sidepanel_title_account_settings": "帐号设定",
"sidepanel_title_newtpk": "开始新会话",
"sidepanel_title_cred": "确认凭据",
"sidepanel_title_reset": "重置密码",
@@ -449,7 +469,6 @@
"more_online_members": "还有{overflow}个",
"label_user_contacts": "往来:",
"validate_credential_action": "确认",
"label_legal": "法律东西",
"link_contact_us": "联系我们",
"link_terms_of_service": "条款和条件",
"link_privacy_policy": "隐私政策",
@@ -468,6 +487,17 @@
"deleted_content": "内容已删除",
"image_caption_prompt": "图片标题",
"menu_item_send_retry": "重试",
"label_incognito_mode": "无痕模式:"
"label_incognito_mode": "无痕模式:",
"delete_account": "删除帐户",
"delete_account_arning": "您确定要删除您的帐户吗?无法撤消。",
"button_delete_account": "删除帐户",
"sidepanel_title_acc_security": "安全",
"sidepanel_title_acc_notifications": "通知",
"sidepanel_title_acc_support": "支持",
"label_sdk": "开发包:",
"label_server_address": "服务器地址:",
"button_edit": "编辑",
"button_add_another": "加上另一个",
"sidepanel_title_acc_general": "常用设定"
}
}
+186
View File
@@ -0,0 +1,186 @@
// Edit account parameters.
import React from 'react';
import { FormattedMessage } from 'react-intl';
import Tinode from 'tinode-sdk';
import AvatarUpload from '../widgets/avatar-upload.jsx';
import InPlaceEdit from '../widgets/in-place-edit.jsx';
import TagManager from '../widgets/tag-manager.jsx';
import { MAX_TITLE_LENGTH } from '../config.js';
import { makeImageUrl } from '../lib/blob-helpers.js';
import { arrayEqual, asEmail, asPhone, vcard } from '../lib/utils.js';
export default class AccGeneralView extends React.Component {
constructor(props) {
super(props);
const me = this.props.tinode.getMeTopic();
this.state = {
fullName: me.public ? me.public.fn : undefined,
avatar: makeImageUrl(me.public ? me.public.photo : null),
tags: me.tags(),
credentials: me.getCredentials() || [],
addCredActive: false,
addCredInvalid: false,
newCred: '',
previousOnTags: me.onTagsUpdated
};
this.tnNewTags = this.tnNewTags.bind(this);
this.tnCredsUpdated = this.tnCredsUpdated.bind(this);
this.handleFullNameUpdate = this.handleFullNameUpdate.bind(this);
this.handleImageChanged = this.handleImageChanged.bind(this);
this.handleCredChange = this.handleCredChange.bind(this);
this.handleCredKeyDown = this.handleCredKeyDown.bind(this);
this.handleCredEntered = this.handleCredEntered.bind(this);
this.handleTagsUpdated = this.handleTagsUpdated.bind(this);
}
componentDidMount() {
const me = this.props.tinode.getMeTopic();
me.onCredsUpdated = this.tnCredsUpdated;
me.onTagsUpdated = this.tnNewTags;
}
componentWillUnmount() {
const me = this.props.tinode.getMeTopic();
me.onTagsUpdated = this.state.previousOnTags;
me.onCredsUpdated = undefined;
}
tnNewTags(tags) {
this.setState({tags: tags});
}
tnCredsUpdated(creds) {
this.setState({credentials: creds || []});
}
handleFullNameUpdate(fn) {
fn = fn.trim().substring(0, MAX_TITLE_LENGTH);
if (fn) {
this.setState({fullName: fn});
this.props.onUpdateAccount(undefined, vcard(fn, null));
}
}
handleImageChanged(img) {
this.setState({avatar: img});
this.props.onUpdateAccount(undefined, vcard(null, img || Tinode.DEL_CHAR));
}
handleCredChange(e) {
this.setState({newCred: e.target.value, addCredInvalid: false});
}
handleCredKeyDown(e) {
if (e.keyCode === 27) {
// Escape pressed
this.setState({newCred: '', addCredActive: false});
} else if (e.keyCode === 13) {
// Enter pressed
this.handleCredEntered(e);
}
}
handleCredEntered(e) {
let value = this.state.newCred.trim();
if (!value) {
this.setState({addCredActive: false, addCredInvalid: false});
return;
}
let val = asPhone(value);
let method;
if (val) {
method = 'tel';
} else {
val = asEmail(value);
if (val) {
method = 'email';
}
}
if (method) {
this.props.onCredAdd(method, val);
this.setState({addCredActive: false, newCred: ''});
} else {
this.setState({addCredInvalid: true});
}
}
handleTagsUpdated(tags) {
// Check if tags have actually changed.
if (arrayEqual(this.state.tags.slice(0), tags.slice(0))) {
return;
}
this.props.onUpdateTags(tags);
}
render() {
const credentials = [];
this.state.credentials.map((cred) => {
credentials.push(<div key={cred.meth + ":" + cred.val + ":" + cred.done}>{cred.meth}: <tt>{cred.val}</tt>
<span > {!cred.done ?
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCredConfirm(cred.meth, cred.val);}}>
<FormattedMessage id="validate_credential_action" defaultMessage="confirm"
description="Validate credentail call to action" />
</a>
: null} <a href="#" onClick={(e) => {e.preventDefault(); this.props.onCredDelete(cred.meth, cred.val);}}><i
className="material-icons gray">delete_outline</i></a></span></div>);
});
return (
<div className="scrollable-panel">
<div className="panel-form-row">
<div className="panel-form-column">
<label className="small"><FormattedMessage id="label_your_name" /></label>
<div><FormattedMessage id="full_name_prompt">{
(full_name_placeholder) => <InPlaceEdit
placeholder={full_name_placeholder}
value={this.state.fullName}
onFinished={this.handleFullNameUpdate} />
}</FormattedMessage></div>
</div>
<AvatarUpload
avatar={this.state.avatar}
uid={this.props.myUserId}
title={this.state.fullName}
onImageChanged={this.handleImageChanged}
onError={this.props.onError} />
</div>
<div className="hr" />
<FormattedMessage id="title_tag_manager" defaultMessage="Tags (user discovery)"
description="Section title for TagManager">{
(title_tag_manager) => <TagManager
title={title_tag_manager}
activated={false}
tags={this.state.tags}
onSubmit={this.handleTagsUpdated} />
}</FormattedMessage>
<div className="hr" />
<div className="panel-form-column">
<label className="small">
<FormattedMessage id="label_user_contacts" defaultMessage="Contacts:"
description="Label for user contacts" />
</label>
<div className="quoted">
{credentials}
{this.state.addCredActive ?
<input type="text" value={this.state.value}
className={this.state.addCredInvalid ? 'invalid' : null}
placeholder="Phone number or email" required="required" autoFocus
onChange={this.handleCredChange} onKeyDown={this.handleCredKeyDown} onBlur={this.handleCredEntered} />
: null}
<div>
<a href="#" onClick={(e) => {e.preventDefault(); this.setState({addCredActive: true});}}>
<i className="material-icons">add</i>
<FormattedMessage id="button_add_another" defaultMessage="Add another" description="Call to action [+ add another]" />
</a></div>
</div>
</div>
</div>
);
}
};
+61
View File
@@ -0,0 +1,61 @@
// Edit account parameters.
import React from 'react';
import { FormattedMessage } from 'react-intl';
import CheckBox from '../widgets/checkbox.jsx';
export default class EditAccountView extends React.PureComponent {
constructor(props) {
super(props);
this.handleCheckboxClick = this.handleCheckboxClick.bind(this);
}
handleCheckboxClick(what, checked) {
if (what == 'sound') {
this.props.onToggleMessageSounds(checked);
} else if (what == 'alert') {
this.props.onTogglePushNotifications(checked);
} else if (what == 'incognito') {
this.props.onToggleIncognitoMode(checked);
}
}
render() {
return (
<div className="scrollable-panel">
<div className="panel-form-row">
<label htmlFor="message-sound">
<FormattedMessage id="label_message_sound" defaultMessage="Message sound:"
description="Label for message sounds toggle" />
</label>
<CheckBox name="sound" id="message-sound"
checked={this.props.messageSounds} onChange={this.handleCheckboxClick} />
</div>
<div className="panel-form-row">
<label htmlFor="desktop-alerts">
{this.props.desktopAlertsEnabled ?
<FormattedMessage id="label_push_notifications" defaultMessage="Notification alerts:"
description="Label for push notifications switch" />
:
<FormattedMessage id="label_push_notifications_disabled"
defaultMessage="Notification alerts (requires HTTPS):"
description="Label for push notifications switch" />
}
</label>
<CheckBox name="alert" id="desktop-alerts"
checked={this.props.desktopAlerts}
onChange={this.props.desktopAlertsEnabled ? this.handleCheckboxClick : null} />
</div>
<div className="panel-form-row">
<label htmlFor="incognito-mode">
<FormattedMessage id="label_incognito_mode" defaultMessage="Incognito mode:"
description="Label for incognito mode toggle" />
</label>
<CheckBox name="incognito" id="incognito-mode"
checked={this.props.incognitoMode} onChange={this.handleCheckboxClick} />
</div>
</div>
);
}
};
+134
View File
@@ -0,0 +1,134 @@
// Edit account parameters.
import React from 'react';
import { FormattedMessage, defineMessages } from 'react-intl';
import InPlaceEdit from '../widgets/in-place-edit.jsx';
import PermissionsEditor from '../widgets/permissions-editor.jsx';
const messages = defineMessages({
delete_account: {
id: 'delete_account',
defaultMessage: 'Delete account',
description: 'Title for delete account warning'
},
delete_account_warning: {
id: 'delete_account_arning',
defaultMessage: 'Are you sure you want to delete your account? It cannot be undone.',
description: 'Warning message when deleting an account'
}
});
export default class AccSecurityView extends React.Component {
constructor(props) {
super(props);
const me = this.props.tinode.getMeTopic();
const defacs = me.getDefaultAccess();
this.state = {
auth: defacs ? defacs.auth : null,
anon: defacs ? defacs.anon : null,
showPermissionEditorFor: undefined,
};
this.handlePasswordUpdate = this.handlePasswordUpdate.bind(this);
this.handleLaunchPermissionsEditor = this.handleLaunchPermissionsEditor.bind(this);
this.handleHidePermissionsEditor = this.handleHidePermissionsEditor.bind(this);
this.handlePermissionsChanged = this.handlePermissionsChanged.bind(this);
this.handleDeleteAccount = this.handleDeleteAccount.bind(this);
}
handlePasswordUpdate(pwd) {
this.setState({password: pwd});
this.props.onUpdateAccount(pwd);
}
handleLaunchPermissionsEditor(which) {
this.setState({
showPermissionEditorFor: which,
editedPermissions: this.state[which]
});
}
handleHidePermissionsEditor() {
this.setState({showPermissionEditorFor: undefined});
}
handlePermissionsChanged(perm) {
let defacs = {};
defacs[this.state.showPermissionEditorFor] = perm;
this.props.onUpdateAccount(undefined, undefined, defacs);
let newState = {showPermissionEditorFor: undefined};
newState[this.state.showPermissionEditorFor] = perm;
this.setState(newState);
}
handleDeleteAccount(e) {
e.preventDefault();
const {formatMessage} = this.props.intl;
this.props.onShowAlert(
formatMessage(messages.delete_account), // title
formatMessage(messages.delete_account_warning), // content
(() => { this.props.onDeleteAccount(); }), // onConfirm
null, // "OK"
null, // Show Reject button
true // "Cancel"
);
}
render() {
return (
<React.Fragment>{this.state.showPermissionEditorFor ?
<PermissionsEditor
mode={this.state.editedPermissions}
skip="O"
onSubmit={this.handlePermissionsChanged}
onCancel={this.handleHidePermissionsEditor} />
:
<div className="scrollable-panel">
<div className="panel-form-column">
<label className="small">
<FormattedMessage id="label_password" defaultMessage="Password"
description="Label for password editing" />
</label>
<div>
<FormattedMessage id="password_unchanged_prompt" defaultMessage="Unchanged"
description="Message in editor while password is unchanged">{
(password_unchanged) => <InPlaceEdit
placeholder={password_unchanged}
type="password"
onFinished={this.handlePasswordUpdate} />
}</FormattedMessage>
</div>
</div>
<div className="hr" />
<div className="panel-form-column">
<a href="#" className="red flat-button" onClick={(e) => {e.preventDefault(); this.props.onLogout();}}>
<i className="material-icons">exit_to_app</i> &nbsp;<FormattedMessage id="button_logout"
defaultMessage="Logout" description="Button [Logout]" />
</a>
<a href="#" className="red flat-button" onClick={(e) => {this.handleDeleteAccount(e);}}>
<i className="material-icons">delete</i> &nbsp;<FormattedMessage id="button_delete_account"
defaultMessage="Delete account" description="Button [Delete account]" />
</a>
</div>
<div className="hr" />
<div className="panel-form-column">
<div>
<label className="small">
<FormattedMessage id="label_default_access_mode" defaultMessage="Default access mode:"
description="Label for default access mode" />
</label>
</div>
<div className="quoted">
<div>Auth: <tt className="clickable"
onClick={this.handleLaunchPermissionsEditor.bind(this, 'auth')}>{this.state.auth}</tt></div>
<div>Anon: <tt className="clickable"
onClick={this.handleLaunchPermissionsEditor.bind(this, 'anon')}>{this.state.anon}</tt></div>
</div>
</div>
</div>
}</React.Fragment>
);
}
};
+49
View File
@@ -0,0 +1,49 @@
// Edit account parameters.
import React from 'react';
import { FormattedMessage } from 'react-intl';
import Tinode from 'tinode-sdk';
import { APP_NAME, LINK_CONTACT_US, LINK_PRIVACY_POLICY, LINK_TERMS_OF_SERVICE } from '../config.js';
export default class AccSupportView extends React.PureComponent {
render() {
return (
<div className="scrollable-panel">
<div className="panel-form-column">
<a href={LINK_CONTACT_US} className="flat-button" target="_blank">
<i className="material-icons">email</i> &nbsp;<FormattedMessage id="link_contact_us"
defaultMessage="Contact Us" description="Ancor text for contacting us by email" />
</a>
<a href={LINK_TERMS_OF_SERVICE} className="flat-button" target="_blank">
<i className="material-icons">description</i> &nbsp;<FormattedMessage id="link_terms_of_service"
defaultMessage="Terms of Service" description="Ancor text for terms of service link" />
</a>
<a href={LINK_PRIVACY_POLICY} className="flat-button" target="_blank">
<i className="material-icons">policy</i> &nbsp;<FormattedMessage id="link_privacy_policy"
defaultMessage="Privacy Policy" description="Ancor text for privacy policy link" />
</a>
</div>
<div className="hr" />
<div className="panel-form-column">
<div className="panel-form-row">
<label className="small"><FormattedMessage id="label_client" /></label>
{APP_NAME}
</div>
<div className="panel-form-row">
<label className="small"><FormattedMessage id="label_sdk" defaultMessage="SDK:" /></label>
{Tinode.getLibrary()}
</div>
<div className="panel-form-row">
<label className="small"><FormattedMessage id="label_server" /></label>
{this.props.serverVersion}
</div>
<div className="panel-form-row">
<label className="small"><FormattedMessage id="label_server_address" defaultMessage="Server address:" /></label>
{this.props.serverAddress}
</div>
</div>
</div>
);
}
};
+34 -312
View File
@@ -2,340 +2,62 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import Tinode from 'tinode-sdk';
import AvatarUpload from '../widgets/avatar-upload.jsx';
import CheckBox from '../widgets/checkbox.jsx';
import InPlaceEdit from '../widgets/in-place-edit.jsx';
import PermissionsEditor from '../widgets/permissions-editor.jsx';
import TagManager from '../widgets/tag-manager.jsx';
import { LINK_CONTACT_US, LINK_PRIVACY_POLICY, LINK_TERMS_OF_SERVICE, MAX_TITLE_LENGTH } from '../config.js';
import { makeImageUrl } from '../lib/blob-helpers.js';
import { arrayEqual, asEmail, asPhone, vcard } from '../lib/utils.js';
export default class EditAccountView extends React.Component {
constructor(props) {
super(props);
const me = this.props.tinode.getMeTopic();
const defacs = me.getDefaultAccess();
this.state = {
fullName: me.public ? me.public.fn : undefined,
avatar: makeImageUrl(me.public ? me.public.photo : null),
auth: defacs ? defacs.auth : null,
anon: defacs ? defacs.anon : null,
tags: me.tags(),
credentials: me.getCredentials() || [],
addCredActive: false,
addCredInvalid: false,
newCred: '',
showPermissionEditorFor: undefined,
previousOnTags: me.onTagsUpdated
avatar: makeImageUrl(me.public ? me.public.photo : null)
};
this.tnNewTags = this.tnNewTags.bind(this);
this.tnCredsUpdated = this.tnCredsUpdated.bind(this);
this.handleFullNameUpdate = this.handleFullNameUpdate.bind(this);
this.handlePasswordUpdate = this.handlePasswordUpdate.bind(this);
this.handleImageChanged = this.handleImageChanged.bind(this);
this.handleCheckboxClick = this.handleCheckboxClick.bind(this);
this.handleCredChange = this.handleCredChange.bind(this);
this.handleCredKeyDown = this.handleCredKeyDown.bind(this);
this.handleCredEntered = this.handleCredEntered.bind(this);
this.handleLaunchPermissionsEditor = this.handleLaunchPermissionsEditor.bind(this);
this.handleHidePermissionsEditor = this.handleHidePermissionsEditor.bind(this);
this.handlePermissionsChanged = this.handlePermissionsChanged.bind(this);
this.handleTagsUpdated = this.handleTagsUpdated.bind(this);
}
componentDidMount() {
const me = this.props.tinode.getMeTopic();
me.onCredsUpdated = this.tnCredsUpdated;
me.onTagsUpdated = this.tnNewTags;
}
componentWillUnmount() {
const me = this.props.tinode.getMeTopic();
me.onTagsUpdated = this.state.previousOnTags;
me.onCredsUpdated = undefined;
}
tnNewTags(tags) {
this.setState({tags: tags});
}
tnCredsUpdated(creds) {
this.setState({credentials: creds || []});
}
handleFullNameUpdate(fn) {
fn = fn.trim().substring(0, MAX_TITLE_LENGTH);
if (fn) {
this.setState({fullName: fn});
this.props.onUpdateAccount(undefined, vcard(fn, null));
}
}
handlePasswordUpdate(pwd) {
this.setState({password: pwd});
this.props.onUpdateAccount(pwd);
}
handleImageChanged(img) {
this.setState({avatar: img});
this.props.onUpdateAccount(undefined, vcard(null, img || Tinode.DEL_CHAR));
}
handleCheckboxClick(what, checked) {
if (what == 'sound') {
this.props.onToggleMessageSounds(checked);
} else if (what == 'alert') {
this.props.onTogglePushNotifications(checked);
} else if (what == 'incognito') {
this.props.onToggleIncognitoMode(checked);
}
}
handleCredChange(e) {
this.setState({newCred: e.target.value, addCredInvalid: false});
}
handleCredKeyDown(e) {
if (e.keyCode === 27) {
// Escape pressed
this.setState({newCred: '', addCredActive: false});
} else if (e.keyCode === 13) {
// Enter pressed
this.handleCredEntered(e);
}
}
handleCredEntered(e) {
let value = this.state.newCred.trim();
if (!value) {
this.setState({addCredActive: false, addCredInvalid: false});
return;
}
let val = asPhone(value);
let method;
if (val) {
method = 'tel';
} else {
val = asEmail(value);
if (val) {
method = 'email';
}
}
if (method) {
this.props.onCredAdd(method, val);
this.setState({addCredActive: false, newCred: ''});
} else {
this.setState({addCredInvalid: true});
}
}
handleLaunchPermissionsEditor(which) {
this.setState({
showPermissionEditorFor: which,
editedPermissions: this.state[which]
});
}
handleHidePermissionsEditor() {
this.setState({showPermissionEditorFor: undefined});
}
handlePermissionsChanged(perm) {
let defacs = {};
defacs[this.state.showPermissionEditorFor] = perm;
this.props.onUpdateAccount(undefined, undefined, defacs);
let newState = {showPermissionEditorFor: undefined};
newState[this.state.showPermissionEditorFor] = perm;
this.setState(newState);
}
handleTagsUpdated(tags) {
// Check if tags have actually changed.
if (arrayEqual(this.state.tags.slice(0), tags.slice(0))) {
return;
}
this.props.onUpdateTags(tags);
}
render() {
let credentials = [];
this.state.credentials.map((cred) => {
credentials.push(<div key={cred.meth + ":" + cred.val + ":" + cred.done}>{cred.meth}: <tt>{cred.val}</tt>
<span > {!cred.done ?
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCredConfirm(cred.meth, cred.val);}}>
<FormattedMessage id="validate_credential_action" defaultMessage="confirm"
description="Validate credentail call to action" />
</a>
: null} <a href="#" onClick={(e) => {e.preventDefault(); this.props.onCredDelete(cred.meth, cred.val);}}><i
className="material-icons gray">delete_outline</i></a></span></div>);
});
return (
<React.Fragment>{this.state.showPermissionEditorFor ?
<PermissionsEditor
mode={this.state.editedPermissions}
skip="O"
onSubmit={this.handlePermissionsChanged}
onCancel={this.handleHidePermissionsEditor} />
:
<div className="scrollable-panel">
<div className="panel-form-row">
<div className="panel-form-column">
<div><label className="small">
<FormattedMessage id="label_your_name" defaultMessage="Your name"
description="Label for full name editing" />
</label></div>
<div><FormattedMessage id="full_name_prompt">{
(full_name_placeholder) => <InPlaceEdit
placeholder={full_name_placeholder}
value={this.state.fullName}
onFinished={this.handleFullNameUpdate} />
}</FormattedMessage></div>
<div><label className="small">
<FormattedMessage id="label_password" defaultMessage="Password"
description="Label for password editing" />
</label></div>
<div>
<FormattedMessage id="password_unchanged_prompt" defaultMessage="Unchanged"
description="Message in editor while password is unchanged">{
(password_unchanged) => <InPlaceEdit
placeholder={password_unchanged}
type="password"
onFinished={this.handlePasswordUpdate} />
}</FormattedMessage>
</div>
</div>
<AvatarUpload
avatar={this.state.avatar}
uid={this.props.myUserId}
title={this.state.fullName}
onImageChanged={this.handleImageChanged}
onError={this.props.onError} />
</div>
<div className="hr" />
<div className="panel-form-row">
<label htmlFor="message-sound">
<FormattedMessage id="label_message_sound" defaultMessage="Message sound:"
description="Label for message sounds toggle" />
</label>
<CheckBox name="sound" id="message-sound"
checked={this.props.messageSounds} onChange={this.handleCheckboxClick} />
</div>
<div className="panel-form-row">
<label htmlFor="desktop-alerts">
{this.props.desktopAlertsEnabled ?
<FormattedMessage id="label_push_notifications" defaultMessage="Notification alerts:"
description="Label for push notifications switch" />
:
<FormattedMessage id="label_push_notifications_disabled"
defaultMessage="Notification alerts (requires HTTPS):"
description="Label for push notifications switch" />
}
</label>
<CheckBox name="alert" id="desktop-alerts"
checked={this.props.desktopAlerts}
onChange={this.props.desktopAlertsEnabled ? this.handleCheckboxClick : null} />
</div>
<div className="panel-form-row">
<label htmlFor="incognito-mode">
<FormattedMessage id="label_incognito_mode" defaultMessage="Incognito mode:"
description="Label for incognito mode toggle" />
</label>
<CheckBox name="incognito" id="incognito-mode"
checked={this.props.incognitoMode} onChange={this.handleCheckboxClick} />
</div>
<div className="hr" />
<FormattedMessage id="title_tag_manager" defaultMessage="Tags (user discovery)"
description="Section title for TagManager">{
(title_tag_manager) => <TagManager
title={title_tag_manager}
activated={false}
tags={this.state.tags}
onSubmit={this.handleTagsUpdated} />
}</FormattedMessage>
<div className="hr" />
<div className="scrollable-panel">
<div className="panel-form-row">
<div className="panel-form-column">
<label className="small">
<FormattedMessage id="label_your_name" defaultMessage="Your name"
description="Label for full name editing" />
</label>
<div className="large">{this.state.fullName}</div>
<div>
<label className="small">
<FormattedMessage id="label_user_contacts" defaultMessage="Contacts:"
description="Label for user contacts" />
</label>
</div>
<div className="quoted">
{credentials}
{this.state.addCredActive ?
<input type="text" value={this.state.value}
className={this.state.addCredInvalid ? "invalid" : null}
placeholder="Phone number or email" required="required" autoFocus
onChange={this.handleCredChange} onKeyDown={this.handleCredKeyDown} onBlur={this.handleCredEntered} />
: null}
<div><a href="#" onClick={(e) => {e.preventDefault(); this.setState({addCredActive: true});}}>+ Add another</a></div>
</div>
</div>
<div className="hr" />
<div className="panel-form-column">
<div className="panel-form-row">
<label className="small"><FormattedMessage id="label_user_id" defaultMessage="Address:"
description="Label for user address (ID)" /></label>
<label className="small"><FormattedMessage id="label_user_id" defaultMessage="ID:"
description="Label for user address (ID)" /></label>&nbsp;
<tt>{this.props.myUserId}</tt>
</div>
<div>
<label className="small">
<FormattedMessage id="label_default_access_mode" defaultMessage="Default access mode:"
description="Label for default access mode" />
</label>
</div>
<div className="quoted">
<div>Auth: <tt className="clickable"
onClick={this.handleLaunchPermissionsEditor.bind(this, 'auth')}>{this.state.auth}</tt></div>
<div>Anon: <tt className="clickable"
onClick={this.handleLaunchPermissionsEditor.bind(this, 'anon')}>{this.state.anon}</tt></div>
</div>
</div>
<div className="hr" />
<div className="panel-form-column">
<a href="#" className="red flat-button" onClick={(e) => {e.preventDefault(); this.props.onLogout();}}>
<i className="material-icons">exit_to_app</i> &nbsp;<FormattedMessage id="button_logout"
defaultMessage="Logout" description="Button [Logout]" />
</a>
</div>
<div className="hr" />
<div className="panel-form-column">
<div>
<label className="small">
<FormattedMessage id="label_legal" defaultMessage="Legal"
description="Section label" />
</label>
</div>
<div className="quoted">
<a href={LINK_CONTACT_US} className="flat-button" target="_blank">
<i className="material-icons">email</i> &nbsp;<FormattedMessage id="link_contact_us"
defaultMessage="Contact Us" description="Ancor text for contacting us by email" />
</a>
</div>
<div className="quoted">
<a href={LINK_TERMS_OF_SERVICE} className="flat-button" target="_blank">
<i className="material-icons">description</i> &nbsp;<FormattedMessage id="link_terms_of_service"
defaultMessage="Terms of Service" description="Ancor text for terms of service link" />
</a>
</div>
<div className="quoted">
<a href={LINK_PRIVACY_POLICY} className="flat-button" target="_blank">
<i className="material-icons">policy</i> &nbsp;<FormattedMessage id="link_privacy_policy"
defaultMessage="Privacy Policy" description="Ancor text for privacy policy link" />
</a>
</div>
</div>
<AvatarUpload
avatar={this.state.avatar}
readOnly={!this.state.owner}
uid={this.props.myUserId}
title={this.state.fullName} />
</div>
}</React.Fragment>
<div className="panel-form-row">
<a href="#" className="flat-button" onClick={(e) => {e.preventDefault(); this.props.onBasicNavigate('general');}}>
<i className="material-icons">edit</i>&nbsp;
<FormattedMessage id="button_edit" defaultMessage="Edit" description="Call to action [Edit]" />
</a>
</div>
<div className="hr" />
<div className="panel-form-column">
<a href="#" className="flat-button" onClick={(e) => {e.preventDefault(); this.props.onBasicNavigate('notif');}}>
<i className="material-icons">notifications</i>&nbsp;<FormattedMessage id="sidepanel_title_acc_notifications" />
</a>
<a href="#" className="flat-button" onClick={(e) => {e.preventDefault(); this.props.onBasicNavigate('security');}}>
<i className="material-icons">security</i>&nbsp;<FormattedMessage id="sidepanel_title_acc_security" />
</a>
<a href="#" className="flat-button" onClick={(e) => {e.preventDefault(); this.props.onBasicNavigate('support');}}>
<i className="material-icons">contact_support</i>&nbsp;<FormattedMessage id="sidepanel_title_acc_support" />
</a>
</div>
</div>
);
}
};
+2 -2
View File
@@ -8,9 +8,9 @@ import { APP_NAME } from '../config.js';
export default class LogoView extends React.PureComponent {
render() {
var version = APP_NAME + ' (' + Tinode.getLibrary() + ')';
const version = APP_NAME + ' (' + Tinode.getLibrary() + ')';
return (
<div id="dummy-view" className={this.props.hideSelf ? 'nodisplay' : null}>
<div id="dummy-view">
<div>
<a href="https://github.com/tinode/chat/">
<img id="logo" alt="logo" src="img/logo.svg" />
+1 -1
View File
@@ -692,7 +692,7 @@ class MessagesView extends React.Component {
component = null;
} else if (!this.state.topic) {
component = (
<LogoView hideSelf={this.props.hideSelf}
<LogoView
serverVersion={this.props.serverVersion}
serverAddress={this.props.serverAddress} />
);
+62 -11
View File
@@ -8,6 +8,10 @@ import SideNavbar from '../widgets/side-navbar.jsx';
import ContactsView from './contacts-view.jsx';
import CreateAccountView from './create-account-view.jsx';
import EditAccountView from './edit-account-view.jsx';
import AccGeneralView from './acc-general-view.jsx';
import AccNotificationsView from './acc-notifications-view.jsx';
import AccSecurityView from './acc-security-view.jsx';
import AccSupportView from './acc-support-view.jsx';
import LoginView from './login-view.jsx';
import NewTopicView from './new-topic-view.jsx';
import PasswordResetView from './password-reset-view.jsx';
@@ -32,9 +36,29 @@ const messages = defineMessages({
defaultMessage: 'Settings'
},
'edit': {
id: 'sidepanel_title_edit_account',
id: 'sidepanel_title_account_settings',
description: 'Sidepanel title for EditAccountView.',
defaultMessage: 'Edit Account'
defaultMessage: 'Account Settings'
},
'general': {
id: 'sidepanel_title_acc_general',
description: 'Sidepanel title for AccGeneralView.',
defaultMessage: 'General'
},
'security': {
id: 'sidepanel_title_acc_security',
description: 'Sidepanel title for AccSecurityView.',
defaultMessage: 'Security'
},
'notif': {
id: 'sidepanel_title_acc_notifications',
description: 'Sidepanel title for AccNotificationsView.',
defaultMessage: 'Notifications'
},
'support': {
id: 'sidepanel_title_acc_support',
description: 'Sidepanel title for AccSupportView.',
defaultMessage: 'Support'
},
'newtpk': {
id: 'sidepanel_title_newtpk',
@@ -63,12 +87,17 @@ class SidepanelView extends React.Component {
super(props);
this.handleLoginRequested = this.handleLoginRequested.bind(this);
this.handleNewTopic = this.handleNewTopic.bind(this);
}
handleLoginRequested(login, password) {
this.props.onLoginRequest(login, password);
}
handleNewTopic() {
this.props.onBasicNavigate('newtpk');
}
render() {
const {formatMessage} = this.props.intl;
const view = this.props.state || (this.props.myUserId ? 'contacts' : 'login');
@@ -96,7 +125,7 @@ class SidepanelView extends React.Component {
myUserId={this.props.myUserId}
onSignUp={this.props.onSignUp}
onSettings={this.props.onSettings}
onNewTopic={this.props.onNewTopic}
onNewTopic={this.handleNewTopic}
onCancel={onCancel} />
<ErrorPanel
@@ -131,21 +160,43 @@ class SidepanelView extends React.Component {
<EditAccountView
tinode={this.props.tinode}
myUserId={this.props.myUserId}
onBasicNavigate={this.props.onBasicNavigate} /> :
view === 'general' ?
<AccGeneralView
tinode={this.props.tinode}
myUserId={this.props.myUserId}
onUpdateAccount={this.props.onUpdateAccount}
onUpdateTags={this.props.onUpdateAccountTags}
onCredAdd={this.props.onCredAdd}
onCredDelete={this.props.onCredDelete}
onCredConfirm={this.props.onCredConfirm}
onBasicNavigate={this.props.onBasicNavigate}
onCancel={this.props.onCancel}
onError={this.props.onError} /> :
view === 'notif' ?
<AccNotificationsView
messageSounds={this.props.messageSounds}
desktopAlerts={this.props.desktopAlerts}
desktopAlertsEnabled={this.props.desktopAlertsEnabled}
incognitoMode={this.props.incognitoMode}
onUpdateAccount={this.props.onUpdateAccount}
onUpdateTags={this.props.onUpdateAccountTags}
onTogglePushNotifications={this.props.onTogglePushNotifications}
onToggleMessageSounds={this.props.onToggleMessageSounds}
onToggleIncognitoMode={this.props.onToggleIncognitoMode}
onCredAdd={this.props.onCredAdd}
onCredDelete={this.props.onCredDelete}
onCredConfirm={this.props.onCredConfirm}
onToggleIncognitoMode={this.props.onToggleIncognitoMode} /> :
view === 'security' ?
<AccSecurityView
tinode={this.props.tinode}
onUpdateAccount={this.props.onUpdateAccount}
onLogout={this.props.onLogout}
onCancel={this.props.onCancel}
onError={this.props.onError} /> :
onDeleteAccount={this.props.onDeleteAccount}
onShowAlert={this.props.onShowAlert} /> :
view === 'support' ?
<AccSupportView
serverAddress={this.props.serverAddress}
serverVersion={this.props.serverVersion} /> :
(view === 'contacts' || view == 'archive') ?
<ContactsView
+24 -8
View File
@@ -92,13 +92,13 @@ class TinodeWeb extends React.Component {
this.togglePushToken = this.togglePushToken.bind(this);
this.requestPushToken = this.requestPushToken.bind(this);
this.handleSidepanelCancel = this.handleSidepanelCancel.bind(this);
this.handleNewTopic = this.handleNewTopic.bind(this);
this.handleNewTopicRequest = this.handleNewTopicRequest.bind(this);
this.handleNewTopicCreated = this.handleNewTopicCreated.bind(this);
this.handleTopicUpdateRequest = this.handleTopicUpdateRequest.bind(this);
this.handleChangePermissions = this.handleChangePermissions.bind(this);
this.handleTagsUpdated = this.handleTagsUpdated.bind(this);
this.handleLogout = this.handleLogout.bind(this);
this.handleDeleteAccount = this.handleDeleteAccount.bind(this);
this.handleDeleteMessagesRequest = this.handleDeleteMessagesRequest.bind(this);
this.handleLeaveUnsubRequest = this.handleLeaveUnsubRequest.bind(this);
this.handleBlockTopicRequest = this.handleBlockTopicRequest.bind(this);
@@ -300,7 +300,8 @@ class TinodeWeb extends React.Component {
const hash = HashNavigation.parseUrlHash(window.location.hash);
if (hash.path && hash.path.length > 0) {
// Left-side panel selector.
if (['register','settings','edit','cred','reset','newtpk','archive','contacts',''].includes(hash.path[0])) {
if (['register','settings','edit','notif','security','support','general',
'cred','reset','newtpk','archive','contacts',''].includes(hash.path[0])) {
this.setState({sidePanelSelected: hash.path[0]});
} else {
console.log("Unknown sidepanel view", hash.path[0]);
@@ -1007,7 +1008,13 @@ class TinodeWeb extends React.Component {
// User clicked Cancel button in Setting or Sign Up panel.
handleSidepanelCancel() {
const parsed = HashNavigation.parseUrlHash(window.location.hash);
parsed.path[0] = this.state.myUserId ? 'contacts' : '';
let path = '';
if (['security','support','general','notif'].includes(parsed.path[0])) {
path = 'edit';
} else if (this.state.myUserId) {
path = 'contacts';
}
parsed.path[0] = path;
if (parsed.params) {
delete parsed.params.code;
delete parsed.params.method;
@@ -1017,9 +1024,9 @@ class TinodeWeb extends React.Component {
this.setState({errorText: '', errorLevel: null});
}
// User clicked a (+) menu item.
handleNewTopic() {
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, 'newtpk'));
// Basic nagigator by hash value. No need to bind to this.
basicNavigator(hash) {
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, hash));
}
// Request to start a new topic. New P2P topic requires peer's name.
@@ -1122,6 +1129,12 @@ class TinodeWeb extends React.Component {
HashNavigation.navigateTo('');
}
handleDeleteAccount() {
this.tinode.delCurrentUser().then((ctrl) => {
this.handleLogout();
});
}
handleDeleteMessagesRequest(topicName) {
const topic = this.tinode.getTopic(topicName);
if (!topic) {
@@ -1396,10 +1409,12 @@ class TinodeWeb extends React.Component {
desktopAlertsEnabled={this.state.desktopAlertsEnabled}
incognitoMode={this.state.incognitoMode}
serverAddress={this.state.serverAddress}
onGlobalSettings={this.handleGlobalSettings}
serverVersion={this.state.serverVersion}
onGlobalSettings={this.handleGlobalSettings}
onSignUp={this.handleNewAccount}
onSettings={this.handleSettings}
onBasicNavigate={this.basicNavigator}
onLoginRequest={this.handleLoginRequest}
onCreateAccount={this.handleNewAccountRequest}
onUpdateAccount={this.handleUpdateAccountRequest}
@@ -1412,8 +1427,9 @@ class TinodeWeb extends React.Component {
onCredConfirm={this.handleCredConfirm}
onTopicSelected={this.handleTopicSelected}
onCreateTopic={this.handleNewTopicRequest}
onNewTopic={this.handleNewTopic}
onLogout={this.handleLogout}
onDeleteAccount={this.handleDeleteAccount}
onShowAlert={this.handleShowAlert}
onCancel={this.handleSidepanelCancel}
onError={this.handleError}
onValidateCredentials={this.handleValidateCredentialsRequest}
+3 -2
View File
@@ -41,9 +41,10 @@ export default class AvatarUpload extends React.Component {
render() {
// Randomize id value in case more than one AvatarUpload is shown
// at the same time.
var randId = "file-input-avatar-" + (Math.random() + '').substr(2);
const randId = 'file-input-avatar-' + (Math.random() + '').substr(2);
const className = 'avatar-upload' + (this.props.readOnly ? ' read-only' : '');
return (
<div className="avatar-upload">
<div className={className}>
{this.props.readOnly || !this.state.dataUrl ?
null :
<a href="#" className="clear-avatar" onClick={(e) => {e.preventDefault(); this.props.onImageChanged(null);}}>
+12
View File
@@ -0,0 +1,12 @@
// The <- button to be displayed in title bars.
import React from 'react';
export default class ButtonBack extends React.PureComponent {
render() {
return (
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onBack();}}>
<i className="material-icons">arrow_back</i>
</a>
);
}
}
+3 -1
View File
@@ -4,7 +4,9 @@ import React from 'react';
export default class MenuCancel extends React.PureComponent {
render() {
return (
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCancel();}}><i className="material-icons">close</i></a>
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCancel();}}>
<i className="material-icons">close</i>
</a>
);
}
}
+11 -9
View File
@@ -1,7 +1,7 @@
import React from 'react';
import LetterTile from './letter-tile.jsx';
import MenuCancel from './menu-cancel.jsx';
import ButtonBack from './button-back.jsx';
import MenuContacts from './menu-contacts.jsx';
import MenuStart from './menu-start.jsx';
@@ -9,20 +9,22 @@ export default class SideNavbar extends React.PureComponent {
render() {
return (
<div id="side-caption-panel" className="caption-panel">
<div id="self-avatar" className="avatar-box">
<LetterTile
avatar={this.props.avatar}
topic={this.props.myUserId}
title={this.props.title} />
</div>
{this.props.onCancel ? <ButtonBack onBack={this.props.onCancel} /> : null}
{this.props.avatar ?
<div id="self-avatar" className="avatar-box">
<LetterTile
avatar={this.props.avatar}
topic={this.props.myUserId}
title={this.props.title} />
</div>
:
null}
<div id="sidepanel-title" className="panel-title">{this.props.title}</div>
{this.props.state === 'login' ?
<MenuStart onSignUp={this.props.onSignUp} onSettings={this.props.onSettings} /> :
this.props.state === 'contacts' ?
<MenuContacts onNewTopic={this.props.onNewTopic} onSettings={this.props.onSettings} /> :
null}
{this.props.onCancel ?
<MenuCancel onCancel={this.props.onCancel} /> : null}
</div>
);
}