refactoring of account settings into multiple screens
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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> <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> <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>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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> <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> <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> <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
@@ -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>
|
||||
<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> <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> <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> <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> <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>
|
||||
<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> <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> <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> <FormattedMessage id="sidepanel_title_acc_support" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user