This commit is contained in:
or-else
2020-01-21 16:18:19 +03:00
17 changed files with 1168 additions and 633 deletions
+24 -6
View File
@@ -181,9 +181,7 @@ textarea, input {
outline: none; outline: none;
color: #666; color: #666;
} }
textarea {
resize: none;
}
input[type="text"], input[type="text"],
input[type="email"], input[type="email"],
input[type="password"], input[type="password"],
@@ -234,6 +232,11 @@ input[type="password"].with-visibility {
margin-right:-2rem; margin-right:-2rem;
padding-right:2rem; padding-right:2rem;
} }
textarea {
resize: none;
padding-top: 0.45rem;
}
/* END of input and buttons */ /* END of input and buttons */
/* BEGIN scrollbar styling. */ /* BEGIN scrollbar styling. */
@@ -392,6 +395,10 @@ div.alert .content {
inset 0 -0.25rem 0.25rem -0.25rem rgba(0,0,0,0.15); inset 0 -0.25rem 0.25rem -0.25rem rgba(0,0,0,0.15);
} }
.hr.thin {
height: 0.015rem;
}
/* Element which can take focus but shows no outline */ /* Element which can take focus but shows no outline */
.group-focus { .group-focus {
outline: none; outline: none;
@@ -777,6 +784,10 @@ i.search {
overflow: hidden; overflow: hidden;
} }
.flex-column.narrow {
height: auto;
}
.scrollable-panel { .scrollable-panel {
overflow-y: auto; overflow-y: auto;
margin-top: 0.25rem; margin-top: 0.25rem;
@@ -1279,10 +1290,12 @@ i.search {
#image-preview-caption-panel { #image-preview-caption-panel {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
background-color: transparent; background-color: #eceff1;
box-shadow: 0 1px 2px silver; /* Faint shadow under the panel */
line-height: 2.5rem; line-height: 2.5rem;
height: 3.5rem; height: 4.6rem;
padding: 1rem 0.75rem 0 0.75rem; padding: 1rem 0.75rem 0 0.75rem;
z-index: 3;
} }
#image-preview-caption-panel a { #image-preview-caption-panel a {
@@ -1308,7 +1321,12 @@ i.search {
justify-content: space-around; justify-content: space-around;
height: 3.2rem; height: 3.2rem;
line-height: 1.5rem; line-height: 1.5rem;
background-color: transparent; background-color: #eceff1;
box-shadow: 0px -2px 3px -1px rgba(0,0,0,0.1); /* Faint shadow above panel */
}
#image-preview-container .material-icons {
font-size: 6rem;
} }
/* Talk bubbles */ /* Talk bubbles */
+1 -1
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -146,6 +146,7 @@
"search_placeholder": "List like email:alice@example.com, tel:17025550003...", "search_placeholder": "List like email:alice@example.com, tel:17025550003...",
"messaging_disabled_prompt": "Messaging disabled", "messaging_disabled_prompt": "Messaging disabled",
"new_message_prompt": "New message", "new_message_prompt": "New message",
"image_caption_prompt": "Image caption",
"file_attachment_too_large": "The file size {size} exceeds the {limit} limit.", "file_attachment_too_large": "The file size {size} exceeds the {limit} limit.",
"cannot_initiate_file_upload": "Cannot initiate file upload.", "cannot_initiate_file_upload": "Cannot initiate file upload.",
"tags_not_found": "No tags defined. Add some.", "tags_not_found": "No tags defined. Add some.",
+2 -1
View File
@@ -150,5 +150,6 @@
"topic_delete_warning": "Вы действительно хотите удалить этот чат?", "topic_delete_warning": "Вы действительно хотите удалить этот чат?",
"topic_block_warning": "Вы действительно хотите заблокировать этот чат?", "topic_block_warning": "Вы действительно хотите заблокировать этот чат?",
"search_no_results": "Ничего не найдено", "search_no_results": "Ничего не найдено",
"deleted_content": "удалено" "deleted_content": "удалено",
"image_caption_prompt": "Подпись к фото"
} }
+2 -1
View File
@@ -150,5 +150,6 @@
"topic_delete_warning": ":!![MISSING]!!:Are you sure you want to delete this conversation?", "topic_delete_warning": ":!![MISSING]!!:Are you sure you want to delete this conversation?",
"topic_block_warning": ":!![MISSING]!!:Are you sure you want to block this conversation?", "topic_block_warning": ":!![MISSING]!!:Are you sure you want to block this conversation?",
"search_no_results": ":!![MISSING]!!:Search returned no results", "search_no_results": ":!![MISSING]!!:Search returned no results",
"deleted_content": "内容已删除" "deleted_content": "内容已删除",
"image_caption_prompt": "图片标题"
} }
+5 -2
View File
@@ -147,6 +147,7 @@
"search_placeholder": "List like email:alice@example.com, tel:17025550003...", "search_placeholder": "List like email:alice@example.com, tel:17025550003...",
"messaging_disabled_prompt": "Messaging disabled", "messaging_disabled_prompt": "Messaging disabled",
"new_message_prompt": "New message", "new_message_prompt": "New message",
"image_caption_prompt": "Image caption",
"file_attachment_too_large": "The file size {size} exceeds the {limit} limit.", "file_attachment_too_large": "The file size {size} exceeds the {limit} limit.",
"cannot_initiate_file_upload": "Cannot initiate file upload.", "cannot_initiate_file_upload": "Cannot initiate file upload.",
"tags_not_found": "No tags defined. Add some.", "tags_not_found": "No tags defined. Add some.",
@@ -305,7 +306,8 @@
"topic_delete_warning": "Вы действительно хотите удалить этот чат?", "topic_delete_warning": "Вы действительно хотите удалить этот чат?",
"topic_block_warning": "Вы действительно хотите заблокировать этот чат?", "topic_block_warning": "Вы действительно хотите заблокировать этот чат?",
"search_no_results": "Ничего не найдено", "search_no_results": "Ничего не найдено",
"deleted_content": "удалено" "deleted_content": "удалено",
"image_caption_prompt": "Подпись к фото"
}, },
"zh": { "zh": {
"archived_contacts": "已归档联系人 ({count})", "archived_contacts": "已归档联系人 ({count})",
@@ -450,6 +452,7 @@
"action_delete_messages": "删除所有帖子", "action_delete_messages": "删除所有帖子",
"action_block_contact": "屏蔽联系人", "action_block_contact": "屏蔽联系人",
"action_report_chat": "检举垃圾邮件", "action_report_chat": "检举垃圾邮件",
"deleted_content": "内容已删除" "deleted_content": "内容已删除",
"image_caption_prompt": "图片标题"
} }
} }
+164 -26
View File
@@ -5,6 +5,7 @@ import Tinode from 'tinode-sdk';
const Drafty = Tinode.Drafty; const Drafty = Tinode.Drafty;
import ChatMessage from '../widgets/chat-message.jsx'; import ChatMessage from '../widgets/chat-message.jsx';
import DocPreview from '../widgets/doc-preview.jsx';
import ErrorPanel from '../widgets/error-panel.jsx'; import ErrorPanel from '../widgets/error-panel.jsx';
import GroupSubs from '../widgets/group-subs.jsx'; import GroupSubs from '../widgets/group-subs.jsx';
import ImagePreview from '../widgets/image-preview.jsx'; import ImagePreview from '../widgets/image-preview.jsx';
@@ -14,9 +15,11 @@ import LoadSpinner from '../widgets/load-spinner.jsx';
import LogoView from './logo-view.jsx'; import LogoView from './logo-view.jsx';
import SendMessage from '../widgets/send-message.jsx'; import SendMessage from '../widgets/send-message.jsx';
import { DEFAULT_P2P_ACCESS_MODE, KEYPRESS_DELAY, MESSAGES_PAGE } from '../config.js'; import { DEFAULT_P2P_ACCESS_MODE, KEYPRESS_DELAY, MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE,
import { makeImageUrl } from '../lib/blob-helpers.js'; MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE } from '../config.js';
import { shortDateFormat } from '../lib/strformat.js'; import { SUPPORTED_IMAGE_FORMATS, filePasted, fileToBase64, imageFileToBase64,
imageFileScaledToBase64, makeImageUrl } from '../lib/blob-helpers.js';
import { bytesToHumanSize, shortDateFormat } from '../lib/strformat.js';
const messages = defineMessages({ const messages = defineMessages({
online_now: { online_now: {
@@ -61,6 +64,9 @@ class MessagesView extends React.Component {
// this.propsChange = this.propsChange.bind(this); // this.propsChange = this.propsChange.bind(this);
this.leave = this.leave.bind(this); this.leave = this.leave.bind(this);
this.sendImageAttachment = this.sendImageAttachment.bind(this);
this.sendFileAttachment = this.sendFileAttachment.bind(this);
this.sendKeyPress = this.sendKeyPress.bind(this);
this.handleScrollReference = this.handleScrollReference.bind(this); this.handleScrollReference = this.handleScrollReference.bind(this);
this.handleScrollEvent = this.handleScrollEvent.bind(this); this.handleScrollEvent = this.handleScrollEvent.bind(this);
this.handleDescChange = this.handleDescChange.bind(this); this.handleDescChange = this.handleDescChange.bind(this);
@@ -68,13 +74,15 @@ class MessagesView extends React.Component {
this.handleNewMessage = this.handleNewMessage.bind(this); this.handleNewMessage = this.handleNewMessage.bind(this);
this.handleAllMessagesReceived = this.handleAllMessagesReceived.bind(this); this.handleAllMessagesReceived = this.handleAllMessagesReceived.bind(this);
this.handleInfoReceipt = this.handleInfoReceipt.bind(this); this.handleInfoReceipt = this.handleInfoReceipt.bind(this);
this.handleImagePreview = this.handleImagePreview.bind(this); this.handleImagePostview = this.handleImagePostview.bind(this);
this.handleCloseImagePreview = this.handleCloseImagePreview.bind(this); this.handleClosePreview = this.handleClosePreview.bind(this);
this.handleFormResponse = this.handleFormResponse.bind(this); this.handleFormResponse = this.handleFormResponse.bind(this);
this.handleContextClick = this.handleContextClick.bind(this); this.handleContextClick = this.handleContextClick.bind(this);
this.handleShowContextMenuMessage = this.handleShowContextMenuMessage.bind(this); this.handleShowContextMenuMessage = this.handleShowContextMenuMessage.bind(this);
this.handleNewChatAcceptance = this.handleNewChatAcceptance.bind(this); this.handleNewChatAcceptance = this.handleNewChatAcceptance.bind(this);
this.handleEnablePeer = this.handleEnablePeer.bind(this); this.handleEnablePeer = this.handleEnablePeer.bind(this);
this.handleAttachFile = this.handleAttachFile.bind(this);
this.handleAttachImage = this.handleAttachImage.bind(this);
} }
componentDidMount() { componentDidMount() {
@@ -169,7 +177,9 @@ class MessagesView extends React.Component {
topic: null, topic: null,
title: '', title: '',
avatar: null, avatar: null,
docPreview: null,
imagePreview: null, imagePreview: null,
imagePostview: null,
typingIndicator: false, typingIndicator: false,
scrollPosition: 0, scrollPosition: 0,
fetchingMessages: false, fetchingMessages: false,
@@ -179,7 +189,9 @@ class MessagesView extends React.Component {
const topic = nextProps.tinode.getTopic(nextProps.topic); const topic = nextProps.tinode.getTopic(nextProps.topic);
nextState = { nextState = {
topic: nextProps.topic, topic: nextProps.topic,
docPreview: null,
imagePreview: null, imagePreview: null,
imagePostview: null,
typingIndicator: false, typingIndicator: false,
scrollPosition: 0, scrollPosition: 0,
fetchingMessages: false fetchingMessages: false
@@ -432,12 +444,12 @@ class MessagesView extends React.Component {
} }
} }
handleImagePreview(content) { handleImagePostview(content) {
this.setState({ imagePreview: content }); this.setState({ imagePostview: content });
} }
handleCloseImagePreview() { handleClosePreview() {
this.setState({ imagePreview: null }); this.setState({ imagePostview: null, imagePreview: null, docPreview: null });
} }
handleFormResponse(action, text, data) { handleFormResponse(action, text, data) {
@@ -492,11 +504,142 @@ class MessagesView extends React.Component {
this.props.onChangePermissions(this.state.topic, DEFAULT_P2P_ACCESS_MODE, this.state.topic); this.props.onChangePermissions(this.state.topic, DEFAULT_P2P_ACCESS_MODE, this.state.topic);
} }
sendKeyPress() {
const topic = this.props.tinode.getTopic(this.state.topic);
if (topic.isSubscribed()) {
topic.noteKeyPress();
}
}
sendFileAttachment(file) {
if (file.size > MAX_INBAND_ATTACHMENT_SIZE) {
// Too large to send inband - uploading out of band and sending as a link.
const uploader = this.props.tinode.getLargeFileHelper();
if (!uploader) {
this.props.onError(formatMessage(messages.cannot_initiate_upload));
return;
}
const uploadCompletionPromise = uploader.upload(file);
const msg = Drafty.attachFile(null, file.type, null, file.name, file.size, uploadCompletionPromise);
// Pass data and the uploader to the TinodeWeb.
this.props.sendMessage(msg, uploadCompletionPromise, uploader);
} else {
// Small enough to send inband.
fileToBase64(file,
(mime, bits, fname) => {
this.props.sendMessage(Drafty.attachFile(null, mime, bits, fname));
},
this.props.onError
);
}
}
handleAttachFile(file) {
if (file.size > MAX_EXTERN_ATTACHMENT_SIZE) {
// Too large.
this.props.onError(formatMessage(messages.file_attachment_too_large,
{size: bytesToHumanSize(file.size), limit: bytesToHumanSize(MAX_EXTERN_ATTACHMENT_SIZE)}), 'err');
} else {
this.setState({ docPreview: {
file: file,
filename: file.name,
size: file.size,
type: file.type
}});
}
}
sendImageAttachment(caption, mime, bits, width, height, fname) {
let msg = Drafty.insertImage(null, 0, mime, bits, width, height, fname);
if (caption) {
msg = Drafty.appendLineBreak(msg);
msg = Drafty.append(msg, Drafty.init(caption));
}
this.props.sendMessage(msg);
}
handleAttachImage(file) {
// Check if the uploaded file is indeed an image and if it isn't too large.
if (file.size > MAX_INBAND_ATTACHMENT_SIZE || SUPPORTED_IMAGE_FORMATS.indexOf(file.type) < 0) {
// Convert image for size or format.
imageFileScaledToBase64(file, MAX_IMAGE_DIM, MAX_IMAGE_DIM, false,
// Success
(bits, mime, width, height, fname) => {
this.setState({imagePreview: {
url: URL.createObjectURL(file),
bits: bits,
filename: fname,
width: width,
height: height,
size: bits.length,
type: mime
}});
},
// Failure
(err) => {
this.props.onError(err, 'err');
});
} else {
// Image can be uploaded as is. No conversion is needed.
imageFileToBase64(file,
// Success
(bits, mime, width, height, fname) => {
this.setState({imagePreview: {
url: URL.createObjectURL(file),
bits: bits,
filename: fname,
width: width,
height: height,
size: bits.length,
type: mime
}});
},
// Failure
(err) => {
this.props.onError(err, 'err');
}
);
}
}
render() { render() {
const {formatMessage} = this.props.intl; const {formatMessage} = this.props.intl;
let component; let component;
if (this.state.topic) { if (this.props.hideSelf) {
component = null;
} else if (!this.state.topic) {
component = (
<LogoView hideSelf={this.props.hideSelf}
serverVersion={this.props.serverVersion}
serverAddress={this.props.serverAddress} />
);
} else {
let component2;
if (this.state.imagePreview) {
// Preview image before sending.
component2 = (
<ImagePreview
content={this.state.imagePreview}
onClose={this.handleClosePreview}
onSendMessage={this.sendImageAttachment} />
);
} else if (this.state.imagePostview) {
// Expand received image.
component2 = (
<ImagePreview
content={this.state.imagePostview}
onClose={this.handleClosePreview} />
);
} else if (this.state.docPreview) {
// Preview attachment before sending.
component2 = (
<DocPreview
content={this.state.docPreview}
onClose={this.handleClosePreview}
onSendMessage={this.sendFileAttachment} />
);
} else {
const topic = this.props.tinode.getTopic(this.state.topic); const topic = this.props.tinode.getTopic(this.state.topic);
const groupTopic = topic.getType() == 'grp'; const groupTopic = topic.getType() == 'grp';
let messageNodes = []; let messageNodes = [];
@@ -548,7 +691,7 @@ class MessagesView extends React.Component {
sequence={sequence} received={deliveryStatus} uploader={msg._uploader} sequence={sequence} received={deliveryStatus} uploader={msg._uploader}
viewportWidth={this.props.viewportWidth} viewportWidth={this.props.viewportWidth}
showContextMenu={this.handleShowContextMenuMessage} showContextMenu={this.handleShowContextMenuMessage}
onImagePreview={this.handleImagePreview} onImagePreview={this.handleImagePostview}
onFormResponse={this.handleFormResponse} onFormResponse={this.handleFormResponse}
onError={this.props.onError} onError={this.props.onError}
key={msg.seq} /> key={msg.seq} />
@@ -569,8 +712,8 @@ class MessagesView extends React.Component {
const avatar = this.state.avatar || true; const avatar = this.state.avatar || true;
const online = this.props.online ? 'online' + (this.state.typingIndicator ? ' typing' : '') : 'offline'; const online = this.props.online ? 'online' + (this.state.typingIndicator ? ' typing' : '') : 'offline';
component = ( component2 = (
<div id="topic-view" className={this.props.hideSelf ? 'nodisplay' : null}> <>
<div id="topic-caption-panel" className="caption-panel"> <div id="topic-caption-panel" className="caption-panel">
{this.props.displayMobile ? {this.props.displayMobile ?
<a href="#" id="hide-message-view" onClick={(e) => {e.preventDefault(); this.props.onHideMessagesView();}}> <a href="#" id="hide-message-view" onClick={(e) => {e.preventDefault(); this.props.onHideMessagesView();}}>
@@ -640,23 +783,18 @@ class MessagesView extends React.Component {
<Invitation onAction={this.handleNewChatAcceptance} /> <Invitation onAction={this.handleNewChatAcceptance} />
: :
<SendMessage <SendMessage
tinode={this.props.tinode}
topic={this.props.topic}
disabled={!this.state.isWriter} disabled={!this.state.isWriter}
sendMessage={this.props.sendMessage} onSendMessage={this.props.sendMessage}
onKeyPress={this.sendKeyPress}
onAttachFile={this.handleAttachFile}
onAttachImage={this.handleAttachImage}
onError={this.props.onError} />} onError={this.props.onError} />}
{this.state.imagePreview ? </>
<ImagePreview content={this.state.imagePreview}
onClose={this.handleCloseImagePreview} /> : null}
</div>
);
} else {
component = (
<LogoView hideSelf={this.props.hideSelf}
serverVersion={this.props.serverVersion}
serverAddress={this.props.serverAddress} />
); );
} }
component = <div id="topic-view">{component2}</div>
}
return component; return component;
} }
}; };
+3 -3
View File
@@ -20,13 +20,13 @@ export default class ChatMessage extends React.Component {
props.uploader.onProgress = this.handleProgress.bind(this); props.uploader.onProgress = this.handleProgress.bind(this);
} }
this.handlePreviewImage = this.handlePreviewImage.bind(this); this.handleImagePreview = this.handleImagePreview.bind(this);
this.handleFormButtonClick = this.handleFormButtonClick.bind(this); this.handleFormButtonClick = this.handleFormButtonClick.bind(this);
this.handleContextClick = this.handleContextClick.bind(this); this.handleContextClick = this.handleContextClick.bind(this);
this.handleCancelUpload = this.handleCancelUpload.bind(this); this.handleCancelUpload = this.handleCancelUpload.bind(this);
} }
handlePreviewImage(e) { handleImagePreview(e) {
e.preventDefault(); e.preventDefault();
this.props.onImagePreview({ this.props.onImagePreview({
url: e.target.src, url: e.target.src,
@@ -177,7 +177,7 @@ function draftyFormatter(style, data, values, key) {
attr.style = { width: dim.dstWidth + 'px', height: dim.dstHeight + 'px' }; attr.style = { width: dim.dstWidth + 'px', height: dim.dstHeight + 'px' };
attr.src = sanitizeImageUrl(attr.src); attr.src = sanitizeImageUrl(attr.src);
if (attr.src) { if (attr.src) {
attr.onClick = this.handlePreviewImage; attr.onClick = this.handleImagePreview;
attr.className += ' image-clickable'; attr.className += ' image-clickable';
} else { } else {
attr.src = 'img/broken_image.png'; attr.src = 'img/broken_image.png';
+51
View File
@@ -0,0 +1,51 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import SendMessage from '../widgets/send-message.jsx';
import { bytesToHumanSize } from '../lib/strformat.js';
// Get material icon name from mime type.
function iconFromMime(mime) {
// If more icons become available in material icons, add them to this mime-to-icon mapping.
const mimeToIcon = {default: 'insert_drive_file', image: 'image', text: 'description', video: 'theaters'};
return mimeToIcon[mime] || mimeToIcon[(mime || '').split('/')[0]] || mimeToIcon['default'];
}
export default class DocPreview extends React.PureComponent {
constructor(props) {
super(props);
this.handleSendDoc = this.handleSendDoc.bind(this);
}
handleSendDoc(caption) {
this.props.onClose();
this.props.onSendMessage(this.props.content.file);
}
render() {
if (!this.props.content) {
return null;
}
return (
<div id="image-preview">
<div id="image-preview-caption-panel">
<span>{this.props.content.filename}</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_content_type" /></b> {this.props.content.type || 'application/octet-stream'}</div>
<div><b><FormattedMessage id="label_size" /></b> {bytesToHumanSize(this.props.content.size)}</div>
</div>
</div>
<SendMessage
noInput={true}
onSendMessage={this.handleSendDoc}
onError={this.props.onError} />
</div>
);
}
};
+23 -3
View File
@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import SendMessage from '../widgets/send-message.jsx';
import { REM_SIZE } from '../config.js'; import { REM_SIZE } from '../config.js';
import { fitImageSize } from '../lib/blob-helpers.js'; import { fitImageSize } from '../lib/blob-helpers.js';
@@ -13,6 +14,8 @@ export default class ImagePreview extends React.PureComponent {
width: 0, width: 0,
height: 0 height: 0
}; };
this.handleSendImage = this.handleSendImage.bind(this);
} }
assignWidth(node) { assignWidth(node) {
@@ -25,6 +28,12 @@ export default class ImagePreview extends React.PureComponent {
} }
} }
handleSendImage(caption) {
this.props.onClose();
this.props.onSendMessage(caption, this.props.content.type, this.props.content.bits,
this.props.content.width, this.props.content.height, this.props.content.filename);
}
render() { render() {
if (!this.props.content) { if (!this.props.content) {
return null; return null;
@@ -41,22 +50,33 @@ export default class ImagePreview extends React.PureComponent {
// Averate font aspect ratio is ~0.5; File name takes 1/3 of the viewport width. // Averate 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); const maxlength = Math.max(((this.state.width / REM_SIZE / 1.5) | 0) - 2, 12);
if (filename.length > maxlength) { if (filename.length > maxlength) {
filename = filename.slice(0, maxlength/2 - 1) + '...' + filename.slice(1 - maxlength/2); filename = filename.slice(0, maxlength/2 - 1) + '…' + filename.slice(1 - maxlength/2);
} }
const width = this.props.content.width || '-'; const width = this.props.content.width || '-';
const height = this.props.content.height || '-'; const height = this.props.content.height || '-';
return ( return (
<div id="image-preview" onClick={this.props.onClose}> <div id="image-preview">
<div id="image-preview-caption-panel"> <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.filename}>
<i className="material-icons">file_download</i> <FormattedMessage <i className="material-icons">file_download</i> <FormattedMessage
id="download_action" defaultMessage="download" description="Call to action [download]" /> id="download_action" defaultMessage="download" description="Call to action [download]" />
</a> </a>
:
<span>{this.props.content.filename}</span>
}
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onClose();}}><i className="material-icons gray">close</i></a> <a href="#" onClick={(e) => {e.preventDefault(); this.props.onClose();}}><i className="material-icons gray">close</i></a>
</div> </div>
<div id="image-preview-container" ref={(node) => this.assignWidth(node)}> <div id="image-preview-container" ref={(node) => this.assignWidth(node)}>
<img src={this.props.content.url} style={size} /> <img src={this.props.content.url} style={size} />
</div> </div>
{this.props.onSendMessage ?
<SendMessage
messagePrompt="add_image_caption"
acceptBlank={true}
onSendMessage={this.handleSendImage}
onError={this.props.onError} />
:
<div id="image-preview-footer"> <div id="image-preview-footer">
<div> <div>
<div><b><FormattedMessage id="label_file_name" defaultMessage="File name:" <div><b><FormattedMessage id="label_file_name" defaultMessage="File name:"
@@ -73,7 +93,7 @@ export default class ImagePreview extends React.PureComponent {
description="Label for file size" /></b></div> description="Label for file size" /></b></div>
<div>{width} &times; {height} px; {bytesToHumanSize(this.props.content.size)}</div> <div>{width} &times; {height} px; {bytesToHumanSize(this.props.content.size)}</div>
</div> </div>
</div> </div>}
</div> </div>
); );
} }
+42 -75
View File
@@ -3,9 +3,8 @@ import React from 'react';
import { defineMessages, injectIntl } from 'react-intl'; import { defineMessages, injectIntl } from 'react-intl';
import { Drafty } from 'tinode-sdk'; import { Drafty } from 'tinode-sdk';
import { KEYPRESS_DELAY, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE } from '../config.js'; import { KEYPRESS_DELAY } from '../config.js';
import { SUPPORTED_IMAGE_FORMATS, filePasted, fileToBase64, imageFileToBase64, imageFileScaledToBase64 } from '../lib/blob-helpers.js'; import { filePasted } from '../lib/blob-helpers.js';
import { bytesToHumanSize } from '../lib/strformat.js';
const messages = defineMessages({ const messages = defineMessages({
'messaging_disabled': { 'messaging_disabled': {
@@ -16,7 +15,12 @@ const messages = defineMessages({
'type_new_message': { 'type_new_message': {
id: 'new_message_prompt', id: 'new_message_prompt',
defaultMessage: 'New message', defaultMessage: 'New message',
description: 'Prompt in SendMessage in read-only topic' description: 'Prompt in send message field'
},
'add_image_caption': {
id: 'image_caption_prompt',
defaultMessage: 'Image caption',
description: 'Prompt in SendMessage for attached image'
}, },
'file_attachment_too_large': { 'file_attachment_too_large': {
id: 'file_attachment_too_large', id: 'file_attachment_too_large',
@@ -49,16 +53,22 @@ class SendMessage extends React.PureComponent {
} }
componentDidMount() { componentDidMount() {
if (this.messageEditArea) {
this.messageEditArea.addEventListener('paste', this.handlePasteEvent, false); this.messageEditArea.addEventListener('paste', this.handlePasteEvent, false);
} }
}
componentWillUnmount() { componentWillUnmount() {
if (this.messageEditArea) {
this.messageEditArea.removeEventListener('paste', this.handlePasteEvent, false); this.messageEditArea.removeEventListener('paste', this.handlePasteEvent, false);
} }
}
componentDidUpdate() { componentDidUpdate() {
if (this.messageEditArea) {
this.messageEditArea.focus(); this.messageEditArea.focus();
} }
}
handlePasteEvent(e) { handlePasteEvent(e) {
if (this.props.disabled) { if (this.props.disabled) {
@@ -67,11 +77,10 @@ class SendMessage extends React.PureComponent {
// FIXME: handle large files too. // FIXME: handle large files too.
if (filePasted(e, if (filePasted(e,
(bits, mime, width, height, fname) => { (bits, mime, width, height, fname) => {
this.props.sendMessage(Drafty.insertImage(null, this.props.onAttachImage(mime, bits, width, height, fname);
0, mime, bits, width, height, fname));
}, },
(mime, bits, fname) => { (mime, bits, fname) => {
this.props.sendMessage(Drafty.attachFile(null, mime, bits, fname)); this.props.onAttachFile(mime, bits, fname);
}, },
this.props.onError)) { this.props.onError)) {
@@ -82,34 +91,7 @@ class SendMessage extends React.PureComponent {
handleAttachImage(e) { handleAttachImage(e) {
if (e.target.files && e.target.files.length > 0) { if (e.target.files && e.target.files.length > 0) {
const file = e.target.files[0]; this.props.onAttachImage(e.target.files[0]);
// Check if the uploaded file is indeed an image and if it isn't too large.
if (file.size > MAX_INBAND_ATTACHMENT_SIZE || SUPPORTED_IMAGE_FORMATS.indexOf(file.type) < 0) {
// Convert image for size or format.
imageFileScaledToBase64(file, MAX_IMAGE_DIM, MAX_IMAGE_DIM, false,
// Success
(bits, mime, width, height, fname) => {
this.props.sendMessage(Drafty.insertImage(null,
0, mime, bits, width, height, fname));
},
// Failure
(err) => {
this.props.onError(err, 'err');
});
} else {
// Image can be uploaded as is. No conversion is needed.
imageFileToBase64(file,
// Success
(bits, mime, width, height, fname) => {
this.props.sendMessage(Drafty.insertImage(null,
0, mime, bits, width, height, fname));
},
// Failure
(err) => {
this.props.onError(err, 'err');
}
);
}
} }
// Clear the value so the same file can be uploaded again. // Clear the value so the same file can be uploaded again.
e.target.value = ''; e.target.value = '';
@@ -118,32 +100,7 @@ class SendMessage extends React.PureComponent {
handleAttachFile(e) { handleAttachFile(e) {
const {formatMessage} = this.props.intl; const {formatMessage} = this.props.intl;
if (e.target.files && e.target.files.length > 0) { if (e.target.files && e.target.files.length > 0) {
const file = e.target.files[0]; this.props.onAttachFile(e.target.files[0]);
if (file.size > MAX_EXTERN_ATTACHMENT_SIZE) {
// Too large.
this.props.onError(formatMessage(messages.file_attachment_too_large,
{size: bytesToHumanSize(file.size), limit: bytesToHumanSize(MAX_EXTERN_ATTACHMENT_SIZE)}), 'err');
} else if (file.size > MAX_INBAND_ATTACHMENT_SIZE) {
// Too large to send inband - uploading out of band and sending as a link.
const uploader = this.props.tinode.getLargeFileHelper();
if (!uploader) {
this.props.onError(formatMessage(messages.cannot_initiate_upload));
return;
}
// Format data and initiate upload.
const uploadCompletionPromise = uploader.upload(file);
const msg = Drafty.attachFile(null, file.type, null, file.name, file.size, uploadCompletionPromise);
// Pass data and the uploader to the TinodeWeb.
this.props.sendMessage(msg, uploadCompletionPromise, uploader);
} else {
// Small enough to send inband.
fileToBase64(file,
(mime, bits, fname) => {
this.props.sendMessage(Drafty.attachFile(null, mime, bits, fname));
},
this.props.onError
);
}
} }
// Clear the value so the same file can be uploaded again. // Clear the value so the same file can be uploaded again.
e.target.value = ''; e.target.value = '';
@@ -152,8 +109,8 @@ class SendMessage extends React.PureComponent {
handleSend(e) { handleSend(e) {
e.preventDefault(); e.preventDefault();
const message = this.state.message.trim(); const message = this.state.message.trim();
if (message) { if (message || this.props.acceptBlank || this.props.noInput) {
this.props.sendMessage(this.state.message.trim()); this.props.onSendMessage(message);
this.setState({message: ''}); this.setState({message: ''});
} }
} }
@@ -174,14 +131,13 @@ class SendMessage extends React.PureComponent {
handleMessageTyping(e) { handleMessageTyping(e) {
const newState = {message: e.target.value}; const newState = {message: e.target.value};
if (this.props.onKeyPress) {
const now = new Date().getTime(); const now = new Date().getTime();
if (now - this.state.keypressTimestamp > KEYPRESS_DELAY) { if (now - this.state.keypressTimestamp > KEYPRESS_DELAY) {
const topic = this.props.tinode.getTopic(this.props.topic); this.props.onKeyPress();
if (topic.isSubscribed()) {
topic.noteKeyPress();
}
newState.keypressTimestamp = now; newState.keypressTimestamp = now;
} }
}
this.setState(newState); this.setState(newState);
} }
@@ -189,24 +145,35 @@ class SendMessage extends React.PureComponent {
const {formatMessage} = this.props.intl; const {formatMessage} = this.props.intl;
const prompt = this.props.disabled ? const prompt = this.props.disabled ?
formatMessage(messages.messaging_disabled) : formatMessage(messages.messaging_disabled) :
formatMessage(messages.type_new_message); (this.props.messagePrompt ?
formatMessage(messages[this.props.messagePrompt]) :
formatMessage(messages.type_new_message));
return ( return (
<div id="send-message-panel"> <div id="send-message-panel">
{this.props.disabled ? {this.props.onAttachFile ?
<i className="material-icons disabled">photo</i> : (this.props.disabled ?
<>
<i className="material-icons disabled">photo</i>
<i className="material-icons disabled">attach_file</i>
</>
:
<>
<a href="#" onClick={(e) => {e.preventDefault(); this.attachImage.click();}} title="Add image"> <a href="#" onClick={(e) => {e.preventDefault(); this.attachImage.click();}} title="Add image">
<i className="material-icons secondary">photo</i> <i className="material-icons secondary">photo</i>
</a>} </a>
{this.props.disabled ?
<i className="material-icons disabled">attach_file</i> :
<a href="#" onClick={(e) => {e.preventDefault(); this.attachFile.click();}} title="Attach file"> <a href="#" onClick={(e) => {e.preventDefault(); this.attachFile.click();}} title="Attach file">
<i className="material-icons secondary">attach_file</i> <i className="material-icons secondary">attach_file</i>
</a>} </a>
</>)
:
null}
{this.props.noInput ?
<div className="hr thin" /> :
<textarea id="sendMessage" placeholder={prompt} <textarea id="sendMessage" placeholder={prompt}
disabled={this.props.disabled} value={this.state.message} disabled={this.props.disabled} value={this.state.message}
onChange={this.handleMessageTyping} onKeyPress={this.handleKeyPress} onChange={this.handleMessageTyping} onKeyPress={this.handleKeyPress}
ref={(ref) => {this.messageEditArea = ref;}} ref={(ref) => {this.messageEditArea = ref;}}
autoFocus /> autoFocus />}
{this.props.disabled ? {this.props.disabled ?
<i className="material-icons disabled">send</i> : <i className="material-icons disabled">send</i> :
<a href="#" onClick={this.handleSend} title="Send"> <a href="#" onClick={this.handleSend} title="Send">
+373 -140
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
+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
+144 -42
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