deleting contacts

This commit is contained in:
or-else
2019-06-07 16:26:42 -07:00
parent 6cb2b77f69
commit 53d748a7bd
16 changed files with 411 additions and 136 deletions
+11 -1
View File
@@ -199,12 +199,22 @@ input[type="search"]:focus {
box-shadow: 0 1px 0 rgba(33, 150, 243, 0.25); box-shadow: 0 1px 0 rgba(33, 150, 243, 0.25);
} }
textarea.invalid,
input[type="text"].invalid,
input[type="email"].invalid,
input[type="password"].invalid,
input[type="search"].invalid {
border-bottom: 1px solid #e57373;
box-shadow: 0 1px 0 rgba(229, 115, 115, 0.25);
}
input[type="text"]::placeholder, input[type="text"]::placeholder,
input[type="email"]::placeholder, input[type="email"]::placeholder,
input[type="password"]::placeholder, input[type="password"]::placeholder,
input[type="search"]::placeholder, input[type="search"]::placeholder,
textarea::placeholder { textarea::placeholder {
color: #bbb; color: #bbb;
font-weight: lighter;
} }
input[type="text"].with-visibility, input[type="text"].with-visibility,
@@ -321,7 +331,7 @@ li.right:hover .menuTrigger {
.hr { .hr {
display: block; display: block;
width: 100%; width: 100%;
height: 0.6rem; height: 0.45rem;
min-height: 0.3rem; min-height: 0.3rem;
background-color: #f0f0f0; background-color: #f0f0f0;
box-shadow: inset 0 0.25rem 0.25rem -0.25rem rgba(0,0,0,0.15), box-shadow: inset 0 0.25rem 0.25rem -0.25rem rgba(0,0,0,0.15),
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -10,7 +10,7 @@
"babel-plugin-react-intl": "^3.0.1", "babel-plugin-react-intl": "^3.0.1",
"browserslist": "^4.4.1", "browserslist": "^4.4.1",
"cssnano": "^4.1.10", "cssnano": "^4.1.10",
"postcss": "^7.0.6", "postcss": "^7.0.17",
"uglifyjs-webpack-plugin": "^2.1.2", "uglifyjs-webpack-plugin": "^2.1.2",
"webpack": "^4.29.6", "webpack": "^4.29.6",
"webpack-cli": "^3.2.3" "webpack-cli": "^3.2.3"
+3 -2
View File
@@ -7,12 +7,13 @@
"label_your_name": "Your name", "label_your_name": "Your name",
"label_password": "Password", "label_password": "Password",
"password_unchanged_prompt": "Unchanged", "password_unchanged_prompt": "Unchanged",
"label_user_id": "Address:",
"label_default_access_mode": "Default access mode:",
"label_message_sound": "Message sound:", "label_message_sound": "Message sound:",
"label_push_notifications": "Notification alerts:", "label_push_notifications": "Notification alerts:",
"label_push_notifications_disabled": "Notification alerts (requires HTTPS):", "label_push_notifications_disabled": "Notification alerts (requires HTTPS):",
"title_tag_manager": "Tags (user discovery)", "title_tag_manager": "Tags (user discovery)",
"label_user_contacts": "Contacts:",
"label_user_id": "Address:",
"label_default_access_mode": "Default access mode:",
"button_logout": "Logout", "button_logout": "Logout",
"requested_permissions": "Requested", "requested_permissions": "Requested",
"granted_permissions": "Granted", "granted_permissions": "Granted",
+2 -1
View File
@@ -132,5 +132,6 @@
"chat_invitation_block": "Заблокировать", "chat_invitation_block": "Заблокировать",
"peers_messaging_disabled": "Чат заблокирован у корреспондента.", "peers_messaging_disabled": "Чат заблокирован у корреспондента.",
"enable_peers_messaging": "Разблокировать.", "enable_peers_messaging": "Разблокировать.",
"more_online_members": "+еще {overflow}" "more_online_members": "+еще {overflow}",
"label_user_contacts": "Конакты"
} }
+2 -1
View File
@@ -132,5 +132,6 @@
"tags_not_found": "尚未定义标签。添加一些。", "tags_not_found": "尚未定义标签。添加一些。",
"tags_editor_no_tags": "添加一些标签", "tags_editor_no_tags": "添加一些标签",
"title_manage_tags": "管理标签", "title_manage_tags": "管理标签",
"more_online_members": "还有{overflow}个" "more_online_members": "还有{overflow}个",
"label_user_contacts": "往来"
} }
+1 -1
View File
@@ -16,7 +16,7 @@ export function shortDateFormat(then, locale) {
// Convert seconds to minutes:seconds, i.e. 156 sec -> 2:36. // Convert seconds to minutes:seconds, i.e. 156 sec -> 2:36.
export function secondsToTime(seconds) { export function secondsToTime(seconds) {
const min = Math.floor(seconds % 3600 / 60); const min = Math.floor(seconds / 60);
let sec = seconds % 60; let sec = seconds % 60;
sec = sec < 10 ? `0${sec}` : sec; sec = sec < 10 ? `0${sec}` : sec;
return `${min}:${sec}`; return `${min}:${sec}`;
+19
View File
@@ -61,3 +61,22 @@ export function arrayEqual(a, b) {
} }
return true; return true;
} }
// Checks (loosely) if the given string is a phone. If so, returns the phone number in a format
// as close to E.164 as possible.
export function asPhone(val) {
val = val.trim();
if (/^\s*(?:\+?(\d{1,3}))?[- (.]*(\d{3})[- ).]*(\d{3})[- .]*(\d{2})[- .]*(\d{2})?\s*$/.test(val)) {
return val.replace(/[- ().]*/, '');
}
return null;
}
// Checks (loosely) if the given string is an email. If so returns the email.
export function asEmail(val) {
val = val.trim();
if (/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/.test(val)) {
return val;
}
return null;
}
+5 -3
View File
@@ -8,12 +8,13 @@
"label_your_name": "Your name", "label_your_name": "Your name",
"label_password": "Password", "label_password": "Password",
"password_unchanged_prompt": "Unchanged", "password_unchanged_prompt": "Unchanged",
"label_user_id": "Address:",
"label_default_access_mode": "Default access mode:",
"label_message_sound": "Message sound:", "label_message_sound": "Message sound:",
"label_push_notifications": "Notification alerts:", "label_push_notifications": "Notification alerts:",
"label_push_notifications_disabled": "Notification alerts (requires HTTPS):", "label_push_notifications_disabled": "Notification alerts (requires HTTPS):",
"title_tag_manager": "Tags (user discovery)", "title_tag_manager": "Tags (user discovery)",
"label_user_contacts": "Contacts:",
"label_user_id": "Address:",
"label_default_access_mode": "Default access mode:",
"button_logout": "Logout", "button_logout": "Logout",
"requested_permissions": "Requested", "requested_permissions": "Requested",
"granted_permissions": "Granted", "granted_permissions": "Granted",
@@ -269,7 +270,8 @@
"chat_invitation_block": "Заблокировать", "chat_invitation_block": "Заблокировать",
"peers_messaging_disabled": "Чат заблокирован у корреспондента.", "peers_messaging_disabled": "Чат заблокирован у корреспондента.",
"enable_peers_messaging": "Разблокировать.", "enable_peers_messaging": "Разблокировать.",
"more_online_members": "+еще {overflow}" "more_online_members": "+еще {overflow}",
"label_user_contacts": "Конакты"
}, },
"zh-CN": { "zh-CN": {
"archived_contacts": "已归档联系人 ({count})", "archived_contacts": "已归档联系人 ({count})",
+121 -45
View File
@@ -10,31 +10,38 @@ import PermissionsEditor from '../widgets/permissions-editor.jsx';
import TagManager from '../widgets/tag-manager.jsx'; import TagManager from '../widgets/tag-manager.jsx';
import { makeImageUrl } from '../lib/blob-helpers.js'; import { makeImageUrl } from '../lib/blob-helpers.js';
import { arrayEqual, vcard } from '../lib/utils.js'; import { arrayEqual, asEmail, asPhone, vcard } from '../lib/utils.js';
export default class EditAccountView extends React.Component { export default class EditAccountView extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
const me = this.props.tinode.getMeTopic(); const me = this.props.tinode.getMeTopic();
const defacs = me ? me.getDefaultAccess() : null; const defacs = me.getDefaultAccess();
const fnd = this.props.tinode.getFndTopic();
this.state = { this.state = {
fullName: me.public ? me.public.fn : undefined, fullName: me.public ? me.public.fn : undefined,
avatar: makeImageUrl(me.public ? me.public.photo : null), avatar: makeImageUrl(me.public ? me.public.photo : null),
auth: defacs ? defacs.auth : null, auth: defacs ? defacs.auth : null,
anon: defacs ? defacs.anon : null, anon: defacs ? defacs.anon : null,
fndSubscribed: false, tags: me.tags(),
tags: fnd.tags(), credentials: me.getCredentials() || [],
addCredActive: false,
addCredInvalid: false,
newCred: '',
showPermissionEditorFor: undefined, showPermissionEditorFor: undefined,
previousOnTags: fnd.onTagsUpdated previousOnTags: me.onTagsUpdated
}; };
this.tnNewTags = this.tnNewTags.bind(this); this.tnNewTags = this.tnNewTags.bind(this);
this.tnCredsUpdated = this.tnCredsUpdated.bind(this);
this.handleFullNameUpdate = this.handleFullNameUpdate.bind(this); this.handleFullNameUpdate = this.handleFullNameUpdate.bind(this);
this.handlePasswordUpdate = this.handlePasswordUpdate.bind(this); this.handlePasswordUpdate = this.handlePasswordUpdate.bind(this);
this.handleImageChanged = this.handleImageChanged.bind(this); this.handleImageChanged = this.handleImageChanged.bind(this);
this.handleCheckboxClick = this.handleCheckboxClick.bind(this); this.handleCheckboxClick = this.handleCheckboxClick.bind(this);
this.handleStartAddCred = this.handleStartAddCred.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.handleLaunchPermissionsEditor = this.handleLaunchPermissionsEditor.bind(this);
this.handleHidePermissionsEditor = this.handleHidePermissionsEditor.bind(this); this.handleHidePermissionsEditor = this.handleHidePermissionsEditor.bind(this);
this.handlePermissionsChanged = this.handlePermissionsChanged.bind(this); this.handlePermissionsChanged = this.handlePermissionsChanged.bind(this);
@@ -42,31 +49,26 @@ export default class EditAccountView extends React.Component {
} }
componentDidMount() { componentDidMount() {
const fnd = this.props.tinode.getFndTopic(); const me = this.props.tinode.getMeTopic();
fnd.onTagsUpdated = this.tnNewTags; me.onCredsUpdated = this.tnCredsUpdated;
if (!fnd.isSubscribed()) { me.onTagsUpdated = this.tnNewTags;
fnd.subscribe(fnd.startMetaQuery().withTags().build())
.then((ctrl) => {
this.setState({fndSubscribed: true});
})
.catch((err) => {
this.props.onError(err.message, 'err');
});
}
} }
componentWillUnmount() { componentWillUnmount() {
const fnd = this.props.tinode.getFndTopic(); const me = this.props.tinode.getMeTopic();
fnd.onTagsUpdated = this.state.previousOnTags; me.onTagsUpdated = this.state.previousOnTags;
if (fnd.isSubscribed() && this.state.fndSubscribed) { me.onCredsUpdated = undefined;
fnd.leave();
}
} }
tnNewTags(tags) { tnNewTags(tags) {
this.setState({tags: tags}); this.setState({tags: tags});
} }
tnCredsUpdated(creds) {
console.log("tnCredsUpdated", creds, new Error());
this.setState({credentials: creds || []});
}
handleFullNameUpdate(fn) { handleFullNameUpdate(fn) {
this.setState({fullName: fn}); this.setState({fullName: fn});
this.props.onUpdateAccount(undefined, vcard(fn, null)); this.props.onUpdateAccount(undefined, vcard(fn, null));
@@ -90,6 +92,49 @@ export default class EditAccountView extends React.Component {
} }
} }
handleStartAddCred() {
this.setState({addCredActive: true});
}
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) { handleLaunchPermissionsEditor(which) {
this.setState({ this.setState({
showPermissionEditorFor: which, showPermissionEditorFor: which,
@@ -120,13 +165,25 @@ export default class EditAccountView extends React.Component {
} }
render() { render() {
var tags = []; /*
this.state.tags.map(function(tag) { let tags = [];
this.state.tags.map((tag) => {
tags.push(<span className="badge" key={tags.length}>{tag}</span>); tags.push(<span className="badge" key={tags.length}>{tag}</span>);
}); });
if (tags.length == 0) { if (tags.length == 0) {
tags = <i><FormattedMessage id="tags_not_found" /></i>; tags = <i><FormattedMessage id="tags_not_found" /></i>;
} }
*/
let credentials = [];
this.state.credentials.map((cred) => {
credentials.push(<div key={cred.meth + ":" + cred.val}>{cred.meth}: <tt>{cred.val}</tt>
{!cred.done ?
<a href="javascript:;"
onClick={this.props.onCredConfirm.bind(this, cred.meth, cred.val)}>confirm</a>
: null} <a href="javascript:;" onClick={this.props.onCredDelete.bind(this, cred.meth, cred.val)}><i
className="material-icons gray">delete_outline</i></a></div>);
});
return ( return (
<React.Fragment>{this.state.showPermissionEditorFor ? <React.Fragment>{this.state.showPermissionEditorFor ?
<PermissionsEditor <PermissionsEditor
@@ -170,26 +227,6 @@ export default class EditAccountView extends React.Component {
onError={this.props.onError} /> onError={this.props.onError} />
</div> </div>
<div className="hr" /> <div className="hr" />
<div className="panel-form-column">
<div className="panel-form-row">
<label><FormattedMessage id="label_user_id" defaultMessage="Address:"
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-row"> <div className="panel-form-row">
<label htmlFor="message-sound"> <label htmlFor="message-sound">
<FormattedMessage id="label_message_sound" defaultMessage="Message sound:" <FormattedMessage id="label_message_sound" defaultMessage="Message sound:"
@@ -207,7 +244,7 @@ export default class EditAccountView extends React.Component {
: :
<FormattedMessage id="label_push_notifications_disabled" <FormattedMessage id="label_push_notifications_disabled"
defaultMessage="Notification alerts (requires HTTPS):" defaultMessage="Notification alerts (requires HTTPS):"
description="Label for push notifications switch (disabled)" /> description="Label for push notifications switch" />
} }
</label> </label>
<CheckBox name="alert" id="desktop-alerts" <CheckBox name="alert" id="desktop-alerts"
@@ -224,6 +261,45 @@ export default class EditAccountView extends React.Component {
onSubmit={this.handleTagsUpdated} /> onSubmit={this.handleTagsUpdated} />
}</FormattedMessage> }</FormattedMessage>
<div className="hr" /> <div className="hr" />
<div className="panel-form-column">
<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="javascript:;" onClick={this.handleStartAddCred}>+ 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>
<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"> <div className="panel-form-column">
<a href="javascript:;" className="red flat-button" onClick={this.props.onLogout}> <a href="javascript:;" className="red flat-button" onClick={this.props.onLogout}>
<i className="material-icons">exit_to_app</i> <FormattedMessage id="button_logout" <i className="material-icons">exit_to_app</i> <FormattedMessage id="button_logout"
+3
View File
@@ -135,6 +135,9 @@ class SidepanelView extends React.Component {
onUpdateTags={this.props.onUpdateAccountTags} onUpdateTags={this.props.onUpdateAccountTags}
onTogglePushNotifications={this.props.onTogglePushNotifications} onTogglePushNotifications={this.props.onTogglePushNotifications}
onToggleMessageSounds={this.props.onToggleMessageSounds} onToggleMessageSounds={this.props.onToggleMessageSounds}
onCredAdd={this.props.onCredAdd}
onCredDelete={this.props.onCredDelete}
onCredConfirm={this.props.onCredConfirm}
onLogout={this.props.onLogout} onLogout={this.props.onLogout}
onCancel={this.props.onCancel} onCancel={this.props.onCancel}
onError={this.props.onError} /> : onError={this.props.onError} /> :
+29 -2
View File
@@ -84,6 +84,9 @@ class TinodeWeb extends React.Component {
this.handleGlobalSettings = this.handleGlobalSettings.bind(this); this.handleGlobalSettings = this.handleGlobalSettings.bind(this);
this.handleShowArchive = this.handleShowArchive.bind(this); this.handleShowArchive = this.handleShowArchive.bind(this);
this.handleToggleMessageSounds = this.handleToggleMessageSounds.bind(this); this.handleToggleMessageSounds = this.handleToggleMessageSounds.bind(this);
this.handleCredAdd = this.handleCredAdd.bind(this);
this.handleCredDelete = this.handleCredDelete.bind(this);
this.handleCredConfirm = this.handleCredConfirm.bind(this);
this.initDesktopAlerts = this.initDesktopAlerts.bind(this); this.initDesktopAlerts = this.initDesktopAlerts.bind(this);
this.togglePushToken = this.togglePushToken.bind(this); this.togglePushToken = this.togglePushToken.bind(this);
this.requestPushToken = this.requestPushToken.bind(this); this.requestPushToken = this.requestPushToken.bind(this);
@@ -493,11 +496,14 @@ class TinodeWeb extends React.Component {
credCode: undefined, credCode: undefined,
myUserId: this.tinode.getCurrentUserID() myUserId: this.tinode.getCurrentUserID()
}); });
console.log("handleLoginSuccessful");
// Subscribe, fetch topic desc, the list of subscriptions. Messages are not fetched. // Subscribe, fetch topic desc, the list of subscriptions. Messages are not fetched.
me.subscribe( me.subscribe(
me.startMetaQuery(). me.startMetaQuery().
withLaterSub(). withLaterSub().
withDesc(). withDesc().
withTags().
withCreds().
build() build()
).catch((err) => { ).catch((err) => {
localStorage.removeItem('auth-token'); localStorage.removeItem('auth-token');
@@ -635,7 +641,7 @@ class TinodeWeb extends React.Component {
if (fnd.isSubscribed()) { if (fnd.isSubscribed()) {
this.tnFndSubsUpdated(); this.tnFndSubsUpdated();
} else { } else {
fnd.subscribe(fnd.startMetaQuery().withSub().withTags().build()).catch((err) => { fnd.subscribe(fnd.startMetaQuery().withSub().build()).catch((err) => {
this.handleError(err.message, 'err'); this.handleError(err.message, 'err');
}); });
} }
@@ -827,7 +833,7 @@ class TinodeWeb extends React.Component {
} }
handleUpdateAccountTagsRequest(tags) { handleUpdateAccountTagsRequest(tags) {
this.tinode.getFndTopic().setMeta({tags: tags}) this.tinode.getMeTopic().setMeta({tags: tags})
.catch((err) => { .catch((err) => {
this.handleError(err.message, 'err'); this.handleError(err.message, 'err');
}); });
@@ -933,6 +939,24 @@ class TinodeWeb extends React.Component {
}); });
} }
handleCredAdd(method, value) {
// TODO: implement
console.log("add credential", method, value);
}
handleCredDelete(method, value) {
console.log("delete credential", method, value);
const me = this.tinode.getMeTopic();
me.delCredential(method, value).catch((err) => {
this.handleError(err.message, 'err');
});
}
handleCredConfirm(method, response) {
// TODO: implement
console.log("confirm credential", method, response);
}
// User clicked Cancel button in Setting or Sign Up panel. // User clicked Cancel button in Setting or Sign Up panel.
handleSidepanelCancel() { handleSidepanelCancel() {
const parsed = HashNavigation.parseUrlHash(window.location.hash); const parsed = HashNavigation.parseUrlHash(window.location.hash);
@@ -1254,6 +1278,9 @@ class TinodeWeb extends React.Component {
onUpdateAccountTags={this.handleUpdateAccountTagsRequest} onUpdateAccountTags={this.handleUpdateAccountTagsRequest}
onTogglePushNotifications={this.togglePushToken} onTogglePushNotifications={this.togglePushToken}
onToggleMessageSounds={this.handleToggleMessageSounds} onToggleMessageSounds={this.handleToggleMessageSounds}
onCredAdd={this.handleCredAdd}
onCredDelete={this.handleCredDelete}
onCredConfirm={this.handleCredConfirm}
onTopicSelected={this.handleTopicSelected} onTopicSelected={this.handleTopicSelected}
onCreateTopic={this.handleNewTopicRequest} onCreateTopic={this.handleNewTopicRequest}
onNewTopic={this.handleNewTopic} onNewTopic={this.handleNewTopic}
+210 -75
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long