2015-07-08 21:34:26 +08:00
|
|
|
/*
|
2016-01-07 12:06:39 +08:00
|
|
|
Copyright 2015, 2016 OpenMarket Ltd
|
2015-07-08 21:34:26 +08:00
|
|
|
|
|
|
|
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';
|
|
|
|
|
2017-07-12 20:58:14 +08:00
|
|
|
import Promise from 'bluebird';
|
2017-10-12 00:56:17 +08:00
|
|
|
const extend = require('./extend');
|
|
|
|
const dis = require('./dispatcher');
|
|
|
|
const MatrixClientPeg = require('./MatrixClientPeg');
|
|
|
|
const sdk = require('./index');
|
2017-05-25 18:39:08 +08:00
|
|
|
import { _t } from './languageHandler';
|
2017-10-12 00:56:17 +08:00
|
|
|
const Modal = require('./Modal');
|
2015-07-08 21:34:26 +08:00
|
|
|
|
2017-10-12 00:56:17 +08:00
|
|
|
const encrypt = require("browser-encrypt-attachment");
|
2016-11-12 20:20:36 +08:00
|
|
|
|
2016-11-15 19:22:39 +08:00
|
|
|
// 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) {
|
2017-07-12 21:04:20 +08:00
|
|
|
const deferred = Promise.defer();
|
2016-11-15 19:22:39 +08:00
|
|
|
|
2017-10-12 00:56:17 +08:00
|
|
|
let targetWidth = inputWidth;
|
|
|
|
let targetHeight = inputHeight;
|
2016-11-15 19:22:39 +08:00
|
|
|
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,
|
|
|
|
},
|
2017-10-12 00:56:17 +08:00
|
|
|
thumbnail: thumbnail,
|
2016-11-15 19:22:39 +08:00
|
|
|
});
|
|
|
|
}, 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) {
|
2017-07-12 21:04:20 +08:00
|
|
|
const deferred = Promise.defer();
|
2015-07-08 21:34:26 +08:00
|
|
|
|
|
|
|
// Load the file into an html element
|
2016-11-15 19:22:39 +08:00
|
|
|
const img = document.createElement("img");
|
2017-10-20 00:16:52 +08:00
|
|
|
const objectUrl = URL.createObjectURL(imageFile);
|
|
|
|
img.src = objectUrl;
|
2015-07-08 21:34:26 +08:00
|
|
|
|
2017-10-20 00:16:52 +08:00
|
|
|
// Once ready, create a thumbnail
|
|
|
|
img.onload = function() {
|
|
|
|
URL.revokeObjectURL(objectUrl);
|
|
|
|
deferred.resolve(img);
|
2015-07-08 21:34:26 +08:00
|
|
|
};
|
2017-10-20 00:16:52 +08:00
|
|
|
img.onerror = function(e) {
|
2015-07-08 21:34:26 +08:00
|
|
|
deferred.reject(e);
|
|
|
|
};
|
|
|
|
|
|
|
|
return deferred.promise;
|
|
|
|
}
|
|
|
|
|
2016-11-15 19:22:39 +08:00
|
|
|
/**
|
|
|
|
* 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) {
|
2017-10-12 00:56:17 +08:00
|
|
|
let thumbnailType = "image/png";
|
2016-11-15 19:22:39 +08:00
|
|
|
if (imageFile.type == "image/jpeg") {
|
|
|
|
thumbnailType = "image/jpeg";
|
|
|
|
}
|
|
|
|
|
2017-10-12 00:56:17 +08:00
|
|
|
let imageInfo;
|
2016-11-15 19:22:39 +08:00
|
|
|
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) {
|
2017-07-12 21:04:20 +08:00
|
|
|
const deferred = Promise.defer();
|
2016-07-19 23:05:15 +08:00
|
|
|
|
|
|
|
// Load the file into an html element
|
2016-11-15 19:22:39 +08:00
|
|
|
const video = document.createElement("video");
|
2016-07-19 23:05:15 +08:00
|
|
|
|
2016-11-15 19:22:39 +08:00
|
|
|
const reader = new FileReader();
|
2016-07-19 23:05:15 +08:00
|
|
|
reader.onload = function(e) {
|
|
|
|
video.src = e.target.result;
|
|
|
|
|
|
|
|
// Once ready, returns its size
|
2016-11-15 19:22:39 +08:00
|
|
|
// Wait until we have enough data to thumbnail the first frame.
|
|
|
|
video.onloadeddata = function() {
|
|
|
|
deferred.resolve(video);
|
2016-07-19 23:05:15 +08:00
|
|
|
};
|
|
|
|
video.onerror = function(e) {
|
|
|
|
deferred.reject(e);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
reader.onerror = function(e) {
|
|
|
|
deferred.reject(e);
|
|
|
|
};
|
|
|
|
reader.readAsDataURL(videoFile);
|
|
|
|
|
|
|
|
return deferred.promise;
|
|
|
|
}
|
|
|
|
|
2016-11-15 19:22:39 +08:00
|
|
|
/**
|
|
|
|
* 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";
|
|
|
|
|
2017-10-12 00:56:17 +08:00
|
|
|
let videoInfo;
|
2016-11-15 19:22:39 +08:00
|
|
|
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;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-11-08 19:42:20 +08:00
|
|
|
/**
|
|
|
|
* Read the file as an ArrayBuffer.
|
|
|
|
* @return {Promise} A promise that resolves with an ArrayBuffer when the file
|
|
|
|
* is read.
|
|
|
|
*/
|
|
|
|
function readFileAsArrayBuffer(file) {
|
2017-07-12 21:04:20 +08:00
|
|
|
const deferred = Promise.defer();
|
2016-11-08 19:42:20 +08:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2016-11-15 19:22:39 +08:00
|
|
|
/**
|
|
|
|
* 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.
|
2017-07-15 00:01:03 +08:00
|
|
|
* @param {Function?} progressHandler optional callback to be called when a chunk of
|
|
|
|
* data is uploaded.
|
2016-11-15 19:22:39 +08:00
|
|
|
* @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.
|
|
|
|
*/
|
2017-07-15 00:01:03 +08:00
|
|
|
function uploadFile(matrixClient, roomId, file, progressHandler) {
|
2016-11-15 19:22:39 +08:00
|
|
|
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]);
|
2017-07-15 00:01:03 +08:00
|
|
|
return matrixClient.uploadContent(blob, {
|
|
|
|
progressHandler: progressHandler,
|
|
|
|
}).then(function(url) {
|
2016-11-15 19:22:39 +08:00
|
|
|
// 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 {
|
2017-07-15 00:01:03 +08:00
|
|
|
const basePromise = matrixClient.uploadContent(file, {
|
|
|
|
progressHandler: progressHandler,
|
|
|
|
});
|
2016-12-17 01:11:28 +08:00
|
|
|
const promise1 = basePromise.then(function(url) {
|
2016-11-15 19:22:39 +08:00
|
|
|
// If the attachment isn't encrypted then include the URL directly.
|
|
|
|
return {"url": url};
|
|
|
|
});
|
2016-12-17 01:11:28 +08:00
|
|
|
// XXX: copy over the abort method to the new promise
|
|
|
|
promise1.abort = basePromise.abort;
|
|
|
|
return promise1;
|
2016-11-15 19:22:39 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-07-19 23:05:15 +08:00
|
|
|
|
2015-12-03 02:16:16 +08:00
|
|
|
class ContentMessages {
|
|
|
|
constructor() {
|
|
|
|
this.inprogress = [];
|
|
|
|
this.nextId = 0;
|
2015-10-03 01:37:15 +08:00
|
|
|
}
|
|
|
|
|
2018-01-04 17:53:26 +08:00
|
|
|
sendURLContentToRoom(url, roomId, info, text, matrixClient) {
|
|
|
|
return MatrixClientPeg.get().sendImageMessage(roomId, url, info, text).catch((e) => {
|
|
|
|
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2015-12-03 02:16:16 +08:00
|
|
|
sendContentToRoom(file, roomId, matrixClient) {
|
2016-11-15 19:22:39 +08:00
|
|
|
const content = {
|
2017-01-08 09:20:59 +08:00
|
|
|
body: file.name || 'Attachment',
|
2015-12-03 02:16:16 +08:00
|
|
|
info: {
|
|
|
|
size: file.size,
|
2017-10-12 00:56:17 +08:00
|
|
|
},
|
2015-12-03 02:16:16 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
// if we have a mime type for the file, add it to the message metadata
|
|
|
|
if (file.type) {
|
|
|
|
content.info.mimetype = file.type;
|
|
|
|
}
|
|
|
|
|
2017-07-12 21:04:20 +08:00
|
|
|
const def = Promise.defer();
|
2015-12-03 02:16:16 +08:00
|
|
|
if (file.type.indexOf('image/') == 0) {
|
|
|
|
content.msgtype = 'm.image';
|
2017-10-12 00:56:17 +08:00
|
|
|
infoForImageFile(matrixClient, roomId, file).then((imageInfo)=>{
|
2015-12-03 02:16:16 +08:00
|
|
|
extend(content.info, imageInfo);
|
|
|
|
def.resolve();
|
2017-10-12 00:56:17 +08:00
|
|
|
}, (error)=>{
|
2016-11-15 19:22:39 +08:00
|
|
|
console.error(error);
|
2016-08-03 20:27:06 +08:00
|
|
|
content.msgtype = 'm.file';
|
|
|
|
def.resolve();
|
2015-12-03 02:16:16 +08:00
|
|
|
});
|
2016-04-13 07:00:24 +08:00
|
|
|
} else if (file.type.indexOf('audio/') == 0) {
|
|
|
|
content.msgtype = 'm.audio';
|
|
|
|
def.resolve();
|
2016-07-19 23:05:15 +08:00
|
|
|
} else if (file.type.indexOf('video/') == 0) {
|
2016-08-03 20:27:06 +08:00
|
|
|
content.msgtype = 'm.video';
|
2017-10-12 00:56:17 +08:00
|
|
|
infoForVideoFile(matrixClient, roomId, file).then((videoInfo)=>{
|
2016-08-03 20:27:06 +08:00
|
|
|
extend(content.info, videoInfo);
|
|
|
|
def.resolve();
|
2017-10-12 00:56:17 +08:00
|
|
|
}, (error)=>{
|
2016-08-03 20:27:06 +08:00
|
|
|
content.msgtype = 'm.file';
|
|
|
|
def.resolve();
|
|
|
|
});
|
2015-12-03 02:16:16 +08:00
|
|
|
} else {
|
|
|
|
content.msgtype = 'm.file';
|
2015-07-08 21:34:26 +08:00
|
|
|
def.resolve();
|
2015-12-03 02:16:16 +08:00
|
|
|
}
|
|
|
|
|
2016-11-15 19:22:39 +08:00
|
|
|
const upload = {
|
2017-01-08 09:20:59 +08:00
|
|
|
fileName: file.name || 'Attachment',
|
2015-12-03 02:16:16 +08:00
|
|
|
roomId: roomId,
|
|
|
|
total: 0,
|
2016-11-15 19:22:39 +08:00
|
|
|
loaded: 0,
|
2015-12-03 02:16:16 +08:00
|
|
|
};
|
|
|
|
this.inprogress.push(upload);
|
|
|
|
dis.dispatch({action: 'upload_started'});
|
|
|
|
|
2017-10-12 00:56:17 +08:00
|
|
|
let error;
|
2017-07-15 00:01:03 +08:00
|
|
|
|
|
|
|
function onProgress(ev) {
|
|
|
|
upload.total = ev.total;
|
|
|
|
upload.loaded = ev.loaded;
|
|
|
|
dis.dispatch({action: 'upload_progress', upload: upload});
|
|
|
|
}
|
|
|
|
|
2015-12-03 02:16:16 +08:00
|
|
|
return def.promise.then(function() {
|
2016-12-17 01:11:28 +08:00
|
|
|
// XXX: upload.promise must be the promise that
|
|
|
|
// is returned by uploadFile as it has an abort()
|
|
|
|
// method hacked onto it.
|
2016-11-15 19:22:39 +08:00
|
|
|
upload.promise = uploadFile(
|
2017-07-15 00:01:03 +08:00
|
|
|
matrixClient, roomId, file, onProgress,
|
2016-12-17 01:11:28 +08:00
|
|
|
);
|
|
|
|
return upload.promise.then(function(result) {
|
2016-11-15 19:22:39 +08:00
|
|
|
content.file = result.file;
|
|
|
|
content.url = result.url;
|
|
|
|
});
|
2015-12-03 02:16:16 +08:00
|
|
|
}).then(function(url) {
|
|
|
|
return matrixClient.sendMessage(roomId, content);
|
|
|
|
}, function(err) {
|
2016-02-16 03:29:56 +08:00
|
|
|
error = err;
|
2015-12-03 18:52:06 +08:00
|
|
|
if (!upload.canceled) {
|
2017-10-12 00:56:17 +08:00
|
|
|
let desc = _t('The file \'%(fileName)s\' failed to upload', {fileName: upload.fileName}) + '.';
|
2015-12-03 18:52:06 +08:00
|
|
|
if (err.http_status == 413) {
|
2017-05-23 22:16:31 +08:00
|
|
|
desc = _t('The file \'%(fileName)s\' exceeds this home server\'s size limit for uploads', {fileName: upload.fileName});
|
2015-12-03 18:52:06 +08:00
|
|
|
}
|
2017-10-12 00:56:17 +08:00
|
|
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
2017-08-10 22:17:52 +08:00
|
|
|
Modal.createTrackedDialog('Upload failed', '', ErrorDialog, {
|
2017-05-23 22:16:31 +08:00
|
|
|
title: _t('Upload Failed'),
|
|
|
|
description: desc,
|
2015-12-03 18:52:06 +08:00
|
|
|
});
|
|
|
|
}
|
2016-11-15 19:22:39 +08:00
|
|
|
}).finally(() => {
|
|
|
|
const inprogressKeys = Object.keys(this.inprogress);
|
2017-10-12 00:56:17 +08:00
|
|
|
for (let i = 0; i < this.inprogress.length; ++i) {
|
|
|
|
const k = inprogressKeys[i];
|
2016-11-15 19:22:39 +08:00
|
|
|
if (this.inprogress[k].promise === upload.promise) {
|
|
|
|
this.inprogress.splice(k, 1);
|
2015-12-03 02:16:16 +08:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2016-02-16 03:29:56 +08:00
|
|
|
if (error) {
|
|
|
|
dis.dispatch({action: 'upload_failed', upload: upload});
|
2017-10-12 00:56:17 +08:00
|
|
|
} else {
|
2016-02-16 03:29:56 +08:00
|
|
|
dis.dispatch({action: 'upload_finished', upload: upload});
|
|
|
|
}
|
2015-07-08 21:34:26 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2015-12-03 02:16:16 +08:00
|
|
|
getCurrentUploads() {
|
|
|
|
return this.inprogress;
|
|
|
|
}
|
|
|
|
|
|
|
|
cancelUpload(promise) {
|
2016-11-15 19:22:39 +08:00
|
|
|
const inprogressKeys = Object.keys(this.inprogress);
|
2017-10-12 00:56:17 +08:00
|
|
|
let upload;
|
|
|
|
for (let i = 0; i < this.inprogress.length; ++i) {
|
|
|
|
const k = inprogressKeys[i];
|
2015-12-03 02:16:16 +08:00
|
|
|
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();
|
2015-07-08 21:34:26 +08:00
|
|
|
}
|
|
|
|
|
2015-12-03 02:16:16 +08:00
|
|
|
module.exports = global.mx_ContentMessage;
|