// View for editing topic (including 'me' topic) parameters. import React from 'react'; 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 { 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 TopicCommonView extends React.Component { constructor(props) { super(props); const topic = this.props.tinode.getTopic(this.props.topic); this.state = { fullName: topic.public ? topic.public.fn : undefined, avatar: makeImageUrl(topic.public ? topic.public.photo : null), tags: topic.tags(), credentials: topic.getCredentials() || [], addCredActive: false, addCredInvalid: false, newCred: '', newAvatar: null, newAvatarMime: null, previousOnTags: topic.onTagsUpdated }; 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.onUpdateAccount(undefined, 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.onUpdateAccount(undefined, 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.onUpdateAccount(undefined, 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.onUpdateAccount(undefined, 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}); } 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}); } } 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 credentials = []; this.state.credentials.map((cred) => { credentials.push(
{cred.meth}: {cred.val} {!cred.done ? {e.preventDefault(); this.props.onCredConfirm(cred.meth, cred.val);}}> : null} {e.preventDefault(); this.props.onCredDelete(cred.meth, cred.val);}}>delete_outline
); }); return (
{ (full_name_placeholder) => }
{ (title_tag_manager) => }
{credentials} {this.state.addCredActive ? : null}
); } };