a ton more refactoring

This commit is contained in:
or-else
2021-08-07 15:34:22 -07:00
parent 6d4ff9d625
commit fcb61dcc3b
27 changed files with 1173 additions and 928 deletions
+3 -1
View File
@@ -131,6 +131,7 @@ export default class ChipInput extends React.Component {
count++;
});
const className = "chip-input" + (this.state.focused ? ' focused' : '');
const autoFocus = !(this.props.tabIndex > 0);
return (
<div className={className}>
{chips}
@@ -141,7 +142,8 @@ export default class ChipInput extends React.Component {
onBlur={this.handleFocusLost}
onKeyDown={this.handleKeyDown}
value={this.state.input}
autoFocus />
tabIndex={this.props.tabIndex}
autoFocus={autoFocus} />
</div>
);
}
+3 -3
View File
@@ -188,12 +188,12 @@ class GroupManager extends React.Component {
showUnread={false}
onTopicSelected={this.handleContactSelected} />
<div id="group-manager-buttons" className="panel-form-row">
<button className="primary" onClick={this.handleSubmit}>
<FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />
</button>
<button className="secondary" onClick={this.handleCancel}>
<FormattedMessage id="button_cancel" defaultMessage="Cancel" description="Button [Cancel]" />
</button>
<button className="primary" onClick={this.handleSubmit}>
<FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />
</button>
</div>
</div>
);
+39 -20
View File
@@ -5,34 +5,36 @@ import AvatarUpload from './avatar-upload.jsx';
import CheckBox from './checkbox.jsx';
import TagManager from './tag-manager.jsx';
import { MAX_TITLE_LENGTH } from '../config.js';
import { MAX_TITLE_LENGTH, MAX_TOPIC_DESCRIPTION_LENGTH } from '../config.js';
export default class NewTopicGroup extends React.PureComponent {
constructor(props) {
super(props);
this.fullName = React.createRef();
this.state = {
fn: '', // full/formatted name
fullName: '', // full/formatted name
private: '',
description: '',
imageDataUrl: null,
tags: [],
isChannel: false
};
this.handleFnChange = this.handleFnChange.bind(this);
this.handlePrivateChange = this.handlePrivateChange.bind(this);
this.handleFieldEdit = this.handleFieldEdit.bind(this);
this.handleImageChanged = this.handleImageChanged.bind(this);
this.handleTagsChanged = this.handleTagsChanged.bind(this);
this.handleChannelToggle = this.handleChannelToggle.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleFnChange(e) {
this.setState({fn: e.target.value});
componentDidMount() {
// this.fullName.current.focus();
}
handlePrivateChange(e) {
this.setState({private: e.target.value});
handleFieldEdit(name, e) {
this.setState({[name]: e.target.value});
}
handleImageChanged(img) {
@@ -50,10 +52,11 @@ export default class NewTopicGroup extends React.PureComponent {
handleSubmit(e) {
e.preventDefault();
const fn = this.state.fn.trim().substring(0, MAX_TITLE_LENGTH);
const fn = this.state.fullName.trim().substring(0, MAX_TITLE_LENGTH);
const comment = this.state.private.trim().substring(0, MAX_TITLE_LENGTH);
const description = this.state.description.trim().substring(0, MAX_TOPIC_DESCRIPTION_LENGTH);
if (fn) {
this.props.onSubmit(fn, this.state.imageDataUrl, comment, this.state.tags, this.state.isChannel);
this.props.onSubmit(fn, description, this.state.imageDataUrl, comment, this.state.tags, this.state.isChannel);
}
}
@@ -64,8 +67,14 @@ export default class NewTopicGroup extends React.PureComponent {
}
return (
<form className="panel-form" onSubmit={this.handleSubmit}>
<div className="panel-form-row">
<div className="panel-form-column">
<div className="panel-form-column">
<center>
<AvatarUpload
tinode={this.props.tinode}
onError={this.props.onError}
onImageChanged={this.handleImageChanged} />
</center>
<div className="group">
<label className="small" htmlFor="new-topic-fn">
<FormattedMessage id="label_topic_name" defaultMessage="Name"
description="Label for editing topic name" />
@@ -73,9 +82,11 @@ export default class NewTopicGroup extends React.PureComponent {
<FormattedMessage id="topic_name_editing_placeholder" defaultMessage="Freeform name of the group"
description="Prompt for entering topic name">{
(placeholder) => <input type="text" id="new-topic-fn" placeholder={placeholder}
value={this.state.fn} onChange={this.handleFnChange} autoFocus required />
ref={this.fullName} value={this.state.fullName} onChange={this.handleFieldEdit.bind('fullName')}
autoFocus required tabIndex={0} />
}</FormattedMessage>
<br />
</div>
<div className="group">
<label className="small" htmlFor="new-topic-priv">
<FormattedMessage id="label_private" defaultMessage="Private comment"
description="Label for editing 'private'" />
@@ -83,16 +94,23 @@ export default class NewTopicGroup extends React.PureComponent {
<FormattedMessage id="private_editing_placeholder" defaultMessage="Visible to you only"
description="Placeholder for editing 'private'">{
(placeholder) => <input type="text" id="new-topic-priv" placeholder={placeholder}
value={this.state.private} onChange={this.handlePrivateChange} />
value={this.state.private} onChange={this.handleFieldEdit.bind('private')} tabIndex={1} />
}</FormattedMessage>
</div>
<div className="group">
<label className="small" htmlFor="new-topic-desc">
<FormattedMessage id="label_description" defaultMessage="Description"
description="Label for editing topic description" />
</label>
<FormattedMessage id="description_editing_placeholder" defaultMessage="Description (optional)"
description="Placeholder for editing topic description">{
(placeholder) => <input type="text" id="new-topic-desc" placeholder={placeholder}
value={this.state.description} onChange={this.handleFieldEdit.bind('description')} tabIndex={2} />
}</FormattedMessage>
</div>
<AvatarUpload
tinode={this.props.tinode}
onError={this.props.onError}
onImageChanged={this.handleImageChanged} />
</div>
<div className="panel-form-row">
<CheckBox checked={this.state.isChannel} onChange={this.handleChannelToggle}/>&nbsp;
<CheckBox checked={this.state.isChannel} tabIndex={3} onChange={this.handleChannelToggle} />&nbsp;
<label onClick={this.handleChannelToggle}><FormattedMessage id="channel_prompt"
defaultMessage="This is a channel"
description="Checkbox label when creating a channel" /></label>
@@ -104,6 +122,7 @@ export default class NewTopicGroup extends React.PureComponent {
activated={true}
onTagsChanged={this.handleTagsChanged}
tinode={this.props.tinode}
tabIndex={4}
title={title} />
}</FormattedMessage>
<div className="dialog-buttons">
+1
View File
@@ -119,6 +119,7 @@ export default class TagManager extends React.Component {
chips={tags}
avatarDisabled={true}
prompt={add_tags_prompt}
tabIndex={this.props.tabIndex}
onEnter={this.handleAddTag}
onFocusLost={this.handleAddTag}
onCancel={this.handleCancel}
+200
View File
@@ -0,0 +1,200 @@
// View for editing topic (including 'me' topic) 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 TopicDescEdit from '../widgets/topic-desc-edit.jsx';
import { AVATAR_SIZE, MAX_AVATAR_BYTES, MAX_EXTERN_ATTACHMENT_SIZE,
MAX_INBAND_ATTACHMENT_SIZE, MAX_TITLE_LENGTH } from '../config.js';
import { imageScaled, blobToBase64, makeImageUrl } from '../lib/blob-helpers.js';
import { arrayEqual, asEmail, asPhone, theCard } from '../lib/utils.js';
export default class TopicCommon extends React.Component {
constructor(props) {
super(props);
const topic = this.props.tinode.getTopic(this.props.topic);
const acs = topic.getAccessMode();
this.state = {
isMe: this.props.topic == 'me',
owner: acs && acs.isOwner(),
credentials: (topic.getCredentials ? topic.getCredentials() : null) || [],
addCredActive: false,
addCredInvalid: false,
newCred: '',
tags: [],
previousTagsUpdated: undefined
};
this.onTagsUpdated = this.onTagsUpdated.bind(this);
this.handleTagsUpdated = this.handleTagsUpdated.bind(this);
this.tnCredsUpdated = this.tnCredsUpdated.bind(this);
this.handleCredChange = this.handleCredChange.bind(this);
this.handleCredKeyDown = this.handleCredKeyDown.bind(this);
this.handleCredEntered = this.handleCredEntered.bind(this);
}
// No need to separately handle component mount.
componentDidUpdate(props) {
const topic = this.props.tinode.getTopic(props.topic);
if (!topic) {
return;
}
topic.onCredsUpdated = this.tnCredsUpdated;
if (topic.onTagsUpdated != this.onTagsUpdated) {
if (topic.getType() == 'grp') {
this.previousTagsUpdated = topic.onTagsUpdated;
topic.onTagsUpdated = this.onTagsUpdated;
} else {
this.previousTagsUpdated = undefined;
}
}
if (this.state.topic != props.topic) {
this.setState({topic: props.topic});
}
}
componentWillUnmount() {
const topic = this.props.tinode.getTopic(this.props.topic);
topic.onCredsUpdated = undefined;
topic.onTagsUpdated = this.previousTagsUpdated;
}
tnCredsUpdated(creds) {
this.setState({credentials: creds || []});
}
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) {
const 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});
}
}
onTagsUpdated(tags) {
this.setState({tags: tags});
if (this.previousTagsUpdated && this.previousTagsUpdated != this.onTagsUpdated) {
this.previousTagsUpdated();
}
}
handleTagsUpdated(tags) {
if (!arrayEqual(this.state.tags.slice(0), tags.slice(0))) {
this.props.onTopicTagsUpdate(this.props.topic, tags);
}
}
render() {
const credentials = [];
if (this.state.isMe) {
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">
<TopicDescEdit
tinode={this.props.tinode}
topic={this.props.topic}
onUpdateTopicDesc={this.props.onUpdateTopicDesc}
onUpdateTags={this.props.onUpdateTags}
onError={this.props.onError} />
{this.state.isMe ?
<>
<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>
</>
: null
}
{this.state.owner ?
<>
<div className="hr" />
<FormattedMessage id="title_tag_manager" defaultMessage="Tags (user discovery)"
description="Section title for TagManager">{
(tags) => <TagManager
title={tags}
tags={this.state.tags}
activated={false}
tinode={this.props.tinode}
onSubmit={this.handleTagsUpdated} />
}</FormattedMessage>
</>
:
null
}
</div>
);
}
};
+21 -18
View File
@@ -211,7 +211,7 @@ export default class TopicDescEdit extends React.Component {
<div>
<InPlaceEdit
placeholder={this.state.groupTopic ? "Group name" : <i>Unknown</i>}
readOnly={!this.state.owner}
readOnly={editable}
value={this.state.fullName}
required={true}
onFinished={this.handleFullNameUpdate} />
@@ -237,24 +237,27 @@ export default class TopicDescEdit extends React.Component {
</div>
</>
}
<div className="group">
<div><label className="small">
<FormattedMessage id="label_description" defaultMessage="Description"
description="Label for editing topic description" />
</label></div>
<div>
<FormattedMessage id="description_editing_placeholder"
defaultMessage="Optional description"
description="Placeholder for editing topic description">{
(private_placeholder) => <InPlaceEdit
placeholder={private_placeholder}
readOnly={!editable}
value={this.state.description}
multiline={2}
onFinished={this.handleDescriptionUpdate} />
}</FormattedMessage>
{editable || this.state.description ?
<div className="group">
<div><label className="small">
<FormattedMessage id="label_description" defaultMessage="Description"
description="Label for editing topic description" />
</label></div>
<div>
<FormattedMessage id="description_editing_placeholder"
defaultMessage="Description (optional)"
description="Placeholder for editing topic description">{
(private_placeholder) => <InPlaceEdit
placeholder={private_placeholder}
readOnly={!editable}
value={this.state.description}
multiline={2}
onFinished={this.handleDescriptionUpdate} />
}</FormattedMessage>
</div>
</div>
</div>
: null
}
</div>
{editable ?
<>
+244
View File
@@ -0,0 +1,244 @@
// Edit account parameters.
import React from 'react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import InPlaceEdit from './in-place-edit.jsx';
const messages = defineMessages({
clear_messages: {
id: 'action_clear_messages',
defaultMessage: 'Clear Messages',
description: 'Flat button [Clear Messages] (soft-delete messages)'
},
clear_messages_warning: {
id: 'clear_messages_warning',
defaultMessage: 'Are you sure you want to clear all messages? It cannot be undone.',
description: 'Alert dialog warning when deleting all messages.'
},
delete_messages: {
id: 'action_delete_messages',
defaultMessage: 'Clear Messages for All',
description: 'Flat button [Clear for All] (hard-delete all messages)'
},
delete_messages_warning: {
id: 'delete_messages_warning',
defaultMessage: 'Are you sure you want to delete all messages for everyone? It cannot be undone.',
description: 'Alert dialog warning when hard-deleting all messages.'
},
leave_chat: {
id: 'action_leave_chat',
defaultMessage: 'Leave Conversation',
description: 'Flat button [Leave Conversation]'
},
leave_chat_warning: {
id: 'leave_chat_warning',
defaultMessage: 'Are you sure you want to leave this conversation?',
description: 'Alert dialog warning when unsubscribing from a chat.'
},
block_contact: {
id: 'action_block_contact',
defaultMessage: "Block Contact",
description: "Flat button [Block Contact]"
},
block_contact_warning: {
id: 'block_contact_warning',
defaultMessage: 'Are you sure you want to block this contact?',
description: 'Alert dialog warning when blocking a contact.'
},
report_chat: {
id: 'action_report_chat',
defaultMessage: 'Report Conversation',
description: 'Flat button [Report Group]'
},
report_chat_warning: {
id: 'report_chat_warning',
defaultMessage: 'Are you sure you want to block and report this conversation?',
description: 'Alert dialog warning when reporting a conversation for abuse'
},
other_user: {
id: 'label_other_user',
defaultMessage: 'Other',
description: 'Label for the other user when the user is unnamed'
},
});
class TopicSecurity extends React.Component {
constructor(props) {
super(props);
const topic = this.props.tinode.getTopic(this.props.topic);
if (!topic) {
return;
}
const defacs = topic.getDefaultAccess() || {};
const acs = topic.getAccessMode();
this.state = {
owner: acs && acs.isOwner(),
admin: acs && acs.isAdmin(),
sharer: acs && acs.isSharer(),
deleter: acs && acs.isDeleter(),
muted: acs && acs.isMuted(),
groupTopic: topic.isGroupType(),
channel: topic.isChannelType(),
access: acs ? acs.getMode() : undefined,
modeGiven: acs ? acs.getGiven() : undefined,
modeWant: acs ? acs.getWant() : undefined,
auth: defacs ? defacs.auth : null,
anon: defacs ? defacs.anon : null,
};
this.handleLaunchPermissionsEditor = this.handleLaunchPermissionsEditor.bind(this);
this.handleDeleteMessages = this.handleDeleteMessages.bind(this);
this.handleLeave = this.handleLeave.bind(this);
this.handleBlock = this.handleBlock.bind(this);
this.handleReport = this.handleReport.bind(this);
}
handleLaunchPermissionsEditor(which) {
this.props.onNavigate(`perm/${which}/${this.state[which] || ''}`);
}
handleDeleteMessages(e) {
e.preventDefault();
const {formatMessage} = this.props.intl;
this.props.onShowAlert(
formatMessage(this.state.deleter ? messages.delete_messages : messages.clear_messages), // title
formatMessage(this.state.deleter ? messages.delete_messages_warning : messages.clear_messages_warning), // content
(() => { this.props.onDeleteMessages(this.props.topic); }), // onConfirm
null, // "OK"
true, // Show Reject button
null // "Cancel"
);
}
handleLeave(e) {
e.preventDefault();
const {formatMessage} = this.props.intl;
this.props.onShowAlert(
formatMessage(messages.leave_chat), // title
formatMessage(messages.leave_chat_warning), // content
(() => { this.props.onLeaveTopic(this.props.topic); }), // onConfirm
null, // "OK"
true, // Show Reject button
null // "Cancel"
);
}
handleBlock(e) {
e.preventDefault();
const {formatMessage} = this.props.intl;
this.props.onShowAlert(
formatMessage(messages.block_contact), // title
formatMessage(messages.block_contact_warning), // content
(() => { this.props.onBlockTopic(this.props.topic); }), // onConfirm
null, // "OK"
true, // Show Reject button
null // "Cancel"
);
}
handleReport(e) {
e.preventDefault();
const {formatMessage} = this.props.intl;
this.props.onShowAlert(
formatMessage(messages.report_chat), // title
formatMessage(messages.report_chat_warning), // content
(() => { this.props.onReportTopic(this.props.topic); }), // onConfirm
null, // "OK"
true, // Show Reject button
null // "Cancel"
);
}
render() {
const {formatMessage} = this.props.intl;
return (
<div className="scrollable-panel">
<div className="panel-form-column">
{!this.state.channel ?
<a href="#" className="flat-button" onClick={this.handleDeleteMessages}>
<i className="material-icons">delete_outline</i> &nbsp;{
formatMessage(this.state.deleter ? messages.delete_messages : messages.clear_messages)
}
</a>
:
null
}
<a href="#" className="danger flat-button" onClick={this.handleLeave}>
<i className="material-icons">exit_to_app</i> &nbsp;{formatMessage(messages.leave_chat)}
</a>
{!this.state.groupTopic ?
<a href="#" className="danger flat-button" onClick={this.handleBlock}>
<i className="material-icons">block</i> &nbsp;{formatMessage(messages.block_contact)}
</a>
:
null
}
{!this.state.owner ?
<a href="#" className="danger flat-button" onClick={this.handleReport}>
<i className="material-icons">report</i> &nbsp;{formatMessage(messages.report_chat)}
</a>
:
null
}
</div>
<div className="hr" />
<div className="panel-form-column">
{this.state.groupTopic ?
<div className="group">
<label>
<FormattedMessage id="label_your_permissions" defaultMessage="Your permissions:"
description="Label for current user permissions" />
</label> <tt className="clickable"
onClick={this.handleLaunchPermissionsEditor.bind(this, 'want')}>
{this.state.access}
</tt>
</div>
:
<div className="group">
<div>
<label className="small">
<FormattedMessage id="label_permissions" defaultMessage="Permissions:"
description="Section title" />
</label>
</div>
<div className="quoted">
<div>
<FormattedMessage id="label_you" defaultMessage="You:"
description="Label for the current user" /> <tt className="clickable"
onClick={this.handleLaunchPermissionsEditor.bind(this, 'want')}>
{this.state.access}
</tt></div>
<div>{this.state.fullName ? this.state.fullName : formatMessage(messages.other_user)}:
&nbsp;<tt className="clickable" onClick={this.handleLaunchPermissionsEditor.bind(this, 'given')}>
{this.state.modeGiven2}
</tt>
</div>
</div>
</div>
}
<div className="group">
<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>
</div>
);
}
};
export default injectIntl(TopicSecurity);