412 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
			
		
		
	
	
			412 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
/*
 | 
						|
Copyright 2015, 2016 OpenMarket Ltd
 | 
						|
 | 
						|
Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
you may not use this file except in compliance with the License.
 | 
						|
You may obtain a copy of the License at
 | 
						|
 | 
						|
    http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
 | 
						|
Unless required by applicable law or agreed to in writing, software
 | 
						|
distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
See the License for the specific language governing permissions and
 | 
						|
limitations under the License.
 | 
						|
*/
 | 
						|
 | 
						|
'use strict';
 | 
						|
 | 
						|
import Promise from 'bluebird';
 | 
						|
const extend = require('./extend');
 | 
						|
const dis = require('./dispatcher');
 | 
						|
const MatrixClientPeg = require('./MatrixClientPeg');
 | 
						|
const sdk = require('./index');
 | 
						|
import { _t } from './languageHandler';
 | 
						|
const Modal = require('./Modal');
 | 
						|
 | 
						|
const encrypt = require("browser-encrypt-attachment");
 | 
						|
 | 
						|
// Polyfill for Canvas.toBlob API using Canvas.toDataURL
 | 
						|
require("blueimp-canvas-to-blob");
 | 
						|
 | 
						|
const MAX_WIDTH = 800;
 | 
						|
const MAX_HEIGHT = 600;
 | 
						|
 | 
						|
 | 
						|
/**
 | 
						|
 * Create a thumbnail for a image DOM element.
 | 
						|
 * The image will be smaller than MAX_WIDTH and MAX_HEIGHT.
 | 
						|
 * The thumbnail will have the same aspect ratio as the original.
 | 
						|
 * Draws the element into a canvas using CanvasRenderingContext2D.drawImage
 | 
						|
 * Then calls Canvas.toBlob to get a blob object for the image data.
 | 
						|
 *
 | 
						|
 * Since it needs to calculate the dimensions of the source image and the
 | 
						|
 * thumbnailed image it returns an info object filled out with information
 | 
						|
 * about the original image and the thumbnail.
 | 
						|
 *
 | 
						|
 * @param {HTMLElement} element The element to thumbnail.
 | 
						|
 * @param {integer} inputWidth The width of the image in the input element.
 | 
						|
 * @param {integer} inputHeight the width of the image in the input element.
 | 
						|
 * @param {String} mimeType The mimeType to save the blob as.
 | 
						|
 * @return {Promise} A promise that resolves with an object with an info key
 | 
						|
 *  and a thumbnail key.
 | 
						|
 */
 | 
						|
function createThumbnail(element, inputWidth, inputHeight, mimeType) {
 | 
						|
    const deferred = Promise.defer();
 | 
						|
 | 
						|
    let targetWidth = inputWidth;
 | 
						|
    let targetHeight = inputHeight;
 | 
						|
    if (targetHeight > MAX_HEIGHT) {
 | 
						|
        targetWidth = Math.floor(targetWidth * (MAX_HEIGHT / targetHeight));
 | 
						|
        targetHeight = MAX_HEIGHT;
 | 
						|
    }
 | 
						|
    if (targetWidth > MAX_WIDTH) {
 | 
						|
        targetHeight = Math.floor(targetHeight * (MAX_WIDTH / targetWidth));
 | 
						|
        targetWidth = MAX_WIDTH;
 | 
						|
    }
 | 
						|
 | 
						|
    const canvas = document.createElement("canvas");
 | 
						|
    canvas.width = targetWidth;
 | 
						|
    canvas.height = targetHeight;
 | 
						|
    canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight);
 | 
						|
    canvas.toBlob(function(thumbnail) {
 | 
						|
        deferred.resolve({
 | 
						|
            info: {
 | 
						|
                thumbnail_info: {
 | 
						|
                    w: targetWidth,
 | 
						|
                    h: targetHeight,
 | 
						|
                    mimetype: thumbnail.type,
 | 
						|
                    size: thumbnail.size,
 | 
						|
                },
 | 
						|
                w: inputWidth,
 | 
						|
                h: inputHeight,
 | 
						|
            },
 | 
						|
            thumbnail: thumbnail,
 | 
						|
        });
 | 
						|
    }, mimeType);
 | 
						|
 | 
						|
    return deferred.promise;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Load a file into a newly created image element.
 | 
						|
 *
 | 
						|
 * @param {File} file The file to load in an image element.
 | 
						|
 * @return {Promise} A promise that resolves with the html image element.
 | 
						|
 */
 | 
						|
function loadImageElement(imageFile) {
 | 
						|
    const deferred = Promise.defer();
 | 
						|
 | 
						|
    // Load the file into an html element
 | 
						|
    const img = document.createElement("img");
 | 
						|
    const objectUrl = URL.createObjectURL(imageFile);
 | 
						|
    img.src = objectUrl;
 | 
						|
 | 
						|
    // Once ready, create a thumbnail
 | 
						|
    img.onload = function() {
 | 
						|
        URL.revokeObjectURL(objectUrl);
 | 
						|
        deferred.resolve(img);
 | 
						|
    };
 | 
						|
    img.onerror = function(e) {
 | 
						|
        deferred.reject(e);
 | 
						|
    };
 | 
						|
 | 
						|
    return deferred.promise;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Read the metadata for an image file and create and upload a thumbnail of the image.
 | 
						|
 *
 | 
						|
 * @param {MatrixClient} matrixClient A matrixClient to upload the thumbnail with.
 | 
						|
 * @param {String} roomId The ID of the room the image will be uploaded in.
 | 
						|
 * @param {File} The image to read and thumbnail.
 | 
						|
 * @return {Promise} A promise that resolves with the attachment info.
 | 
						|
 */
 | 
						|
function infoForImageFile(matrixClient, roomId, imageFile) {
 | 
						|
    let thumbnailType = "image/png";
 | 
						|
    if (imageFile.type == "image/jpeg") {
 | 
						|
        thumbnailType = "image/jpeg";
 | 
						|
    }
 | 
						|
 | 
						|
    let imageInfo;
 | 
						|
    return loadImageElement(imageFile).then(function(img) {
 | 
						|
        return createThumbnail(img, img.width, img.height, thumbnailType);
 | 
						|
    }).then(function(result) {
 | 
						|
        imageInfo = result.info;
 | 
						|
        return uploadFile(matrixClient, roomId, result.thumbnail);
 | 
						|
    }).then(function(result) {
 | 
						|
        imageInfo.thumbnail_url = result.url;
 | 
						|
        imageInfo.thumbnail_file = result.file;
 | 
						|
        return imageInfo;
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Load a file into a newly created video element.
 | 
						|
 *
 | 
						|
 * @param {File} file The file to load in an video element.
 | 
						|
 * @return {Promise} A promise that resolves with the video image element.
 | 
						|
 */
 | 
						|
function loadVideoElement(videoFile) {
 | 
						|
    const deferred = Promise.defer();
 | 
						|
 | 
						|
    // Load the file into an html element
 | 
						|
    const video = document.createElement("video");
 | 
						|
 | 
						|
    const reader = new FileReader();
 | 
						|
    reader.onload = function(e) {
 | 
						|
        video.src = e.target.result;
 | 
						|
 | 
						|
        // Once ready, returns its size
 | 
						|
        // Wait until we have enough data to thumbnail the first frame.
 | 
						|
        video.onloadeddata = function() {
 | 
						|
            deferred.resolve(video);
 | 
						|
        };
 | 
						|
        video.onerror = function(e) {
 | 
						|
            deferred.reject(e);
 | 
						|
        };
 | 
						|
    };
 | 
						|
    reader.onerror = function(e) {
 | 
						|
        deferred.reject(e);
 | 
						|
    };
 | 
						|
    reader.readAsDataURL(videoFile);
 | 
						|
 | 
						|
    return deferred.promise;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Read the metadata for a video file and create and upload a thumbnail of the video.
 | 
						|
 *
 | 
						|
 * @param {MatrixClient} matrixClient A matrixClient to upload the thumbnail with.
 | 
						|
 * @param {String} roomId The ID of the room the video will be uploaded to.
 | 
						|
 * @param {File} The video to read and thumbnail.
 | 
						|
 * @return {Promise} A promise that resolves with the attachment info.
 | 
						|
 */
 | 
						|
function infoForVideoFile(matrixClient, roomId, videoFile) {
 | 
						|
    const thumbnailType = "image/jpeg";
 | 
						|
 | 
						|
    let videoInfo;
 | 
						|
    return loadVideoElement(videoFile).then(function(video) {
 | 
						|
        return createThumbnail(video, video.videoWidth, video.videoHeight, thumbnailType);
 | 
						|
    }).then(function(result) {
 | 
						|
        videoInfo = result.info;
 | 
						|
        return uploadFile(matrixClient, roomId, result.thumbnail);
 | 
						|
    }).then(function(result) {
 | 
						|
        videoInfo.thumbnail_url = result.url;
 | 
						|
        videoInfo.thumbnail_file = result.file;
 | 
						|
        return videoInfo;
 | 
						|
    });
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Read the file as an ArrayBuffer.
 | 
						|
 * @return {Promise} A promise that resolves with an ArrayBuffer when the file
 | 
						|
 *   is read.
 | 
						|
 */
 | 
						|
function readFileAsArrayBuffer(file) {
 | 
						|
    const deferred = Promise.defer();
 | 
						|
    const reader = new FileReader();
 | 
						|
    reader.onload = function(e) {
 | 
						|
        deferred.resolve(e.target.result);
 | 
						|
    };
 | 
						|
    reader.onerror = function(e) {
 | 
						|
        deferred.reject(e);
 | 
						|
    };
 | 
						|
    reader.readAsArrayBuffer(file);
 | 
						|
    return deferred.promise;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Upload the file to the content repository.
 | 
						|
 * If the room is encrypted then encrypt the file before uploading.
 | 
						|
 *
 | 
						|
 * @param {MatrixClient} matrixClient The matrix client to upload the file with.
 | 
						|
 * @param {String} roomId The ID of the room being uploaded to.
 | 
						|
 * @param {File} file The file to upload.
 | 
						|
 * @param {Function?} progressHandler optional callback to be called when a chunk of
 | 
						|
 *    data is uploaded.
 | 
						|
 * @return {Promise} A promise that resolves with an object.
 | 
						|
 *  If the file is unencrypted then the object will have a "url" key.
 | 
						|
 *  If the file is encrypted then the object will have a "file" key.
 | 
						|
 */
 | 
						|
function uploadFile(matrixClient, roomId, file, progressHandler) {
 | 
						|
    if (matrixClient.isRoomEncrypted(roomId)) {
 | 
						|
        // If the room is encrypted then encrypt the file before uploading it.
 | 
						|
        // First read the file into memory.
 | 
						|
        return readFileAsArrayBuffer(file).then(function(data) {
 | 
						|
            // Then encrypt the file.
 | 
						|
            return encrypt.encryptAttachment(data);
 | 
						|
        }).then(function(encryptResult) {
 | 
						|
            // Record the information needed to decrypt the attachment.
 | 
						|
            const encryptInfo = encryptResult.info;
 | 
						|
            // Pass the encrypted data as a Blob to the uploader.
 | 
						|
            const blob = new Blob([encryptResult.data]);
 | 
						|
            return matrixClient.uploadContent(blob, {
 | 
						|
                progressHandler: progressHandler,
 | 
						|
            }).then(function(url) {
 | 
						|
                // If the attachment is encrypted then bundle the URL along
 | 
						|
                // with the information needed to decrypt the attachment and
 | 
						|
                // add it under a file key.
 | 
						|
                encryptInfo.url = url;
 | 
						|
                if (file.type) {
 | 
						|
                    encryptInfo.mimetype = file.type;
 | 
						|
                }
 | 
						|
                return {"file": encryptInfo};
 | 
						|
            });
 | 
						|
        });
 | 
						|
    } else {
 | 
						|
        const basePromise = matrixClient.uploadContent(file, {
 | 
						|
            progressHandler: progressHandler,
 | 
						|
        });
 | 
						|
        const promise1 = basePromise.then(function(url) {
 | 
						|
            // If the attachment isn't encrypted then include the URL directly.
 | 
						|
            return {"url": url};
 | 
						|
        });
 | 
						|
        // XXX: copy over the abort method to the new promise
 | 
						|
        promise1.abort = basePromise.abort;
 | 
						|
        return promise1;
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
class ContentMessages {
 | 
						|
    constructor() {
 | 
						|
        this.inprogress = [];
 | 
						|
        this.nextId = 0;
 | 
						|
    }
 | 
						|
 | 
						|
    sendStickerContentToRoom(url, roomId, info, text, matrixClient) {
 | 
						|
        return MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => {
 | 
						|
            console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
 | 
						|
            throw e;
 | 
						|
        });
 | 
						|
    }
 | 
						|
 | 
						|
    sendContentToRoom(file, roomId, matrixClient) {
 | 
						|
        const content = {
 | 
						|
            body: file.name || 'Attachment',
 | 
						|
            info: {
 | 
						|
                size: file.size,
 | 
						|
            },
 | 
						|
        };
 | 
						|
 | 
						|
        // if we have a mime type for the file, add it to the message metadata
 | 
						|
        if (file.type) {
 | 
						|
            content.info.mimetype = file.type;
 | 
						|
        }
 | 
						|
 | 
						|
        const def = Promise.defer();
 | 
						|
        if (file.type.indexOf('image/') == 0) {
 | 
						|
            content.msgtype = 'm.image';
 | 
						|
            infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{
 | 
						|
                extend(content.info, imageInfo);
 | 
						|
                def.resolve();
 | 
						|
            }, (error)=>{
 | 
						|
                console.error(error);
 | 
						|
                content.msgtype = 'm.file';
 | 
						|
                def.resolve();
 | 
						|
            });
 | 
						|
        } else if (file.type.indexOf('audio/') == 0) {
 | 
						|
            content.msgtype = 'm.audio';
 | 
						|
            def.resolve();
 | 
						|
        } else if (file.type.indexOf('video/') == 0) {
 | 
						|
            content.msgtype = 'm.video';
 | 
						|
            infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{
 | 
						|
                extend(content.info, videoInfo);
 | 
						|
                def.resolve();
 | 
						|
            }, (error)=>{
 | 
						|
                content.msgtype = 'm.file';
 | 
						|
                def.resolve();
 | 
						|
            });
 | 
						|
        } else {
 | 
						|
            content.msgtype = 'm.file';
 | 
						|
            def.resolve();
 | 
						|
        }
 | 
						|
 | 
						|
        const upload = {
 | 
						|
            fileName: file.name || 'Attachment',
 | 
						|
            roomId: roomId,
 | 
						|
            total: 0,
 | 
						|
            loaded: 0,
 | 
						|
        };
 | 
						|
        this.inprogress.push(upload);
 | 
						|
        dis.dispatch({action: 'upload_started'});
 | 
						|
 | 
						|
        let error;
 | 
						|
 | 
						|
        function onProgress(ev) {
 | 
						|
            upload.total = ev.total;
 | 
						|
            upload.loaded = ev.loaded;
 | 
						|
            dis.dispatch({action: 'upload_progress', upload: upload});
 | 
						|
        }
 | 
						|
 | 
						|
        return def.promise.then(function() {
 | 
						|
            // XXX: upload.promise must be the promise that
 | 
						|
            // is returned by uploadFile as it has an abort()
 | 
						|
            // method hacked onto it.
 | 
						|
            upload.promise = uploadFile(
 | 
						|
                matrixClient, roomId, file, onProgress,
 | 
						|
            );
 | 
						|
            return upload.promise.then(function(result) {
 | 
						|
                content.file = result.file;
 | 
						|
                content.url = result.url;
 | 
						|
            });
 | 
						|
        }).then(function(url) {
 | 
						|
            return matrixClient.sendMessage(roomId, content);
 | 
						|
        }, function(err) {
 | 
						|
            error = err;
 | 
						|
            if (!upload.canceled) {
 | 
						|
                let desc = _t('The file \'%(fileName)s\' failed to upload', {fileName: upload.fileName}) + '.';
 | 
						|
                if (err.http_status == 413) {
 | 
						|
                    desc = _t('The file \'%(fileName)s\' exceeds this home server\'s size limit for uploads', {fileName: upload.fileName});
 | 
						|
                }
 | 
						|
                const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
 | 
						|
                Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
 | 
						|
                    title: _t('Upload Failed'),
 | 
						|
                    description: desc,
 | 
						|
                });
 | 
						|
            }
 | 
						|
        }).finally(() => {
 | 
						|
            const inprogressKeys = Object.keys(this.inprogress);
 | 
						|
            for (let i = 0; i < this.inprogress.length; ++i) {
 | 
						|
                const k = inprogressKeys[i];
 | 
						|
                if (this.inprogress[k].promise === upload.promise) {
 | 
						|
                    this.inprogress.splice(k, 1);
 | 
						|
                    break;
 | 
						|
                }
 | 
						|
            }
 | 
						|
            if (error) {
 | 
						|
                dis.dispatch({action: 'upload_failed', upload: upload});
 | 
						|
            } else {
 | 
						|
                dis.dispatch({action: 'upload_finished', upload: upload});
 | 
						|
            }
 | 
						|
        });
 | 
						|
    }
 | 
						|
 | 
						|
    getCurrentUploads() {
 | 
						|
        return this.inprogress;
 | 
						|
    }
 | 
						|
 | 
						|
    cancelUpload(promise) {
 | 
						|
        const inprogressKeys = Object.keys(this.inprogress);
 | 
						|
        let upload;
 | 
						|
        for (let i = 0; i < this.inprogress.length; ++i) {
 | 
						|
            const k = inprogressKeys[i];
 | 
						|
            if (this.inprogress[k].promise === promise) {
 | 
						|
                upload = this.inprogress[k];
 | 
						|
                break;
 | 
						|
            }
 | 
						|
        }
 | 
						|
        if (upload) {
 | 
						|
            upload.canceled = true;
 | 
						|
            MatrixClientPeg.get().cancelUpload(upload.promise);
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
if (global.mx_ContentMessage === undefined) {
 | 
						|
    global.mx_ContentMessage = new ContentMessages();
 | 
						|
}
 | 
						|
 | 
						|
module.exports = global.mx_ContentMessage;
 |