@@ -2513,6 +2513,14 @@ ul.tabbar li.active > a {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#reply-quote-preview .inline-image {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
#reply-quote-preview img.inline-image::before {
|
||||
content: '';
|
||||
}
|
||||
|
||||
.mention {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
@@ -93,6 +93,9 @@ export const MAX_TOPIC_DESCRIPTION_LENGTH = 360;
|
||||
// Length of message previews in chat list, in characters.
|
||||
export const MESSAGE_PREVIEW_LENGTH = 80;
|
||||
|
||||
// Length of a quote in quoted reply (for outgoing messages).
|
||||
export const QUOTED_REPLY_LENGTH = 30;
|
||||
|
||||
// Link for "Contact Us".
|
||||
export const LINK_CONTACT_US = 'email:support@tinode.co';
|
||||
|
||||
|
||||
+83
-16
@@ -3,11 +3,12 @@ import { defineMessages } from 'react-intl';
|
||||
|
||||
import { Drafty } from 'tinode-sdk';
|
||||
|
||||
import LazyImage from '../widgets/lazy-image.jsx'
|
||||
import UploadingImage from '../widgets/uploading-image.jsx'
|
||||
|
||||
import { IMAGE_THUMBNAIL_DIM, BROKEN_IMAGE_SIZE, REM_SIZE } from '../config.js';
|
||||
import { fitImageSize } from './blob-helpers.js';
|
||||
import { idToColorClass } from './strformat.js';
|
||||
import { base64ToBlob, blobToBase64, fitImageSize, imageScaled } from './blob-helpers.js';
|
||||
import { idToColorClass, shortenFileName } from './strformat.js';
|
||||
import { sanitizeImageUrl } from './utils.js';
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -35,9 +36,6 @@ function handleImageData(el, data, attr) {
|
||||
attr.style = {
|
||||
width: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
height: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
// Looks like a Chrome bug: broken image does not respect 'width' and 'height'.
|
||||
minWidth: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
minHeight: IMAGE_THUMBNAIL_DIM + 'px'
|
||||
};
|
||||
return el;
|
||||
}
|
||||
@@ -94,7 +92,7 @@ function quotedImage(data) {
|
||||
// Scale the blob.
|
||||
return promise
|
||||
.then(blob => {
|
||||
return imageScaled(blob, IMAGE_THUMBNAIL_DIM, IMAGE_THUMBNAIL_DIM, -1, false)
|
||||
return imageScaled(blob, IMAGE_THUMBNAIL_DIM, IMAGE_THUMBNAIL_DIM, -1, true)
|
||||
}).then(scaled => {
|
||||
data.mime = scaled.mime;
|
||||
data.size = scaled.blob.size;
|
||||
@@ -119,10 +117,6 @@ function quotedImage(data) {
|
||||
});
|
||||
}
|
||||
|
||||
function shortenFileName(filename) {
|
||||
return filename.length > 16 ? filename.slice(0, 7) + '…' + filename.slice(-7) : filename;
|
||||
}
|
||||
|
||||
// The main Drafty formatter: converts Drafty elements into React classes. 'this' is set by the caller.
|
||||
// 'this' must contain:
|
||||
// viewportWidth: this.props.viewportWidth;
|
||||
@@ -169,8 +163,9 @@ export function fullFormatter(style, data, values, key) {
|
||||
break;
|
||||
case 'MN':
|
||||
// Mention
|
||||
if (data && data.hasOwnProperty('colorId')) {
|
||||
attr.className = 'mn-dark-color' + data.colorId;
|
||||
attr.className = 'mention'
|
||||
if (data) {
|
||||
attr.className += ' ' + idToColorClass(data.val, false, true);
|
||||
}
|
||||
break;
|
||||
case 'FM':
|
||||
@@ -274,6 +269,23 @@ export function previewFormatter(style, data, values, key) {
|
||||
return React.createElement(el, attr, values);
|
||||
};
|
||||
|
||||
// Converts Drafty object into a quoted reply. 'this' is set by the caller.
|
||||
function inlineImageAttr(attr, data) {
|
||||
attr.style = {
|
||||
width: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
height: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
maxWidth: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
maxHeight: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
}
|
||||
attr.className = 'inline-image';
|
||||
attr.alt = shortenFileName(data && data.name, 16) || this.formatMessage(messages.drafty_image);
|
||||
if (!data) {
|
||||
attr.src = 'img/broken_image.png';
|
||||
}
|
||||
attr.title = attr.alt;
|
||||
return attr;
|
||||
}
|
||||
|
||||
// Displays a portion of Drafty within 'QQ' quotes. 'this' is set by the caller.
|
||||
// 'this' must contain:
|
||||
// formatMessage: this.props.intl.formatMessage
|
||||
@@ -287,9 +299,8 @@ export function quoteFormatter(style, data, values, key) {
|
||||
attr.key = key;
|
||||
switch(style) {
|
||||
case 'IM':
|
||||
const img = handleImageData.call(this, el, data, attr);
|
||||
values = [React.createElement(img, attr, null), ' ',
|
||||
shortenFileName((data && data.name) || this.formatMessage(messages.drafty_image))];
|
||||
attr = inlineImageAttr.call(this, attr, data);
|
||||
values = [React.createElement('img', attr, null), ' ', attr.alt];
|
||||
el = React.Fragment;
|
||||
// Fragment attributes.
|
||||
attr = {key: key};
|
||||
@@ -319,10 +330,66 @@ export function quoteFormatter(style, data, values, key) {
|
||||
}
|
||||
el = React.Fragment;
|
||||
values = [<i key="ex" className="material-icons">attachment</i>,
|
||||
shortenFileName(fname || this.formatMessage(messages.drafty_attachment))];
|
||||
shortenFileName(fname, 16) || this.formatMessage(messages.drafty_attachment)];
|
||||
break;
|
||||
}
|
||||
return React.createElement(el, attr, values);
|
||||
}
|
||||
return previewFormatter.call(this, style, data, values, key);
|
||||
}
|
||||
|
||||
// Create image thumbnail suitable for inclusion in a quote.
|
||||
function quoteImage(data) {
|
||||
let promise;
|
||||
// Get the blob from the image data.
|
||||
if (data.val) {
|
||||
const blob = base64ToBlob(data.val, data.mime);
|
||||
promise = blob ? Promise.resolve(blob) : Promise.reject(new Error("Invalid image"));
|
||||
} else {
|
||||
promise = fetch(this.authorizeURL(sanitizeImageUrl(data.ref))).then(evt => {
|
||||
if (evt.ok) {
|
||||
return evt.blob();
|
||||
} else {
|
||||
throw new Error(`Image fetch unsuccessful: ${evt.status} ${evt.statusText}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Scale the blob.
|
||||
return promise
|
||||
.then(blob => {
|
||||
// Cut the square from the center of the image and shrink it.
|
||||
return imageScaled(blob, IMAGE_THUMBNAIL_DIM, IMAGE_THUMBNAIL_DIM, -1, true)
|
||||
}).then(scaled => {
|
||||
data.mime = scaled.mime;
|
||||
data.size = scaled.blob.size;
|
||||
data.width = scaled.width;
|
||||
data.height = scaled.height;
|
||||
delete data.ref;
|
||||
// Keeping the original file name, if provided: ex.data.name;
|
||||
|
||||
data.src = URL.createObjectURL(scaled.blob);
|
||||
return blobToBase64(scaled.blob);
|
||||
}).then(b64 => {
|
||||
data.val = b64.bits;
|
||||
return data;
|
||||
}).catch(err => {
|
||||
delete data.val;
|
||||
delete data.src;
|
||||
data.width = IMAGE_THUMBNAIL_DIM;
|
||||
data.height = IMAGE_THUMBNAIL_DIM;
|
||||
// Rethrow.
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
// Create a preview of a reply.
|
||||
export function replyFormatter(style, data, values, key) {
|
||||
if (style != 'IM') {
|
||||
return quoteFormatter.call(this, style, data, values, key);
|
||||
}
|
||||
const attr = inlineImageAttr.call(this, {key: key}, data);
|
||||
attr.whenDone = quoteImage.call(this, data);
|
||||
values = [React.createElement(LazyImage, attr, null), ' ', attr.alt];
|
||||
return React.createElement(React.Fragment, {key: key}, values);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,15 @@ export function bytesToHumanSize(bytes) {
|
||||
return count.toFixed(round) + ' ' + sizes[bucket];
|
||||
}
|
||||
|
||||
// Shorten a file name to be under maxLength by clipping out the middle.
|
||||
export function shortenFileName(filename, maxLength) {
|
||||
if (typeof filename != 'string') {
|
||||
return filename;
|
||||
}
|
||||
return filename.length > maxLength ?
|
||||
filename.slice(0, maxLength/2 - 1) + '…' + filename.slice(1 - maxLength/2) : filename;
|
||||
}
|
||||
|
||||
// Get 32 bit integer hash value for a string. Ideally it should produce the same value
|
||||
// as Java's String#hash().
|
||||
function stringToColorHash(value) {
|
||||
|
||||
+20
-81
@@ -16,9 +16,9 @@ import LoadSpinner from '../widgets/load-spinner.jsx';
|
||||
import LogoView from './logo-view.jsx';
|
||||
import SendMessage from '../widgets/send-message.jsx';
|
||||
|
||||
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,
|
||||
IMAGE_THUMBNAIL_DIM } from '../config.js';
|
||||
import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, IMAGE_THUMBNAIL_DIM, KEYPRESS_DELAY,
|
||||
MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE,
|
||||
READ_DELAY, QUOTED_REPLY_LENGTH } from '../config.js';
|
||||
import { SUPPORTED_IMAGE_FORMATS, blobToBase64, base64ToBlob, filePasted, fileToBase64,
|
||||
imageScaled, makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import HashNavigation from '../lib/navigation.js';
|
||||
@@ -719,21 +719,23 @@ class MessagesView extends React.Component {
|
||||
this.props.onError(this.props.intl.formatMessage(messages.file_attachment_too_large,
|
||||
{size: bytesToHumanSize(file.size), limit: bytesToHumanSize(maxExternAttachmentSize)}), 'err');
|
||||
} else {
|
||||
this.setState({ docPreview: {
|
||||
file: file,
|
||||
filename: file.name,
|
||||
size: file.size,
|
||||
type: file.type
|
||||
}});
|
||||
this.setState({
|
||||
docPreview: {
|
||||
file: file,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// sendImageAttachment sends the image bits inband as Drafty message.
|
||||
sendImageAttachment(caption, blob) {
|
||||
const mime = this.state.imagePreview.type;
|
||||
const mime = this.state.imagePreview.mime;
|
||||
const width = this.state.imagePreview.width;
|
||||
const height = this.state.imagePreview.height;
|
||||
const fname = this.state.imagePreview.filename;
|
||||
const fname = this.state.imagePreview.name;
|
||||
|
||||
// Server-provided limit reduced for base64 encoding and overhead.
|
||||
const maxInbandAttachmentSize = (this.props.tinode.getServerLimit('maxMessageSize',
|
||||
@@ -803,11 +805,11 @@ class MessagesView extends React.Component {
|
||||
this.setState({imagePreview: {
|
||||
url: URL.createObjectURL(scaled.blob),
|
||||
blob: scaled.blob,
|
||||
filename: scaled.fname,
|
||||
name: scaled.name,
|
||||
width: scaled.width,
|
||||
height: scaled.height,
|
||||
size: scaled.blob.size,
|
||||
type: scaled.mime
|
||||
mime: scaled.mime
|
||||
}});
|
||||
}).catch(err => {
|
||||
this.props.onError(err, 'err');
|
||||
@@ -834,7 +836,7 @@ class MessagesView extends React.Component {
|
||||
content = Drafty.init(content);
|
||||
}
|
||||
if (Drafty.isValid(content)) {
|
||||
content = Drafty.preview(content, 30);
|
||||
content = Drafty.preview(content, QUOTED_REPLY_LENGTH);
|
||||
} else {
|
||||
// /!\ invalid content.
|
||||
content = Drafty.append(Drafty.init('\u26A0 '),
|
||||
@@ -858,75 +860,12 @@ class MessagesView extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Make small image previews.
|
||||
const images = [];
|
||||
Drafty.entities(content, (data, idx, tp) => {
|
||||
if (tp == 'IM') {
|
||||
images.push({
|
||||
tp: tp,
|
||||
data: data
|
||||
});
|
||||
this.setState({
|
||||
reply: {
|
||||
content: Drafty.quote(senderName, senderId, content),
|
||||
seq: seq
|
||||
}
|
||||
});
|
||||
|
||||
const quotedImage = (data) => {
|
||||
let promise;
|
||||
// Get the blob from the image data.
|
||||
if (data.val) {
|
||||
const blob = base64ToBlob(data.val, data.mime);
|
||||
promise = blob ? Promise.resolve(blob) : Prmise.reject(new Error("Invalid image"));
|
||||
} else {
|
||||
promise = fetch(this.props.tinode.authorizeURL(sanitizeImageUrl(data.ref))).then(evt => {
|
||||
if (evt.ok) {
|
||||
return evt.blob();
|
||||
} else {
|
||||
throw new Error(`Image fetch unsuccessful: ${evt.status} ${evt.statusText}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Scale the blob.
|
||||
return promise
|
||||
.then(blob => {
|
||||
return imageScaled(blob, IMAGE_THUMBNAIL_DIM, IMAGE_THUMBNAIL_DIM, -1, false)
|
||||
}).then(scaled => {
|
||||
data.mime = scaled.mime;
|
||||
data.size = scaled.blob.size;
|
||||
data.width = scaled.width;
|
||||
data.height = scaled.height;
|
||||
delete data.ref;
|
||||
// Keeping the original file name, if provided: ex.data.name;
|
||||
|
||||
return blobToBase64(scaled.blob);
|
||||
}).then(b64 => {
|
||||
data.val = b64.bits;
|
||||
return data;
|
||||
}).catch(err => {
|
||||
delete data.val;
|
||||
delete data.name;
|
||||
data.width = IMAGE_THUMBNAIL_DIM;
|
||||
data.height = IMAGE_THUMBNAIL_DIM;
|
||||
data.maxWidth = IMAGE_THUMBNAIL_DIM;
|
||||
data.maxHeight = IMAGE_THUMBNAIL_DIM;
|
||||
// Rethrow.
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
// Turn all images into thumbnails.
|
||||
Promise.all(images.map(ex => quotedImage(ex.data)))
|
||||
.catch((err) => {
|
||||
this.props.onError(err, 'err');
|
||||
})
|
||||
.finally(() => {
|
||||
// All done. Create a reply quote.
|
||||
this.setState({
|
||||
reply: {
|
||||
content: Drafty.quote(senderName, senderId, content),
|
||||
seq: seq
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handleCancelReply() {
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import SendMessage from '../widgets/send-message.jsx';
|
||||
|
||||
import { bytesToHumanSize } from '../lib/strformat.js';
|
||||
import { bytesToHumanSize, shortenFileName } from '../lib/strformat.js';
|
||||
|
||||
// Get material icon name from mime type.
|
||||
function iconFromMime(mime) {
|
||||
@@ -31,12 +31,14 @@ export default class DocPreview extends React.PureComponent {
|
||||
return (
|
||||
<div id="image-preview">
|
||||
<div id="image-preview-caption-panel">
|
||||
<span>{this.props.content.filename}</span>
|
||||
<span>{this.props.content.name}</span>
|
||||
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onClose();}}><i className="material-icons gray">close</i></a>
|
||||
</div>
|
||||
<div id="image-preview-container">
|
||||
<div className="flex-column narrow">
|
||||
<i className="material-icons gray">{iconFromMime(this.props.content.type)}</i>
|
||||
<div><b><FormattedMessage id="label_file_name" defaultMessage="File name:"
|
||||
description="Label for a file name" /></b> {shortenFileName(this.props.content.name, 24) || '-'}</div>
|
||||
<div><b><FormattedMessage id="label_content_type" defaultMessage="Content type:"
|
||||
description="Label for file content type (mime)" /></b> {this.props.content.type || 'application/octet-stream'}</div>
|
||||
<div><b><FormattedMessage id="label_size" defaultMessage="Size:"
|
||||
|
||||
@@ -4,7 +4,7 @@ import SendMessage from '../widgets/send-message.jsx';
|
||||
|
||||
import { REM_SIZE } from '../config.js';
|
||||
import { fitImageSize } from '../lib/blob-helpers.js';
|
||||
import { bytesToHumanSize } from '../lib/strformat.js';
|
||||
import { bytesToHumanSize, shortenFileName } from '../lib/strformat.js';
|
||||
|
||||
export default class ImagePreview extends React.PureComponent {
|
||||
constructor(props) {
|
||||
@@ -45,29 +45,27 @@ export default class ImagePreview extends React.PureComponent {
|
||||
size.maxWidth = '100%';
|
||||
size.maxHeight = '100%';
|
||||
|
||||
let filename = this.props.content.filename;
|
||||
// Average font aspect ratio is ~0.5; File name takes 1/3 of the viewport width.
|
||||
const maxlength = Math.max(((this.state.width / REM_SIZE / 1.5) | 0) - 2, 12);
|
||||
if (filename.length > maxlength) {
|
||||
filename = filename.slice(0, maxlength/2 - 1) + '…' + filename.slice(1 - maxlength/2);
|
||||
}
|
||||
const fname = shortenFileName(this.props.content.name, maxlength) || '-';
|
||||
|
||||
const width = this.props.content.width || '-';
|
||||
const height = this.props.content.height || '-';
|
||||
return (
|
||||
<div id="image-preview">
|
||||
<div id="image-preview-caption-panel">
|
||||
{!this.props.onSendMessage ?
|
||||
<a href={this.props.content.url} download={this.props.content.filename}>
|
||||
<a href={this.props.content.url} download={this.props.content.name}>
|
||||
<i className="material-icons">file_download</i> <FormattedMessage
|
||||
id="download_action" defaultMessage="download" description="Call to action [download]" />
|
||||
</a>
|
||||
:
|
||||
<span>{this.props.content.filename}</span>
|
||||
<span>{fname}</span>
|
||||
}
|
||||
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onClose();}}><i className="material-icons gray">close</i></a>
|
||||
</div>
|
||||
<div id="image-preview-container" ref={(node) => this.assignWidth(node)}>
|
||||
<img src={this.props.content.url} style={size} className="image-preview" alt={this.props.content.filename} />
|
||||
<img src={this.props.content.url} style={size} className="image-preview" alt={this.props.content.name} />
|
||||
</div>
|
||||
{this.props.onSendMessage ?
|
||||
<SendMessage
|
||||
@@ -83,7 +81,7 @@ export default class ImagePreview extends React.PureComponent {
|
||||
<div>
|
||||
<div><b><FormattedMessage id="label_file_name" defaultMessage="File name:"
|
||||
description="Label for a file name" /></b></div>
|
||||
<div><span title={this.props.content.filename}>{filename ? filename : '-'}</span></div>
|
||||
<div><span title={this.props.content.name}>{fname}</span></div>
|
||||
</div>
|
||||
<div>
|
||||
<div><b><FormattedMessage id="label_content_type" defaultMessage="Content type:"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Image with a placeholder which is replaced when the promise is resolved.
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export default class LazyImage extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
src: 'img/placeholder.png',
|
||||
style: Object.assign({padding: '4px'}, this.props.style),
|
||||
className: this.props.className,
|
||||
alt: this.props.alt,
|
||||
onClick: this.props.onClick,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.whenDone
|
||||
.then(data => this.setState({src: data.src, style: {...this.state.style, padding: 0}}))
|
||||
.catch(() => this.setState({src: 'img/broken_image.png'}));
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.whenDone != this.props.whenDone) {
|
||||
this.setState({src: 'img/placeholder.png', style: {...this.state.style, padding: '4px'}});
|
||||
this.props.whenDone
|
||||
.then(data => this.setState({src: data.src, style: {...this.state.style, padding: 0}}))
|
||||
.catch(() => this.setState({src: 'img/broken_image.png'}));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return React.createElement('img', this.state);
|
||||
}
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import { Drafty } from 'tinode-sdk';
|
||||
|
||||
import { KEYPRESS_DELAY } from '../config.js';
|
||||
import { filePasted } from '../lib/blob-helpers.js';
|
||||
import { quoteFormatter } from '../lib/formatters.js';
|
||||
import { quoteFormatter, replyFormatter } from '../lib/formatters.js';
|
||||
|
||||
const messages = defineMessages({
|
||||
messaging_disabled: {
|
||||
@@ -59,6 +59,13 @@ class SendMessage extends React.PureComponent {
|
||||
if (this.messageEditArea) {
|
||||
this.messageEditArea.addEventListener('paste', this.handlePasteEvent, false);
|
||||
}
|
||||
|
||||
this.setState({quote: this.props.replyTo ?
|
||||
Drafty.format(this.props.replyTo.content, replyFormatter, {
|
||||
formatMessage: this.props.intl.formatMessage.bind(this.props.intl),
|
||||
authorizeURL: this.props.tinode.authorizeURL.bind(this.props.tinode)
|
||||
}) :
|
||||
null});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -67,10 +74,19 @@ class SendMessage extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.messageEditArea) {
|
||||
this.messageEditArea.focus();
|
||||
}
|
||||
|
||||
if (prevProps.replyTo != this.props.replyTo) {
|
||||
this.setState({quote: this.props.replyTo ?
|
||||
Drafty.format(this.props.replyTo.content, replyFormatter, {
|
||||
formatMessage: this.props.intl.formatMessage.bind(this.props.intl),
|
||||
authorizeURL: this.props.tinode.authorizeURL.bind(this.props.tinode)
|
||||
}) :
|
||||
null});
|
||||
}
|
||||
}
|
||||
|
||||
handlePasteEvent(e) {
|
||||
@@ -161,21 +177,14 @@ class SendMessage extends React.PureComponent {
|
||||
formatMessage(messages[this.props.messagePrompt]) :
|
||||
formatMessage(messages.type_new_message));
|
||||
|
||||
const quote = this.props.replyTo ?
|
||||
Drafty.format(this.props.replyTo.content, quoteFormatter, {
|
||||
formatMessage: formatMessage.bind(this.props.intl),
|
||||
authorizeURL: this.props.tinode.authorizeURL.bind(this.props.tinode)
|
||||
}) :
|
||||
null;
|
||||
|
||||
return (
|
||||
<div id="send-message-wrapper">
|
||||
{quote ?
|
||||
{this.state.quote ?
|
||||
<div id="reply-quote-preview">
|
||||
<div className="cancel">
|
||||
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCancelReply();}}><i className="material-icons gray">close</i></a>
|
||||
</div>
|
||||
{quote}
|
||||
{this.state.quote}
|
||||
</div>
|
||||
:
|
||||
null}
|
||||
|
||||
+226
-116
@@ -40,6 +40,7 @@ __webpack_require__.r(__webpack_exports__);
|
||||
/* harmony export */ "MAX_TITLE_LENGTH": () => (/* binding */ MAX_TITLE_LENGTH),
|
||||
/* harmony export */ "MAX_TOPIC_DESCRIPTION_LENGTH": () => (/* binding */ MAX_TOPIC_DESCRIPTION_LENGTH),
|
||||
/* harmony export */ "MESSAGE_PREVIEW_LENGTH": () => (/* binding */ MESSAGE_PREVIEW_LENGTH),
|
||||
/* harmony export */ "QUOTED_REPLY_LENGTH": () => (/* binding */ QUOTED_REPLY_LENGTH),
|
||||
/* harmony export */ "LINK_CONTACT_US": () => (/* binding */ LINK_CONTACT_US),
|
||||
/* harmony export */ "LINK_PRIVACY_POLICY": () => (/* binding */ LINK_PRIVACY_POLICY),
|
||||
/* harmony export */ "LINK_TERMS_OF_SERVICE": () => (/* binding */ LINK_TERMS_OF_SERVICE)
|
||||
@@ -79,6 +80,7 @@ const MAX_ONLINE_IN_TOPIC = 4;
|
||||
const MAX_TITLE_LENGTH = 60;
|
||||
const MAX_TOPIC_DESCRIPTION_LENGTH = 360;
|
||||
const MESSAGE_PREVIEW_LENGTH = 80;
|
||||
const QUOTED_REPLY_LENGTH = 30;
|
||||
const LINK_CONTACT_US = 'email:support@tinode.co';
|
||||
const LINK_PRIVACY_POLICY = 'https://tinode.co/privacy.html';
|
||||
const LINK_TERMS_OF_SERVICE = 'https://tinode.co/terms.html';
|
||||
@@ -398,7 +400,8 @@ __webpack_require__.r(__webpack_exports__);
|
||||
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
||||
/* harmony export */ "fullFormatter": () => (/* binding */ fullFormatter),
|
||||
/* harmony export */ "previewFormatter": () => (/* binding */ previewFormatter),
|
||||
/* harmony export */ "quoteFormatter": () => (/* binding */ quoteFormatter)
|
||||
/* harmony export */ "quoteFormatter": () => (/* binding */ quoteFormatter),
|
||||
/* harmony export */ "replyFormatter": () => (/* binding */ replyFormatter)
|
||||
/* harmony export */ });
|
||||
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "react");
|
||||
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
|
||||
@@ -406,11 +409,13 @@ __webpack_require__.r(__webpack_exports__);
|
||||
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_1__);
|
||||
/* harmony import */ var tinode_sdk__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! tinode-sdk */ "tinode-sdk");
|
||||
/* harmony import */ var tinode_sdk__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(tinode_sdk__WEBPACK_IMPORTED_MODULE_2__);
|
||||
/* harmony import */ var _widgets_uploading_image_jsx__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../widgets/uploading-image.jsx */ "./src/widgets/uploading-image.jsx");
|
||||
/* harmony import */ var _config_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../config.js */ "./src/config.js");
|
||||
/* harmony import */ var _blob_helpers_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./blob-helpers.js */ "./src/lib/blob-helpers.js");
|
||||
/* harmony import */ var _strformat_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./strformat.js */ "./src/lib/strformat.js");
|
||||
/* harmony import */ var _utils_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./utils.js */ "./src/lib/utils.js");
|
||||
/* harmony import */ var _widgets_lazy_image_jsx__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../widgets/lazy-image.jsx */ "./src/widgets/lazy-image.jsx");
|
||||
/* harmony import */ var _widgets_uploading_image_jsx__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../widgets/uploading-image.jsx */ "./src/widgets/uploading-image.jsx");
|
||||
/* harmony import */ var _config_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../config.js */ "./src/config.js");
|
||||
/* harmony import */ var _blob_helpers_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./blob-helpers.js */ "./src/lib/blob-helpers.js");
|
||||
/* harmony import */ var _strformat_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./strformat.js */ "./src/lib/strformat.js");
|
||||
/* harmony import */ var _utils_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./utils.js */ "./src/lib/utils.js");
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -447,18 +452,16 @@ function handleImageData(el, data, attr) {
|
||||
if (!data) {
|
||||
attr.src = 'img/broken_image.png';
|
||||
attr.style = {
|
||||
width: _config_js__WEBPACK_IMPORTED_MODULE_4__.IMAGE_THUMBNAIL_DIM + 'px',
|
||||
height: _config_js__WEBPACK_IMPORTED_MODULE_4__.IMAGE_THUMBNAIL_DIM + 'px',
|
||||
minWidth: _config_js__WEBPACK_IMPORTED_MODULE_4__.IMAGE_THUMBNAIL_DIM + 'px',
|
||||
minHeight: _config_js__WEBPACK_IMPORTED_MODULE_4__.IMAGE_THUMBNAIL_DIM + 'px'
|
||||
width: _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM + 'px',
|
||||
height: _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM + 'px'
|
||||
};
|
||||
return el;
|
||||
}
|
||||
|
||||
attr.className = 'inline-image';
|
||||
const dim = (0,_blob_helpers_js__WEBPACK_IMPORTED_MODULE_5__.fitImageSize)(data.width, data.height, this.viewportWidth > 0 ? Math.min(this.viewportWidth - _config_js__WEBPACK_IMPORTED_MODULE_4__.REM_SIZE * 6.5, _config_js__WEBPACK_IMPORTED_MODULE_4__.REM_SIZE * 34.5) : _config_js__WEBPACK_IMPORTED_MODULE_4__.REM_SIZE * 34.5, _config_js__WEBPACK_IMPORTED_MODULE_4__.REM_SIZE * 24, false) || {
|
||||
dstWidth: _config_js__WEBPACK_IMPORTED_MODULE_4__.BROKEN_IMAGE_SIZE,
|
||||
dstHeight: _config_js__WEBPACK_IMPORTED_MODULE_4__.BROKEN_IMAGE_SIZE
|
||||
const dim = (0,_blob_helpers_js__WEBPACK_IMPORTED_MODULE_6__.fitImageSize)(data.width, data.height, this.viewportWidth > 0 ? Math.min(this.viewportWidth - _config_js__WEBPACK_IMPORTED_MODULE_5__.REM_SIZE * 6.5, _config_js__WEBPACK_IMPORTED_MODULE_5__.REM_SIZE * 34.5) : _config_js__WEBPACK_IMPORTED_MODULE_5__.REM_SIZE * 34.5, _config_js__WEBPACK_IMPORTED_MODULE_5__.REM_SIZE * 24, false) || {
|
||||
dstWidth: _config_js__WEBPACK_IMPORTED_MODULE_5__.BROKEN_IMAGE_SIZE,
|
||||
dstHeight: _config_js__WEBPACK_IMPORTED_MODULE_5__.BROKEN_IMAGE_SIZE
|
||||
};
|
||||
attr.style = {
|
||||
width: dim.dstWidth + 'px',
|
||||
@@ -468,11 +471,11 @@ function handleImageData(el, data, attr) {
|
||||
};
|
||||
|
||||
if (!tinode_sdk__WEBPACK_IMPORTED_MODULE_2__.Drafty.isProcessing(data)) {
|
||||
attr.src = this.authorizeURL((0,_utils_js__WEBPACK_IMPORTED_MODULE_7__.sanitizeImageUrl)(attr.src));
|
||||
attr.src = this.authorizeURL((0,_utils_js__WEBPACK_IMPORTED_MODULE_8__.sanitizeImageUrl)(attr.src));
|
||||
attr.alt = data.name;
|
||||
|
||||
if (attr.src) {
|
||||
if (Math.max(data.width || 0, data.height || 0) > _config_js__WEBPACK_IMPORTED_MODULE_4__.IMAGE_THUMBNAIL_DIM) {
|
||||
if (Math.max(data.width || 0, data.height || 0) > _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM) {
|
||||
attr.onClick = this.onImagePreview;
|
||||
attr.className += ' image-clickable';
|
||||
}
|
||||
@@ -482,7 +485,7 @@ function handleImageData(el, data, attr) {
|
||||
attr.src = 'img/broken_image.png';
|
||||
}
|
||||
} else {
|
||||
el = _widgets_uploading_image_jsx__WEBPACK_IMPORTED_MODULE_3__.default;
|
||||
el = _widgets_uploading_image_jsx__WEBPACK_IMPORTED_MODULE_4__.default;
|
||||
}
|
||||
|
||||
return el;
|
||||
@@ -492,10 +495,10 @@ function quotedImage(data) {
|
||||
let promise;
|
||||
|
||||
if (data.val) {
|
||||
const blob = base64ToBlob(data.val, data.mime);
|
||||
const blob = (0,_blob_helpers_js__WEBPACK_IMPORTED_MODULE_6__.base64ToBlob)(data.val, data.mime);
|
||||
promise = blob ? Promise.resolve(blob) : Prmise.reject(new Error("Invalid image"));
|
||||
} else {
|
||||
promise = fetch(this.authorizeURL((0,_utils_js__WEBPACK_IMPORTED_MODULE_7__.sanitizeImageUrl)(data.ref))).then(evt => {
|
||||
promise = fetch(this.authorizeURL((0,_utils_js__WEBPACK_IMPORTED_MODULE_8__.sanitizeImageUrl)(data.ref))).then(evt => {
|
||||
if (evt.ok) {
|
||||
return evt.blob();
|
||||
} else {
|
||||
@@ -505,32 +508,28 @@ function quotedImage(data) {
|
||||
}
|
||||
|
||||
return promise.then(blob => {
|
||||
return imageScaled(blob, _config_js__WEBPACK_IMPORTED_MODULE_4__.IMAGE_THUMBNAIL_DIM, _config_js__WEBPACK_IMPORTED_MODULE_4__.IMAGE_THUMBNAIL_DIM, -1, false);
|
||||
return (0,_blob_helpers_js__WEBPACK_IMPORTED_MODULE_6__.imageScaled)(blob, _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM, _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM, -1, true);
|
||||
}).then(scaled => {
|
||||
data.mime = scaled.mime;
|
||||
data.size = scaled.blob.size;
|
||||
data.width = scaled.width;
|
||||
data.height = scaled.height;
|
||||
delete data.ref;
|
||||
return blobToBase64(scaled.blob);
|
||||
return (0,_blob_helpers_js__WEBPACK_IMPORTED_MODULE_6__.blobToBase64)(scaled.blob);
|
||||
}).then(b64 => {
|
||||
data.val = b64.bits;
|
||||
return data;
|
||||
}).catch(err => {
|
||||
delete data.val;
|
||||
delete data.name;
|
||||
data.width = _config_js__WEBPACK_IMPORTED_MODULE_4__.IMAGE_THUMBNAIL_DIM;
|
||||
data.height = _config_js__WEBPACK_IMPORTED_MODULE_4__.IMAGE_THUMBNAIL_DIM;
|
||||
data.maxWidth = _config_js__WEBPACK_IMPORTED_MODULE_4__.IMAGE_THUMBNAIL_DIM;
|
||||
data.maxHeight = _config_js__WEBPACK_IMPORTED_MODULE_4__.IMAGE_THUMBNAIL_DIM;
|
||||
data.width = _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM;
|
||||
data.height = _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM;
|
||||
data.maxWidth = _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM;
|
||||
data.maxHeight = _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
function shortenFileName(filename) {
|
||||
return filename.length > 16 ? filename.slice(0, 7) + '…' + filename.slice(-7) : filename;
|
||||
}
|
||||
|
||||
function fullFormatter(style, data, values, key) {
|
||||
if (!style) {
|
||||
return values;
|
||||
@@ -568,8 +567,10 @@ function fullFormatter(style, data, values, key) {
|
||||
break;
|
||||
|
||||
case 'MN':
|
||||
if (data && data.hasOwnProperty('colorId')) {
|
||||
attr.className = 'mn-dark-color' + data.colorId;
|
||||
attr.className = 'mention';
|
||||
|
||||
if (data) {
|
||||
attr.className += ' ' + (0,_strformat_js__WEBPACK_IMPORTED_MODULE_7__.idToColorClass)(data.val, false, true);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -694,6 +695,25 @@ function previewFormatter(style, data, values, key) {
|
||||
return react__WEBPACK_IMPORTED_MODULE_0___default().createElement(el, attr, values);
|
||||
}
|
||||
;
|
||||
|
||||
function inlineImageAttr(attr, data) {
|
||||
attr.style = {
|
||||
width: _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM + 'px',
|
||||
height: _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM + 'px',
|
||||
maxWidth: _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM + 'px',
|
||||
maxHeight: _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM + 'px'
|
||||
};
|
||||
attr.className = 'inline-image';
|
||||
attr.alt = (0,_strformat_js__WEBPACK_IMPORTED_MODULE_7__.shortenFileName)(data && data.name, 16) || this.formatMessage(messages.drafty_image);
|
||||
|
||||
if (!data) {
|
||||
attr.src = 'img/broken_image.png';
|
||||
}
|
||||
|
||||
attr.title = attr.alt;
|
||||
return attr;
|
||||
}
|
||||
|
||||
function quoteFormatter(style, data, values, key) {
|
||||
if (['BR', 'EX', 'IM', 'MN', 'QQ'].includes(style)) {
|
||||
let el = tinode_sdk__WEBPACK_IMPORTED_MODULE_2__.Drafty.tagName(style);
|
||||
@@ -702,8 +722,8 @@ function quoteFormatter(style, data, values, key) {
|
||||
|
||||
switch (style) {
|
||||
case 'IM':
|
||||
const img = handleImageData.call(this, el, data, attr);
|
||||
values = [react__WEBPACK_IMPORTED_MODULE_0___default().createElement(img, attr, null), ' ', shortenFileName(data && data.name || this.formatMessage(messages.drafty_image))];
|
||||
attr = inlineImageAttr.call(this, attr, data);
|
||||
values = [react__WEBPACK_IMPORTED_MODULE_0___default().createElement('img', attr, null), ' ', attr.alt];
|
||||
el = (react__WEBPACK_IMPORTED_MODULE_0___default().Fragment);
|
||||
attr = {
|
||||
key: key
|
||||
@@ -715,7 +735,7 @@ function quoteFormatter(style, data, values, key) {
|
||||
attr.className = 'mention';
|
||||
|
||||
if (data) {
|
||||
attr.className += ' ' + (0,_strformat_js__WEBPACK_IMPORTED_MODULE_6__.idToColorClass)(data.val, false, true);
|
||||
attr.className += ' ' + (0,_strformat_js__WEBPACK_IMPORTED_MODULE_7__.idToColorClass)(data.val, false, true);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -742,7 +762,7 @@ function quoteFormatter(style, data, values, key) {
|
||||
values = [react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
|
||||
key: "ex",
|
||||
className: "material-icons"
|
||||
}, "attachment"), shortenFileName(fname || this.formatMessage(messages.drafty_attachment))];
|
||||
}, "attachment"), (0,_strformat_js__WEBPACK_IMPORTED_MODULE_7__.shortenFileName)(fname, 16) || this.formatMessage(messages.drafty_attachment)];
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -752,6 +772,59 @@ function quoteFormatter(style, data, values, key) {
|
||||
return previewFormatter.call(this, style, data, values, key);
|
||||
}
|
||||
|
||||
function quoteImage(data) {
|
||||
let promise;
|
||||
|
||||
if (data.val) {
|
||||
const blob = (0,_blob_helpers_js__WEBPACK_IMPORTED_MODULE_6__.base64ToBlob)(data.val, data.mime);
|
||||
promise = blob ? Promise.resolve(blob) : Promise.reject(new Error("Invalid image"));
|
||||
} else {
|
||||
promise = fetch(this.authorizeURL((0,_utils_js__WEBPACK_IMPORTED_MODULE_8__.sanitizeImageUrl)(data.ref))).then(evt => {
|
||||
if (evt.ok) {
|
||||
return evt.blob();
|
||||
} else {
|
||||
throw new Error(`Image fetch unsuccessful: ${evt.status} ${evt.statusText}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return promise.then(blob => {
|
||||
return (0,_blob_helpers_js__WEBPACK_IMPORTED_MODULE_6__.imageScaled)(blob, _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM, _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM, -1, true);
|
||||
}).then(scaled => {
|
||||
data.mime = scaled.mime;
|
||||
data.size = scaled.blob.size;
|
||||
data.width = scaled.width;
|
||||
data.height = scaled.height;
|
||||
delete data.ref;
|
||||
data.src = URL.createObjectURL(scaled.blob);
|
||||
return (0,_blob_helpers_js__WEBPACK_IMPORTED_MODULE_6__.blobToBase64)(scaled.blob);
|
||||
}).then(b64 => {
|
||||
data.val = b64.bits;
|
||||
return data;
|
||||
}).catch(err => {
|
||||
delete data.val;
|
||||
delete data.src;
|
||||
data.width = _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM;
|
||||
data.height = _config_js__WEBPACK_IMPORTED_MODULE_5__.IMAGE_THUMBNAIL_DIM;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
function replyFormatter(style, data, values, key) {
|
||||
if (style != 'IM') {
|
||||
return quoteFormatter.call(this, style, data, values, key);
|
||||
}
|
||||
|
||||
const attr = inlineImageAttr.call(this, {
|
||||
key: key
|
||||
}, data);
|
||||
attr.whenDone = quoteImage.call(this, data);
|
||||
values = [react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_widgets_lazy_image_jsx__WEBPACK_IMPORTED_MODULE_3__.default, attr, null), ' ', attr.alt];
|
||||
return react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), {
|
||||
key: key
|
||||
}, values);
|
||||
}
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "./src/lib/host-name.js":
|
||||
@@ -940,6 +1013,7 @@ __webpack_require__.r(__webpack_exports__);
|
||||
/* harmony export */ "shortDateFormat": () => (/* binding */ shortDateFormat),
|
||||
/* harmony export */ "secondsToTime": () => (/* binding */ secondsToTime),
|
||||
/* harmony export */ "bytesToHumanSize": () => (/* binding */ bytesToHumanSize),
|
||||
/* harmony export */ "shortenFileName": () => (/* binding */ shortenFileName),
|
||||
/* harmony export */ "letterTileColorId": () => (/* binding */ letterTileColorId),
|
||||
/* harmony export */ "idToColorClass": () => (/* binding */ idToColorClass)
|
||||
/* harmony export */ });
|
||||
@@ -988,6 +1062,13 @@ function bytesToHumanSize(bytes) {
|
||||
const round = bucket > 0 ? count < 3 ? 2 : count < 30 ? 1 : 0 : 0;
|
||||
return count.toFixed(round) + ' ' + sizes[bucket];
|
||||
}
|
||||
function shortenFileName(filename, maxLength) {
|
||||
if (typeof filename != 'string') {
|
||||
return filename;
|
||||
}
|
||||
|
||||
return filename.length > maxLength ? filename.slice(0, maxLength / 2 - 1) + '…' + filename.slice(1 - maxLength / 2) : filename;
|
||||
}
|
||||
|
||||
function stringToColorHash(value) {
|
||||
let hash = 0;
|
||||
@@ -3934,7 +4015,7 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
|
||||
this.setState({
|
||||
docPreview: {
|
||||
file: file,
|
||||
filename: file.name,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type
|
||||
}
|
||||
@@ -3943,10 +4024,10 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
|
||||
}
|
||||
|
||||
sendImageAttachment(caption, blob) {
|
||||
const mime = this.state.imagePreview.type;
|
||||
const mime = this.state.imagePreview.mime;
|
||||
const width = this.state.imagePreview.width;
|
||||
const height = this.state.imagePreview.height;
|
||||
const fname = this.state.imagePreview.filename;
|
||||
const fname = this.state.imagePreview.name;
|
||||
const maxInbandAttachmentSize = this.props.tinode.getServerLimit('maxMessageSize', _config_js__WEBPACK_IMPORTED_MODULE_14__.MAX_INBAND_ATTACHMENT_SIZE) * 0.75 - 1024 | 0;
|
||||
|
||||
if (blob.size > maxInbandAttachmentSize) {
|
||||
@@ -4007,11 +4088,11 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
|
||||
imagePreview: {
|
||||
url: URL.createObjectURL(scaled.blob),
|
||||
blob: scaled.blob,
|
||||
filename: scaled.fname,
|
||||
name: scaled.name,
|
||||
width: scaled.width,
|
||||
height: scaled.height,
|
||||
size: scaled.blob.size,
|
||||
type: scaled.mime
|
||||
mime: scaled.mime
|
||||
}
|
||||
});
|
||||
}).catch(err => {
|
||||
@@ -4044,7 +4125,7 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
|
||||
}
|
||||
|
||||
if (Drafty.isValid(content)) {
|
||||
content = Drafty.preview(content, 30);
|
||||
content = Drafty.preview(content, _config_js__WEBPACK_IMPORTED_MODULE_14__.QUOTED_REPLY_LENGTH);
|
||||
} else {
|
||||
content = Drafty.append(Drafty.init('\u26A0 '), Drafty.wrapInto(this.props.intl.formatMessage(messages.invalid_content), 'EM'));
|
||||
}
|
||||
@@ -4068,65 +4149,12 @@ class MessagesView extends (react__WEBPACK_IMPORTED_MODULE_0___default().Compone
|
||||
}
|
||||
}
|
||||
|
||||
const images = [];
|
||||
Drafty.entities(content, (data, idx, tp) => {
|
||||
if (tp == 'IM') {
|
||||
images.push({
|
||||
tp: tp,
|
||||
data: data
|
||||
});
|
||||
this.setState({
|
||||
reply: {
|
||||
content: Drafty.quote(senderName, senderId, content),
|
||||
seq: seq
|
||||
}
|
||||
});
|
||||
|
||||
const quotedImage = data => {
|
||||
let promise;
|
||||
|
||||
if (data.val) {
|
||||
const blob = (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.base64ToBlob)(data.val, data.mime);
|
||||
promise = blob ? Promise.resolve(blob) : Prmise.reject(new Error("Invalid image"));
|
||||
} else {
|
||||
promise = fetch(this.props.tinode.authorizeURL((0,_lib_utils_js__WEBPACK_IMPORTED_MODULE_18__.sanitizeImageUrl)(data.ref))).then(evt => {
|
||||
if (evt.ok) {
|
||||
return evt.blob();
|
||||
} else {
|
||||
throw new Error(`Image fetch unsuccessful: ${evt.status} ${evt.statusText}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return promise.then(blob => {
|
||||
return (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.imageScaled)(blob, _config_js__WEBPACK_IMPORTED_MODULE_14__.IMAGE_THUMBNAIL_DIM, _config_js__WEBPACK_IMPORTED_MODULE_14__.IMAGE_THUMBNAIL_DIM, -1, false);
|
||||
}).then(scaled => {
|
||||
data.mime = scaled.mime;
|
||||
data.size = scaled.blob.size;
|
||||
data.width = scaled.width;
|
||||
data.height = scaled.height;
|
||||
delete data.ref;
|
||||
return (0,_lib_blob_helpers_js__WEBPACK_IMPORTED_MODULE_15__.blobToBase64)(scaled.blob);
|
||||
}).then(b64 => {
|
||||
data.val = b64.bits;
|
||||
return data;
|
||||
}).catch(err => {
|
||||
delete data.val;
|
||||
delete data.name;
|
||||
data.width = _config_js__WEBPACK_IMPORTED_MODULE_14__.IMAGE_THUMBNAIL_DIM;
|
||||
data.height = _config_js__WEBPACK_IMPORTED_MODULE_14__.IMAGE_THUMBNAIL_DIM;
|
||||
data.maxWidth = _config_js__WEBPACK_IMPORTED_MODULE_14__.IMAGE_THUMBNAIL_DIM;
|
||||
data.maxHeight = _config_js__WEBPACK_IMPORTED_MODULE_14__.IMAGE_THUMBNAIL_DIM;
|
||||
throw err;
|
||||
});
|
||||
};
|
||||
|
||||
Promise.all(images.map(ex => quotedImage(ex.data))).catch(err => {
|
||||
this.props.onError(err, 'err');
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
reply: {
|
||||
content: Drafty.quote(senderName, senderId, content),
|
||||
seq: seq
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handleCancelReply() {
|
||||
@@ -9376,7 +9404,7 @@ class DocPreview extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompo
|
||||
id: "image-preview"
|
||||
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||
id: "image-preview-caption-panel"
|
||||
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, this.props.content.filename), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
|
||||
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, this.props.content.name), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
|
||||
href: "#",
|
||||
onClick: e => {
|
||||
e.preventDefault();
|
||||
@@ -9391,6 +9419,12 @@ class DocPreview extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompo
|
||||
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
|
||||
className: "material-icons gray"
|
||||
}, iconFromMime(this.props.content.type)), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("b", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
|
||||
id: "label_file_name",
|
||||
defaultMessage: [{
|
||||
"type": 0,
|
||||
"value": "File name:"
|
||||
}]
|
||||
})), " ", (0,_lib_strformat_js__WEBPACK_IMPORTED_MODULE_3__.shortenFileName)(this.props.content.name, 24) || '-'), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("b", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
|
||||
id: "label_content_type",
|
||||
defaultMessage: [{
|
||||
"type": 0,
|
||||
@@ -10020,13 +10054,8 @@ class ImagePreview extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
|
||||
};
|
||||
size.maxWidth = '100%';
|
||||
size.maxHeight = '100%';
|
||||
let filename = this.props.content.filename;
|
||||
const maxlength = Math.max((this.state.width / _config_js__WEBPACK_IMPORTED_MODULE_3__.REM_SIZE / 1.5 | 0) - 2, 12);
|
||||
|
||||
if (filename.length > maxlength) {
|
||||
filename = filename.slice(0, maxlength / 2 - 1) + '…' + filename.slice(1 - maxlength / 2);
|
||||
}
|
||||
|
||||
const fname = (0,_lib_strformat_js__WEBPACK_IMPORTED_MODULE_5__.shortenFileName)(this.props.content.name, maxlength) || '-';
|
||||
const width = this.props.content.width || '-';
|
||||
const height = this.props.content.height || '-';
|
||||
return react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||
@@ -10035,7 +10064,7 @@ class ImagePreview extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
|
||||
id: "image-preview-caption-panel"
|
||||
}, !this.props.onSendMessage ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
|
||||
href: this.props.content.url,
|
||||
download: this.props.content.filename
|
||||
download: this.props.content.name
|
||||
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
|
||||
className: "material-icons"
|
||||
}, "file_download"), " ", react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
|
||||
@@ -10044,7 +10073,7 @@ class ImagePreview extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
|
||||
"type": 0,
|
||||
"value": "download"
|
||||
}]
|
||||
})) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, this.props.content.filename), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
|
||||
})) : react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, fname), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
|
||||
href: "#",
|
||||
onClick: e => {
|
||||
e.preventDefault();
|
||||
@@ -10059,7 +10088,7 @@ class ImagePreview extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
|
||||
src: this.props.content.url,
|
||||
style: size,
|
||||
className: "image-preview",
|
||||
alt: this.props.content.filename
|
||||
alt: this.props.content.name
|
||||
})), this.props.onSendMessage ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_widgets_send_message_jsx__WEBPACK_IMPORTED_MODULE_2__.default, {
|
||||
messagePrompt: "add_image_caption",
|
||||
acceptBlank: true,
|
||||
@@ -10077,8 +10106,8 @@ class ImagePreview extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCom
|
||||
"value": "File name:"
|
||||
}]
|
||||
}))), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", {
|
||||
title: this.props.content.filename
|
||||
}, filename ? filename : '-'))), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("b", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
|
||||
title: this.props.content.name
|
||||
}, fname))), react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("b", null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__.FormattedMessage, {
|
||||
id: "label_content_type",
|
||||
defaultMessage: [{
|
||||
"type": 0,
|
||||
@@ -10335,6 +10364,75 @@ class Invitation extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureCompo
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "./src/widgets/lazy-image.jsx":
|
||||
/*!************************************!*\
|
||||
!*** ./src/widgets/lazy-image.jsx ***!
|
||||
\************************************/
|
||||
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
||||
|
||||
__webpack_require__.r(__webpack_exports__);
|
||||
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
||||
/* harmony export */ "default": () => (/* binding */ LazyImage)
|
||||
/* harmony export */ });
|
||||
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "react");
|
||||
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
|
||||
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! react-intl */ "react-intl");
|
||||
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_1__);
|
||||
|
||||
|
||||
class LazyImage extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComponent) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
src: 'img/placeholder.png',
|
||||
style: Object.assign({
|
||||
padding: '4px'
|
||||
}, this.props.style),
|
||||
className: this.props.className,
|
||||
alt: this.props.alt,
|
||||
onClick: this.props.onClick
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.whenDone.then(data => this.setState({
|
||||
src: data.src,
|
||||
style: { ...this.state.style,
|
||||
padding: 0
|
||||
}
|
||||
})).catch(() => this.setState({
|
||||
src: 'img/broken_image.png'
|
||||
}));
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.whenDone != this.props.whenDone) {
|
||||
this.setState({
|
||||
src: 'img/placeholder.png',
|
||||
style: { ...this.state.style,
|
||||
padding: '4px'
|
||||
}
|
||||
});
|
||||
this.props.whenDone.then(data => this.setState({
|
||||
src: data.src,
|
||||
style: { ...this.state.style,
|
||||
padding: 0
|
||||
}
|
||||
})).catch(() => this.setState({
|
||||
src: 'img/broken_image.png'
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return react__WEBPACK_IMPORTED_MODULE_0___default().createElement('img', this.state);
|
||||
}
|
||||
|
||||
}
|
||||
;
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "./src/widgets/letter-tile.jsx":
|
||||
/*!*************************************!*\
|
||||
!*** ./src/widgets/letter-tile.jsx ***!
|
||||
@@ -11415,6 +11513,13 @@ class SendMessage extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComp
|
||||
if (this.messageEditArea) {
|
||||
this.messageEditArea.addEventListener('paste', this.handlePasteEvent, false);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
quote: this.props.replyTo ? tinode_sdk__WEBPACK_IMPORTED_MODULE_2__.Drafty.format(this.props.replyTo.content, _lib_formatters_js__WEBPACK_IMPORTED_MODULE_5__.replyFormatter, {
|
||||
formatMessage: this.props.intl.formatMessage.bind(this.props.intl),
|
||||
authorizeURL: this.props.tinode.authorizeURL.bind(this.props.tinode)
|
||||
}) : null
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -11423,10 +11528,19 @@ class SendMessage extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComp
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.messageEditArea) {
|
||||
this.messageEditArea.focus();
|
||||
}
|
||||
|
||||
if (prevProps.replyTo != this.props.replyTo) {
|
||||
this.setState({
|
||||
quote: this.props.replyTo ? tinode_sdk__WEBPACK_IMPORTED_MODULE_2__.Drafty.format(this.props.replyTo.content, _lib_formatters_js__WEBPACK_IMPORTED_MODULE_5__.replyFormatter, {
|
||||
formatMessage: this.props.intl.formatMessage.bind(this.props.intl),
|
||||
authorizeURL: this.props.tinode.authorizeURL.bind(this.props.tinode)
|
||||
}) : null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handlePasteEvent(e) {
|
||||
@@ -11517,13 +11631,9 @@ class SendMessage extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComp
|
||||
formatMessage
|
||||
} = this.props.intl;
|
||||
const prompt = this.props.disabled ? formatMessage(messages.messaging_disabled) : this.props.messagePrompt ? formatMessage(messages[this.props.messagePrompt]) : formatMessage(messages.type_new_message);
|
||||
const quote = this.props.replyTo ? tinode_sdk__WEBPACK_IMPORTED_MODULE_2__.Drafty.format(this.props.replyTo.content, _lib_formatters_js__WEBPACK_IMPORTED_MODULE_5__.quoteFormatter, {
|
||||
formatMessage: formatMessage.bind(this.props.intl),
|
||||
authorizeURL: this.props.tinode.authorizeURL.bind(this.props.tinode)
|
||||
}) : null;
|
||||
return react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||
id: "send-message-wrapper"
|
||||
}, quote ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||
}, this.state.quote ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||
id: "reply-quote-preview"
|
||||
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||
className: "cancel"
|
||||
@@ -11535,7 +11645,7 @@ class SendMessage extends (react__WEBPACK_IMPORTED_MODULE_0___default().PureComp
|
||||
}
|
||||
}, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("i", {
|
||||
className: "material-icons gray"
|
||||
}, "close"))), quote) : null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||
}, "close"))), this.state.quote) : null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||
id: "send-message-panel"
|
||||
}, !this.props.disabled ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, this.props.onAttachFile ? react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, react__WEBPACK_IMPORTED_MODULE_0___default().createElement("a", {
|
||||
href: "#",
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+14
-26
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user