refactoring info-view into reusable components
This commit is contained in:
@@ -7,7 +7,7 @@ import BadgeList from '../widgets/badge-list.jsx';
|
||||
|
||||
import { makeImageUrl } from '../lib/blob-helpers.js';
|
||||
|
||||
export default class EditAccountView extends React.Component {
|
||||
export default class AccountSettingsView extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -38,7 +38,7 @@ export default class EditAccountView extends React.Component {
|
||||
<AvatarUpload
|
||||
tinode={this.props.tinode}
|
||||
avatar={this.state.avatar}
|
||||
readOnly={!this.state.owner}
|
||||
readOnly={true}
|
||||
uid={this.props.myUserId}
|
||||
title={this.state.fullName} />
|
||||
</div>
|
||||
@@ -1,70 +0,0 @@
|
||||
// Edit account parameters.
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Cropper from '../widgets/cropper.jsx';
|
||||
import { imageCrop } from '../lib/blob-helpers.js';
|
||||
|
||||
export default class AvatarCropView extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
scale: 1
|
||||
};
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
|
||||
// Cut out in image coordinates and desired scale factor.
|
||||
handleChange(left, top, width, height, scale) {
|
||||
this.setState({
|
||||
left: left,
|
||||
top: top,
|
||||
width: width,
|
||||
height: height,
|
||||
scale: scale
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
imageCrop(this.props.mime, this.props.avatar,
|
||||
this.state.left, this.state.top,
|
||||
this.state.width, this.state.height,
|
||||
this.state.scale,
|
||||
(mime, blob, width, height) => {
|
||||
this.props.onSubmit(mime, blob, width, height);
|
||||
},
|
||||
(err) => {
|
||||
this.props.onError(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="panel-form">
|
||||
<div className="panel-form-row">
|
||||
<Cropper
|
||||
source={this.props.avatar}
|
||||
onChange={this.handleChange} />
|
||||
</div>
|
||||
<div className="dialog-buttons">
|
||||
{this.props.onCancel ?
|
||||
<button className="secondary" onClick={this.props.onCancel}>
|
||||
<FormattedMessage id="button_cancel" defaultMessage="Cancel" description="Button [Cancel]" />
|
||||
</button>
|
||||
: null}
|
||||
<button className="primary" onClick={this.handleSubmit}>
|
||||
<FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
+22
-29
@@ -575,38 +575,36 @@ class InfoView extends React.Component {
|
||||
<FormattedMessage id="label_topic_name" defaultMessage="Name"
|
||||
description="Label for editing topic name" />
|
||||
</label></div>
|
||||
<div><InPlaceEdit
|
||||
placeholder={this.state.groupTopic ? "Group name" : <i>Unknown</i>}
|
||||
readOnly={!this.state.owner}
|
||||
value={this.state.fullName}
|
||||
required={true}
|
||||
onFinished={this.handleFullNameUpdate} /></div>
|
||||
<div className="large">{this.state.fullName}</div>
|
||||
{this.state.private ?
|
||||
<>
|
||||
<div><label className="small">
|
||||
<FormattedMessage id="label_private" defaultMessage="Private comment"
|
||||
description="Label for editing 'private'" />
|
||||
</label></div>
|
||||
<div className="large">{this.state.private}</div>
|
||||
</>
|
||||
: null
|
||||
}
|
||||
<div>
|
||||
<label className="small">
|
||||
<FormattedMessage id="label_private" defaultMessage="Private comment"
|
||||
description="Label for editing 'private'" />
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<FormattedMessage id="private_editing_placeholder"
|
||||
defaultMessage="Visible to you only"
|
||||
description="Placeholder for editing 'private'">{
|
||||
(private_placeholder) => <InPlaceEdit
|
||||
placeholder={private_placeholder}
|
||||
value={this.state.private}
|
||||
onFinished={this.handlePrivateUpdate} />
|
||||
}</FormattedMessage>
|
||||
<label className="small"><FormattedMessage id="label_user_id" defaultMessage="ID:"
|
||||
description="Label for user address (ID)" /></label>
|
||||
<tt>{this.props.myUserId}</tt>
|
||||
</div>
|
||||
<BadgeList trustedBadges={this.state.trustedBadges} />
|
||||
</div>
|
||||
<AvatarUpload
|
||||
tinode={this.props.tinode}
|
||||
avatar={this.state.avatar}
|
||||
readOnly={!this.state.owner}
|
||||
readOnly={true}
|
||||
uid={this.props.topic}
|
||||
title={this.state.fullName}
|
||||
onImageUpdated={this.handleImageChanged}
|
||||
onError={this.props.onError} />
|
||||
title={this.state.fullName} />
|
||||
</div>
|
||||
<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">
|
||||
@@ -627,11 +625,6 @@ class InfoView extends React.Component {
|
||||
}</FormattedMessage>
|
||||
{this.state.moreInfoExpanded ?
|
||||
<div className="panel-form-column">
|
||||
<div className="panel-form-row">
|
||||
<label><FormattedMessage id="label_user_id" defaultMessage="ID:"
|
||||
description="Label for user address (ID)" /></label>
|
||||
<tt>{this.state.address}</tt>
|
||||
</div>
|
||||
{this.state.groupTopic ?
|
||||
<div className="panel-form-row">
|
||||
<label>
|
||||
|
||||
@@ -5,9 +5,9 @@ import ErrorPanel from '../widgets/error-panel.jsx';
|
||||
import LoadSpinner from '../widgets/load-spinner.jsx';
|
||||
import SideNavbar from '../widgets/side-navbar.jsx';
|
||||
|
||||
import AccountSettingsView from './account-settings-view.jsx';
|
||||
import ContactsView from './contacts-view.jsx';
|
||||
import CreateAccountView from './create-account-view.jsx';
|
||||
import EditAccountView from './edit-account-view.jsx';
|
||||
import TopicCommonView from './topic-common-view.jsx';
|
||||
import AccNotificationsView from './acc-notifications-view.jsx';
|
||||
import AccSecurityView from './acc-security-view.jsx';
|
||||
@@ -37,7 +37,7 @@ const messages = defineMessages({
|
||||
},
|
||||
edit: {
|
||||
id: 'sidepanel_title_account_settings',
|
||||
description: 'Sidepanel title for EditAccountView.',
|
||||
description: 'Sidepanel title for AccountSettingsView.',
|
||||
defaultMessage: 'Account Settings'
|
||||
},
|
||||
general: {
|
||||
@@ -169,7 +169,7 @@ class SidepanelView extends React.Component {
|
||||
onUpdate={this.props.onGlobalSettings} /> :
|
||||
|
||||
view === 'edit' ?
|
||||
<EditAccountView
|
||||
<AccountSettingsView
|
||||
tinode={this.props.tinode}
|
||||
myUserId={this.props.myUserId}
|
||||
trustedBadges={this.props.trustedBadges}
|
||||
|
||||
+49
-177
@@ -5,11 +5,10 @@ import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Tinode from 'tinode-sdk';
|
||||
|
||||
import AvatarCropView from './avatar-crop-view.jsx';
|
||||
|
||||
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';
|
||||
@@ -21,126 +20,35 @@ export default class TopicCommonView extends React.Component {
|
||||
super(props);
|
||||
|
||||
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||
const acs = topic.getAccessMode();
|
||||
this.state = {
|
||||
fullName: topic.public ? topic.public.fn : undefined,
|
||||
avatar: makeImageUrl(topic.public ? topic.public.photo : null),
|
||||
tags: topic.tags(),
|
||||
isMe: this.props.tinode.isMe(this.props.topic),
|
||||
owner: acs && acs.isOwner(),
|
||||
credentials: topic.getCredentials() || [],
|
||||
addCredActive: false,
|
||||
addCredInvalid: false,
|
||||
newCred: '',
|
||||
newAvatar: null,
|
||||
newAvatarMime: null,
|
||||
previousOnTags: topic.onTagsUpdated
|
||||
addCredInvalid: false
|
||||
};
|
||||
|
||||
this.tnNewTags = this.tnNewTags.bind(this);
|
||||
this.tnCredsUpdated = this.tnCredsUpdated.bind(this);
|
||||
this.handleFullNameUpdate = this.handleFullNameUpdate.bind(this);
|
||||
this.handleImageUpdated = this.handleImageUpdated.bind(this);
|
||||
this.handleAvatarCropped = this.handleAvatarCropped.bind(this);
|
||||
this.uploadAvatar = this.uploadAvatar.bind(this);
|
||||
this.handleAvatarCropCancel = this.handleAvatarCropCancel.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 topic = this.props.tinode.getTopic(this.props.topic);
|
||||
topic.onCredsUpdated = this.tnCredsUpdated;
|
||||
topic.onTagsUpdated = this.tnNewTags;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||
topic.onTagsUpdated = this.state.previousOnTags;
|
||||
topic.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.onUpdateTopicDesc(this.props.topic, theCard(fn, null));
|
||||
}
|
||||
}
|
||||
|
||||
// AvatarUpload widget has changed the avatar: received a new file or deleted the avatar.
|
||||
handleImageUpdated(mime, img) {
|
||||
// This will show the AvatarCropView is img is not null.
|
||||
this.setState({newAvatar: img, newAvatarMime: mime});
|
||||
if (!img) {
|
||||
// Deleting the avatar.
|
||||
this.props.onUpdateTopicDesc(this.props.topic, theCard(null, Tinode.DEL_CHAR));
|
||||
}
|
||||
}
|
||||
|
||||
// AvatarCropView calls this method when the user has cropped the image.
|
||||
handleAvatarCropped(mime, blob, width, height) {
|
||||
const url = blob ? URL.createObjectURL(blob) : null;
|
||||
this.setState({avatar: url, newAvatar: null, newAvatarMime: null});
|
||||
if (blob) {
|
||||
this.uploadAvatar(mime, blob, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
// Utility method for converting cropped avatar blob to bytes for sending inband or
|
||||
// for uploading it to the server out of band.
|
||||
uploadAvatar(mime, blob, width, height) {
|
||||
const readyToUpload = (mime, blob, width, height) => {
|
||||
if (blob.size > MAX_AVATAR_BYTES) {
|
||||
// Too large to send inband - uploading out of band and sending as a link.
|
||||
const uploader = this.props.tinode.getLargeFileHelper();
|
||||
|
||||
this.setState({uploading: true});
|
||||
uploader.upload(blob)
|
||||
.then((url) => {
|
||||
this.props.onUpdateTopicDesc(this.props.topic, theCard(null, url));
|
||||
})
|
||||
.catch((err) => {
|
||||
this.props.onError(err, 'err');
|
||||
})
|
||||
.finally(() => {
|
||||
this.setState({uploading: false});
|
||||
});
|
||||
} else {
|
||||
this.setState({uploading: true});
|
||||
// Convert blob to base64-encoded bits.
|
||||
blobToBase64(blob, (unused, base64bits) => {
|
||||
const du = makeImageUrl({data: base64bits, type: mime});
|
||||
this.setState({source: du});
|
||||
this.props.onUpdateTopicDesc(this.props.topic, theCard(null, du));
|
||||
this.setState({uploading: false});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (width > AVATAR_SIZE || height > AVATAR_SIZE || width != height) {
|
||||
// Avatar is not square or too large even after cropping. Shrink it and make square.
|
||||
imageScaled(blob, AVATAR_SIZE, AVATAR_SIZE, MAX_EXTERN_ATTACHMENT_SIZE, true,
|
||||
readyToUpload,
|
||||
(err) => {
|
||||
this.props.onError(err, 'err');
|
||||
});
|
||||
} else {
|
||||
readyToUpload(mime, blob, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
handleAvatarCropCancel(img) {
|
||||
this.setState({newAvatar: null, newAvatarMime: null});
|
||||
}
|
||||
|
||||
handleCredChange(e) {
|
||||
this.setState({newCred: e.target.value, addCredInvalid: false});
|
||||
}
|
||||
@@ -180,91 +88,55 @@ export default class TopicCommonView extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleTagsUpdated(tags) {
|
||||
// Check if tags have actually changed.
|
||||
if (arrayEqual(this.state.tags.slice(0), tags.slice(0))) {
|
||||
return;
|
||||
}
|
||||
this.props.onUpdateTags(tags);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.newAvatar) {
|
||||
return (
|
||||
<AvatarCropView
|
||||
avatar={this.state.newAvatar}
|
||||
mime={this.state.newAvatarMime}
|
||||
onSubmit={this.handleAvatarCropped}
|
||||
onCancel={this.handleAvatarCropCancel}
|
||||
onError={this.props.onError} />
|
||||
);
|
||||
}
|
||||
|
||||
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>);
|
||||
});
|
||||
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">
|
||||
<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><FormattedMessage id="full_name_prompt" defaultMessage="Full name, e.g. John Doe"
|
||||
description="Input placeholder for person's full name">{
|
||||
(full_name_placeholder) => <InPlaceEdit
|
||||
placeholder={full_name_placeholder}
|
||||
value={this.state.fullName}
|
||||
onFinished={this.handleFullNameUpdate} />
|
||||
}</FormattedMessage></div>
|
||||
</div>
|
||||
<AvatarUpload
|
||||
tinode={this.props.tinode}
|
||||
avatar={this.state.avatar}
|
||||
uid={this.props.myUserId}
|
||||
title={this.state.fullName}
|
||||
onImageUpdated={this.handleImageUpdated}
|
||||
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}
|
||||
tinode={this.props.tinode}
|
||||
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>
|
||||
<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
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user