Finish new Message component, integrate into application
Also: - New schema version 8 with video/image thumbnails, screenshots, sizes - Upgrade messages not at current schema version when loading messages to show in conversation - New MessageDetail react component - New ConversationHeader react component
This commit is contained in:
parent
69f11c4a7b
commit
3c69886320
102 changed files with 9644 additions and 7381 deletions
|
@ -4,7 +4,6 @@ const Backbone = require('../../ts/backbone');
|
|||
const Crypto = require('./crypto');
|
||||
const Database = require('./database');
|
||||
const Emoji = require('../../ts/util/emoji');
|
||||
const Message = require('./types/message');
|
||||
const Notifications = require('../../ts/notifications');
|
||||
const OS = require('../../ts/OS');
|
||||
const Settings = require('./settings');
|
||||
|
@ -18,19 +17,38 @@ const {
|
|||
const { ContactListItem } = require('../../ts/components/ContactListItem');
|
||||
const { ContactName } = require('../../ts/components/conversation/ContactName');
|
||||
const {
|
||||
ConversationTitle,
|
||||
} = require('../../ts/components/conversation/ConversationTitle');
|
||||
ConversationHeader,
|
||||
} = require('../../ts/components/conversation/ConversationHeader');
|
||||
const {
|
||||
EmbeddedContact,
|
||||
} = require('../../ts/components/conversation/EmbeddedContact');
|
||||
const { Emojify } = require('../../ts/components/conversation/Emojify');
|
||||
const {
|
||||
GroupNotification,
|
||||
} = require('../../ts/components/conversation/GroupNotification');
|
||||
const { Lightbox } = require('../../ts/components/Lightbox');
|
||||
const { LightboxGallery } = require('../../ts/components/LightboxGallery');
|
||||
const {
|
||||
MediaGallery,
|
||||
} = require('../../ts/components/conversation/media-gallery/MediaGallery');
|
||||
const { Message } = require('../../ts/components/conversation/Message');
|
||||
const { MessageBody } = require('../../ts/components/conversation/MessageBody');
|
||||
const {
|
||||
MessageDetail,
|
||||
} = require('../../ts/components/conversation/MessageDetail');
|
||||
const { Quote } = require('../../ts/components/conversation/Quote');
|
||||
const {
|
||||
ResetSessionNotification,
|
||||
} = require('../../ts/components/conversation/ResetSessionNotification');
|
||||
const {
|
||||
SafetyNumberNotification,
|
||||
} = require('../../ts/components/conversation/SafetyNumberNotification');
|
||||
const {
|
||||
TimerNotification,
|
||||
} = require('../../ts/components/conversation/TimerNotification');
|
||||
const {
|
||||
VerificationNotification,
|
||||
} = require('../../ts/components/conversation/VerificationNotification');
|
||||
|
||||
// Migrations
|
||||
const {
|
||||
|
@ -42,11 +60,14 @@ const Migrations1DatabaseWithoutAttachmentData = require('./migrations/migration
|
|||
|
||||
// Types
|
||||
const AttachmentType = require('./types/attachment');
|
||||
const VisualAttachment = require('./types/visual_attachment');
|
||||
const Contact = require('../../ts/types/Contact');
|
||||
const Conversation = require('../../ts/types/Conversation');
|
||||
const Errors = require('./types/errors');
|
||||
const MediaGalleryMessage = require('../../ts/components/conversation/media-gallery/types/Message');
|
||||
const MessageType = require('./types/message');
|
||||
const MIME = require('../../ts/types/MIME');
|
||||
const PhoneNumber = require('../../ts/types/PhoneNumber');
|
||||
const SettingsType = require('../../ts/types/Settings');
|
||||
|
||||
// Views
|
||||
|
@ -57,39 +78,59 @@ const { IdleDetector } = require('./idle_detector');
|
|||
const MessageDataMigrator = require('./messages_data_migrator');
|
||||
|
||||
function initializeMigrations({
|
||||
Attachments,
|
||||
userDataPath,
|
||||
Type,
|
||||
getRegionCode,
|
||||
Attachments,
|
||||
Type,
|
||||
VisualType,
|
||||
}) {
|
||||
if (!Attachments) {
|
||||
return null;
|
||||
}
|
||||
const {
|
||||
getPath,
|
||||
createReader,
|
||||
createAbsolutePathGetter,
|
||||
createWriterForNew,
|
||||
createWriterForExisting,
|
||||
} = Attachments;
|
||||
const {
|
||||
makeObjectUrl,
|
||||
revokeObjectUrl,
|
||||
getImageDimensions,
|
||||
makeImageThumbnail,
|
||||
makeVideoScreenshot,
|
||||
} = VisualType;
|
||||
|
||||
const attachmentsPath = Attachments.getPath(userDataPath);
|
||||
const readAttachmentData = Attachments.createReader(attachmentsPath);
|
||||
const attachmentsPath = getPath(userDataPath);
|
||||
const readAttachmentData = createReader(attachmentsPath);
|
||||
const loadAttachmentData = Type.loadData(readAttachmentData);
|
||||
const getAbsoluteAttachmentPath = createAbsolutePathGetter(attachmentsPath);
|
||||
|
||||
return {
|
||||
attachmentsPath,
|
||||
deleteAttachmentData: Type.deleteData(
|
||||
Attachments.createDeleter(attachmentsPath)
|
||||
),
|
||||
getAbsoluteAttachmentPath: Attachments.createAbsolutePathGetter(
|
||||
attachmentsPath
|
||||
),
|
||||
getAbsoluteAttachmentPath,
|
||||
getPlaceholderMigrations,
|
||||
loadAttachmentData,
|
||||
loadMessage: Message.createAttachmentLoader(loadAttachmentData),
|
||||
loadMessage: MessageType.createAttachmentLoader(loadAttachmentData),
|
||||
Migrations0DatabaseWithAttachmentData,
|
||||
Migrations1DatabaseWithoutAttachmentData,
|
||||
upgradeMessageSchema: message =>
|
||||
Message.upgradeSchema(message, {
|
||||
writeNewAttachmentData: Attachments.createWriterForNew(attachmentsPath),
|
||||
MessageType.upgradeSchema(message, {
|
||||
writeNewAttachmentData: createWriterForNew(attachmentsPath),
|
||||
getRegionCode,
|
||||
getAbsoluteAttachmentPath,
|
||||
makeObjectUrl,
|
||||
revokeObjectUrl,
|
||||
getImageDimensions,
|
||||
makeImageThumbnail,
|
||||
makeVideoScreenshot,
|
||||
}),
|
||||
writeMessageAttachments: Message.createAttachmentDataWriter(
|
||||
Attachments.createWriterForExisting(attachmentsPath)
|
||||
writeMessageAttachments: MessageType.createAttachmentDataWriter(
|
||||
createWriterForExisting(attachmentsPath)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
@ -98,27 +139,35 @@ exports.setup = (options = {}) => {
|
|||
const { Attachments, userDataPath, getRegionCode } = options;
|
||||
|
||||
const Migrations = initializeMigrations({
|
||||
Attachments,
|
||||
userDataPath,
|
||||
Type: AttachmentType,
|
||||
getRegionCode,
|
||||
Attachments,
|
||||
Type: AttachmentType,
|
||||
VisualType: VisualAttachment,
|
||||
});
|
||||
|
||||
const Components = {
|
||||
ContactDetail,
|
||||
ContactListItem,
|
||||
ContactName,
|
||||
ConversationTitle,
|
||||
ConversationHeader,
|
||||
EmbeddedContact,
|
||||
Emojify,
|
||||
GroupNotification,
|
||||
Lightbox,
|
||||
LightboxGallery,
|
||||
MediaGallery,
|
||||
Message,
|
||||
MessageBody,
|
||||
MessageDetail,
|
||||
Quote,
|
||||
ResetSessionNotification,
|
||||
SafetyNumberNotification,
|
||||
TimerNotification,
|
||||
Types: {
|
||||
Message: MediaGalleryMessage,
|
||||
},
|
||||
Quote,
|
||||
VerificationNotification,
|
||||
};
|
||||
|
||||
const Types = {
|
||||
|
@ -126,9 +175,11 @@ exports.setup = (options = {}) => {
|
|||
Contact,
|
||||
Conversation,
|
||||
Errors,
|
||||
Message,
|
||||
Message: MessageType,
|
||||
MIME,
|
||||
PhoneNumber,
|
||||
Settings: SettingsType,
|
||||
VisualAttachment,
|
||||
};
|
||||
|
||||
const Views = {
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
const is = require('@sindresorhus/is');
|
||||
|
||||
const AttachmentTS = require('../../../ts/types/Attachment');
|
||||
const GoogleChrome = require('../../../ts/util/GoogleChrome');
|
||||
const MIME = require('../../../ts/types/MIME');
|
||||
const { toLogFormat } = require('./errors');
|
||||
const {
|
||||
arrayBufferToBlob,
|
||||
blobToArrayBuffer,
|
||||
|
@ -181,3 +183,112 @@ exports.deleteData = deleteAttachmentData => {
|
|||
|
||||
exports.isVoiceMessage = AttachmentTS.isVoiceMessage;
|
||||
exports.save = AttachmentTS.save;
|
||||
|
||||
const THUMBNAIL_SIZE = 150;
|
||||
const THUMBNAIL_CONTENT_TYPE = 'image/png';
|
||||
|
||||
exports.captureDimensionsAndScreenshot = async (
|
||||
attachment,
|
||||
{
|
||||
writeNewAttachmentData,
|
||||
getAbsoluteAttachmentPath,
|
||||
makeObjectUrl,
|
||||
revokeObjectUrl,
|
||||
getImageDimensions,
|
||||
makeImageThumbnail,
|
||||
makeVideoScreenshot,
|
||||
}
|
||||
) => {
|
||||
const { contentType } = attachment;
|
||||
|
||||
if (
|
||||
!GoogleChrome.isImageTypeSupported(contentType) &&
|
||||
!GoogleChrome.isVideoTypeSupported(contentType)
|
||||
) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
const absolutePath = await getAbsoluteAttachmentPath(attachment.path);
|
||||
|
||||
if (GoogleChrome.isImageTypeSupported(contentType)) {
|
||||
try {
|
||||
const { width, height } = await getImageDimensions(absolutePath);
|
||||
const thumbnailBuffer = await blobToArrayBuffer(
|
||||
await makeImageThumbnail(
|
||||
THUMBNAIL_SIZE,
|
||||
absolutePath,
|
||||
THUMBNAIL_CONTENT_TYPE
|
||||
)
|
||||
);
|
||||
|
||||
const thumbnailPath = await writeNewAttachmentData(thumbnailBuffer);
|
||||
return {
|
||||
...attachment,
|
||||
width,
|
||||
height,
|
||||
thumbnail: {
|
||||
path: thumbnailPath,
|
||||
contentType: THUMBNAIL_CONTENT_TYPE,
|
||||
width: THUMBNAIL_SIZE,
|
||||
height: THUMBNAIL_SIZE,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(
|
||||
'captureDimensionsAndScreenshot:',
|
||||
'error processing image; skipping screenshot generation',
|
||||
toLogFormat(error)
|
||||
);
|
||||
return attachment;
|
||||
}
|
||||
}
|
||||
|
||||
let screenshotObjectUrl;
|
||||
try {
|
||||
const screenshotBuffer = await blobToArrayBuffer(
|
||||
await makeVideoScreenshot(absolutePath, THUMBNAIL_CONTENT_TYPE)
|
||||
);
|
||||
screenshotObjectUrl = makeObjectUrl(
|
||||
screenshotBuffer,
|
||||
THUMBNAIL_CONTENT_TYPE
|
||||
);
|
||||
const { width, height } = await getImageDimensions(screenshotObjectUrl);
|
||||
const screenshotPath = await writeNewAttachmentData(screenshotBuffer);
|
||||
|
||||
const thumbnailBuffer = await blobToArrayBuffer(
|
||||
await makeImageThumbnail(
|
||||
THUMBNAIL_SIZE,
|
||||
screenshotObjectUrl,
|
||||
THUMBNAIL_CONTENT_TYPE
|
||||
)
|
||||
);
|
||||
|
||||
const thumbnailPath = await writeNewAttachmentData(thumbnailBuffer);
|
||||
|
||||
return {
|
||||
...attachment,
|
||||
screenshot: {
|
||||
contentType: THUMBNAIL_CONTENT_TYPE,
|
||||
path: screenshotPath,
|
||||
width,
|
||||
height,
|
||||
},
|
||||
thumbnail: {
|
||||
path: thumbnailPath,
|
||||
contentType: THUMBNAIL_CONTENT_TYPE,
|
||||
width: THUMBNAIL_SIZE,
|
||||
height: THUMBNAIL_SIZE,
|
||||
},
|
||||
width,
|
||||
height,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(
|
||||
'captureDimensionsAndScreenshot: error processing video; skipping screenshot generation',
|
||||
toLogFormat(error)
|
||||
);
|
||||
return attachment;
|
||||
} finally {
|
||||
revokeObjectUrl(screenshotObjectUrl);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -41,6 +41,9 @@ const PRIVATE = 'private';
|
|||
// - `hasVisualMediaAttachments`: Include all images and video regardless of
|
||||
// whether Chromium can render it or not.
|
||||
// - `hasFileAttachments`: Exclude voice messages.
|
||||
// Version 8
|
||||
// - Attachments: Capture video/image dimensions and thumbnails, as well as a
|
||||
// full-size screenshot for video.
|
||||
|
||||
const INITIAL_SCHEMA_VERSION = 0;
|
||||
|
||||
|
@ -128,7 +131,7 @@ exports._withSchemaVersion = (schemaVersion, upgrade) => {
|
|||
upgradedMessage = await upgrade(message, context);
|
||||
} catch (error) {
|
||||
console.log(
|
||||
'Message._withSchemaVersion: error:',
|
||||
`Message._withSchemaVersion: error updating message ${message.id}:`,
|
||||
Errors.toLogFormat(error)
|
||||
);
|
||||
return message;
|
||||
|
@ -242,6 +245,11 @@ const toVersion6 = exports._withSchemaVersion(
|
|||
// classified:
|
||||
const toVersion7 = exports._withSchemaVersion(7, initializeAttachmentMetadata);
|
||||
|
||||
const toVersion8 = exports._withSchemaVersion(
|
||||
8,
|
||||
exports._mapAttachments(Attachment.captureDimensionsAndScreenshot)
|
||||
);
|
||||
|
||||
const VERSIONS = [
|
||||
toVersion0,
|
||||
toVersion1,
|
||||
|
@ -251,19 +259,47 @@ const VERSIONS = [
|
|||
toVersion5,
|
||||
toVersion6,
|
||||
toVersion7,
|
||||
toVersion8,
|
||||
];
|
||||
exports.CURRENT_SCHEMA_VERSION = VERSIONS.length - 1;
|
||||
|
||||
// UpgradeStep
|
||||
exports.upgradeSchema = async (
|
||||
rawMessage,
|
||||
{ writeNewAttachmentData, getRegionCode } = {}
|
||||
{
|
||||
writeNewAttachmentData,
|
||||
getRegionCode,
|
||||
getAbsoluteAttachmentPath,
|
||||
makeObjectUrl,
|
||||
revokeObjectUrl,
|
||||
getImageDimensions,
|
||||
makeImageThumbnail,
|
||||
makeVideoScreenshot,
|
||||
} = {}
|
||||
) => {
|
||||
if (!isFunction(writeNewAttachmentData)) {
|
||||
throw new TypeError('`context.writeNewAttachmentData` is required');
|
||||
throw new TypeError('context.writeNewAttachmentData is required');
|
||||
}
|
||||
if (!isFunction(getRegionCode)) {
|
||||
throw new TypeError('`context.getRegionCode` is required');
|
||||
throw new TypeError('context.getRegionCode is required');
|
||||
}
|
||||
if (!isFunction(getAbsoluteAttachmentPath)) {
|
||||
throw new TypeError('context.getAbsoluteAttachmentPath is required');
|
||||
}
|
||||
if (!isFunction(makeObjectUrl)) {
|
||||
throw new TypeError('context.makeObjectUrl is required');
|
||||
}
|
||||
if (!isFunction(revokeObjectUrl)) {
|
||||
throw new TypeError('context.revokeObjectUrl is required');
|
||||
}
|
||||
if (!isFunction(getImageDimensions)) {
|
||||
throw new TypeError('context.getImageDimensions is required');
|
||||
}
|
||||
if (!isFunction(makeImageThumbnail)) {
|
||||
throw new TypeError('context.makeImageThumbnail is required');
|
||||
}
|
||||
if (!isFunction(makeVideoScreenshot)) {
|
||||
throw new TypeError('context.makeVideoScreenshot is required');
|
||||
}
|
||||
|
||||
let message = rawMessage;
|
||||
|
@ -275,6 +311,12 @@ exports.upgradeSchema = async (
|
|||
message = await currentVersion(message, {
|
||||
writeNewAttachmentData,
|
||||
regionCode: getRegionCode(),
|
||||
getAbsoluteAttachmentPath,
|
||||
makeObjectUrl,
|
||||
revokeObjectUrl,
|
||||
getImageDimensions,
|
||||
makeImageThumbnail,
|
||||
makeVideoScreenshot,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
126
js/modules/types/visual_attachment.js
Normal file
126
js/modules/types/visual_attachment.js
Normal file
|
@ -0,0 +1,126 @@
|
|||
/* global document, URL, Blob */
|
||||
|
||||
const loadImage = require('blueimp-load-image');
|
||||
const { toLogFormat } = require('./errors');
|
||||
const dataURLToBlobSync = require('blueimp-canvas-to-blob');
|
||||
const { blobToArrayBuffer } = require('blob-util');
|
||||
const {
|
||||
arrayBufferToObjectURL,
|
||||
} = require('../../../ts/util/arrayBufferToObjectURL');
|
||||
|
||||
exports.blobToArrayBuffer = blobToArrayBuffer;
|
||||
|
||||
exports.getImageDimensions = objectUrl =>
|
||||
new Promise((resolve, reject) => {
|
||||
const image = document.createElement('img');
|
||||
|
||||
image.addEventListener('load', () => {
|
||||
resolve({
|
||||
height: image.naturalHeight,
|
||||
width: image.naturalWidth,
|
||||
});
|
||||
});
|
||||
image.addEventListener('error', error => {
|
||||
console.log('getImageDimensions error', toLogFormat(error));
|
||||
reject(error);
|
||||
});
|
||||
|
||||
image.src = objectUrl;
|
||||
});
|
||||
|
||||
exports.makeImageThumbnail = (size, objectUrl, contentType = 'image/png') =>
|
||||
new Promise((resolve, reject) => {
|
||||
const image = document.createElement('img');
|
||||
|
||||
image.addEventListener('load', () => {
|
||||
// using components/blueimp-load-image
|
||||
|
||||
// first, make the correct size
|
||||
let canvas = loadImage.scale(image, {
|
||||
canvas: true,
|
||||
cover: true,
|
||||
maxWidth: size,
|
||||
maxHeight: size,
|
||||
minWidth: size,
|
||||
minHeight: size,
|
||||
});
|
||||
|
||||
// then crop
|
||||
canvas = loadImage.scale(canvas, {
|
||||
canvas: true,
|
||||
crop: true,
|
||||
maxWidth: size,
|
||||
maxHeight: size,
|
||||
minWidth: size,
|
||||
minHeight: size,
|
||||
});
|
||||
|
||||
const blob = dataURLToBlobSync(canvas.toDataURL(contentType));
|
||||
|
||||
resolve(blob);
|
||||
});
|
||||
|
||||
image.addEventListener('error', error => {
|
||||
console.log('makeImageThumbnail error', toLogFormat(error));
|
||||
reject(error);
|
||||
});
|
||||
|
||||
image.src = objectUrl;
|
||||
});
|
||||
|
||||
exports.makeVideoScreenshot = (objectUrl, contentType = 'image/png') =>
|
||||
new Promise((resolve, reject) => {
|
||||
const video = document.createElement('video');
|
||||
|
||||
function capture() {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
canvas
|
||||
.getContext('2d')
|
||||
.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
|
||||
const image = dataURLToBlobSync(canvas.toDataURL(contentType));
|
||||
|
||||
video.removeEventListener('canplay', capture);
|
||||
|
||||
resolve(image);
|
||||
}
|
||||
|
||||
video.addEventListener('canplay', capture);
|
||||
video.addEventListener('error', error => {
|
||||
console.log('makeVideoThumbnail error', toLogFormat(error));
|
||||
reject(error);
|
||||
});
|
||||
|
||||
video.src = objectUrl;
|
||||
});
|
||||
|
||||
exports.makeVideoThumbnail = async (size, videoObjectUrl) => {
|
||||
let screenshotObjectUrl;
|
||||
try {
|
||||
const type = 'image/png';
|
||||
const blob = await exports.makeVideoScreenshot(videoObjectUrl, type);
|
||||
const data = await blobToArrayBuffer(blob);
|
||||
screenshotObjectUrl = arrayBufferToObjectURL({
|
||||
data,
|
||||
type,
|
||||
});
|
||||
|
||||
return exports.makeImageThumbnail(size, screenshotObjectUrl);
|
||||
} finally {
|
||||
exports.revokeObjectUrl(screenshotObjectUrl);
|
||||
}
|
||||
};
|
||||
|
||||
exports.makeObjectUrl = (data, contentType) => {
|
||||
const blob = new Blob([data], {
|
||||
type: contentType,
|
||||
});
|
||||
|
||||
return URL.createObjectURL(blob);
|
||||
};
|
||||
|
||||
exports.revokeObjectUrl = objectUrl => {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue