working drag and drop, but no scrolling on the conversation messages

pull/1387/head
Audric Ackermann 5 years ago
parent b46ed237b3
commit a7bdc93ca8
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -53,7 +53,6 @@
</div>
</div>
</div>
<div class='lightbox-container'></div>
</script>

@ -52,7 +52,6 @@
</div>
</div>
</div>
<div class='lightbox-container'></div>
</script>

@ -1,7 +1,6 @@
// The idea with this file is to make it webpackable for the style guide
const { bindActionCreators } = require('redux');
const Backbone = require('../../ts/backbone');
const Crypto = require('./crypto');
const Data = require('./data');
const Database = require('./database');
@ -363,7 +362,6 @@ exports.setup = (options = {}) => {
return {
AttachmentDownloads,
Backbone,
Components,
Crypto,
Data,

@ -109,7 +109,6 @@
"react-autosize-textarea": "^7.0.0",
"react-contextmenu": "2.11.0",
"react-dom": "16.8.3",
"react-dropzone": "^11.0.2",
"react-emoji": "^0.5.0",
"react-emoji-render": "^1.2.4",
"react-h5-audio-player": "^3.2.0",

@ -40,7 +40,6 @@
</div>
</div>
</div>
<div class="lightbox-container"></div>
</script>

@ -1,3 +0,0 @@
import * as Views from './views';
export { Views };

@ -1,24 +0,0 @@
export const show = (element: HTMLElement): void => {
const container: HTMLDivElement | null = document.querySelector(
'.lightbox-container'
);
if (!container) {
throw new TypeError("'.lightbox-container' is required");
}
// tslint:disable-next-line:no-inner-html
container.innerHTML = '';
container.style.display = 'block';
container.appendChild(element);
};
export const hide = (): void => {
const container: HTMLDivElement | null = document.querySelector(
'.lightbox-container'
);
if (!container) {
return;
}
// tslint:disable-next-line:no-inner-html
container.innerHTML = '';
container.style.display = 'none';
};

@ -1,3 +0,0 @@
import * as Lightbox from './Lightbox';
export { Lightbox };

@ -36,7 +36,6 @@ import { isFileDangerous } from '../../util/isFileDangerous';
import { ColorType, LocalizerType } from '../../types/Util';
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
import { SessionIcon, SessionIconSize, SessionIconType } from '../session/icon';
import { ReplyingToMessageProps } from '../session/conversation/SessionCompositionBox';
import _ from 'lodash';
import { MessageModel } from '../../../js/models/messages';

@ -9,7 +9,6 @@ import TextareaAutosize from 'react-autosize-textarea';
import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon';
import { SessionEmojiPanel } from './SessionEmojiPanel';
import { SessionRecording } from './SessionRecording';
import * as GoogleChrome from '../../../util/GoogleChrome';
import { SignalService } from '../../../protobuf';
@ -20,6 +19,7 @@ import { SessionQuotedMessageComposition } from './SessionQuotedMessageCompositi
import { Flex } from '../Flex';
import { AttachmentList } from '../../conversation/AttachmentList';
import { ToastUtils } from '../../../session/utils';
import { AttachmentUtil } from '../../../util';
export interface ReplyingToMessageProps {
convoId: string;
@ -30,7 +30,7 @@ export interface ReplyingToMessageProps {
attachments?: Array<any>;
}
interface StagedAttachmentType extends AttachmentType {
export interface StagedAttachmentType extends AttachmentType {
file: File;
}
@ -45,11 +45,14 @@ interface Props {
onLoadVoiceNoteView: any;
onExitVoiceNoteView: any;
dropZoneFiles: FileList;
quotedMessageProps?: ReplyingToMessageProps;
removeQuotedMessage: () => void;
textarea: React.RefObject<HTMLDivElement>;
stagedAttachments: Array<StagedAttachmentType>;
clearAttachments: () => any;
removeAttachment: (toRemove: AttachmentType) => void;
onChoseAttachments: (newAttachments: FileList) => void;
}
interface State {
@ -58,7 +61,6 @@ interface State {
mediaSetting: boolean | null;
showEmojiPanel: boolean;
stagedAttachments: Array<StagedAttachmentType>;
voiceRecording?: Blob;
}
@ -71,7 +73,6 @@ export class SessionCompositionBox extends React.Component<Props, State> {
super(props);
this.state = {
message: '',
stagedAttachments: [],
voiceRecording: undefined,
showRecordingView: false,
mediaSetting: null,
@ -101,8 +102,6 @@ export class SessionCompositionBox extends React.Component<Props, State> {
// Attachments
this.onChoseAttachment = this.onChoseAttachment.bind(this);
this.onChooseAttachment = this.onChooseAttachment.bind(this);
this.clearAttachments = this.clearAttachments.bind(this);
this.removeAttachment = this.removeAttachment.bind(this);
// On Sending
this.onSendMessage = this.onSendMessage.bind(this);
@ -267,7 +266,7 @@ export class SessionCompositionBox extends React.Component<Props, State> {
}
private renderAttachmentsStaged() {
const { stagedAttachments } = this.state;
const { stagedAttachments } = this.props;
if (stagedAttachments && stagedAttachments.length) {
return (
<AttachmentList
@ -275,8 +274,8 @@ export class SessionCompositionBox extends React.Component<Props, State> {
// tslint:disable-next-line: no-empty
onClickAttachment={() => {}}
onAddAttachment={this.onChooseAttachment}
onCloseAttachment={this.removeAttachment}
onClose={this.clearAttachments}
onCloseAttachment={this.props.removeAttachment}
onClose={this.props.clearAttachments}
/>
);
}
@ -293,11 +292,7 @@ export class SessionCompositionBox extends React.Component<Props, State> {
if (!attachmentsFileList || attachmentsFileList.length === 0) {
return;
}
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < attachmentsFileList.length; i++) {
await this.maybeAddAttachment(attachmentsFileList[i]);
}
this.props.onChoseAttachments(attachmentsFileList);
}
private async onKeyDown(event: any) {
@ -331,7 +326,7 @@ export class SessionCompositionBox extends React.Component<Props, State> {
ToastUtils.pushMessageBodyTooLong();
return;
}
if (msgLen === 0 && this.state.stagedAttachments?.length === 0) {
if (msgLen === 0 && this.props.stagedAttachments?.length === 0) {
ToastUtils.pushMessageBodyMissing();
return;
}
@ -361,13 +356,13 @@ export class SessionCompositionBox extends React.Component<Props, State> {
// Message sending sucess
this.props.onMessageSuccess();
// Empty stagedAttachments
// Empty composition box
this.setState({
message: '',
showEmojiPanel: false,
});
this.clearAttachments();
// Empty stagedAttachments
this.props.clearAttachments();
} catch (e) {
// Message sending failed
window.log.error(e);
@ -377,11 +372,11 @@ export class SessionCompositionBox extends React.Component<Props, State> {
// this function is called right before sending a message, to gather really files bejind attachments.
private async getFiles() {
const { stagedAttachments } = this.state;
const { stagedAttachments } = this.props;
const files = await Promise.all(
stagedAttachments.map(attachment => this.getFile(attachment))
stagedAttachments.map(attachment => AttachmentUtil.getFile(attachment))
);
this.clearAttachments();
this.props.clearAttachments();
return files;
}
@ -488,316 +483,4 @@ export class SessionCompositionBox extends React.Component<Props, State> {
// Focus the textarea when user clicks anywhere in the composition box
this.textarea.current?.focus();
}
// tslint:disable: max-func-body-length cyclomatic-complexity
private async maybeAddAttachment(file: any) {
if (!file) {
return;
}
const fileName = file.name;
const contentType = file.type;
const { stagedAttachments } = this.state;
if (window.Signal.Util.isFileDangerous(fileName)) {
ToastUtils.pushDangerousFileError();
return;
}
if (stagedAttachments.length >= 32) {
ToastUtils.pushMaximumAttachmentsError();
return;
}
const haveNonImage = _.some(
stagedAttachments,
attachment => !MIME.isImage(attachment.contentType)
);
// You can't add another attachment if you already have a non-image staged
if (haveNonImage) {
ToastUtils.pushMultipleNonImageError();
return;
}
// You can't add a non-image attachment if you already have attachments staged
if (!MIME.isImage(contentType) && stagedAttachments.length > 0) {
ToastUtils.pushCannotMixError();
return;
}
const { VisualAttachment } = window.Signal.Types;
const renderVideoPreview = async () => {
const objectUrl = URL.createObjectURL(file);
try {
const type = 'image/png';
const thumbnail = await VisualAttachment.makeVideoScreenshot({
objectUrl,
contentType: type,
logger: window.log,
});
const data = await VisualAttachment.blobToArrayBuffer(thumbnail);
const url = window.Signal.Util.arrayBufferToObjectURL({
data,
type,
});
this.addAttachment({
file,
size: file.size,
fileName,
contentType,
videoUrl: objectUrl,
url,
isVoiceMessage: false,
});
} catch (error) {
URL.revokeObjectURL(objectUrl);
}
};
const renderImagePreview = async () => {
if (!MIME.isJPEG(contentType)) {
const urlImage = URL.createObjectURL(file);
if (!urlImage) {
throw new Error('Failed to create object url for image!');
}
this.addAttachment({
file,
size: file.size,
fileName,
contentType,
url: urlImage,
isVoiceMessage: false,
});
return;
}
const url = await window.autoOrientImage(file);
this.addAttachment({
file,
size: file.size,
fileName,
contentType,
url,
isVoiceMessage: false,
});
};
try {
const blob = await this.autoScale({
contentType,
file,
});
let limitKb = 10000;
const blobType =
file.type === 'image/gif' ? 'gif' : contentType.split('/')[0];
switch (blobType) {
case 'image':
limitKb = 6000;
break;
case 'gif':
limitKb = 10000;
break;
case 'audio':
limitKb = 10000;
break;
case 'video':
limitKb = 10000;
break;
default:
limitKb = 10000;
}
// if ((blob.file.size / 1024).toFixed(4) >= limitKb) {
// const units = ['kB', 'MB', 'GB'];
// let u = -1;
// let limit = limitKb * 1000;
// do {
// limit /= 1000;
// u += 1;
// } while (limit >= 1000 && u < units.length - 1);
// // this.showFileSizeError(limit, units[u]);
// return;
// }
} catch (error) {
window.log.error(
'Error ensuring that image is properly sized:',
error && error.stack ? error.stack : error
);
ToastUtils.pushLoadAttachmentFailure();
return;
}
try {
if (GoogleChrome.isImageTypeSupported(contentType)) {
await renderImagePreview();
} else if (GoogleChrome.isVideoTypeSupported(contentType)) {
await renderVideoPreview();
} else {
this.addAttachment({
file,
size: file.size,
contentType,
fileName,
url: '',
isVoiceMessage: false,
});
}
} catch (e) {
window.log.error(
`Was unable to generate thumbnail for file type ${contentType}`,
e && e.stack ? e.stack : e
);
this.addAttachment({
file,
size: file.size,
contentType,
fileName,
isVoiceMessage: false,
url: '',
});
}
}
private addAttachment(attachment: StagedAttachmentType) {
const { stagedAttachments } = this.state;
if (attachment.isVoiceMessage && stagedAttachments.length > 0) {
throw new Error('A voice note cannot be sent with other attachments');
}
this.setState(
{
stagedAttachments: [...stagedAttachments, { ...attachment }],
},
this.focusCompositionBox
);
}
private async autoScale<T extends { contentType: string; file: any }>(
attachment: T
): Promise<T> {
const { contentType, file } = attachment;
if (contentType.split('/')[0] !== 'image' || contentType === 'image/tiff') {
// nothing to do
return Promise.resolve(attachment);
}
return new Promise((resolve, reject) => {
const url = URL.createObjectURL(file);
const img = document.createElement('img');
img.onerror = reject;
img.onload = () => {
URL.revokeObjectURL(url);
const maxSize = 6000 * 1024;
const maxHeight = 4096;
const maxWidth = 4096;
if (
img.naturalWidth <= maxWidth &&
img.naturalHeight <= maxHeight &&
file.size <= maxSize
) {
resolve(attachment);
return;
}
const gifMaxSize = 25000 * 1024;
if (file.type === 'image/gif' && file.size <= gifMaxSize) {
resolve(attachment);
return;
}
if (file.type === 'image/gif') {
reject(new Error('GIF is too large'));
return;
}
const canvas = window.loadImage.scale(img, {
canvas: true,
maxWidth,
maxHeight,
});
let quality = 0.95;
let i = 4;
let blob;
do {
i -= 1;
blob = window.dataURLToBlobSync(
canvas.toDataURL('image/jpeg', quality)
);
quality = (quality * maxSize) / blob.size;
// NOTE: During testing with a large image, we observed the
// `quality` value being > 1. Should we clamp it to [0.5, 1.0]?
// See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Syntax
if (quality < 0.5) {
quality = 0.5;
}
} while (i > 0 && blob.size > maxSize);
resolve({
...attachment,
file: blob,
});
};
img.src = url;
});
}
private clearAttachments() {
this.state.stagedAttachments.forEach(attachment => {
if (attachment.url) {
URL.revokeObjectURL(attachment.url);
}
if (attachment.videoUrl) {
URL.revokeObjectURL(attachment.videoUrl);
}
});
this.setState({ stagedAttachments: [] });
}
private removeAttachment(attachment: AttachmentType) {
const { stagedAttachments } = this.state;
const updatedStagedAttachments = (stagedAttachments || []).filter(
m => m.fileName !== attachment.fileName
);
this.setState({ stagedAttachments: updatedStagedAttachments });
}
private async getFile(attachment: any) {
if (!attachment) {
return Promise.resolve();
}
const attachmentFlags = attachment.isVoiceMessage
? SignalService.AttachmentPointer.Flags.VOICE_MESSAGE
: null;
const scaled = await this.autoScale(attachment);
const fileRead = await this.readFile(scaled);
return {
...fileRead,
url: undefined,
flags: attachmentFlags || null,
};
}
private async readFile(attachment: any): Promise<object> {
return new Promise((resolve, reject) => {
const FR = new FileReader();
FR.onload = e => {
const data = e?.target?.result as ArrayBuffer;
resolve({
...attachment,
data,
size: data.byteLength,
});
};
FR.onerror = reject;
FR.onabort = reject;
FR.readAsArrayBuffer(attachment.file);
});
}
}

@ -4,12 +4,15 @@ import React from 'react';
import classNames from 'classnames';
import { SessionCompositionBox } from './SessionCompositionBox';
import {
SessionCompositionBox,
StagedAttachmentType,
} from './SessionCompositionBox';
import { Constants } from '../../../session';
import { SessionKeyVerification } from '../SessionKeyVerification';
import _ from 'lodash';
import { UserUtil } from '../../../util';
import { AttachmentUtil, GoogleChrome, UserUtil } from '../../../util';
import { MultiDeviceProtocol } from '../../../session/protocols';
import { ConversationHeaderWithDetails } from '../../conversation/ConversationHeader';
import { SessionRightPanelWithDetails } from './SessionRightPanel';
@ -20,6 +23,8 @@ import { LightboxGallery, MediaItemType } from '../../LightboxGallery';
import { Message } from '../../conversation/media-gallery/types/Message';
import { AttachmentType } from '../../../types/Attachment';
import { ToastUtils } from '../../../session/utils';
import * as MIME from '../../../types/MIME';
interface State {
conversationKey: string;
@ -50,8 +55,7 @@ interface State {
// For displaying `More Info` on messages, and `Safety Number`, etc.
infoViewState?: 'safetyNumber' | 'messageDetails';
// dropZoneFiles?: FileList
dropZoneFiles: any;
stagedAttachments: Array<StagedAttachmentType>;
// quoted message
quotedMessageTimestamp?: number;
@ -97,7 +101,7 @@ export class SessionConversation extends React.Component<Props, State> {
showRecordingView: false,
showOptionsPane: false,
infoViewState: undefined,
dropZoneFiles: undefined, // <-- FileList or something else?
stagedAttachments: [],
};
this.compositionBoxRef = React.createRef();
@ -129,6 +133,12 @@ export class SessionConversation extends React.Component<Props, State> {
this.renderLightBox = this.renderLightBox.bind(this);
// attachments
this.clearAttachments = this.clearAttachments.bind(this);
this.addAttachments = this.addAttachments.bind(this);
this.removeAttachment = this.removeAttachment.bind(this);
this.onChoseAttachments = this.onChoseAttachments.bind(this);
const conversationModel = window.ConversationController.getOrThrow(
this.state.conversationKey
);
@ -238,7 +248,7 @@ export class SessionConversation extends React.Component<Props, State> {
{!isRss && (
<SessionCompositionBox
sendMessage={sendMessageFn}
dropZoneFiles={this.state.dropZoneFiles}
stagedAttachments={this.state.stagedAttachments}
onMessageSending={this.onMessageSending}
onMessageSuccess={this.onMessageSuccess}
onMessageFailure={this.onMessageFailure}
@ -249,6 +259,9 @@ export class SessionConversation extends React.Component<Props, State> {
void this.replyToMessage(undefined);
}}
textarea={this.compositionBoxRef}
clearAttachments={this.clearAttachments}
removeAttachment={this.removeAttachment}
onChoseAttachments={this.onChoseAttachments}
/>
)}
</div>
@ -491,6 +504,7 @@ export class SessionConversation extends React.Component<Props, State> {
replyToMessage: this.replyToMessage,
doneInitialScroll: this.state.doneInitialScroll,
onClickAttachment: this.onClickAttachment,
handleFilesDropped: this.onChoseAttachments,
};
}
@ -846,6 +860,43 @@ export class SessionConversation extends React.Component<Props, State> {
// }
}
private clearAttachments() {
this.state.stagedAttachments.forEach(attachment => {
if (attachment.url) {
URL.revokeObjectURL(attachment.url);
}
if (attachment.videoUrl) {
URL.revokeObjectURL(attachment.videoUrl);
}
});
this.setState({ stagedAttachments: [] });
}
private removeAttachment(attachment: AttachmentType) {
const { stagedAttachments } = this.state;
const updatedStagedAttachments = (stagedAttachments || []).filter(
m => m.fileName !== attachment.fileName
);
this.setState({ stagedAttachments: updatedStagedAttachments });
}
private addAttachments(newAttachments: Array<StagedAttachmentType>) {
const { stagedAttachments } = this.state;
if (newAttachments?.length > 0) {
if (
newAttachments.some(a => a.isVoiceMessage) &&
stagedAttachments.length > 0
) {
throw new Error('A voice note cannot be sent with other attachments');
}
}
this.setState({
stagedAttachments: [...stagedAttachments, ...newAttachments],
});
}
private renderLightBox({
media,
attachment,
@ -891,4 +942,197 @@ export class SessionConversation extends React.Component<Props, State> {
timestamp: message?.received_at || Date.now(),
});
}
private async onChoseAttachments(attachmentsFileList: FileList) {
if (!attachmentsFileList || attachmentsFileList.length === 0) {
return;
}
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < attachmentsFileList.length; i++) {
await this.maybeAddAttachment(attachmentsFileList[i]);
}
}
// tslint:disable: max-func-body-length cyclomatic-complexity
private async maybeAddAttachment(file: any) {
if (!file) {
return;
}
const fileName = file.name;
const contentType = file.type;
const { stagedAttachments } = this.state;
if (window.Signal.Util.isFileDangerous(fileName)) {
ToastUtils.pushDangerousFileError();
return;
}
if (stagedAttachments.length >= 32) {
ToastUtils.pushMaximumAttachmentsError();
return;
}
const haveNonImage = _.some(
stagedAttachments,
attachment => !MIME.isImage(attachment.contentType)
);
// You can't add another attachment if you already have a non-image staged
if (haveNonImage) {
ToastUtils.pushMultipleNonImageError();
return;
}
// You can't add a non-image attachment if you already have attachments staged
if (!MIME.isImage(contentType) && stagedAttachments.length > 0) {
ToastUtils.pushCannotMixError();
return;
}
const { VisualAttachment } = window.Signal.Types;
const renderVideoPreview = async () => {
const objectUrl = URL.createObjectURL(file);
try {
const type = 'image/png';
const thumbnail = await VisualAttachment.makeVideoScreenshot({
objectUrl,
contentType: type,
logger: window.log,
});
const data = await VisualAttachment.blobToArrayBuffer(thumbnail);
const url = window.Signal.Util.arrayBufferToObjectURL({
data,
type,
});
this.addAttachments([
{
file,
size: file.size,
fileName,
contentType,
videoUrl: objectUrl,
url,
isVoiceMessage: false,
},
]);
} catch (error) {
URL.revokeObjectURL(objectUrl);
}
};
const renderImagePreview = async () => {
if (!MIME.isJPEG(contentType)) {
const urlImage = URL.createObjectURL(file);
if (!urlImage) {
throw new Error('Failed to create object url for image!');
}
this.addAttachments([
{
file,
size: file.size,
fileName,
contentType,
url: urlImage,
isVoiceMessage: false,
},
]);
return;
}
const url = await window.autoOrientImage(file);
this.addAttachments([
{
file,
size: file.size,
fileName,
contentType,
url,
isVoiceMessage: false,
},
]);
};
try {
const blob = await AttachmentUtil.autoScale({
contentType,
file,
});
let limitKb = 10000;
const blobType =
file.type === 'image/gif' ? 'gif' : contentType.split('/')[0];
switch (blobType) {
case 'image':
limitKb = 6000;
break;
case 'gif':
limitKb = 10000;
break;
case 'audio':
limitKb = 10000;
break;
case 'video':
limitKb = 10000;
break;
default:
limitKb = 10000;
}
// if ((blob.file.size / 1024).toFixed(4) >= limitKb) {
// const units = ['kB', 'MB', 'GB'];
// let u = -1;
// let limit = limitKb * 1000;
// do {
// limit /= 1000;
// u += 1;
// } while (limit >= 1000 && u < units.length - 1);
// // this.showFileSizeError(limit, units[u]);
// return;
// }
} catch (error) {
window.log.error(
'Error ensuring that image is properly sized:',
error && error.stack ? error.stack : error
);
ToastUtils.pushLoadAttachmentFailure();
return;
}
try {
if (GoogleChrome.isImageTypeSupported(contentType)) {
await renderImagePreview();
} else if (GoogleChrome.isVideoTypeSupported(contentType)) {
await renderVideoPreview();
} else {
this.addAttachments([
{
file,
size: file.size,
contentType,
fileName,
url: '',
isVoiceMessage: false,
},
]);
}
} catch (e) {
window.log.error(
`Was unable to generate thumbnail for file type ${contentType}`,
e && e.stack ? e.stack : e
);
this.addAttachments([
{
file,
size: file.size,
contentType,
fileName,
isVoiceMessage: false,
url: '',
},
]);
}
}
}

@ -8,6 +8,7 @@ import { ResetSessionNotification } from '../../conversation/ResetSessionNotific
import { Constants } from '../../../session';
import _ from 'lodash';
import { ConversationModel } from '../../../../js/models/conversations';
import { SessionFileDropzone } from './SessionFileDropzone';
interface State {
isScrolledToBottom: boolean;
@ -30,6 +31,7 @@ interface Props {
) => Promise<{ previousTopMessage: string }>;
replyToMessage: (messageId: number) => Promise<void>;
onClickAttachment: (attachment: any, message: any) => void;
handleFilesDropped: (droppedFiles: FileList) => void;
}
export class SessionConversationMessagesList extends React.Component<
@ -111,6 +113,10 @@ export class SessionConversationMessagesList extends React.Component<
show={showScrollButton}
onClick={this.scrollToBottom}
/>
<SessionFileDropzone
handleDrop={this.props.handleFilesDropped}
handleWheel={this.handleScroll}
/>
</>
);
}

@ -0,0 +1,119 @@
import React, { Component } from 'react';
import { Flex } from '../Flex';
import { SessionIcon, SessionIconSize, SessionIconType } from '../icon';
interface Props {
handleDrop: (files: FileList) => void;
}
interface State {
dragging: boolean;
}
export class SessionFileDropzone extends Component<Props, State> {
private readonly dropRef: React.RefObject<any>;
private dragCounter: number;
constructor(props: any) {
super(props);
this.state = {
dragging: false,
};
this.dragCounter = 0;
this.dropRef = React.createRef();
}
public handleDrag = (e: any) => {
e.preventDefault();
e.stopPropagation();
};
public handleDragIn = (e: any) => {
e.preventDefault();
e.stopPropagation();
this.dragCounter++;
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
this.setState({ dragging: true });
}
};
public handleDragOut = (e: any) => {
e.preventDefault();
e.stopPropagation();
this.dragCounter--;
if (this.dragCounter === 0) {
this.setState({ dragging: false });
}
};
public handleDrop = (e: any) => {
e.preventDefault();
e.stopPropagation();
this.setState({ dragging: false });
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
this.props.handleDrop(e.dataTransfer.files);
e.dataTransfer.clearData();
this.dragCounter = 0;
}
};
public componentDidMount() {
const div = this.dropRef.current;
div.addEventListener('dragenter', this.handleDragIn);
div.addEventListener('dragleave', this.handleDragOut);
div.addEventListener('dragover', this.handleDrag);
div.addEventListener('drop', this.handleDrop);
}
public componentWillUnmount() {
const div = this.dropRef.current;
div.removeEventListener('dragenter', this.handleDragIn);
div.removeEventListener('dragleave', this.handleDragOut);
div.removeEventListener('dragover', this.handleDrag);
div.removeEventListener('drop', this.handleDrop);
}
public render() {
return (
<div
style={{
display: 'inline-block',
position: 'absolute',
width: '100%',
height: '100%',
}}
ref={this.dropRef}
>
<div
style={{
border: 'dashed grey 4px',
backgroundColor: 'rgba(255,255,255,0.5)',
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
right: 0,
zIndex: 100,
opacity: this.state.dragging ? 1.0 : 0,
transition: '0.25s',
}}
>
<Flex
container={true}
justifyContent="space-around"
height="100%"
alignItems="center"
>
<SessionIcon
iconSize={SessionIconSize.Max}
iconType={SessionIconType.CirclePlus}
/>
</Flex>
</div>
{this.props.children}
</div>
);
}
}

@ -0,0 +1,108 @@
import { StagedAttachmentType } from '../components/session/conversation/SessionCompositionBox';
import { SignalService } from '../protobuf';
export async function autoScale<T extends { contentType: string; file: any }>(
attachment: T
): Promise<T> {
const { contentType, file } = attachment;
if (contentType.split('/')[0] !== 'image' || contentType === 'image/tiff') {
// nothing to do
return Promise.resolve(attachment);
}
return new Promise((resolve, reject) => {
const url = URL.createObjectURL(file);
const img = document.createElement('img');
img.onerror = reject;
img.onload = () => {
URL.revokeObjectURL(url);
const maxSize = 6000 * 1024;
const maxHeight = 4096;
const maxWidth = 4096;
if (
img.naturalWidth <= maxWidth &&
img.naturalHeight <= maxHeight &&
file.size <= maxSize
) {
resolve(attachment);
return;
}
const gifMaxSize = 25000 * 1024;
if (file.type === 'image/gif' && file.size <= gifMaxSize) {
resolve(attachment);
return;
}
if (file.type === 'image/gif') {
reject(new Error('GIF is too large'));
return;
}
const canvas = window.loadImage.scale(img, {
canvas: true,
maxWidth,
maxHeight,
});
let quality = 0.95;
let i = 4;
let blob;
do {
i -= 1;
blob = window.dataURLToBlobSync(
canvas.toDataURL('image/jpeg', quality)
);
quality = (quality * maxSize) / blob.size;
// NOTE: During testing with a large image, we observed the
// `quality` value being > 1. Should we clamp it to [0.5, 1.0]?
// See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Syntax
if (quality < 0.5) {
quality = 0.5;
}
} while (i > 0 && blob.size > maxSize);
resolve({
...attachment,
file: blob,
});
};
img.src = url;
});
}
export async function getFile(attachment: StagedAttachmentType) {
if (!attachment) {
return Promise.resolve();
}
const attachmentFlags = attachment.isVoiceMessage
? SignalService.AttachmentPointer.Flags.VOICE_MESSAGE
: null;
const scaled = await autoScale(attachment);
const fileRead = await readFile(scaled);
return {
...fileRead,
url: undefined,
flags: attachmentFlags || null,
};
}
async function readFile(attachment: any): Promise<object> {
return new Promise((resolve, reject) => {
const FR = new FileReader();
FR.onload = e => {
const data = e?.target?.result as ArrayBuffer;
resolve({
...attachment,
data,
size: data.byteLength,
});
};
FR.onerror = reject;
FR.onabort = reject;
FR.readAsArrayBuffer(attachment.file);
});
}

@ -7,6 +7,7 @@ import { makeLookup } from './makeLookup';
import { FindMember } from './findMember';
import * as UserUtil from './user';
import * as PasswordUtil from './passwordUtils';
import * as AttachmentUtil from './attachmentsUtil';
export * from './blockedNumberController';
@ -20,4 +21,5 @@ export {
UserUtil,
PasswordUtil,
FindMember,
AttachmentUtil,
};

@ -1240,11 +1240,6 @@ atob@^2.1.2:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
attr-accept@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.1.0.tgz#a231a854385d36ff7a99647bb77b33c8a5175aee"
integrity sha512-sLzVM3zCCmmDtDNhI0i96k6PUztkotSOXqE4kDGQt/6iDi5M+H0srjeF+QC6jN581l4X/Zq3Zu/tgcErEssavg==
autoprefixer@^6.3.1:
version "6.7.7"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-6.7.7.tgz#1dbd1c835658e35ce3f9984099db00585c782014"
@ -4072,13 +4067,6 @@ file-entry-cache@^2.0.0:
flat-cache "^1.2.1"
object-assign "^4.0.1"
file-selector@^0.1.12:
version "0.1.12"
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.1.12.tgz#fe726547be219a787a9dcc640575a04a032b1fd0"
integrity sha512-Kx7RTzxyQipHuiqyZGf+Nz4vY9R1XGxuQl/hLoJwq+J4avk/9wxxgZyHKtbyIPJmbD4A66DWGYfyykWNpcYutQ==
dependencies:
tslib "^1.9.0"
file-sync-cmp@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz#a5e7a8ffbfa493b43b923bbd4ca89a53b63b612b"
@ -8716,15 +8704,6 @@ react-dom@16.8.3:
prop-types "^15.6.2"
scheduler "^0.13.3"
react-dropzone@^11.0.2:
version "11.0.2"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.0.2.tgz#1a4084f520c2eafbeb24026760b3ee8f3759cfd3"
integrity sha512-/Wde9Il1aJ1FtWllg3N2taIeJh4aftx6UGUG8R1TmLnZit2RnDcEjcKwEEbKwgLXTTh8QQpiZWQJq45jTy1jCA==
dependencies:
attr-accept "^2.0.0"
file-selector "^0.1.12"
prop-types "^15.7.2"
react-emoji-render@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/react-emoji-render/-/react-emoji-render-1.2.4.tgz#fa3542a692e1eed3236f0f12b8e3a61b2818e2c2"
@ -10751,11 +10730,6 @@ tslib@^1.8.0, tslib@^1.8.1:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
tslib@^1.9.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
tslint-microsoft-contrib@6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/tslint-microsoft-contrib/-/tslint-microsoft-contrib-6.0.0.tgz#7bff73c9ad7a0b7eb5cdb04906de58f42a2bf7a2"

Loading…
Cancel
Save