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';
|
import { makeImageUrl } from '../lib/blob-helpers.js';
|
||||||
|
|
||||||
export default class EditAccountView extends React.Component {
|
export default class AccountSettingsView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ export default class EditAccountView extends React.Component {
|
|||||||
<AvatarUpload
|
<AvatarUpload
|
||||||
tinode={this.props.tinode}
|
tinode={this.props.tinode}
|
||||||
avatar={this.state.avatar}
|
avatar={this.state.avatar}
|
||||||
readOnly={!this.state.owner}
|
readOnly={true}
|
||||||
uid={this.props.myUserId}
|
uid={this.props.myUserId}
|
||||||
title={this.state.fullName} />
|
title={this.state.fullName} />
|
||||||
</div>
|
</div>
|
||||||
+22
-29
@@ -575,38 +575,36 @@ class InfoView extends React.Component {
|
|||||||
<FormattedMessage id="label_topic_name" defaultMessage="Name"
|
<FormattedMessage id="label_topic_name" defaultMessage="Name"
|
||||||
description="Label for editing topic name" />
|
description="Label for editing topic name" />
|
||||||
</label></div>
|
</label></div>
|
||||||
<div><InPlaceEdit
|
<div className="large">{this.state.fullName}</div>
|
||||||
placeholder={this.state.groupTopic ? "Group name" : <i>Unknown</i>}
|
{this.state.private ?
|
||||||
readOnly={!this.state.owner}
|
<>
|
||||||
value={this.state.fullName}
|
<div><label className="small">
|
||||||
required={true}
|
<FormattedMessage id="label_private" defaultMessage="Private comment"
|
||||||
onFinished={this.handleFullNameUpdate} /></div>
|
description="Label for editing 'private'" />
|
||||||
|
</label></div>
|
||||||
|
<div className="large">{this.state.private}</div>
|
||||||
|
</>
|
||||||
|
: null
|
||||||
|
}
|
||||||
<div>
|
<div>
|
||||||
<label className="small">
|
<label className="small"><FormattedMessage id="label_user_id" defaultMessage="ID:"
|
||||||
<FormattedMessage id="label_private" defaultMessage="Private comment"
|
description="Label for user address (ID)" /></label>
|
||||||
description="Label for editing 'private'" />
|
<tt>{this.props.myUserId}</tt>
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
<BadgeList trustedBadges={this.state.trustedBadges} />
|
<BadgeList trustedBadges={this.state.trustedBadges} />
|
||||||
</div>
|
</div>
|
||||||
<AvatarUpload
|
<AvatarUpload
|
||||||
tinode={this.props.tinode}
|
tinode={this.props.tinode}
|
||||||
avatar={this.state.avatar}
|
avatar={this.state.avatar}
|
||||||
readOnly={!this.state.owner}
|
readOnly={true}
|
||||||
uid={this.props.topic}
|
uid={this.props.topic}
|
||||||
title={this.state.fullName}
|
title={this.state.fullName} />
|
||||||
onImageUpdated={this.handleImageChanged}
|
</div>
|
||||||
onError={this.props.onError} />
|
<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>
|
||||||
<div className="hr" />
|
<div className="hr" />
|
||||||
<div className="panel-form-column">
|
<div className="panel-form-column">
|
||||||
@@ -627,11 +625,6 @@ class InfoView extends React.Component {
|
|||||||
}</FormattedMessage>
|
}</FormattedMessage>
|
||||||
{this.state.moreInfoExpanded ?
|
{this.state.moreInfoExpanded ?
|
||||||
<div className="panel-form-column">
|
<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 ?
|
{this.state.groupTopic ?
|
||||||
<div className="panel-form-row">
|
<div className="panel-form-row">
|
||||||
<label>
|
<label>
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import ErrorPanel from '../widgets/error-panel.jsx';
|
|||||||
import LoadSpinner from '../widgets/load-spinner.jsx';
|
import LoadSpinner from '../widgets/load-spinner.jsx';
|
||||||
import SideNavbar from '../widgets/side-navbar.jsx';
|
import SideNavbar from '../widgets/side-navbar.jsx';
|
||||||
|
|
||||||
|
import AccountSettingsView from './account-settings-view.jsx';
|
||||||
import ContactsView from './contacts-view.jsx';
|
import ContactsView from './contacts-view.jsx';
|
||||||
import CreateAccountView from './create-account-view.jsx';
|
import CreateAccountView from './create-account-view.jsx';
|
||||||
import EditAccountView from './edit-account-view.jsx';
|
|
||||||
import TopicCommonView from './topic-common-view.jsx';
|
import TopicCommonView from './topic-common-view.jsx';
|
||||||
import AccNotificationsView from './acc-notifications-view.jsx';
|
import AccNotificationsView from './acc-notifications-view.jsx';
|
||||||
import AccSecurityView from './acc-security-view.jsx';
|
import AccSecurityView from './acc-security-view.jsx';
|
||||||
@@ -37,7 +37,7 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
id: 'sidepanel_title_account_settings',
|
id: 'sidepanel_title_account_settings',
|
||||||
description: 'Sidepanel title for EditAccountView.',
|
description: 'Sidepanel title for AccountSettingsView.',
|
||||||
defaultMessage: 'Account Settings'
|
defaultMessage: 'Account Settings'
|
||||||
},
|
},
|
||||||
general: {
|
general: {
|
||||||
@@ -169,7 +169,7 @@ class SidepanelView extends React.Component {
|
|||||||
onUpdate={this.props.onGlobalSettings} /> :
|
onUpdate={this.props.onGlobalSettings} /> :
|
||||||
|
|
||||||
view === 'edit' ?
|
view === 'edit' ?
|
||||||
<EditAccountView
|
<AccountSettingsView
|
||||||
tinode={this.props.tinode}
|
tinode={this.props.tinode}
|
||||||
myUserId={this.props.myUserId}
|
myUserId={this.props.myUserId}
|
||||||
trustedBadges={this.props.trustedBadges}
|
trustedBadges={this.props.trustedBadges}
|
||||||
|
|||||||
+49
-177
@@ -5,11 +5,10 @@ import { FormattedMessage } from 'react-intl';
|
|||||||
|
|
||||||
import Tinode from 'tinode-sdk';
|
import Tinode from 'tinode-sdk';
|
||||||
|
|
||||||
import AvatarCropView from './avatar-crop-view.jsx';
|
|
||||||
|
|
||||||
import AvatarUpload from '../widgets/avatar-upload.jsx';
|
import AvatarUpload from '../widgets/avatar-upload.jsx';
|
||||||
import InPlaceEdit from '../widgets/in-place-edit.jsx';
|
import InPlaceEdit from '../widgets/in-place-edit.jsx';
|
||||||
import TagManager from '../widgets/tag-manager.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,
|
import { AVATAR_SIZE, MAX_AVATAR_BYTES, MAX_EXTERN_ATTACHMENT_SIZE,
|
||||||
MAX_INBAND_ATTACHMENT_SIZE, MAX_TITLE_LENGTH } from '../config.js';
|
MAX_INBAND_ATTACHMENT_SIZE, MAX_TITLE_LENGTH } from '../config.js';
|
||||||
@@ -21,126 +20,35 @@ export default class TopicCommonView extends React.Component {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const topic = this.props.tinode.getTopic(this.props.topic);
|
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||||
|
const acs = topic.getAccessMode();
|
||||||
this.state = {
|
this.state = {
|
||||||
fullName: topic.public ? topic.public.fn : undefined,
|
isMe: this.props.tinode.isMe(this.props.topic),
|
||||||
avatar: makeImageUrl(topic.public ? topic.public.photo : null),
|
owner: acs && acs.isOwner(),
|
||||||
tags: topic.tags(),
|
|
||||||
credentials: topic.getCredentials() || [],
|
credentials: topic.getCredentials() || [],
|
||||||
addCredActive: false,
|
addCredActive: false,
|
||||||
addCredInvalid: false,
|
addCredInvalid: false
|
||||||
newCred: '',
|
|
||||||
newAvatar: null,
|
|
||||||
newAvatarMime: null,
|
|
||||||
previousOnTags: topic.onTagsUpdated
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.tnNewTags = this.tnNewTags.bind(this);
|
|
||||||
this.tnCredsUpdated = this.tnCredsUpdated.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.handleCredChange = this.handleCredChange.bind(this);
|
||||||
this.handleCredKeyDown = this.handleCredKeyDown.bind(this);
|
this.handleCredKeyDown = this.handleCredKeyDown.bind(this);
|
||||||
this.handleCredEntered = this.handleCredEntered.bind(this);
|
this.handleCredEntered = this.handleCredEntered.bind(this);
|
||||||
this.handleTagsUpdated = this.handleTagsUpdated.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const topic = this.props.tinode.getTopic(this.props.topic);
|
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||||
topic.onCredsUpdated = this.tnCredsUpdated;
|
topic.onCredsUpdated = this.tnCredsUpdated;
|
||||||
topic.onTagsUpdated = this.tnNewTags;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
const topic = this.props.tinode.getTopic(this.props.topic);
|
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||||
topic.onTagsUpdated = this.state.previousOnTags;
|
|
||||||
topic.onCredsUpdated = undefined;
|
topic.onCredsUpdated = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
tnNewTags(tags) {
|
|
||||||
this.setState({tags: tags});
|
|
||||||
}
|
|
||||||
|
|
||||||
tnCredsUpdated(creds) {
|
tnCredsUpdated(creds) {
|
||||||
this.setState({credentials: 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) {
|
handleCredChange(e) {
|
||||||
this.setState({newCred: e.target.value, addCredInvalid: false});
|
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() {
|
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 = [];
|
const credentials = [];
|
||||||
this.state.credentials.map((cred) => {
|
if (this.state.isMe) {
|
||||||
credentials.push(<div key={cred.meth + ":" + cred.val + ":" + cred.done}>{cred.meth}: <tt>{cred.val}</tt>
|
this.state.credentials.map((cred) => {
|
||||||
<span > {!cred.done ?
|
credentials.push(<div key={cred.meth + ":" + cred.val + ":" + cred.done}>{cred.meth}: <tt>{cred.val}</tt>
|
||||||
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCredConfirm(cred.meth, cred.val);}}>
|
<span > {!cred.done ?
|
||||||
<FormattedMessage id="validate_credential_action" defaultMessage="confirm"
|
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCredConfirm(cred.meth, cred.val);}}>
|
||||||
description="Validate credentail call to action" />
|
<FormattedMessage id="validate_credential_action" defaultMessage="confirm"
|
||||||
</a>
|
description="Validate credentail call to action" />
|
||||||
: null} <a href="#" onClick={(e) => {e.preventDefault(); this.props.onCredDelete(cred.meth, cred.val);}}><i
|
</a>
|
||||||
className="material-icons gray">delete_outline</i></a></span></div>);
|
: 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 (
|
return (
|
||||||
<div className="scrollable-panel">
|
<div className="scrollable-panel">
|
||||||
<div className="panel-form-row">
|
<TopicDescEdit
|
||||||
<div className="panel-form-column">
|
tinode={this.props.tinode}
|
||||||
<label className="small"><FormattedMessage id="label_your_name" defaultMessage="Your name"
|
topic={this.props.topic}
|
||||||
description="Label for full name editing" /></label>
|
onUpdateTopicDesc={this.props.onUpdateTopicDesc}
|
||||||
<div><FormattedMessage id="full_name_prompt" defaultMessage="Full name, e.g. John Doe"
|
onUpdateTags={this.props.onUpdateTags}
|
||||||
description="Input placeholder for person's full name">{
|
onError={this.props.onError} />
|
||||||
(full_name_placeholder) => <InPlaceEdit
|
{this.state.isMe ?
|
||||||
placeholder={full_name_placeholder}
|
<>
|
||||||
value={this.state.fullName}
|
<div className="hr" />
|
||||||
onFinished={this.handleFullNameUpdate} />
|
<div className="panel-form-column">
|
||||||
}</FormattedMessage></div>
|
<label className="small">
|
||||||
</div>
|
<FormattedMessage id="label_user_contacts" defaultMessage="Contacts:"
|
||||||
<AvatarUpload
|
description="Label for user contacts" />
|
||||||
tinode={this.props.tinode}
|
</label>
|
||||||
avatar={this.state.avatar}
|
<div className="quoted">
|
||||||
uid={this.props.myUserId}
|
{credentials}
|
||||||
title={this.state.fullName}
|
{this.state.addCredActive ?
|
||||||
onImageUpdated={this.handleImageUpdated}
|
<input type="text" value={this.state.value}
|
||||||
onError={this.props.onError} />
|
className={this.state.addCredInvalid ? 'invalid' : null}
|
||||||
</div>
|
placeholder="Phone number or email" required="required" autoFocus
|
||||||
<div className="hr" />
|
onChange={this.handleCredChange} onKeyDown={this.handleCredKeyDown} onBlur={this.handleCredEntered} />
|
||||||
<FormattedMessage id="title_tag_manager" defaultMessage="Tags (user discovery)"
|
: null}
|
||||||
description="Section title for TagManager">{
|
<div>
|
||||||
(title_tag_manager) => <TagManager
|
<a href="#" onClick={(e) => {e.preventDefault(); this.setState({addCredActive: true});}}>
|
||||||
title={title_tag_manager}
|
<i className="material-icons">add</i>
|
||||||
activated={false}
|
<FormattedMessage id="button_add_another" defaultMessage="Add another" description="Call to action [+ add another]" />
|
||||||
tags={this.state.tags}
|
</a></div>
|
||||||
tinode={this.props.tinode}
|
</div>
|
||||||
onSubmit={this.handleTagsUpdated} />
|
</div>
|
||||||
}</FormattedMessage>
|
</>
|
||||||
<div className="hr" />
|
: null
|
||||||
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import Cropper from '../widgets/cropper.jsx';
|
import Cropper from './cropper.jsx';
|
||||||
import { imageCrop } from '../lib/blob-helpers.js';
|
import { imageCrop } from '../lib/blob-helpers.js';
|
||||||
|
|
||||||
export default class AvatarCropView extends React.PureComponent {
|
export default class AvatarCrop extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
// Widget for editing topic description.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import Tinode from 'tinode-sdk';
|
||||||
|
|
||||||
|
import AvatarCrop from './avatar-crop.jsx';
|
||||||
|
import AvatarUpload from './avatar-upload.jsx';
|
||||||
|
import InPlaceEdit from './in-place-edit.jsx';
|
||||||
|
import TagManager from './tag-manager.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, theCard } from '../lib/utils.js';
|
||||||
|
|
||||||
|
export default class TopicDescEdit 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.tinode.isMe(this.props.topic),
|
||||||
|
owner: acs && acs.isOwner(),
|
||||||
|
fullName: topic.public ? topic.public.fn : undefined,
|
||||||
|
avatar: makeImageUrl(topic.public ? topic.public.photo : null),
|
||||||
|
tags: topic.tags(),
|
||||||
|
newAvatar: null,
|
||||||
|
newAvatarMime: null,
|
||||||
|
previousOnTags: topic.onTagsUpdated
|
||||||
|
};
|
||||||
|
|
||||||
|
this.tnNewTags = this.tnNewTags.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.handleTagsUpdated = this.handleTagsUpdated.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||||
|
topic.onTagsUpdated = this.tnNewTags;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
const topic = this.props.tinode.getTopic(this.props.topic);
|
||||||
|
topic.onTagsUpdated = this.state.previousOnTags;
|
||||||
|
}
|
||||||
|
|
||||||
|
tnNewTags(tags) {
|
||||||
|
this.setState({tags: tags});
|
||||||
|
}
|
||||||
|
|
||||||
|
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});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<AvatarCrop
|
||||||
|
avatar={this.state.newAvatar}
|
||||||
|
mime={this.state.newAvatarMime}
|
||||||
|
onSubmit={this.handleAvatarCropped}
|
||||||
|
onCancel={this.handleAvatarCropCancel}
|
||||||
|
onError={this.props.onError} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const editable = this.state.isMe || this.state.owner;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="panel-form-row">
|
||||||
|
<div className="panel-form-column">
|
||||||
|
{this.state.me ? <>
|
||||||
|
<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}
|
||||||
|
required={true}
|
||||||
|
onFinished={this.handleFullNameUpdate} />
|
||||||
|
}</FormattedMessage></div>
|
||||||
|
</> : <>
|
||||||
|
<div><label className="small">
|
||||||
|
<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><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>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<AvatarUpload
|
||||||
|
tinode={this.props.tinode}
|
||||||
|
avatar={this.state.avatar}
|
||||||
|
readOnly={!editable}
|
||||||
|
uid={this.props.topic}
|
||||||
|
title={this.state.fullName}
|
||||||
|
onImageUpdated={this.handleImageUpdated}
|
||||||
|
onError={this.props.onError} />
|
||||||
|
</div>
|
||||||
|
{editable ?
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user