// 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, MAX_TOPIC_DESCRIPTION_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: Tinode.isMeTopicName(this.props.topic), owner: acs && acs.isOwner(), fullName: topic.public ? topic.public.fn : undefined, private: topic.private ? topic.private.comment : null, description: topic.public ? topic.public.note : 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.handlePrivateUpdate = this.handlePrivateUpdate.bind(this); this.handleDescriptionUpdate = this.handleDescriptionUpdate.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.state.fullName !== fn) { this.setState({fullName: fn}); this.props.onUpdateTopicDesc(this.props.topic, theCard(fn, null)); } } handlePrivateUpdate(comment) { comment = comment.trim().substring(0, MAX_TITLE_LENGTH); if (this.state.private !== comment) { this.setState({private: comment}); this.props.onUpdateTopicDesc(this.props.topic, null, comment || Tinode.DEL_CHAR); } } handleDescriptionUpdate(desc) { desc = desc.trim().substring(0, MAX_TOPIC_DESCRIPTION_LENGTH); if (desc) { this.setState({description: desc}); this.props.onUpdateTopicDesc(this.props.topic, theCard(null, null, null, desc)); } } // 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 ( ); } const editable = this.state.isMe || this.state.owner; return ( <>
{this.state.isMe ?
{ (full_name_placeholder) => }
: <>
Unknown} readOnly={!editable} value={this.state.fullName} required={true} onFinished={this.handleFullNameUpdate} />
{ (private_placeholder) => }
} {editable || this.state.description ?
{ (private_placeholder) => }
: null }
{editable ? <>
{ (title_tag_manager) => } : null } ); } };