fixing display of avatars by URL

This commit is contained in:
or-else
2021-07-15 17:29:06 -07:00
parent 4219e47529
commit 0afef6c587
24 changed files with 220 additions and 165 deletions
+18 -1
View File
@@ -1101,13 +1101,29 @@ i.search {
border-radius: 50%; border-radius: 50%;
} }
.avatar-upload .preview { .avatar-upload img.preview {
width: 8rem; width: 8rem;
height: 8rem; height: 8rem;
overflow: hidden; overflow: hidden;
border-radius: 50%; border-radius: 50%;
} }
.avatar-upload img.preview::before {
position: absolute;
content: " ";
display: flex;
font-size: 0.9rem;
font-weight: lighter;
color: #666;
top: 0;
left: 0;
width: 100%;
height: 100%;
justify-content: center;
background: center no-repeat url("../img/broken_image.png");
border-radius: 50%;
}
.avatar-upload .inputfile.hidden { .avatar-upload .inputfile.hidden {
width: 0.1px; width: 0.1px;
height: 0.1px; height: 0.1px;
@@ -2177,6 +2193,7 @@ ul.tabbar li.active > a {
border-radius: 0.25rem; border-radius: 0.25rem;
border: solid 2px #1e88e5; border: solid 2px #1e88e5;
box-shadow: 1px 1px 3px grey; box-shadow: 1px 1px 3px grey;
background-color: #999;
} }
.cropper .preview { .cropper .preview {
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -45,12 +45,12 @@ export const MEDIA_BREAKPOINT = 640;
// Size of css 'rem' unit in pixels. Default 1rem = 10pt = 13px. // Size of css 'rem' unit in pixels. Default 1rem = 10pt = 13px.
export const REM_SIZE = 13; export const REM_SIZE = 13;
// Size of the avatar image: when an image is uploaded, it's resized to // Size of the avatar image: when image dimensions are greater or the image is not square, it's resized to
// a square of this size or less. // a square of this size or less.
export const AVATAR_SIZE = 384; export const AVATAR_SIZE = 384;
// Maximum size of an avatar in bytes for sending in-band. Bigger avatars will be sent out of band (as uploads). // Maximum size of an avatar in bytes for sending in-band. Bigger avatars will be sent out of band (as uploads).
export const MAX_AVATAR_BYTES = 8192; export const MAX_AVATAR_BYTES = 4096;
// Size of the broken_image shown in MessagesView // Size of the broken_image shown in MessagesView
export const BROKEN_IMAGE_SIZE = 32; export const BROKEN_IMAGE_SIZE = 32;
+2 -2
View File
@@ -6,8 +6,8 @@ import { bytesToHumanSize } from './strformat.js'
export const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml']; export const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml'];
export const MIME_EXTENSIONS = ['jpg', 'gif', 'png', 'svg', 'svg']; export const MIME_EXTENSIONS = ['jpg', 'gif', 'png', 'svg', 'svg'];
// Make a data: URL from public.photo // Get an URL from a theCard photo: either create a data: URL or return reference URL.
export function makeImageDataUrl(photo) { export function makeImageUrl(photo) {
if (photo) { if (photo) {
if (photo.data && photo.type) { if (photo.data && photo.type) {
const mime = photo.type.startsWith('image/') ? photo.type : ('image/' + photo.type); const mime = photo.type.startsWith('image/') ? photo.type : ('image/' + photo.type);
+5 -5
View File
@@ -38,10 +38,12 @@ export function theCard(fn, imageUrl, imageMimeType) {
if (matches) { if (matches) {
mimeType = matches[1]; mimeType = matches[1];
card.photo = { card.photo = {
data: imageUrl.substring(imageUrl.indexOf(',') + 1) data: imageUrl.substring(imageUrl.indexOf(',') + 1),
ref: Tinode.DEL_CHAR
}; };
} else { } else {
card.photo = { card.photo = {
data: Tinode.DEL_CHAR,
ref: imageUrl ref: imageUrl
}; };
} }
@@ -107,10 +109,9 @@ export function isUrlRelative(url) {
// Ensure URL does not present an XSS risk. Optional allowedSchemes may contain an array of // Ensure URL does not present an XSS risk. Optional allowedSchemes may contain an array of
// strings with permitted URL schemes, such as ['ftp', 'ftps']; otherwise accept http and https only. // strings with permitted URL schemes, such as ['ftp', 'ftps']; otherwise accept http and https only.
// If apikey and/or token is defined, it's appended to relative URL.
export function sanitizeUrl(url, allowedSchemes) { export function sanitizeUrl(url, allowedSchemes) {
if (!url) { if (typeof url != 'string') {
return null; return url;
} }
// Strip control characters and whitespace. They are not valid URL characters anyway. // Strip control characters and whitespace. They are not valid URL characters anyway.
@@ -139,7 +140,6 @@ export function sanitizeUrl(url, allowedSchemes) {
// Ensure URL is suitable for <img src="url"> field: the URL must be a relative URL or // Ensure URL is suitable for <img src="url"> field: the URL must be a relative URL or
// have http:, https:, blob: or data: scheme. // have http:, https:, blob: or data: scheme.
// The relative URL is appended with the API key and/or auth token.
// In case of data: scheme, the URL must start with a 'data:image/XXXX;base64,'. // In case of data: scheme, the URL must start with a 'data:image/XXXX;base64,'.
export function sanitizeImageUrl(url) { export function sanitizeImageUrl(url) {
if (!url) { if (!url) {
+7 -9
View File
@@ -12,7 +12,7 @@ import TagManager from '../widgets/tag-manager.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';
import { imageScaled, blobToBase64, makeImageDataUrl } from '../lib/blob-helpers.js'; import { imageScaled, blobToBase64, makeImageUrl } from '../lib/blob-helpers.js';
import { arrayEqual, asEmail, asPhone, theCard } from '../lib/utils.js'; import { arrayEqual, asEmail, asPhone, theCard } from '../lib/utils.js';
export default class AccGeneralView extends React.Component { export default class AccGeneralView extends React.Component {
@@ -22,7 +22,7 @@ export default class AccGeneralView extends React.Component {
const me = this.props.tinode.getMeTopic(); const me = this.props.tinode.getMeTopic();
this.state = { this.state = {
fullName: me.public ? me.public.fn : undefined, fullName: me.public ? me.public.fn : undefined,
avatar: makeImageDataUrl(me.public ? me.public.photo : null), avatar: makeImageUrl(me.public ? me.public.photo : null),
tags: me.tags(), tags: me.tags(),
credentials: me.getCredentials() || [], credentials: me.getCredentials() || [],
addCredActive: false, addCredActive: false,
@@ -100,10 +100,6 @@ export default class AccGeneralView extends React.Component {
if (blob.size > MAX_AVATAR_BYTES) { if (blob.size > MAX_AVATAR_BYTES) {
// Too large to send inband - uploading out of band and sending as a link. // Too large to send inband - uploading out of band and sending as a link.
const uploader = this.props.tinode.getLargeFileHelper(); const uploader = this.props.tinode.getLargeFileHelper();
if (!uploader) {
this.props.onError(this.props.intl.formatMessage(messages.cannot_initiate_upload));
return;
}
this.setState({uploading: true}); this.setState({uploading: true});
uploader.upload(blob) uploader.upload(blob)
@@ -117,17 +113,19 @@ export default class AccGeneralView extends React.Component {
this.setState({uploading: false}); this.setState({uploading: false});
}); });
} else { } else {
this.setState({uploading: true});
// Convert blob to base64-encoded bits. // Convert blob to base64-encoded bits.
blobToBase64(blob, (unused, base64bits) => { blobToBase64(blob, (unused, base64bits) => {
const du = makeImageDataUrl({data: base64bits, type: mime}); const du = makeImageUrl({data: base64bits, type: mime});
this.setState({source: du}); this.setState({source: du});
this.props.onUpdateAccount(undefined, theCard(null, du)); this.props.onUpdateAccount(undefined, theCard(null, du));
this.setState({uploading: false});
}); });
} }
}; };
if (width > AVATAR_SIZE || height > AVATAR_SIZE) { if (width > AVATAR_SIZE || height > AVATAR_SIZE || width != height) {
// Avatar is too large even after scaling. Shring it and make square. // 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, imageScaled(blob, AVATAR_SIZE, AVATAR_SIZE, MAX_EXTERN_ATTACHMENT_SIZE, true,
readyToUpload, readyToUpload,
(err) => { (err) => {
+6 -2
View File
@@ -55,9 +55,13 @@ export default class AvatarCropView extends React.PureComponent {
onChange={this.handleChange} /> onChange={this.handleChange} />
</div> </div>
<div className="dialog-buttons"> <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}> <button className="primary" onClick={this.handleSubmit}>
<FormattedMessage id="button_ok" <FormattedMessage id="button_ok" defaultMessage="OK" description="Button [OK]" />
defaultMessage="OK" description="Button [OK]" />
</button> </button>
</div> </div>
</div> </div>
+2 -2
View File
@@ -5,7 +5,7 @@ import { FormattedMessage } from 'react-intl';
import AvatarUpload from '../widgets/avatar-upload.jsx'; import AvatarUpload from '../widgets/avatar-upload.jsx';
import BadgeList from '../widgets/badge-list.jsx'; import BadgeList from '../widgets/badge-list.jsx';
import { makeImageDataUrl } from '../lib/blob-helpers.js'; import { makeImageUrl } from '../lib/blob-helpers.js';
export default class EditAccountView extends React.Component { export default class EditAccountView extends React.Component {
constructor(props) { constructor(props) {
@@ -14,7 +14,7 @@ export default class EditAccountView extends React.Component {
const me = this.props.tinode.getMeTopic(); const me = this.props.tinode.getMeTopic();
this.state = { this.state = {
fullName: me.public ? me.public.fn : undefined, fullName: me.public ? me.public.fn : undefined,
avatar: makeImageDataUrl(me.public ? me.public.photo : null) avatar: makeImageUrl(me.public ? me.public.photo : null)
}; };
} }
+2 -2
View File
@@ -18,7 +18,7 @@ import TagManager from '../widgets/tag-manager.jsx';
import { MAX_TITLE_LENGTH, NO_ACCESS_MODE } from '../config.js'; import { MAX_TITLE_LENGTH, NO_ACCESS_MODE } from '../config.js';
import { makeImageDataUrl } from '../lib/blob-helpers.js'; import { makeImageUrl } from '../lib/blob-helpers.js';
import { arrayEqual, theCard } from '../lib/utils.js'; import { arrayEqual, theCard } from '../lib/utils.js';
const messages = defineMessages({ const messages = defineMessages({
@@ -237,7 +237,7 @@ class InfoView extends React.Component {
muted: acs && acs.isMuted(), muted: acs && acs.isMuted(),
fullName: topic.public ? topic.public.fn : undefined, fullName: topic.public ? topic.public.fn : undefined,
avatar: makeImageDataUrl(topic.public ? topic.public.photo : null), avatar: makeImageUrl(topic.public ? topic.public.photo : null),
trustedBadges: badges, trustedBadges: badges,
private: topic.private ? topic.private.comment : null, private: topic.private ? topic.private.comment : null,
address: topic.name, address: topic.name,
+4 -8
View File
@@ -19,7 +19,7 @@ import SendMessage from '../widgets/send-message.jsx';
import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY, MESSAGES_PAGE, import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY, MESSAGES_PAGE,
MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE, READ_DELAY } from '../config.js'; MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE, READ_DELAY } from '../config.js';
import { SUPPORTED_IMAGE_FORMATS, blobToBase64, filePasted, fileToBase64, import { SUPPORTED_IMAGE_FORMATS, blobToBase64, filePasted, fileToBase64,
imageScaled, makeImageDataUrl } from '../lib/blob-helpers.js'; imageScaled, makeImageUrl } from '../lib/blob-helpers.js';
import HashNavigation from '../lib/navigation.js'; import HashNavigation from '../lib/navigation.js';
import { bytesToHumanSize, shortDateFormat } from '../lib/strformat.js'; import { bytesToHumanSize, shortDateFormat } from '../lib/strformat.js';
@@ -265,7 +265,7 @@ class MessagesView extends React.Component {
if (topic.public) { if (topic.public) {
Object.assign(nextState, { Object.assign(nextState, {
title: topic.public.fn, title: topic.public.fn,
avatar: makeImageDataUrl(topic.public.photo) avatar: makeImageUrl(topic.public.photo)
}); });
} else { } else {
Object.assign(nextState, { Object.assign(nextState, {
@@ -395,7 +395,7 @@ class MessagesView extends React.Component {
if (desc.public) { if (desc.public) {
this.setState({ this.setState({
title: desc.public.fn, title: desc.public.fn,
avatar: makeImageDataUrl(desc.public.photo) avatar: makeImageUrl(desc.public.photo)
}); });
} else { } else {
this.setState({ this.setState({
@@ -645,10 +645,6 @@ class MessagesView extends React.Component {
if (file.size > maxInbandAttachmentSize) { if (file.size > maxInbandAttachmentSize) {
// Too large to send inband - uploading out of band and sending as a link. // Too large to send inband - uploading out of band and sending as a link.
const uploader = this.props.tinode.getLargeFileHelper(); const uploader = this.props.tinode.getLargeFileHelper();
if (!uploader) {
this.props.onError(this.props.intl.formatMessage(messages.cannot_initiate_upload));
return;
}
const uploadCompletionPromise = uploader.upload(file); const uploadCompletionPromise = uploader.upload(file);
const msg = Drafty.attachFile(null, { const msg = Drafty.attachFile(null, {
mime: file.type, mime: file.type,
@@ -873,7 +869,7 @@ class MessagesView extends React.Component {
const user = topic.userDesc(thisFrom); const user = topic.userDesc(thisFrom);
if (user && user.public) { if (user && user.public) {
userName = user.public.fn; userName = user.public.fn;
userAvatar = makeImageDataUrl(user.public.photo); userAvatar = makeImageUrl(user.public.photo);
} }
userFrom = thisFrom; userFrom = thisFrom;
chatBoxClass='chat-box group'; chatBoxClass='chat-box group';
+3
View File
@@ -118,6 +118,8 @@ class SidepanelView extends React.Component {
badges = null; badges = null;
} }
console.log("avatar URL=", avatar);
let onCancel; let onCancel;
if (['login', 'contacts'].indexOf(view) == -1) { if (['login', 'contacts'].indexOf(view) == -1) {
onCancel = this.props.onCancel; onCancel = this.props.onCancel;
@@ -129,6 +131,7 @@ class SidepanelView extends React.Component {
state={view} state={view}
title={title} title={title}
avatar={avatar} avatar={avatar}
tinode={this.props.tinode}
trustedBadges={badges} trustedBadges={badges}
myUserId={this.props.myUserId} myUserId={this.props.myUserId}
onSignUp={this.props.onSignUp} onSignUp={this.props.onSignUp}
+10 -12
View File
@@ -18,7 +18,7 @@ import SidepanelView from './sidepanel-view.jsx';
import { API_KEY, APP_NAME, DEFAULT_P2P_ACCESS_MODE, LOGGING_ENABLED, import { API_KEY, APP_NAME, DEFAULT_P2P_ACCESS_MODE, LOGGING_ENABLED,
MEDIA_BREAKPOINT, RECEIVED_DELAY } from '../config.js'; MEDIA_BREAKPOINT, RECEIVED_DELAY } from '../config.js';
import { PACKAGE_VERSION } from '../version.js'; import { PACKAGE_VERSION } from '../version.js';
import { base64ReEncode, makeImageDataUrl } from '../lib/blob-helpers.js'; import { base64ReEncode, makeImageUrl } from '../lib/blob-helpers.js';
import { detectServerAddress, isLocalHost, isSecureConnection } from '../lib/host-name.js'; import { detectServerAddress, isLocalHost, isSecureConnection } from '../lib/host-name.js';
import LocalStorageUtil from '../lib/local-storage.js'; import LocalStorageUtil from '../lib/local-storage.js';
import HashNavigation from '../lib/navigation.js'; import HashNavigation from '../lib/navigation.js';
@@ -308,7 +308,7 @@ class TinodeWeb extends React.Component {
initFCMessaging() { initFCMessaging() {
const {formatMessage, locale} = this.props.intl; const {formatMessage, locale} = this.props.intl;
const onError = (msg, err) => { const onError = (msg, err) => {
console.log(msg, err); console.error(msg, err);
this.handleError(formatMessage(messages.push_init_failed), 'err'); this.handleError(formatMessage(messages.push_init_failed), 'err');
this.setState({desktopAlertsEnabled: false}); this.setState({desktopAlertsEnabled: false});
} }
@@ -384,7 +384,7 @@ class TinodeWeb extends React.Component {
'cred','reset','newtpk','archive','blocked','contacts',''].includes(hash.path[0])) { 'cred','reset','newtpk','archive','blocked','contacts',''].includes(hash.path[0])) {
this.setState({sidePanelSelected: hash.path[0]}); this.setState({sidePanelSelected: hash.path[0]});
} else { } else {
console.log("Unknown sidepanel view", hash.path[0]); console.info("Unknown sidepanel view", hash.path[0]);
} }
// Topic for MessagesView selector. // Topic for MessagesView selector.
@@ -464,13 +464,11 @@ class TinodeWeb extends React.Component {
handlePersistenceChange(persist) { handlePersistenceChange(persist) {
if (persist) { if (persist) {
this.tinode.initStorage().then(() => { this.tinode.initStorage().then(() => {
console.log("storage initialized");
LocalStorageUtil.setObject('keep-logged-in', true); LocalStorageUtil.setObject('keep-logged-in', true);
this.setState({persist: true}); this.setState({persist: true});
}); });
} else { } else {
this.tinode.clearStorage().then(() => { this.tinode.clearStorage().then(() => {
console.log("storage cleared");
LocalStorageUtil.setObject('keep-logged-in', false); LocalStorageUtil.setObject('keep-logged-in', false);
this.setState({persist: false}); this.setState({persist: false});
}); });
@@ -653,7 +651,7 @@ class TinodeWeb extends React.Component {
if (desc.public) { if (desc.public) {
this.setState({ this.setState({
sidePanelTitle: desc.public.fn, sidePanelTitle: desc.public.fn,
sidePanelAvatar: makeImageDataUrl(desc.public.photo) sidePanelAvatar: makeImageUrl(desc.public.photo)
}); });
} }
if (desc.trusted) { if (desc.trusted) {
@@ -721,7 +719,7 @@ class TinodeWeb extends React.Component {
} else { } else {
// TODO(gene): handle other types of notifications: // TODO(gene): handle other types of notifications:
// * ua -- user agent changes (maybe display a pictogram for mobile/desktop). // * ua -- user agent changes (maybe display a pictogram for mobile/desktop).
console.log("Unsupported (yet) presence update:" + what + " in: " + cont.topic); console.info("Unsupported (yet) presence update:" + what + " in: " + cont.topic);
} }
} }
@@ -945,7 +943,7 @@ class TinodeWeb extends React.Component {
}); });
break; break;
default: default:
console.log("Unknown invitation action", '"' + action + '""'); console.info("Unknown invitation action", '"' + action + '""');
} }
if (response != null) { if (response != null) {
@@ -1077,7 +1075,7 @@ class TinodeWeb extends React.Component {
}).then(() => { }).then(() => {
this.requestPushToken(); this.requestPushToken();
}).catch((err) => { }).catch((err) => {
console.log("Failed to get notification permission.", err); console.error("Failed to get notification permission.", err);
this.handleError(err.message, 'err'); this.handleError(err.message, 'err');
this.setState({desktopAlerts: false, firebaseToken: null}); this.setState({desktopAlerts: false, firebaseToken: null});
LocalStorageUtil.updateObject('settings', {desktopAlerts: false}); LocalStorageUtil.updateObject('settings', {desktopAlerts: false});
@@ -1090,7 +1088,7 @@ class TinodeWeb extends React.Component {
} }
} else if (this.state.firebaseToken && this.fbPush) { } else if (this.state.firebaseToken && this.fbPush) {
this.fbPush.deleteToken(this.state.firebaseToken).catch((err) => { this.fbPush.deleteToken(this.state.firebaseToken).catch((err) => {
console.log("Unable to delete token.", err); console.error("Unable to delete token.", err);
}).finally(() => { }).finally(() => {
LocalStorageUtil.updateObject('settings', {desktopAlerts: false}); LocalStorageUtil.updateObject('settings', {desktopAlerts: false});
localStorage.removeItem('firebase-token'); localStorage.removeItem('firebase-token');
@@ -1119,7 +1117,7 @@ class TinodeWeb extends React.Component {
} }
}).catch((err) => { }).catch((err) => {
this.handleError(err.message, 'err'); this.handleError(err.message, 'err');
console.log("Failed to retrieve firebase token", err); console.error("Failed to retrieve firebase token", err);
}); });
} }
@@ -1154,7 +1152,7 @@ class TinodeWeb extends React.Component {
let path = ''; let path = '';
if (['security','support','general','notif'].includes(parsed.path[0])) { if (['security','support','general','notif'].includes(parsed.path[0])) {
path = 'edit'; path = 'edit';
} else if ('crop') { } else if ('crop' == parsed.path[0]) {
path = 'general'; path = 'general';
} else if ('blocked' == parsed.path[0]) { } else if ('blocked' == parsed.path[0]) {
path = 'security'; path = 'security';
+3 -1
View File
@@ -3,6 +3,8 @@ import React from 'react';
import LetterTile from './letter-tile.jsx'; import LetterTile from './letter-tile.jsx';
import LoadSpinner from './load-spinner.jsx'; import LoadSpinner from './load-spinner.jsx';
import { sanitizeImageUrl } from '../lib/utils.js';
import { AVATAR_SIZE } from '../config.js'; import { AVATAR_SIZE } from '../config.js';
export default class AvatarUpload extends React.Component { export default class AvatarUpload extends React.Component {
@@ -42,7 +44,7 @@ export default class AvatarUpload extends React.Component {
<i className="material-icons">clear</i> <i className="material-icons">clear</i>
</a>} </a>}
{this.state.source ? {this.state.source ?
<img src={this.state.source} className="preview" /> : <img src={this.props.tinode.authorizeURL(sanitizeImageUrl(this.state.source))} className="preview" /> :
this.props.readOnly && this.props.uid ? this.props.readOnly && this.props.uid ?
<div className="avatar-box"> <div className="avatar-box">
<LetterTile <LetterTile
+2 -2
View File
@@ -2,7 +2,7 @@ import React from 'react';
import Chip from './chip.jsx'; import Chip from './chip.jsx';
import { makeImageDataUrl } from '../lib/blob-helpers.js'; import { makeImageUrl } from '../lib/blob-helpers.js';
/* BEGIN ChipInput: group membership widget */ /* BEGIN ChipInput: group membership widget */
export default class ChipInput extends React.Component { export default class ChipInput extends React.Component {
@@ -119,7 +119,7 @@ export default class ChipInput extends React.Component {
chips.push( chips.push(
<Chip <Chip
onCancel={this.handleChipCancel} onCancel={this.handleChipCancel}
avatar={makeImageDataUrl(item.public ? item.public.photo : null)} avatar={makeImageUrl(item.public ? item.public.photo : null)}
title={item.public ? item.public.fn : undefined} title={item.public ? item.public.fn : undefined}
noAvatar={this.props.avatarDisabled} noAvatar={this.props.avatarDisabled}
topic={item.user} topic={item.user}
+2 -2
View File
@@ -9,7 +9,7 @@ import Tinode from 'tinode-sdk';
import Contact from './contact.jsx'; import Contact from './contact.jsx';
import ContactAction from './contact-action.jsx'; import ContactAction from './contact-action.jsx';
import { makeImageDataUrl } from '../lib/blob-helpers.js'; import { makeImageUrl } from '../lib/blob-helpers.js';
import { MESSAGE_PREVIEW_LENGTH } from '../config.js'; import { MESSAGE_PREVIEW_LENGTH } from '../config.js';
@@ -90,7 +90,7 @@ class ContactList extends React.Component {
contactNodes.push( contactNodes.push(
<Contact <Contact
title={c.public ? c.public.fn : null} title={c.public ? c.public.fn : null}
avatar={makeImageDataUrl(c.public ? c.public.photo : null)} avatar={makeImageUrl(c.public ? c.public.photo : null)}
comment={comment} comment={comment}
preview={preview} preview={preview}
received={deliveryStatus} received={deliveryStatus}
+5 -3
View File
@@ -129,10 +129,12 @@ export default class Cropper extends React.Component {
}); });
// Initial zoom level fills the bounding box at the smallest image dimension and overflows the largest, i.e. "fill" not "fit". // Initial zoom level fills the bounding box at the smallest image dimension and overflows the largest, i.e. "fill" not "fit".
let zoom = Math.max((this.bBoxRect.width / imgRect.width), (this.bBoxRect.height / imgRect.height)); const zoom = Math.max(this.bBoxRect.width / imgRect.width, this.bBoxRect.height / imgRect.height);
// Converting from viewport coordinates to container, then panning. // Converting from viewport coordinates to container, then panning.
let panX = (this.cutoutRect.left - this.bBoxRect.left) - ((imgRect.width - this.cutoutRect.width) / 2); const panX = this.cutoutRect.left - this.bBoxRect.left -
let panY = (this.cutoutRect.top - this.bBoxRect.top) - ((imgRect.height - this.cutoutRect.height) / 2); (imgRect.width - this.cutoutRect.width) / 2;
const panY = this.cutoutRect.top - this.bBoxRect.top -
(imgRect.height - this.cutoutRect.height) / 2;
this.positionAll(panX, panY, zoom); this.positionAll(panX, panY, zoom);
} }
+2 -2
View File
@@ -6,7 +6,7 @@ import { FormattedMessage } from 'react-intl';
import { MAX_ONLINE_IN_TOPIC } from '../config.js' import { MAX_ONLINE_IN_TOPIC } from '../config.js'
import LetterTile from './letter-tile.jsx'; import LetterTile from './letter-tile.jsx';
import { makeImageDataUrl } from '../lib/blob-helpers.js'; import { makeImageUrl } from '../lib/blob-helpers.js';
export default class GroupSubs extends React.Component { export default class GroupSubs extends React.Component {
constructor(props) { constructor(props) {
@@ -23,7 +23,7 @@ export default class GroupSubs extends React.Component {
<div className="avatar-box" key={sub.user}> <div className="avatar-box" key={sub.user}>
<LetterTile <LetterTile
topic={sub.user} topic={sub.user}
avatar={makeImageDataUrl(sub.public ? sub.public.photo : null) || true} avatar={makeImageUrl(sub.public ? sub.public.photo : null) || true}
title={sub.public ? sub.public.fn : null} /> title={sub.public ? sub.public.fn : null} />
</div> </div>
); );
+1 -1
View File
@@ -23,7 +23,7 @@ export default class LetterTile extends React.PureComponent {
} else if (this.props.avatar) { } else if (this.props.avatar) {
// If avatar image is invalid, show a placeholder. // If avatar image is invalid, show a placeholder.
avatar = <img className="avatar" alt="avatar" src={this.props.avatar} avatar = <img className="avatar" alt="avatar" src={this.props.avatar}
onError={(e)=>{e.target.onerror = null; e.target.src="/img/broken_image.png"}} />; onError={(e)=>{e.target.onerror = null; e.target.src="../img/broken_image.png"}} />;
} else { } else {
avatar = null; avatar = null;
} }
+2 -2
View File
@@ -6,7 +6,7 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import CheckBox from './checkbox.jsx'; import CheckBox from './checkbox.jsx';
import Contact from './contact.jsx'; import Contact from './contact.jsx';
import { makeImageDataUrl } from '../lib/blob-helpers.js'; import { makeImageUrl } from '../lib/blob-helpers.js';
// Translatable permission names. // Translatable permission names.
const messages = defineMessages({ const messages = defineMessages({
@@ -135,7 +135,7 @@ class PermissionsEditor extends React.Component {
<ul className="contact-box"><Contact <ul className="contact-box"><Contact
item={this.props.item} item={this.props.item}
title={this.props.userTitle} title={this.props.userTitle}
avatar={makeImageDataUrl(this.props.userAvatar ? this.props.userAvatar : null)} /></ul> : null} avatar={makeImageUrl(this.props.userAvatar ? this.props.userAvatar : null)} /></ul> : null}
<label className="small"><FormattedMessage id="title_permissions" <label className="small"><FormattedMessage id="title_permissions"
defaultMessage="Permissions" description="Section title"/></label> defaultMessage="Permissions" description="Section title"/></label>
<table className="permission-editor"> <table className="permission-editor">
+8 -3
View File
@@ -1,12 +1,13 @@
import React from 'react'; import React from 'react';
import LetterTile from './letter-tile.jsx'; import LetterTile from './letter-tile.jsx';
import ButtonBack from './button-back.jsx'; import ButtonBack from './button-back.jsx';
import ContactBadges from './contact-badges.jsx'; import ContactBadges from './contact-badges.jsx';
import MenuContacts from './menu-contacts.jsx'; import MenuContacts from './menu-contacts.jsx';
import MenuStart from './menu-start.jsx'; import MenuStart from './menu-start.jsx';
import { sanitizeImageUrl } from '../lib/utils.js';
export default class SideNavbar extends React.PureComponent { export default class SideNavbar extends React.PureComponent {
render() { render() {
const icon_badges = []; const icon_badges = [];
@@ -15,13 +16,17 @@ export default class SideNavbar extends React.PureComponent {
icon_badges.push({icon: b, color: 'badge-inv'}); icon_badges.push({icon: b, color: 'badge-inv'});
}); });
} }
let avatar = null;
if (this.props.tinode) {
avatar = this.props.tinode.authorizeURL(sanitizeImageUrl(this.props.avatar));
}
return ( return (
<div id="side-caption-panel" className="caption-panel"> <div id="side-caption-panel" className="caption-panel">
{this.props.onCancel ? <ButtonBack onBack={this.props.onCancel} /> : null} {this.props.onCancel ? <ButtonBack onBack={this.props.onCancel} /> : null}
{this.props.avatar ? {avatar ?
<div id="self-avatar" className="avatar-box"> <div id="self-avatar" className="avatar-box">
<LetterTile <LetterTile
avatar={this.props.avatar} avatar={avatar}
topic={this.props.myUserId} topic={this.props.myUserId}
title={this.props.title} /> title={this.props.title} />
</div> </div>
+130 -100
View File
@@ -65,7 +65,7 @@ const NO_ACCESS_MODE = 'N';
const MEDIA_BREAKPOINT = 640; const MEDIA_BREAKPOINT = 640;
const REM_SIZE = 13; const REM_SIZE = 13;
const AVATAR_SIZE = 384; const AVATAR_SIZE = 384;
const MAX_AVATAR_BYTES = 8192; const MAX_AVATAR_BYTES = 4096;
const BROKEN_IMAGE_SIZE = 32; const BROKEN_IMAGE_SIZE = 32;
const MESSAGES_PAGE = 24; const MESSAGES_PAGE = 24;
const MAX_INBAND_ATTACHMENT_SIZE = 262144; const MAX_INBAND_ATTACHMENT_SIZE = 262144;
@@ -91,7 +91,7 @@ __webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "SUPPORTED_IMAGE_FORMATS": () => (/* binding */ SUPPORTED_IMAGE_FORMATS), /* harmony export */ "SUPPORTED_IMAGE_FORMATS": () => (/* binding */ SUPPORTED_IMAGE_FORMATS),
/* harmony export */ "MIME_EXTENSIONS": () => (/* binding */ MIME_EXTENSIONS), /* harmony export */ "MIME_EXTENSIONS": () => (/* binding */ MIME_EXTENSIONS),
/* harmony export */ "makeImageDataUrl": () => (/* binding */ makeImageDataUrl), /* harmony export */ "makeImageUrl": () => (/* binding */ makeImageUrl),
/* harmony export */ "fitImageSize": () => (/* binding */ fitImageSize), /* harmony export */ "fitImageSize": () => (/* binding */ fitImageSize),
/* harmony export */ "fileNameForMime": () => (/* binding */ fileNameForMime), /* harmony export */ "fileNameForMime": () => (/* binding */ fileNameForMime),
/* harmony export */ "imageScaled": () => (/* binding */ imageScaled), /* harmony export */ "imageScaled": () => (/* binding */ imageScaled),
@@ -110,7 +110,7 @@ __webpack_require__.r(__webpack_exports__);
const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml']; const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml'];
const MIME_EXTENSIONS = ['jpg', 'gif', 'png', 'svg', 'svg']; const MIME_EXTENSIONS = ['jpg', 'gif', 'png', 'svg', 'svg'];
function makeImageDataUrl(photo) { function makeImageUrl(photo) {
if (photo) { if (photo) {
if (photo.data && photo.type) { if (photo.data && photo.type) {
const mime = photo.type.startsWith('image/') ? photo.type : 'image/' + photo.type; const mime = photo.type.startsWith('image/') ? photo.type : 'image/' + photo.type;
@@ -611,10 +611,12 @@ function theCard(fn, imageUrl, imageMimeType) {
if (matches) { if (matches) {
mimeType = matches[1]; mimeType = matches[1];
card.photo = { card.photo = {
data: imageUrl.substring(imageUrl.indexOf(',') + 1) data: imageUrl.substring(imageUrl.indexOf(',') + 1),
ref: (tinode_sdk__WEBPACK_IMPORTED_MODULE_0___default().DEL_CHAR)
}; };
} else { } else {
card.photo = { card.photo = {
data: (tinode_sdk__WEBPACK_IMPORTED_MODULE_0___default().DEL_CHAR),
ref: imageUrl ref: imageUrl
}; };
} }
@@ -670,8 +672,8 @@ function isUrlRelative(url) {
return url && !/^\s*([a-z][a-z0-9+.-]*:|\/\/)/im.test(url); return url && !/^\s*([a-z][a-z0-9+.-]*:|\/\/)/im.test(url);
} }
function sanitizeUrl(url, allowedSchemes) { function sanitizeUrl(url, allowedSchemes) {
if (!url) { if (typeof url != 'string') {
return null; return url;
} }
url = url.replace(/[^\x20-\x7E]/gmi, '').trim(); url = url.replace(/[^\x20-\x7E]/gmi, '').trim();
@@ -798,7 +800,7 @@ class AccGeneralView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compo
const me = this.props.tinode.getMeTopic(); const me = this.props.tinode.getMeTopic();
this.state = { this.state = {
fullName: me.public ? me.public.fn : undefined, fullName: me.public ? me.public.fn : undefined,
avatar: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_8__.makeImageDataUrl)(me.public ? me.public.photo : null), avatar: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_8__.makeImageUrl)(me.public ? me.public.photo : null),
tags: me.tags(), tags: me.tags(),
credentials: me.getCredentials() || [], credentials: me.getCredentials() || [],
addCredActive: false, addCredActive: false,
@@ -811,9 +813,10 @@ class AccGeneralView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compo
this.tnNewTags = this.tnNewTags.bind(this); 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.handleFullNameUpdate = this.handleFullNameUpdate.bind(this);
this.handleImageUploaded = this.handleImageUploaded.bind(this); this.handleImageUpdated = this.handleImageUpdated.bind(this);
this.handleAvatarCropped = this.handleAvatarCropped.bind(this); this.handleAvatarCropped = this.handleAvatarCropped.bind(this);
this.handleCancel = this.handleCancel.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);
@@ -855,11 +858,15 @@ class AccGeneralView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compo
} }
} }
handleImageUploaded(mime, img) { handleImageUpdated(mime, img) {
this.setState({ this.setState({
newAvatar: img, newAvatar: img,
newAvatarMime: mime newAvatarMime: mime
}); });
if (!img) {
this.props.onUpdateAccount(undefined, (0,_lib_utils_js__WEBPACK_IMPORTED_MODULE_9__.theCard)(null, (tinode_sdk__WEBPACK_IMPORTED_MODULE_2___default().DEL_CHAR)));
}
} }
handleAvatarCropped(mime, blob, width, height) { handleAvatarCropped(mime, blob, width, height) {
@@ -869,9 +876,58 @@ class AccGeneralView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compo
newAvatar: null, newAvatar: null,
newAvatarMime: null newAvatarMime: null
}); });
if (blob) {
this.uploadAvatar(mime, blob, width, height);
}
} }
handleCancel(img) { uploadAvatar(mime, blob, width, height) {
const readyToUpload = (mime, blob, width, height) => {
if (blob.size > _config_js__WEBPACK_IMPORTED_MODULE_7__.MAX_AVATAR_BYTES) {
const uploader = this.props.tinode.getLargeFileHelper();
this.setState({
uploading: true
});
uploader.upload(blob).then(url => {
this.props.onUpdateAccount(undefined, (0,_lib_utils_js__WEBPACK_IMPORTED_MODULE_9__.theCard)(null, url));
}).catch(err => {
this.props.onError(err, 'err');
}).finally(() => {
this.setState({
uploading: false
});
});
} else {
this.setState({
uploading: true
});
(0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_8__.blobToBase64)(blob, (unused, base64bits) => {
const du = (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_8__.makeImageUrl)({
data: base64bits,
type: mime
});
this.setState({
source: du
});
this.props.onUpdateAccount(undefined, (0,_lib_utils_js__WEBPACK_IMPORTED_MODULE_9__.theCard)(null, du));
this.setState({
uploading: false
});
});
}
};
if (width > _config_js__WEBPACK_IMPORTED_MODULE_7__.AVATAR_SIZE || height > _config_js__WEBPACK_IMPORTED_MODULE_7__.AVATAR_SIZE || width != height) {
(0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_8__.imageScaled)(blob, _config_js__WEBPACK_IMPORTED_MODULE_7__.AVATAR_SIZE, _config_js__WEBPACK_IMPORTED_MODULE_7__.AVATAR_SIZE, _config_js__WEBPACK_IMPORTED_MODULE_7__.MAX_EXTERN_ATTACHMENT_SIZE, true, readyToUpload, err => {
this.props.onError(err, 'err');
});
} else {
readyToUpload(mime, blob, width, height);
}
}
handleAvatarCropCancel(img) {
this.setState({ this.setState({
newAvatar: null, newAvatar: null,
newAvatarMime: null newAvatarMime: null
@@ -947,7 +1003,7 @@ class AccGeneralView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compo
avatar: this.state.newAvatar, avatar: this.state.newAvatar,
mime: this.state.newAvatarMime, mime: this.state.newAvatarMime,
onSubmit: this.handleAvatarCropped, onSubmit: this.handleAvatarCropped,
onCancel: this.handleCancel, onCancel: this.handleAvatarCropCancel,
onError: this.props.onError onError: this.props.onError
}); });
} }
@@ -1007,8 +1063,7 @@ class AccGeneralView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compo
avatar: this.state.avatar, avatar: this.state.avatar,
uid: this.props.myUserId, uid: this.props.myUserId,
title: this.state.fullName, title: this.state.fullName,
onImageReceived: this.handleImageUploaded, onImageUpdated: this.handleImageUpdated,
onImageChanged: this.handleImageChanged,
onError: this.props.onError onError: this.props.onError
})), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { })), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "hr" className: "hr"
@@ -1566,7 +1621,16 @@ class AvatarCropView extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureC
onChange: this.handleChange onChange: this.handleChange
})), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { })), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "dialog-buttons" className: "dialog-buttons"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { }, this.props.onCancel ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", {
className: "secondary",
onClick: this.props.onCancel
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
id: "button_cancel",
defaultMessage: [{
"type": 0,
"value": "Cancel"
}]
})) : null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", {
className: "primary", className: "primary",
onClick: this.handleSubmit onClick: this.handleSubmit
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, { }, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
@@ -1951,7 +2015,7 @@ class EditAccountView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Comp
const me = this.props.tinode.getMeTopic(); const me = this.props.tinode.getMeTopic();
this.state = { this.state = {
fullName: me.public ? me.public.fn : undefined, fullName: me.public ? me.public.fn : undefined,
avatar: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_4__.makeImageDataUrl)(me.public ? me.public.photo : null) avatar: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_4__.makeImageUrl)(me.public ? me.public.photo : null)
}; };
} }
@@ -2358,7 +2422,7 @@ class InfoView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
deleter: acs && acs.isDeleter(), deleter: acs && acs.isDeleter(),
muted: acs && acs.isMuted(), muted: acs && acs.isMuted(),
fullName: topic.public ? topic.public.fn : undefined, fullName: topic.public ? topic.public.fn : undefined,
avatar: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.makeImageDataUrl)(topic.public ? topic.public.photo : null), avatar: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.makeImageUrl)(topic.public ? topic.public.photo : null),
trustedBadges: badges, trustedBadges: badges,
private: topic.private ? topic.private.comment : null, private: topic.private ? topic.private.comment : null,
address: topic.name, address: topic.name,
@@ -2783,7 +2847,7 @@ class InfoView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
readOnly: !this.state.owner, readOnly: !this.state.owner,
uid: this.props.topic, uid: this.props.topic,
title: this.state.fullName, title: this.state.fullName,
onImageChanged: this.handleImageChanged, onImageUpdated: this.handleImageChanged,
onError: this.props.onError onError: this.props.onError
})), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { })), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "hr" className: "hr"
@@ -3463,7 +3527,7 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
if (topic.public) { if (topic.public) {
Object.assign(nextState, { Object.assign(nextState, {
title: topic.public.fn, title: topic.public.fn,
avatar: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.makeImageDataUrl)(topic.public.photo) avatar: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.makeImageUrl)(topic.public.photo)
}); });
} else { } else {
Object.assign(nextState, { Object.assign(nextState, {
@@ -3606,7 +3670,7 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
if (desc.public) { if (desc.public) {
this.setState({ this.setState({
title: desc.public.fn, title: desc.public.fn,
avatar: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.makeImageDataUrl)(desc.public.photo) avatar: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.makeImageUrl)(desc.public.photo)
}); });
} else { } else {
this.setState({ this.setState({
@@ -3879,12 +3943,6 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
if (file.size > maxInbandAttachmentSize) { if (file.size > maxInbandAttachmentSize) {
const uploader = this.props.tinode.getLargeFileHelper(); const uploader = this.props.tinode.getLargeFileHelper();
if (!uploader) {
this.props.onError(this.props.intl.formatMessage(messages.cannot_initiate_upload));
return;
}
const uploadCompletionPromise = uploader.upload(file); const uploadCompletionPromise = uploader.upload(file);
const msg = Drafty.attachFile(null, { const msg = Drafty.attachFile(null, {
mime: file.type, mime: file.type,
@@ -4110,7 +4168,7 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
if (user && user.public) { if (user && user.public) {
userName = user.public.fn; userName = user.public.fn;
userAvatar = (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.makeImageDataUrl)(user.public.photo); userAvatar = (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.makeImageUrl)(user.public.photo);
} }
userFrom = thisFrom; userFrom = thisFrom;
@@ -4871,6 +4929,7 @@ class SidepanelView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compon
badges = null; badges = null;
} }
console.log("avatar URL=", avatar);
let onCancel; let onCancel;
if (['login', 'contacts'].indexOf(view) == -1) { if (['login', 'contacts'].indexOf(view) == -1) {
@@ -4884,6 +4943,7 @@ class SidepanelView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compon
state: view, state: view,
title: title, title: title,
avatar: avatar, avatar: avatar,
tinode: this.props.tinode,
trustedBadges: badges, trustedBadges: badges,
myUserId: this.props.myUserId, myUserId: this.props.myUserId,
onSignUp: this.props.onSignUp, onSignUp: this.props.onSignUp,
@@ -5315,7 +5375,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
} = this.props.intl; } = this.props.intl;
const onError = (msg, err) => { const onError = (msg, err) => {
console.log(msg, err); console.error(msg, err);
this.handleError(formatMessage(messages.push_init_failed), 'err'); this.handleError(formatMessage(messages.push_init_failed), 'err');
this.setState({ this.setState({
desktopAlertsEnabled: false desktopAlertsEnabled: false
@@ -5399,7 +5459,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
sidePanelSelected: hash.path[0] sidePanelSelected: hash.path[0]
}); });
} else { } else {
console.log("Unknown sidepanel view", hash.path[0]); console.info("Unknown sidepanel view", hash.path[0]);
} }
const topicName = hash.path[1]; const topicName = hash.path[1];
@@ -5499,7 +5559,6 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
handlePersistenceChange(persist) { handlePersistenceChange(persist) {
if (persist) { if (persist) {
this.tinode.initStorage().then(() => { this.tinode.initStorage().then(() => {
console.log("storage initialized");
_lib_local_storage_js__WEBPACK_IMPORTED_MODULE_15__.default.setObject('keep-logged-in', true); _lib_local_storage_js__WEBPACK_IMPORTED_MODULE_15__.default.setObject('keep-logged-in', true);
this.setState({ this.setState({
persist: true persist: true
@@ -5507,7 +5566,6 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
}); });
} else { } else {
this.tinode.clearStorage().then(() => { this.tinode.clearStorage().then(() => {
console.log("storage cleared");
_lib_local_storage_js__WEBPACK_IMPORTED_MODULE_15__.default.setObject('keep-logged-in', false); _lib_local_storage_js__WEBPACK_IMPORTED_MODULE_15__.default.setObject('keep-logged-in', false);
this.setState({ this.setState({
persist: false persist: false
@@ -5684,7 +5742,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
if (desc.public) { if (desc.public) {
this.setState({ this.setState({
sidePanelTitle: desc.public.fn, sidePanelTitle: desc.public.fn,
sidePanelAvatar: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_13__.makeImageDataUrl)(desc.public.photo) sidePanelAvatar: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_13__.makeImageUrl)(desc.public.photo)
}); });
} }
@@ -5745,7 +5803,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
}); });
} }
} else if (what == 'del') {} else if (what == 'upd') {} else { } else if (what == 'del') {} else if (what == 'upd') {} else {
console.log("Unsupported (yet) presence update:" + what + " in: " + cont.topic); console.info("Unsupported (yet) presence update:" + what + " in: " + cont.topic);
} }
} }
@@ -5965,7 +6023,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
break; break;
default: default:
console.log("Unknown invitation action", '"' + action + '""'); console.info("Unknown invitation action", '"' + action + '""');
} }
if (response != null) { if (response != null) {
@@ -6094,7 +6152,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
}).then(() => { }).then(() => {
this.requestPushToken(); this.requestPushToken();
}).catch(err => { }).catch(err => {
console.log("Failed to get notification permission.", err); console.error("Failed to get notification permission.", err);
this.handleError(err.message, 'err'); this.handleError(err.message, 'err');
this.setState({ this.setState({
desktopAlerts: false, desktopAlerts: false,
@@ -6117,7 +6175,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
} }
} else if (this.state.firebaseToken && this.fbPush) { } else if (this.state.firebaseToken && this.fbPush) {
this.fbPush.deleteToken(this.state.firebaseToken).catch(err => { this.fbPush.deleteToken(this.state.firebaseToken).catch(err => {
console.log("Unable to delete token.", err); console.error("Unable to delete token.", err);
}).finally(() => { }).finally(() => {
_lib_local_storage_js__WEBPACK_IMPORTED_MODULE_15__.default.updateObject('settings', { _lib_local_storage_js__WEBPACK_IMPORTED_MODULE_15__.default.updateObject('settings', {
desktopAlerts: false desktopAlerts: false
@@ -6164,7 +6222,7 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
} }
}).catch(err => { }).catch(err => {
this.handleError(err.message, 'err'); this.handleError(err.message, 'err');
console.log("Failed to retrieve firebase token", err); console.error("Failed to retrieve firebase token", err);
}); });
} }
@@ -6208,9 +6266,13 @@ class TinodeWeb extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
if (['security', 'support', 'general', 'notif'].includes(parsed.path[0])) { if (['security', 'support', 'general', 'notif'].includes(parsed.path[0])) {
path = 'edit'; path = 'edit';
} else if (true) { } else if ('crop' == parsed.path[0]) {
path = 'general'; path = 'general';
} else {} } else if ('blocked' == parsed.path[0]) {
path = 'security';
} else if (this.state.myUserId) {
path = 'contacts';
}
parsed.path[0] = path; parsed.path[0] = path;
@@ -7102,8 +7164,8 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var _letter_tile_jsx__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./letter-tile.jsx */ "./src/widgets/letter-tile.jsx"); /* harmony import */ var _letter_tile_jsx__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./letter-tile.jsx */ "./src/widgets/letter-tile.jsx");
/* harmony import */ var _load_spinner_jsx__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./load-spinner.jsx */ "./src/widgets/load-spinner.jsx"); /* harmony import */ var _load_spinner_jsx__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./load-spinner.jsx */ "./src/widgets/load-spinner.jsx");
/* harmony import */ var _config_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../config.js */ "./src/config.js"); /* harmony import */ var _lib_utils_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../lib/utils.js */ "./src/lib/utils.js");
/* harmony import */ var _lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../lib/blob-helpers.js */ "./src/lib/blob-helpers.js"); /* harmony import */ var _config_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../config.js */ "./src/config.js");
@@ -7113,10 +7175,9 @@ class AvatarUpload extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
source: props.avatar, source: props.avatar
uploading: false
}; };
this.handleFileUpload = this.handleFileUpload.bind(this); this.handleFileReceived = this.handleFileReceived.bind(this);
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
@@ -7127,48 +7188,9 @@ class AvatarUpload extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
} }
} }
handleFileUpload(e) { handleFileReceived(e) {
const image = e.target.files[0]; const image = e.target.files[0];
this.props.onImageUpdated(image.type, URL.createObjectURL(image), image.name);
if (this.props.onImageReceived) {
this.props.onImageReceived(image.type, URL.createObjectURL(image));
return;
}
if (image.size > _config_js__WEBPACK_IMPORTED_MODULE_3__.MAX_AVATAR_BYTES) {
const uploader = this.props.tinode.getLargeFileHelper();
if (!uploader) {
this.props.onError(this.props.intl.formatMessage(messages.cannot_initiate_upload));
return;
}
uploader.upload(image).then(url => {
this.props.onImageChanged(url);
}).catch(err => {
this.props.onError(err, 'err');
}).finally(() => {
this.setState({
uploading: false
});
});
} else {
(0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_4__.imageScaled)(image, _config_js__WEBPACK_IMPORTED_MODULE_3__.AVATAR_SIZE, _config_js__WEBPACK_IMPORTED_MODULE_3__.AVATAR_SIZE, _config_js__WEBPACK_IMPORTED_MODULE_3__.MAX_EXTERN_ATTACHMENT_SIZE, true, (mime, blob) => {
(0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_4__.blobToBase64)(blob, (unused, base64bits) => {
const du = (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_4__.makeImageDataUrl)({
data: base64bits,
type: mime
});
this.setState({
source: du
});
this.props.onImageChanged(du);
});
}, err => {
this.props.onError(err, 'err');
});
}
e.target.value = ''; e.target.value = '';
} }
@@ -7182,12 +7204,12 @@ class AvatarUpload extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
className: "clear-avatar", className: "clear-avatar",
onClick: e => { onClick: e => {
e.preventDefault(); e.preventDefault();
this.props.onImageChanged(null); this.props.onImageUpdated();
} }
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", { }, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
className: "material-icons" className: "material-icons"
}, "clear")), this.state.source ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("img", { }, "clear")), this.state.source ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("img", {
src: this.state.source, src: this.props.tinode.authorizeURL((0,_lib_utils_js__WEBPACK_IMPORTED_MODULE_3__.sanitizeImageUrl)(this.state.source)),
className: "preview" className: "preview"
}) : this.props.readOnly && this.props.uid ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { }) : this.props.readOnly && this.props.uid ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "avatar-box" className: "avatar-box"
@@ -7197,19 +7219,19 @@ class AvatarUpload extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
title: this.props.title title: this.props.title
})) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { })) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
className: "blank" className: "blank"
}, _config_js__WEBPACK_IMPORTED_MODULE_3__.AVATAR_SIZE, "\xD7", _config_js__WEBPACK_IMPORTED_MODULE_3__.AVATAR_SIZE), this.props.readOnly ? null : react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", { }, _config_js__WEBPACK_IMPORTED_MODULE_4__.AVATAR_SIZE, "\xD7", _config_js__WEBPACK_IMPORTED_MODULE_4__.AVATAR_SIZE), this.props.readOnly ? null : react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", {
type: "file", type: "file",
id: randId, id: randId,
className: "inputfile hidden", className: "inputfile hidden",
accept: "image/*", accept: "image/*",
onChange: this.handleFileUpload onChange: this.handleFileReceived
}), this.props.readOnly ? null : react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { }), this.props.readOnly ? null : react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", {
htmlFor: randId, htmlFor: randId,
className: "round" className: "round"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", { }, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
className: "material-icons" className: "material-icons"
}, "file_upload")), react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_load_spinner_jsx__WEBPACK_IMPORTED_MODULE_2__.default, { }, "file_upload")), react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_load_spinner_jsx__WEBPACK_IMPORTED_MODULE_2__.default, {
show: this.state.uploading, show: this.props.uploading,
large: true, large: true,
clear: true, clear: true,
centered: true centered: true
@@ -7788,7 +7810,7 @@ class ChipInput extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
this.state.sortedChips.map(item => { this.state.sortedChips.map(item => {
chips.push(react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_chip_jsx__WEBPACK_IMPORTED_MODULE_1__.default, { chips.push(react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_chip_jsx__WEBPACK_IMPORTED_MODULE_1__.default, {
onCancel: this.handleChipCancel, onCancel: this.handleChipCancel,
avatar: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_2__.makeImageDataUrl)(item.public ? item.public.photo : null), avatar: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_2__.makeImageUrl)(item.public ? item.public.photo : null),
title: item.public ? item.public.fn : undefined, title: item.public ? item.public.fn : undefined,
noAvatar: this.props.avatarDisabled, noAvatar: this.props.avatarDisabled,
topic: item.user, topic: item.user,
@@ -8087,7 +8109,7 @@ class ContactList extends (react__WEBPACK_IMPORTED_MODULE_0___default().Componen
contactNodes.push(react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_contact_jsx__WEBPACK_IMPORTED_MODULE_3__.default, { contactNodes.push(react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_contact_jsx__WEBPACK_IMPORTED_MODULE_3__.default, {
title: c.public ? c.public.fn : null, title: c.public ? c.public.fn : null,
avatar: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_5__.makeImageDataUrl)(c.public ? c.public.photo : null), avatar: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_5__.makeImageUrl)(c.public ? c.public.photo : null),
comment: comment, comment: comment,
preview: preview, preview: preview,
received: deliveryStatus, received: deliveryStatus,
@@ -8989,9 +9011,9 @@ class Cropper extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component) {
minZoom: minZoom, minZoom: minZoom,
maxZoom: Math.max(DEFAULT_MAX_ZOOM, minZoom + 1) maxZoom: Math.max(DEFAULT_MAX_ZOOM, minZoom + 1)
}); });
let zoom = Math.max(this.bBoxRect.width / imgRect.width, this.bBoxRect.height / imgRect.height); const zoom = Math.max(this.bBoxRect.width / imgRect.width, this.bBoxRect.height / imgRect.height);
let panX = this.cutoutRect.left - this.bBoxRect.left - (imgRect.width - this.cutoutRect.width) / 2; const panX = this.cutoutRect.left - this.bBoxRect.left - (imgRect.width - this.cutoutRect.width) / 2;
let panY = this.cutoutRect.top - this.bBoxRect.top - (imgRect.height - this.cutoutRect.height) / 2; const panY = this.cutoutRect.top - this.bBoxRect.top - (imgRect.height - this.cutoutRect.height) / 2;
this.positionAll(panX, panY, zoom); this.positionAll(panX, panY, zoom);
} }
@@ -9663,7 +9685,7 @@ class GroupSubs extends (react__WEBPACK_IMPORTED_MODULE_0___default().Component)
key: sub.user key: sub.user
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_letter_tile_jsx__WEBPACK_IMPORTED_MODULE_3__.default, { }, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_letter_tile_jsx__WEBPACK_IMPORTED_MODULE_3__.default, {
topic: sub.user, topic: sub.user,
avatar: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_4__.makeImageDataUrl)(sub.public ? sub.public.photo : null) || true, avatar: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_4__.makeImageUrl)(sub.public ? sub.public.photo : null) || true,
title: sub.public ? sub.public.fn : null title: sub.public ? sub.public.fn : null
}))); })));
return usersOnline.length == countToShow; return usersOnline.length == countToShow;
@@ -10181,7 +10203,7 @@ class LetterTile extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompo
src: this.props.avatar, src: this.props.avatar,
onError: e => { onError: e => {
e.target.onerror = null; e.target.onerror = null;
e.target.src = "/img/broken_image.png"; e.target.src = "../img/broken_image.png";
} }
}); });
} else { } else {
@@ -10908,7 +10930,7 @@ class PermissionsEditor extends (react__WEBPACK_IMPORTED_MODULE_0___default().Co
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_contact_jsx__WEBPACK_IMPORTED_MODULE_3__.default, { }, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_contact_jsx__WEBPACK_IMPORTED_MODULE_3__.default, {
item: this.props.item, item: this.props.item,
title: this.props.userTitle, title: this.props.userTitle,
avatar: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_4__.makeImageDataUrl)(this.props.userAvatar ? this.props.userAvatar : null) avatar: (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_4__.makeImageUrl)(this.props.userAvatar ? this.props.userAvatar : null)
})) : null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { })) : null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", {
className: "small" className: "small"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, { }, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
@@ -11403,6 +11425,8 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var _contact_badges_jsx__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./contact-badges.jsx */ "./src/widgets/contact-badges.jsx"); /* harmony import */ var _contact_badges_jsx__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./contact-badges.jsx */ "./src/widgets/contact-badges.jsx");
/* harmony import */ var _menu_contacts_jsx__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./menu-contacts.jsx */ "./src/widgets/menu-contacts.jsx"); /* harmony import */ var _menu_contacts_jsx__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./menu-contacts.jsx */ "./src/widgets/menu-contacts.jsx");
/* harmony import */ var _menu_start_jsx__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./menu-start.jsx */ "./src/widgets/menu-start.jsx"); /* harmony import */ var _menu_start_jsx__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./menu-start.jsx */ "./src/widgets/menu-start.jsx");
/* harmony import */ var _lib_utils_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ../lib/utils.js */ "./src/lib/utils.js");
@@ -11422,16 +11446,22 @@ class SideNavbar extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompo
}); });
} }
let avatar = null;
if (this.props.tinode) {
avatar = this.props.tinode.authorizeURL((0,_lib_utils_js__WEBPACK_IMPORTED_MODULE_6__.sanitizeImageUrl)(this.props.avatar));
}
return react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { return react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
id: "side-caption-panel", id: "side-caption-panel",
className: "caption-panel" className: "caption-panel"
}, this.props.onCancel ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_button_back_jsx__WEBPACK_IMPORTED_MODULE_2__.default, { }, this.props.onCancel ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_button_back_jsx__WEBPACK_IMPORTED_MODULE_2__.default, {
onBack: this.props.onCancel onBack: this.props.onCancel
}) : null, this.props.avatar ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { }) : null, avatar ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
id: "self-avatar", id: "self-avatar",
className: "avatar-box" className: "avatar-box"
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_letter_tile_jsx__WEBPACK_IMPORTED_MODULE_1__.default, { }, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_letter_tile_jsx__WEBPACK_IMPORTED_MODULE_1__.default, {
avatar: this.props.avatar, avatar: avatar,
topic: this.props.myUserId, topic: this.props.myUserId,
title: this.props.title title: this.props.title
})) : null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { })) : null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long