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:
Scott Nonnenberg 2018-07-09 14:29:13 -07:00
parent 69f11c4a7b
commit 3c69886320
102 changed files with 9644 additions and 7381 deletions

View file

@ -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 = {

View file

@ -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);
}
};

View file

@ -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,
});
}

View 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);
};