Merge branch 'next' into drag-and-drop
This commit is contained in:
+96
-44
@@ -6,6 +6,7 @@ import { Drafty } from 'tinode-sdk';
|
||||
import AudioPlayer from '../widgets/audio-player.jsx'
|
||||
import CallMessage from '../widgets/call-message.jsx'
|
||||
import CallStatus from '../widgets/call-status.jsx';
|
||||
import InlineVideo from '../widgets/inline-video.jsx';
|
||||
import LazyImage from '../widgets/lazy-image.jsx'
|
||||
import UploadingImage from '../widgets/uploading-image.jsx'
|
||||
|
||||
@@ -37,50 +38,6 @@ const messages = defineMessages({
|
||||
}
|
||||
});
|
||||
|
||||
// Additional processing of image data.
|
||||
function handleImageData(el, data, attr) {
|
||||
if (!data) {
|
||||
attr.src = 'img/broken_image.png';
|
||||
attr.style = {
|
||||
width: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
height: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
};
|
||||
return el;
|
||||
}
|
||||
|
||||
attr.className = 'inline-image';
|
||||
const dim = fitImageSize(data.width, data.height,
|
||||
this.viewportWidth > 0 ? Math.min(this.viewportWidth - REM_SIZE * 6.5, REM_SIZE * 34.5) :
|
||||
REM_SIZE * 34.5, REM_SIZE * 24, false) ||
|
||||
{dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
|
||||
attr.style = {
|
||||
width: dim.dstWidth + 'px',
|
||||
height: dim.dstHeight + 'px',
|
||||
// Looks like a Chrome bug: broken image does not respect 'width' and 'height'.
|
||||
minWidth: dim.dstWidth + 'px',
|
||||
minHeight: dim.dstHeight + 'px'
|
||||
};
|
||||
if (!Drafty.isProcessing(data)) {
|
||||
attr.src = this.authorizeURL(sanitizeUrlForMime(attr.src, 'image'));
|
||||
attr.alt = data.name;
|
||||
if (attr.src) {
|
||||
if (Math.max(data.width || 0, data.height || 0) > IMAGE_THUMBNAIL_DIM) {
|
||||
// Allow previews for large enough images.
|
||||
attr.onClick = this.onImagePreview;
|
||||
attr.className += ' image-clickable';
|
||||
}
|
||||
attr.loading = 'lazy';
|
||||
} else {
|
||||
attr.src = 'img/broken_image.png';
|
||||
}
|
||||
} else {
|
||||
// Use custom element instead of <img>.
|
||||
el = UploadingImage;
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
// The main Drafty formatter: converts Drafty elements into React classes. 'this' is set by the caller.
|
||||
// 'this' must contain:
|
||||
// viewportWidth: this.props.viewportWidth;
|
||||
@@ -175,6 +132,12 @@ export function fullFormatter(style, data, values, key, stack) {
|
||||
attr.duration = data.duration;
|
||||
}
|
||||
break;
|
||||
case 'VD':
|
||||
// Additional processing for videos.
|
||||
el = handleVideoData.call(this, el, data, attr);
|
||||
// Video element cannot have content.
|
||||
values = null;
|
||||
break;
|
||||
default:
|
||||
if (!el) {
|
||||
// Unknown element.
|
||||
@@ -195,6 +158,95 @@ export function fullFormatter(style, data, values, key, stack) {
|
||||
return React.createElement(el, attr, values);
|
||||
}
|
||||
|
||||
// Additional processing of image data.
|
||||
function handleImageData(el, data, attr) {
|
||||
if (!data) {
|
||||
attr.src = 'img/broken_image.png';
|
||||
attr.style = {
|
||||
width: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
height: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
};
|
||||
return el;
|
||||
}
|
||||
|
||||
attr.className = 'inline-image';
|
||||
const dim = fitImageSize(data.width, data.height,
|
||||
this.viewportWidth > 0 ? Math.min(this.viewportWidth - REM_SIZE * 6.5, REM_SIZE * 34.5) :
|
||||
REM_SIZE * 34.5, REM_SIZE * 24, false) ||
|
||||
{dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
|
||||
attr.style = {
|
||||
width: dim.dstWidth + 'px',
|
||||
height: dim.dstHeight + 'px',
|
||||
// Looks like a Chrome bug: broken image does not respect 'width' and 'height'.
|
||||
minWidth: dim.dstWidth + 'px',
|
||||
minHeight: dim.dstHeight + 'px'
|
||||
};
|
||||
if (!Drafty.isProcessing(data)) {
|
||||
attr.src = this.authorizeURL(sanitizeUrlForMime(attr.src, 'image'));
|
||||
attr.alt = data.name;
|
||||
if (attr.src) {
|
||||
if (Math.max(data.width || 0, data.height || 0) > IMAGE_THUMBNAIL_DIM) {
|
||||
// Allow previews for large enough images.
|
||||
attr.onClick = this.onImagePreview;
|
||||
attr.className += ' image-clickable';
|
||||
}
|
||||
attr.loading = 'lazy';
|
||||
} else {
|
||||
attr.src = 'img/broken_image.png';
|
||||
}
|
||||
} else {
|
||||
// Use custom element instead of <img>.
|
||||
el = UploadingImage;
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
// Additional processing of image data.
|
||||
function handleVideoData(el, data, attr) {
|
||||
if (!data) {
|
||||
attr.src = 'img/broken_video.png';
|
||||
attr.style = {
|
||||
width: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
height: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
};
|
||||
return el;
|
||||
}
|
||||
|
||||
attr.className = 'inline-image';
|
||||
const dim = fitImageSize(data.width, data.height,
|
||||
this.viewportWidth > 0 ? Math.min(this.viewportWidth - REM_SIZE * 6.5, REM_SIZE * 34.5) :
|
||||
REM_SIZE * 34.5, REM_SIZE * 24, false) ||
|
||||
{dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
|
||||
attr.style = {
|
||||
width: dim.dstWidth + 'px',
|
||||
height: dim.dstHeight + 'px',
|
||||
// Looks like a Chrome bug: broken image does not respect 'width' and 'height'.
|
||||
minWidth: dim.dstWidth + 'px',
|
||||
minHeight: dim.dstHeight + 'px'
|
||||
};
|
||||
if (!Drafty.isProcessing(data)) {
|
||||
/*
|
||||
attr.src = this.authorizeURL(sanitizeUrlForMime(attr.src, 'video'));
|
||||
attr.alt = data.name;
|
||||
if (attr.src) {
|
||||
attr.onClick = this.onImagePreview;
|
||||
attr.className += ' image-clickable';
|
||||
attr.loading = 'lazy';
|
||||
attr.controls = 'controls';
|
||||
} else {
|
||||
attr.src = 'img/broken_video.png';
|
||||
}
|
||||
*/
|
||||
el = InlineVideo;
|
||||
} else {
|
||||
// Use custom element instead of <img>.
|
||||
el = UploadingImage;
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
// Converts Drafty object into a one-line preview. 'this' is set by the caller.
|
||||
// 'this' must contain:
|
||||
// formatMessage: this.props.intl.formatMessage
|
||||
|
||||
+102
-9
@@ -19,6 +19,7 @@ import LoadSpinner from '../widgets/load-spinner.jsx';
|
||||
import LogoView from './logo-view.jsx';
|
||||
import MetaMessage from '../widgets/meta-message.jsx';
|
||||
import SendMessage from '../widgets/send-message.jsx';
|
||||
import VideoPreview from '../widgets/video-preview.jsx';
|
||||
|
||||
import { DEFAULT_P2P_ACCESS_MODE, EDIT_PREVIEW_LENGTH, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY,
|
||||
MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE,
|
||||
@@ -110,6 +111,7 @@ class MessagesView extends React.Component {
|
||||
this.sendMessage = this.sendMessage.bind(this);
|
||||
this.retrySend = this.retrySend.bind(this);
|
||||
this.sendImageAttachment = this.sendImageAttachment.bind(this);
|
||||
this.sendVideoAttachment = this.sendVideoAttachment.bind(this);
|
||||
this.sendFileAttachment = this.sendFileAttachment.bind(this);
|
||||
this.sendAudioAttachment = this.sendAudioAttachment.bind(this);
|
||||
this.sendKeyPress = this.sendKeyPress.bind(this);
|
||||
@@ -129,7 +131,7 @@ class MessagesView extends React.Component {
|
||||
this.handleNewChatAcceptance = this.handleNewChatAcceptance.bind(this);
|
||||
this.handleEnablePeer = this.handleEnablePeer.bind(this);
|
||||
this.handleAttachFile = this.handleAttachFile.bind(this);
|
||||
this.handleAttachImage = this.handleAttachImage.bind(this);
|
||||
this.handleAttachImageOrVideo = this.handleAttachImageOrVideo.bind(this);
|
||||
this.handleCancelUpload = this.handleCancelUpload.bind(this);
|
||||
this.postReadNotification = this.postReadNotification.bind(this);
|
||||
this.clearNotificationQueue = this.clearNotificationQueue.bind(this);
|
||||
@@ -687,7 +689,10 @@ class MessagesView extends React.Component {
|
||||
if (this.state.imagePreview && this.state.imagePreview.url) {
|
||||
URL.revokeObjectURL(this.state.imagePreview.url);
|
||||
}
|
||||
this.setState({ imagePostview: null, imagePreview: null, docPreview: null });
|
||||
if (this.state.videoPreview && this.state.videoPreview.url) {
|
||||
URL.revokeObjectURL(this.state.videoPreview.url);
|
||||
}
|
||||
this.setState({ imagePostview: null, imagePreview: null, docPreview: null, videoPreview: null });
|
||||
}
|
||||
|
||||
handleFormResponse(action, text, data) {
|
||||
@@ -862,7 +867,7 @@ class MessagesView extends React.Component {
|
||||
this.props.onCallHangup(topic, seq);
|
||||
}
|
||||
|
||||
// sendImageAttachment sends the image bits inband as Drafty message.
|
||||
// sendImageAttachment sends the image bits as Drafty message.
|
||||
sendImageAttachment(caption, blob) {
|
||||
const mime = this.state.imagePreview.mime;
|
||||
const width = this.state.imagePreview.width;
|
||||
@@ -904,10 +909,10 @@ class MessagesView extends React.Component {
|
||||
this.sendMessage(msg, uploadCompletionPromise, uploader);
|
||||
})
|
||||
.catch(err => this.props.onError(err, 'err'));
|
||||
return;
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload the image if it's too big to be send inband.
|
||||
// Send the image inband if it's not too big.
|
||||
blobToBase64(blob)
|
||||
.then(b64 => {
|
||||
let msg = Drafty.insertImage(null, 0, {
|
||||
@@ -926,10 +931,87 @@ class MessagesView extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
// handleAttachImage method is called when [Attach image] button is clicked: launch image preview.
|
||||
handleAttachImage(file) {
|
||||
// sendVideoAttachment sends the video bits as Drafty message.
|
||||
sendVideoAttachment(caption, videoBlob, previewBlob, params) {
|
||||
const mime = params.mime;
|
||||
const width = params.width;
|
||||
const height = params.height;
|
||||
const fname = params.name;
|
||||
const duration = params.duration;
|
||||
|
||||
// Server-provided limit reduced for base64 encoding and overhead.
|
||||
const maxInbandAttachmentSize = (this.props.tinode.getServerParam('maxMessageSize',
|
||||
MAX_INBAND_ATTACHMENT_SIZE) * 0.75 - 1024) | 0;
|
||||
|
||||
if (videoBlob.size > maxInbandAttachmentSize) {
|
||||
// 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(this.props.intl.formatMessage(messages.cannot_initiate_upload));
|
||||
return;
|
||||
}
|
||||
const uploadCompletionPromise = uploader.upload(videoBlob);
|
||||
// Make small preview to show while uploading.
|
||||
imageScaled(previewBlob, IMAGE_PREVIEW_DIM, IMAGE_PREVIEW_DIM, -1, false)
|
||||
// Convert tiny image into base64 for serialization and previewing.
|
||||
.then(scaled => blobToBase64(scaled.blob))
|
||||
.then(b64 => {
|
||||
let msg = Drafty.insertVideo(null, 0, {
|
||||
mime: mime,
|
||||
_tempPreview: b64.bits, // This preview will not be serialized.
|
||||
width: width,
|
||||
height: height,
|
||||
duration: duration,
|
||||
filename: fname,
|
||||
size: videoBlob.size,
|
||||
urlPromise: uploadCompletionPromise
|
||||
});
|
||||
if (caption) {
|
||||
msg = Drafty.appendLineBreak(msg);
|
||||
msg = Drafty.append(msg, Drafty.parse(caption));
|
||||
}
|
||||
// Pass data and the uploader to the TinodeWeb.
|
||||
this.sendMessage(msg, uploadCompletionPromise, uploader);
|
||||
})
|
||||
.catch(err => /* this.props.onError(err, 'err') */ console.log(err));
|
||||
return;
|
||||
}
|
||||
|
||||
// Send the video inband if it's small enough.
|
||||
blobToBase64(videoBlob)
|
||||
.then(b64 => {
|
||||
let msg = Drafty.insertVideo(null, 0, {
|
||||
mime: b64.mime,
|
||||
preview: b64.bits, // Serializable preview
|
||||
width: width,
|
||||
height: height,
|
||||
duration: duration,
|
||||
filename: fname,
|
||||
size: blob.size
|
||||
});
|
||||
if (caption) {
|
||||
msg = Drafty.appendLineBreak(msg);
|
||||
msg = Drafty.append(msg, Drafty.parse(caption));
|
||||
}
|
||||
this.sendMessage(msg);
|
||||
});
|
||||
}
|
||||
|
||||
// handleAttachImageOrVideo method is called when [Attach image or video] button is clicked: launch image or video preview.
|
||||
handleAttachImageOrVideo(file) {
|
||||
const maxExternAttachmentSize = this.props.tinode.getServerParam('maxFileUploadSize', MAX_EXTERN_ATTACHMENT_SIZE);
|
||||
|
||||
if (file.type.startsWith('video/')) {
|
||||
this.setState({videoPreview: {
|
||||
url: URL.createObjectURL(file),
|
||||
blob: file,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
mime: file.type
|
||||
}});
|
||||
return;
|
||||
}
|
||||
|
||||
// Get image dimensions and size, optionally scale it down.
|
||||
imageScaled(file, MAX_IMAGE_DIM, MAX_IMAGE_DIM, maxExternAttachmentSize, false)
|
||||
.then(scaled => {
|
||||
@@ -954,7 +1036,7 @@ class MessagesView extends React.Component {
|
||||
}
|
||||
const file = files[0];
|
||||
if (file.type && file.type.startsWith('image/')) {
|
||||
this.handleAttachImage(file);
|
||||
this.handleAttachImageOrVideo(file);
|
||||
} else {
|
||||
this.handleAttachFile(file);
|
||||
}
|
||||
@@ -1109,6 +1191,17 @@ class MessagesView extends React.Component {
|
||||
onClose={this.handleClosePreview}
|
||||
onSendMessage={this.sendImageAttachment} />
|
||||
);
|
||||
} else if (this.state.videoPreview) {
|
||||
// Preview video.
|
||||
component2 = (
|
||||
<VideoPreview
|
||||
content={this.state.videoPreview}
|
||||
tinode={this.props.tinode}
|
||||
reply={this.state.reply}
|
||||
onCancelReply={this.handleCancelReply}
|
||||
onClose={this.handleClosePreview}
|
||||
onSendMessage={this.sendVideoAttachment} />
|
||||
);
|
||||
} else if (this.state.imagePostview) {
|
||||
// Expand received image.
|
||||
component2 = (
|
||||
@@ -1318,7 +1411,7 @@ class MessagesView extends React.Component {
|
||||
onRecordingProgress={this.sendKeyPress}
|
||||
onSendMessage={this.sendMessage}
|
||||
onAttachFile={this.props.forwardMessage ? null : this.handleAttachFile}
|
||||
onAttachImage={this.props.forwardMessage ? null : this.handleAttachImage}
|
||||
onAttachImage={this.props.forwardMessage ? null : this.handleAttachImageOrVideo}
|
||||
onAttachAudio={this.props.forwardMessage ? null : this.sendAudioAttachment}
|
||||
onError={this.props.onError}
|
||||
onQuoteClick={this.handleQuoteClick}
|
||||
|
||||
@@ -169,7 +169,7 @@ class TinodeWeb extends React.Component {
|
||||
this.handleCallIceCandidate = this.handleCallIceCandidate.bind(this);
|
||||
this.handleCallSendAnswer = this.handleCallSendAnswer.bind(this);
|
||||
|
||||
this.handleCallAcceptCall = this.handleCallAcceptCall.bind(this);
|
||||
this.handleCallAccept = this.handleCallAccept.bind(this);
|
||||
|
||||
this.sendMessageToTopic = this.sendMessageToTopic.bind(this);
|
||||
|
||||
@@ -1791,7 +1791,7 @@ class TinodeWeb extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
handleCallAcceptCall(topicName) {
|
||||
handleCallAccept(topicName) {
|
||||
const topic = this.tinode.getTopic(topicName);
|
||||
if (!topic) {
|
||||
return;
|
||||
@@ -1914,7 +1914,7 @@ class TinodeWeb extends React.Component {
|
||||
seq={this.state.callSeq}
|
||||
callState={this.state.callState}
|
||||
onRinging={this.handleCallRinging}
|
||||
onAcceptCall={this.handleCallAcceptCall}
|
||||
onAcceptCall={this.handleCallAccept}
|
||||
onReject={this.handleCallHangup}
|
||||
/>
|
||||
:
|
||||
|
||||
@@ -37,7 +37,7 @@ export default class CallIncoming extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
this.resetDesc(topic, this.props);
|
||||
this.resetDesc(topic);
|
||||
if (this.props.callState == CALL_STATE_INCOMING_RECEIVED) {
|
||||
// play() throws if the user did not click the app first: https://goo.gl/xX8pDD.
|
||||
RING_SOUND.play().catch(_ => {});
|
||||
@@ -77,9 +77,7 @@ export default class CallIncoming extends React.Component {
|
||||
topic.onMetaDesc = this.previousMetaDesc;
|
||||
}
|
||||
|
||||
resetDesc(topic, props) {
|
||||
const defacs = topic.getDefaultAccess() || {};
|
||||
const acs = topic.getAccessMode();
|
||||
resetDesc(topic) {
|
||||
const badges = [];
|
||||
if (topic.trusted) {
|
||||
for (const [key, val] of Object.entries(topic.trusted)) {
|
||||
@@ -101,7 +99,7 @@ export default class CallIncoming extends React.Component {
|
||||
if (!topic) {
|
||||
return;
|
||||
}
|
||||
this.resetDesc(topic, this.props);
|
||||
this.resetDesc(topic);
|
||||
|
||||
if (this.previousMetaDesc && this.previousMetaDesc != this.onMetaDesc) {
|
||||
this.previousMetaDesc(desc);
|
||||
|
||||
@@ -421,14 +421,14 @@ class CallPanel extends React.PureComponent {
|
||||
<div id="video-container">
|
||||
<div id="video-container-panel">
|
||||
<div className="call-party self">
|
||||
<video ref={this.localRef} autoPlay muted playsInline></video>
|
||||
<video ref={this.localRef} autoPlay muted playsInline />
|
||||
<div className="caller-name inactive">
|
||||
<FormattedMessage id="calls_you_label"
|
||||
defaultMessage="You" description="Shown over the local video screen" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="call-party peer">
|
||||
<video ref={this.remoteRef} autoPlay playsInline></video>
|
||||
<video ref={this.remoteRef} autoPlay playsInline />
|
||||
{remoteActive ?
|
||||
<div className="caller-name inactive">{peerTitle}</div> :
|
||||
<div className={`caller-card${pulseAnimation}`}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import SendMessage from '../widgets/send-message.jsx';
|
||||
import SendMessage from './send-message.jsx';
|
||||
|
||||
import { REM_SIZE } from '../config.js';
|
||||
import { fitImageSize } from '../lib/blob-helpers.js';
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Element which shows static video preview:
|
||||
// an image with duration and a play button in the middle.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class InlineVideo extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="inline-image video">
|
||||
{React.createElement('img', this.props)}
|
||||
<div className="play-control">
|
||||
<i class="material-icons white bigger">play_arrow</i>
|
||||
</div>
|
||||
<div className="duration">1:12:45</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -284,7 +284,7 @@ class SendMessage extends React.PureComponent {
|
||||
}
|
||||
<input type="file" ref={ref => {this.attachFile = ref}}
|
||||
onChange={this.handleAttachFile} style={{display: 'none'}} />
|
||||
<input type="file" ref={ref => {this.attachImage = ref}} accept="image/*"
|
||||
<input type="file" ref={ref => {this.attachImage = ref}} accept="image/*, video/*"
|
||||
onChange={this.handleAttachImage} style={{display: 'none'}} />
|
||||
</>
|
||||
:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// Image view with progress bar and a cancel button.
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import FileProgress from './file-progress.jsx';
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import SendMessage from './send-message.jsx';
|
||||
|
||||
export default class VideoPreview extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.videoRef = React.createRef();
|
||||
|
||||
this.handleSendVideo = this.handleSendVideo.bind(this);
|
||||
}
|
||||
|
||||
handleSendVideo(caption) {
|
||||
this.props.onClose();
|
||||
const params = {
|
||||
width: this.videoRef.current.videoWidth,
|
||||
height: this.videoRef.current.videoHeight,
|
||||
duration: (this.videoRef.current.duration * 1000) | 0,
|
||||
mime: this.props.content.mime,
|
||||
name: this.props.content.name
|
||||
}
|
||||
|
||||
// Capture screen from a video.
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = params.width;
|
||||
canvas.height = params.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(this.videoRef.current, 0, 0, canvas.width, canvas.height);
|
||||
ctx.canvas.toBlob(
|
||||
preview => this.props.onSendMessage(caption, this.props.content.blob, preview, params),
|
||||
'image/jpeg', 0.75
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="image-preview">
|
||||
<div id="image-preview-caption-panel">
|
||||
<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">
|
||||
<video controls ref={this.videoRef} src={this.props.content.url} className="image-preview" alt={this.props.content.name} />
|
||||
</div>
|
||||
<SendMessage
|
||||
messagePrompt="add_image_caption"
|
||||
acceptBlank={true}
|
||||
tinode={this.props.tinode}
|
||||
reply={this.props.reply}
|
||||
onCancelReply={this.props.onCancelReply}
|
||||
onSendMessage={this.handleSendVideo}
|
||||
onError={this.props.onError} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user