commit
787d023557
40 changed files with 2424 additions and 160 deletions
|
@ -15,6 +15,7 @@ test/test.js
|
||||||
ts/**/*.js
|
ts/**/*.js
|
||||||
ts/protobuf/*.d.ts
|
ts/protobuf/*.d.ts
|
||||||
ts/protobuf/*.js
|
ts/protobuf/*.js
|
||||||
|
stylesheets/manifest.css
|
||||||
|
|
||||||
# Third-party files
|
# Third-party files
|
||||||
components/**
|
components/**
|
||||||
|
@ -37,3 +38,4 @@ _locales/**
|
||||||
|
|
||||||
# Symlink into third-party `components`:
|
# Symlink into third-party `components`:
|
||||||
stylesheets/_intlTelInput.scss
|
stylesheets/_intlTelInput.scss
|
||||||
|
|
||||||
|
|
|
@ -460,6 +460,26 @@
|
||||||
"selectAContact": {
|
"selectAContact": {
|
||||||
"message": "Select a contact or group to start chatting."
|
"message": "Select a contact or group to start chatting."
|
||||||
},
|
},
|
||||||
|
"sendMessageToContact": {
|
||||||
|
"message": "Send Message",
|
||||||
|
"description": "Shown when you are sent a contact and that contact has a signal account"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"message": "home",
|
||||||
|
"description": "Shown on contact detail screen as a label for an address/phone/email"
|
||||||
|
},
|
||||||
|
"work": {
|
||||||
|
"message": "work",
|
||||||
|
"description": "Shown on contact detail screen as a label for an address/phone/email"
|
||||||
|
},
|
||||||
|
"mobile": {
|
||||||
|
"message": "mobile",
|
||||||
|
"description": "Shown on contact detail screen as a label for aa phone or email"
|
||||||
|
},
|
||||||
|
"poBox": {
|
||||||
|
"message": "PO Box",
|
||||||
|
"description": "When rendering an address, used to provide context to a post office box"
|
||||||
|
},
|
||||||
"replyToMessage": {
|
"replyToMessage": {
|
||||||
"message": "Reply to Message",
|
"message": "Reply to Message",
|
||||||
"description": "Shown in triple-dot menu next to message to allow user to start crafting a message with a quotation"
|
"description": "Shown in triple-dot menu next to message to allow user to start crafting a message with a quotation"
|
||||||
|
|
1
images/chat-bubble-outline.svg
Normal file
1
images/chat-bubble-outline.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="20" height="19" xmlns="http://www.w3.org/2000/svg"><path d="M.503 18.58c1.944-.05 3.635-.87 5.03-2.075l.192-.167.237.096c1.27.519 2.636.792 4.038.792 5.304 0 9.583-3.774 9.583-8.405 0-4.63-4.279-8.404-9.583-8.404S.417 4.19.417 8.82c0 1.94.753 3.777 2.116 5.262l.149.16-.05.214c-.225.989-.688 1.989-1.3 2.955-.303.48-.635.93-.829 1.169z" fill-rule="nonzero" stroke="#000" stroke-width=".833" fill="none"/></svg>
|
After Width: | Height: | Size: 422 B |
1
images/chat-bubble.svg
Normal file
1
images/chat-bubble.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="18" height="17" xmlns="http://www.w3.org/2000/svg"><path d="M9 0C4.029 0 0 3.533 0 7.893c0 1.882.752 3.605 2.004 4.96-.44 1.912-1.91 3.616-1.927 3.635a.32.32 0 0 0-.052.33A.276.276 0 0 0 .28 17c2.331 0 4.078-1.207 4.943-1.95 1.15.466 2.426.736 3.776.736 4.971 0 9-3.533 9-7.893C18 3.533 13.971 0 9 0z" fill="#000" fill-rule="nonzero"/></svg>
|
After Width: | Height: | Size: 353 B |
|
@ -238,6 +238,12 @@
|
||||||
appView.openInbox();
|
appView.openInbox();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Whisper.events.on('showConversation', function(conversation) {
|
||||||
|
if (appView) {
|
||||||
|
appView.openConversation(conversation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Whisper.Notifications.on('click', function(conversation) {
|
Whisper.Notifications.on('click', function(conversation) {
|
||||||
showWindow();
|
showWindow();
|
||||||
if (conversation) {
|
if (conversation) {
|
||||||
|
|
|
@ -711,13 +711,21 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
async makeQuote(quotedMessage) {
|
async makeQuote(quotedMessage) {
|
||||||
|
const { getName } = Signal.Types.Contact;
|
||||||
const contact = quotedMessage.getContact();
|
const contact = quotedMessage.getContact();
|
||||||
const attachments = quotedMessage.get('attachments');
|
const attachments = quotedMessage.get('attachments');
|
||||||
|
|
||||||
|
const body = quotedMessage.get('body');
|
||||||
|
const embeddedContact = quotedMessage.get('contact');
|
||||||
|
const embeddedContactName =
|
||||||
|
embeddedContact && embeddedContact.length > 0
|
||||||
|
? getName(embeddedContact[0])
|
||||||
|
: '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
author: contact.id,
|
author: contact.id,
|
||||||
id: quotedMessage.get('sent_at'),
|
id: quotedMessage.get('sent_at'),
|
||||||
text: quotedMessage.get('body'),
|
text: body || embeddedContactName,
|
||||||
attachments: await Promise.all(
|
attachments: await Promise.all(
|
||||||
(attachments || []).map(async attachment => {
|
(attachments || []).map(async attachment => {
|
||||||
const { contentType } = attachment;
|
const { contentType } = attachment;
|
||||||
|
|
|
@ -588,6 +588,7 @@
|
||||||
message.set({
|
message.set({
|
||||||
attachments: dataMessage.attachments,
|
attachments: dataMessage.attachments,
|
||||||
body: dataMessage.body,
|
body: dataMessage.body,
|
||||||
|
contact: dataMessage.contact,
|
||||||
conversationId: conversation.id,
|
conversationId: conversation.id,
|
||||||
decrypted_at: now,
|
decrypted_at: now,
|
||||||
errors: [],
|
errors: [],
|
||||||
|
|
|
@ -570,6 +570,64 @@ async function writeAttachments(rawAttachments, options) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function writeAvatar(avatar, options) {
|
||||||
|
console.log('writeAvatar', { avatar, options });
|
||||||
|
const { dir, message, index, key, newKey } = options;
|
||||||
|
const name = _getAnonymousAttachmentFileName(message, index);
|
||||||
|
const filename = `${name}-contact-avatar`;
|
||||||
|
|
||||||
|
const target = path.join(dir, filename);
|
||||||
|
if (!avatar || !avatar.path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeEncryptedAttachment(target, avatar.data, {
|
||||||
|
key,
|
||||||
|
newKey,
|
||||||
|
filename,
|
||||||
|
dir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeContactAvatars(contact, options) {
|
||||||
|
const { name } = options;
|
||||||
|
|
||||||
|
const { loadAttachmentData } = Signal.Migrations;
|
||||||
|
const promises = contact.map(async item => {
|
||||||
|
if (
|
||||||
|
!item ||
|
||||||
|
!item.avatar ||
|
||||||
|
!item.avatar.avatar ||
|
||||||
|
!item.avatar.avatar.path
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadAttachmentData(item.avatar.avatar);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
_.map(await Promise.all(promises), (item, index) =>
|
||||||
|
writeAvatar(
|
||||||
|
item,
|
||||||
|
Object.assign({}, options, {
|
||||||
|
index,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(
|
||||||
|
'writeContactAvatars: error exporting conversation',
|
||||||
|
name,
|
||||||
|
':',
|
||||||
|
error && error.stack ? error.stack : error
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function writeEncryptedAttachment(target, data, options = {}) {
|
async function writeEncryptedAttachment(target, data, options = {}) {
|
||||||
const { key, newKey, filename, dir } = options;
|
const { key, newKey, filename, dir } = options;
|
||||||
|
|
||||||
|
@ -714,6 +772,21 @@ async function exportConversation(db, conversation, options) {
|
||||||
promiseChain = promiseChain.then(exportQuoteThumbnails);
|
promiseChain = promiseChain.then(exportQuoteThumbnails);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { contact } = message;
|
||||||
|
if (contact && contact.length > 0) {
|
||||||
|
const exportContactAvatars = () =>
|
||||||
|
writeContactAvatars(contact, {
|
||||||
|
dir: attachmentsDir,
|
||||||
|
name,
|
||||||
|
message,
|
||||||
|
key,
|
||||||
|
newKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line more/no-then
|
||||||
|
promiseChain = promiseChain.then(exportContactAvatars);
|
||||||
|
}
|
||||||
|
|
||||||
count += 1;
|
count += 1;
|
||||||
cursor.continue();
|
cursor.continue();
|
||||||
} else {
|
} else {
|
||||||
|
@ -870,27 +943,44 @@ function getDirContents(dir) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadAttachments(dir, getName, options) {
|
async function loadAttachments(dir, getName, options) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
const { message } = options;
|
const { message } = options;
|
||||||
|
|
||||||
const attachmentPromises = _.map(message.attachments, (attachment, index) => {
|
await Promise.all(
|
||||||
const name = getName(message, index, attachment);
|
_.map(message.attachments, (attachment, index) => {
|
||||||
return readAttachment(dir, attachment, name, options);
|
const name = getName(message, index, attachment);
|
||||||
});
|
return readAttachment(dir, attachment, name, options);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const quoteAttachments = message.quote && message.quote.attachments;
|
const quoteAttachments = message.quote && message.quote.attachments;
|
||||||
const thumbnailPromises = _.map(quoteAttachments, (attachment, index) => {
|
await Promise.all(
|
||||||
const thumbnail = attachment && attachment.thumbnail;
|
_.map(quoteAttachments, (attachment, index) => {
|
||||||
if (!thumbnail) {
|
const thumbnail = attachment && attachment.thumbnail;
|
||||||
return null;
|
if (!thumbnail) {
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const name = `${getName(message, index, thumbnail)}-thumbnail`;
|
const name = `${getName(message, index)}-thumbnail`;
|
||||||
return readAttachment(dir, thumbnail, name, options);
|
return readAttachment(dir, thumbnail, name, options);
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return Promise.all(attachmentPromises.concat(thumbnailPromises));
|
const { contact } = message;
|
||||||
|
await Promise.all(
|
||||||
|
_.map(contact, (item, index) => {
|
||||||
|
const avatar = item && item.avatar && item.avatar.avatar;
|
||||||
|
if (!avatar) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = `${getName(message, index)}-contact-avatar`;
|
||||||
|
return readAttachment(dir, avatar, name, options);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('loadAttachments', { message });
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveMessage(db, message) {
|
function saveMessage(db, message) {
|
||||||
|
|
142
js/modules/types/contact.js
Normal file
142
js/modules/types/contact.js
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
const { omit, compact, map } = require('lodash');
|
||||||
|
|
||||||
|
const { toLogFormat } = require('./errors');
|
||||||
|
const { SignalService } = require('../../../ts/protobuf');
|
||||||
|
|
||||||
|
const DEFAULT_PHONE_TYPE = SignalService.DataMessage.Contact.Phone.Type.HOME;
|
||||||
|
const DEFAULT_EMAIL_TYPE = SignalService.DataMessage.Contact.Email.Type.HOME;
|
||||||
|
const DEFAULT_ADDRESS_TYPE =
|
||||||
|
SignalService.DataMessage.Contact.PostalAddress.Type.HOME;
|
||||||
|
|
||||||
|
exports.parseAndWriteAvatar = upgradeAttachment => async (
|
||||||
|
contact,
|
||||||
|
context = {}
|
||||||
|
) => {
|
||||||
|
const { message } = context;
|
||||||
|
const { avatar } = contact;
|
||||||
|
const contactWithUpdatedAvatar =
|
||||||
|
avatar && avatar.avatar
|
||||||
|
? Object.assign({}, contact, {
|
||||||
|
avatar: Object.assign({}, avatar, {
|
||||||
|
avatar: await upgradeAttachment(avatar.avatar, context),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
: omit(contact, ['avatar']);
|
||||||
|
|
||||||
|
// eliminates empty numbers, emails, and addresses; adds type if not provided
|
||||||
|
const parsedContact = parseContact(contactWithUpdatedAvatar);
|
||||||
|
|
||||||
|
const error = exports._validate(parsedContact, {
|
||||||
|
messageId: idForLogging(message),
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
console.log(
|
||||||
|
'Contact.parseAndWriteAvatar: contact was malformed.',
|
||||||
|
toLogFormat(error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedContact;
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseContact(contact) {
|
||||||
|
return Object.assign(
|
||||||
|
{},
|
||||||
|
omit(contact, ['avatar', 'number', 'email', 'address']),
|
||||||
|
parseAvatar(contact.avatar),
|
||||||
|
createArrayKey('number', compact(map(contact.number, parsePhoneItem))),
|
||||||
|
createArrayKey('email', compact(map(contact.email, parseEmailItem))),
|
||||||
|
createArrayKey('address', compact(map(contact.address, parseAddress)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function idForLogging(message) {
|
||||||
|
return `${message.source}.${message.sourceDevice} ${message.sent_at}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports._validate = (contact, options = {}) => {
|
||||||
|
const { messageId } = options;
|
||||||
|
const { name, number, email, address, organization } = contact;
|
||||||
|
|
||||||
|
if ((!name || !name.displayName) && !organization) {
|
||||||
|
return new Error(
|
||||||
|
`Message ${messageId}: Contact had neither 'displayName' nor 'organization'`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(!number || !number.length) &&
|
||||||
|
(!email || !email.length) &&
|
||||||
|
(!address || !address.length)
|
||||||
|
) {
|
||||||
|
return new Error(
|
||||||
|
`Message ${messageId}: Contact had no included numbers, email or addresses`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function parsePhoneItem(item) {
|
||||||
|
if (!item.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.assign({}, item, {
|
||||||
|
type: item.type || DEFAULT_PHONE_TYPE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseEmailItem(item) {
|
||||||
|
if (!item.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.assign({}, item, {
|
||||||
|
type: item.type || DEFAULT_EMAIL_TYPE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAddress(address) {
|
||||||
|
if (!address) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!address.street &&
|
||||||
|
!address.pobox &&
|
||||||
|
!address.neighborhood &&
|
||||||
|
!address.city &&
|
||||||
|
!address.region &&
|
||||||
|
!address.postcode &&
|
||||||
|
!address.country
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.assign({}, address, {
|
||||||
|
type: address.type || DEFAULT_ADDRESS_TYPE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAvatar(avatar) {
|
||||||
|
if (!avatar) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
avatar: Object.assign({}, avatar, {
|
||||||
|
isProfile: avatar.isProfile || false,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createArrayKey(key, array) {
|
||||||
|
if (!array || !array.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
[key]: array,
|
||||||
|
};
|
||||||
|
}
|
1
js/modules/types/errors.d.ts
vendored
Normal file
1
js/modules/types/errors.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export function toLogFormat(error: any): string;
|
|
@ -1,5 +1,6 @@
|
||||||
const { isFunction, isString, omit } = require('lodash');
|
const { isFunction, isString, omit } = require('lodash');
|
||||||
|
|
||||||
|
const Contact = require('./contact');
|
||||||
const Attachment = require('./attachment');
|
const Attachment = require('./attachment');
|
||||||
const Errors = require('./errors');
|
const Errors = require('./errors');
|
||||||
const SchemaVersion = require('./schema_version');
|
const SchemaVersion = require('./schema_version');
|
||||||
|
@ -29,6 +30,8 @@ const PRIVATE = 'private';
|
||||||
// - `hasAttachments?: 1 | 0`
|
// - `hasAttachments?: 1 | 0`
|
||||||
// - `hasVisualMediaAttachments?: 1 | undefined` (for media gallery ‘Media’ view)
|
// - `hasVisualMediaAttachments?: 1 | undefined` (for media gallery ‘Media’ view)
|
||||||
// - `hasFileAttachments?: 1 | undefined` (for media gallery ‘Documents’ view)
|
// - `hasFileAttachments?: 1 | undefined` (for media gallery ‘Documents’ view)
|
||||||
|
// Version 6
|
||||||
|
// - Contact: Write contact avatar to disk, ensure contact data is well-formed
|
||||||
|
|
||||||
const INITIAL_SCHEMA_VERSION = 0;
|
const INITIAL_SCHEMA_VERSION = 0;
|
||||||
|
|
||||||
|
@ -37,7 +40,7 @@ const INITIAL_SCHEMA_VERSION = 0;
|
||||||
// add more upgrade steps, we could design a pipeline that does this
|
// add more upgrade steps, we could design a pipeline that does this
|
||||||
// incrementally, e.g. from version 0 / unknown -> 1, 1 --> 2, etc., similar to
|
// incrementally, e.g. from version 0 / unknown -> 1, 1 --> 2, etc., similar to
|
||||||
// how we do database migrations:
|
// how we do database migrations:
|
||||||
exports.CURRENT_SCHEMA_VERSION = 5;
|
exports.CURRENT_SCHEMA_VERSION = 6;
|
||||||
|
|
||||||
// Public API
|
// Public API
|
||||||
exports.GROUP = GROUP;
|
exports.GROUP = GROUP;
|
||||||
|
@ -154,6 +157,20 @@ exports._mapAttachments = upgradeAttachment => async (message, context) => {
|
||||||
return Object.assign({}, message, { attachments });
|
return Object.assign({}, message, { attachments });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Public API
|
||||||
|
// _mapContact :: (Contact -> Promise Contact) ->
|
||||||
|
// (Message, Context) ->
|
||||||
|
// Promise Message
|
||||||
|
exports._mapContact = upgradeContact => async (message, context) => {
|
||||||
|
const contextWithMessage = Object.assign({}, context, { message });
|
||||||
|
const upgradeWithContext = contact =>
|
||||||
|
upgradeContact(contact, contextWithMessage);
|
||||||
|
const contact = await Promise.all(
|
||||||
|
(message.contact || []).map(upgradeWithContext)
|
||||||
|
);
|
||||||
|
return Object.assign({}, message, { contact });
|
||||||
|
};
|
||||||
|
|
||||||
// _mapQuotedAttachments :: (QuotedAttachment -> Promise QuotedAttachment) ->
|
// _mapQuotedAttachments :: (QuotedAttachment -> Promise QuotedAttachment) ->
|
||||||
// (Message, Context) ->
|
// (Message, Context) ->
|
||||||
// Promise Message
|
// Promise Message
|
||||||
|
@ -214,6 +231,13 @@ const toVersion4 = exports._withSchemaVersion(
|
||||||
);
|
);
|
||||||
const toVersion5 = exports._withSchemaVersion(5, initializeAttachmentMetadata);
|
const toVersion5 = exports._withSchemaVersion(5, initializeAttachmentMetadata);
|
||||||
|
|
||||||
|
const toVersion6 = exports._withSchemaVersion(
|
||||||
|
6,
|
||||||
|
exports._mapContact(
|
||||||
|
Contact.parseAndWriteAvatar(Attachment.migrateDataToFileSystem)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// UpgradeStep
|
// UpgradeStep
|
||||||
exports.upgradeSchema = async (rawMessage, { writeNewAttachmentData } = {}) => {
|
exports.upgradeSchema = async (rawMessage, { writeNewAttachmentData } = {}) => {
|
||||||
if (!isFunction(writeNewAttachmentData)) {
|
if (!isFunction(writeNewAttachmentData)) {
|
||||||
|
@ -228,6 +252,7 @@ exports.upgradeSchema = async (rawMessage, { writeNewAttachmentData } = {}) => {
|
||||||
toVersion3,
|
toVersion3,
|
||||||
toVersion4,
|
toVersion4,
|
||||||
toVersion5,
|
toVersion5,
|
||||||
|
toVersion6,
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let i = 0, max = versions.length; i < max; i += 1) {
|
for (let i = 0, max = versions.length; i < max; i += 1) {
|
||||||
|
@ -269,10 +294,11 @@ exports.createAttachmentDataWriter = writeExistingAttachmentData => {
|
||||||
|
|
||||||
const message = exports.initializeSchemaVersion(rawMessage);
|
const message = exports.initializeSchemaVersion(rawMessage);
|
||||||
|
|
||||||
const { attachments, quote } = message;
|
const { attachments, quote, contact } = message;
|
||||||
const hasFilesToWrite =
|
const hasFilesToWrite =
|
||||||
(quote && quote.attachments && quote.attachments.length > 0) ||
|
(quote && quote.attachments && quote.attachments.length > 0) ||
|
||||||
(attachments && attachments.length > 0);
|
(attachments && attachments.length > 0) ||
|
||||||
|
(contact && contact.length > 0);
|
||||||
|
|
||||||
if (!hasFilesToWrite) {
|
if (!hasFilesToWrite) {
|
||||||
return message;
|
return message;
|
||||||
|
@ -318,10 +344,26 @@ exports.createAttachmentDataWriter = writeExistingAttachmentData => {
|
||||||
return omit(thumbnail, ['data']);
|
return omit(thumbnail, ['data']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const writeContactAvatar = async messageContact => {
|
||||||
|
const { avatar } = messageContact;
|
||||||
|
if (avatar && !avatar.avatar) {
|
||||||
|
return omit(messageContact, ['avatar']);
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeExistingAttachmentData(avatar.avatar);
|
||||||
|
|
||||||
|
return Object.assign({}, messageContact, {
|
||||||
|
avatar: Object.assign({}, avatar, {
|
||||||
|
avatar: omit(avatar.avatar, ['data']),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const messageWithoutAttachmentData = Object.assign(
|
const messageWithoutAttachmentData = Object.assign(
|
||||||
{},
|
{},
|
||||||
await writeThumbnails(message),
|
await writeThumbnails(message),
|
||||||
{
|
{
|
||||||
|
contact: await Promise.all((contact || []).map(writeContactAvatar)),
|
||||||
attachments: await Promise.all(
|
attachments: await Promise.all(
|
||||||
(attachments || []).map(async attachment => {
|
(attachments || []).map(async attachment => {
|
||||||
await writeExistingAttachmentData(attachment);
|
await writeExistingAttachmentData(attachment);
|
||||||
|
|
128
js/signal.js
Normal file
128
js/signal.js
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
// The idea with this file is to make it webpackable for the style guide
|
||||||
|
|
||||||
|
const Backbone = require('../ts/backbone');
|
||||||
|
const Crypto = require('./modules/crypto');
|
||||||
|
const Database = require('./modules/database');
|
||||||
|
const HTML = require('../ts/html');
|
||||||
|
const Message = require('./modules/types/message');
|
||||||
|
const Notifications = require('../ts/notifications');
|
||||||
|
const OS = require('../ts/OS');
|
||||||
|
const Settings = require('./modules/settings');
|
||||||
|
const Startup = require('./modules/startup');
|
||||||
|
const Util = require('../ts/util');
|
||||||
|
|
||||||
|
// Components
|
||||||
|
const {
|
||||||
|
ContactDetail,
|
||||||
|
} = require('../ts/components/conversation/ContactDetail');
|
||||||
|
const {
|
||||||
|
EmbeddedContact,
|
||||||
|
} = require('../ts/components/conversation/EmbeddedContact');
|
||||||
|
const { Lightbox } = require('../ts/components/Lightbox');
|
||||||
|
const { LightboxGallery } = require('../ts/components/LightboxGallery');
|
||||||
|
const {
|
||||||
|
MediaGallery,
|
||||||
|
} = require('../ts/components/conversation/media-gallery/MediaGallery');
|
||||||
|
const { Quote } = require('../ts/components/conversation/Quote');
|
||||||
|
|
||||||
|
// Migrations
|
||||||
|
const {
|
||||||
|
getPlaceholderMigrations,
|
||||||
|
} = require('./modules/migrations/get_placeholder_migrations');
|
||||||
|
|
||||||
|
const Migrations0DatabaseWithAttachmentData = require('./modules/migrations/migrations_0_database_with_attachment_data');
|
||||||
|
const Migrations1DatabaseWithoutAttachmentData = require('./modules/migrations/migrations_1_database_without_attachment_data');
|
||||||
|
|
||||||
|
// Types
|
||||||
|
const AttachmentType = require('./modules/types/attachment');
|
||||||
|
const Contact = require('../ts/types/Contact');
|
||||||
|
const Conversation = require('../ts/types/Conversation');
|
||||||
|
const Errors = require('./modules/types/errors');
|
||||||
|
const MediaGalleryMessage = require('../ts/components/conversation/media-gallery/types/Message');
|
||||||
|
const MIME = require('../ts/types/MIME');
|
||||||
|
const SettingsType = require('../ts/types/Settings');
|
||||||
|
|
||||||
|
// Views
|
||||||
|
const Initialization = require('./modules/views/initialization');
|
||||||
|
|
||||||
|
// Workflow
|
||||||
|
const { IdleDetector } = require('./modules/idle_detector');
|
||||||
|
const MessageDataMigrator = require('./modules/messages_data_migrator');
|
||||||
|
|
||||||
|
exports.setup = (options = {}) => {
|
||||||
|
const { Attachments, userDataPath } = options;
|
||||||
|
|
||||||
|
const Components = {
|
||||||
|
ContactDetail,
|
||||||
|
EmbeddedContact,
|
||||||
|
Lightbox,
|
||||||
|
LightboxGallery,
|
||||||
|
MediaGallery,
|
||||||
|
Types: {
|
||||||
|
Message: MediaGalleryMessage,
|
||||||
|
},
|
||||||
|
Quote,
|
||||||
|
};
|
||||||
|
|
||||||
|
const attachmentsPath = Attachments.getPath(userDataPath);
|
||||||
|
const readAttachmentData = Attachments.createReader(attachmentsPath);
|
||||||
|
const loadAttachmentData = AttachmentType.loadData(readAttachmentData);
|
||||||
|
|
||||||
|
const Migrations = {
|
||||||
|
attachmentsPath,
|
||||||
|
deleteAttachmentData: AttachmentType.deleteData(
|
||||||
|
Attachments.createDeleter(attachmentsPath)
|
||||||
|
),
|
||||||
|
getAbsoluteAttachmentPath: Attachments.createAbsolutePathGetter(
|
||||||
|
attachmentsPath
|
||||||
|
),
|
||||||
|
getPlaceholderMigrations,
|
||||||
|
loadAttachmentData,
|
||||||
|
loadMessage: Message.createAttachmentLoader(loadAttachmentData),
|
||||||
|
Migrations0DatabaseWithAttachmentData,
|
||||||
|
Migrations1DatabaseWithoutAttachmentData,
|
||||||
|
upgradeMessageSchema: message =>
|
||||||
|
Message.upgradeSchema(message, {
|
||||||
|
writeNewAttachmentData: Attachments.createWriterForNew(attachmentsPath),
|
||||||
|
}),
|
||||||
|
writeMessageAttachments: Message.createAttachmentDataWriter(
|
||||||
|
Attachments.createWriterForExisting(attachmentsPath)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const Types = {
|
||||||
|
Attachment: AttachmentType,
|
||||||
|
Contact,
|
||||||
|
Conversation,
|
||||||
|
Errors,
|
||||||
|
Message,
|
||||||
|
MIME,
|
||||||
|
Settings: SettingsType,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Views = {
|
||||||
|
Initialization,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Workflow = {
|
||||||
|
IdleDetector,
|
||||||
|
MessageDataMigrator,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
Backbone,
|
||||||
|
Components,
|
||||||
|
Crypto,
|
||||||
|
Database,
|
||||||
|
HTML,
|
||||||
|
Migrations,
|
||||||
|
Notifications,
|
||||||
|
OS,
|
||||||
|
Settings,
|
||||||
|
Startup,
|
||||||
|
Types,
|
||||||
|
Util,
|
||||||
|
Views,
|
||||||
|
Workflow,
|
||||||
|
};
|
||||||
|
};
|
|
@ -146,6 +146,16 @@
|
||||||
'reply',
|
'reply',
|
||||||
this.setQuoteMessage
|
this.setQuoteMessage
|
||||||
);
|
);
|
||||||
|
this.listenTo(
|
||||||
|
this.model.messageCollection,
|
||||||
|
'show-contact-detail',
|
||||||
|
this.showContactDetail
|
||||||
|
);
|
||||||
|
this.listenTo(
|
||||||
|
this.model.messageCollection,
|
||||||
|
'open-conversation',
|
||||||
|
this.openConversation
|
||||||
|
);
|
||||||
|
|
||||||
this.lazyUpdateVerified = _.debounce(
|
this.lazyUpdateVerified = _.debounce(
|
||||||
this.model.updateVerified.bind(this.model),
|
this.model.updateVerified.bind(this.model),
|
||||||
|
@ -996,6 +1006,41 @@
|
||||||
this.listenBack(view);
|
this.listenBack(view);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showContactDetail(contact) {
|
||||||
|
const regionCode = storage.get('regionCode');
|
||||||
|
const { contactSelector } = Signal.Types.Contact;
|
||||||
|
const { getAbsoluteAttachmentPath } = window.Signal.Migrations;
|
||||||
|
|
||||||
|
const view = new Whisper.ReactWrapperView({
|
||||||
|
Component: Signal.Components.ContactDetail,
|
||||||
|
props: {
|
||||||
|
contact: contactSelector(contact, {
|
||||||
|
regionCode,
|
||||||
|
getAbsoluteAttachmentPath,
|
||||||
|
}),
|
||||||
|
hasSignalAccount: true,
|
||||||
|
onSendMessage: () => {
|
||||||
|
const number =
|
||||||
|
contact.number && contact.number[0] && contact.number[0].value;
|
||||||
|
if (number) {
|
||||||
|
this.openConversation(number);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onClose: () => this.resetPanel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.listenBack(view);
|
||||||
|
},
|
||||||
|
|
||||||
|
async openConversation(number) {
|
||||||
|
const conversation = await window.ConversationController.getOrCreateAndWait(
|
||||||
|
number,
|
||||||
|
'private'
|
||||||
|
);
|
||||||
|
window.Whisper.events.trigger('showConversation', conversation);
|
||||||
|
},
|
||||||
|
|
||||||
listenBack(view) {
|
listenBack(view) {
|
||||||
this.panels = this.panels || [];
|
this.panels = this.panels || [];
|
||||||
if (this.panels.length > 0) {
|
if (this.panels.length > 0) {
|
||||||
|
@ -1199,7 +1244,6 @@
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
const quote = await this.model.makeQuote(this.quotedMessage);
|
const quote = await this.model.makeQuote(this.quotedMessage);
|
||||||
console.log({ quote });
|
|
||||||
this.quote = quote;
|
this.quote = quote;
|
||||||
|
|
||||||
this.focusMessageFieldAndClearDisabled();
|
this.focusMessageFieldAndClearDisabled();
|
||||||
|
|
|
@ -5,13 +5,17 @@
|
||||||
/* global emoji_util: false */
|
/* global emoji_util: false */
|
||||||
/* global Mustache: false */
|
/* global Mustache: false */
|
||||||
/* global $: false */
|
/* global $: false */
|
||||||
|
/* global storage: false */
|
||||||
|
/* global Signal: false */
|
||||||
|
|
||||||
// eslint-disable-next-line func-names
|
// eslint-disable-next-line func-names
|
||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { Signal } = window;
|
const {
|
||||||
const { loadAttachmentData } = window.Signal.Migrations;
|
loadAttachmentData,
|
||||||
|
getAbsoluteAttachmentPath,
|
||||||
|
} = window.Signal.Migrations;
|
||||||
|
|
||||||
window.Whisper = window.Whisper || {};
|
window.Whisper = window.Whisper || {};
|
||||||
|
|
||||||
|
@ -290,6 +294,9 @@
|
||||||
if (this.quoteView) {
|
if (this.quoteView) {
|
||||||
this.quoteView.remove();
|
this.quoteView.remove();
|
||||||
}
|
}
|
||||||
|
if (this.contactView) {
|
||||||
|
this.contactView.remove();
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE: We have to do this in the background (`then` instead of `await`)
|
// NOTE: We have to do this in the background (`then` instead of `await`)
|
||||||
// as our tests rely on `onUnload` synchronously removing the view from
|
// as our tests rely on `onUnload` synchronously removing the view from
|
||||||
|
@ -436,6 +443,73 @@
|
||||||
});
|
});
|
||||||
this.$('.inner-bubble').prepend(this.quoteView.el);
|
this.$('.inner-bubble').prepend(this.quoteView.el);
|
||||||
},
|
},
|
||||||
|
renderContact() {
|
||||||
|
const contacts = this.model.get('contact');
|
||||||
|
if (!contacts || !contacts.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const contact = contacts[0];
|
||||||
|
|
||||||
|
const regionCode = storage.get('regionCode');
|
||||||
|
const { contactSelector } = Signal.Types.Contact;
|
||||||
|
|
||||||
|
const number =
|
||||||
|
contact.number && contact.number[0] && contact.number[0].value;
|
||||||
|
const haveConversation =
|
||||||
|
number && Boolean(window.ConversationController.get(number));
|
||||||
|
const hasLocalSignalAccount = number && haveConversation;
|
||||||
|
|
||||||
|
const onSendMessage = number
|
||||||
|
? () => {
|
||||||
|
this.model.trigger('open-conversation', number);
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
const onOpenContact = () => {
|
||||||
|
this.model.trigger('show-contact-detail', contact);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProps = ({ hasSignalAccount }) => ({
|
||||||
|
contact: contactSelector(contact, {
|
||||||
|
regionCode,
|
||||||
|
getAbsoluteAttachmentPath,
|
||||||
|
}),
|
||||||
|
hasSignalAccount,
|
||||||
|
onSendMessage,
|
||||||
|
onOpenContact,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.contactView) {
|
||||||
|
this.contactView.remove();
|
||||||
|
this.contactView = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.contactView = new Whisper.ReactWrapperView({
|
||||||
|
className: 'contact-wrapper',
|
||||||
|
Component: window.Signal.Components.EmbeddedContact,
|
||||||
|
props: getProps({
|
||||||
|
hasSignalAccount: hasLocalSignalAccount,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$('.inner-bubble').prepend(this.contactView.el);
|
||||||
|
|
||||||
|
// If we can't verify a signal account locally, we'll go to the Signal Server.
|
||||||
|
if (number && !hasLocalSignalAccount) {
|
||||||
|
// eslint-disable-next-line more/no-then
|
||||||
|
window.textsecure.messaging
|
||||||
|
.getProfile(number)
|
||||||
|
.then(() => {
|
||||||
|
if (!this.contactView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.contactView.update(getProps({ hasSignalAccount: true }));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// No account available, or network connectivity problem
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
isImageWithoutCaption() {
|
isImageWithoutCaption() {
|
||||||
const attachments = this.model.get('attachments');
|
const attachments = this.model.get('attachments');
|
||||||
const body = this.model.get('body');
|
const body = this.model.get('body');
|
||||||
|
@ -458,7 +532,10 @@
|
||||||
const attachments = this.model.get('attachments');
|
const attachments = this.model.get('attachments');
|
||||||
const hasAttachments = attachments && attachments.length > 0;
|
const hasAttachments = attachments && attachments.length > 0;
|
||||||
|
|
||||||
return this.hasTextContents() || hasAttachments;
|
const contacts = this.model.get('contact');
|
||||||
|
const hasContact = contacts && contacts.length > 0;
|
||||||
|
|
||||||
|
return this.hasTextContents() || hasAttachments || hasContact;
|
||||||
},
|
},
|
||||||
hasTextContents() {
|
hasTextContents() {
|
||||||
const body = this.model.get('body');
|
const body = this.model.get('body');
|
||||||
|
@ -525,6 +602,7 @@
|
||||||
this.renderErrors();
|
this.renderErrors();
|
||||||
this.renderExpiring();
|
this.renderExpiring();
|
||||||
this.renderQuote();
|
this.renderQuote();
|
||||||
|
this.renderContact();
|
||||||
|
|
||||||
// NOTE: We have to do this in the background (`then` instead of `await`)
|
// NOTE: We have to do this in the background (`then` instead of `await`)
|
||||||
// as our code / Backbone seems to rely on `render` synchronously returning
|
// as our code / Backbone seems to rely on `render` synchronously returning
|
||||||
|
|
|
@ -1065,6 +1065,14 @@ MessageReceiver.prototype.extend({
|
||||||
promises.push(this.handleAttachment(attachment));
|
promises.push(this.handleAttachment(attachment));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
decrypted.contact &&
|
||||||
|
decrypted.contact.avatar &&
|
||||||
|
decrypted.contact.avatar.avatar
|
||||||
|
) {
|
||||||
|
promises.push(this.handleAttachment(decrypted.contact.avatar.avatar));
|
||||||
|
}
|
||||||
|
|
||||||
if (decrypted.quote && decrypted.quote.id) {
|
if (decrypted.quote && decrypted.quote.id) {
|
||||||
decrypted.quote.id = decrypted.quote.id.toNumber();
|
decrypted.quote.id = decrypted.quote.id.toNumber();
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sindresorhus/is": "^0.8.0",
|
"@sindresorhus/is": "^0.8.0",
|
||||||
|
"@types/google-libphonenumber": "^7.4.14",
|
||||||
"archiver": "^2.1.1",
|
"archiver": "^2.1.1",
|
||||||
"blob-util": "^1.3.0",
|
"blob-util": "^1.3.0",
|
||||||
"blueimp-canvas-to-blob": "^3.14.0",
|
"blueimp-canvas-to-blob": "^3.14.0",
|
||||||
|
|
105
preload.js
105
preload.js
|
@ -5,9 +5,6 @@ console.log('preload');
|
||||||
|
|
||||||
const electron = require('electron');
|
const electron = require('electron');
|
||||||
|
|
||||||
const Attachment = require('./js/modules/types/attachment');
|
|
||||||
const Attachments = require('./app/attachments');
|
|
||||||
const Message = require('./js/modules/types/message');
|
|
||||||
const { deferredToPromise } = require('./js/modules/deferred_to_promise');
|
const { deferredToPromise } = require('./js/modules/deferred_to_promise');
|
||||||
|
|
||||||
const { app } = electron.remote;
|
const { app } = electron.remote;
|
||||||
|
@ -114,10 +111,12 @@ window.React = require('react');
|
||||||
window.ReactDOM = require('react-dom');
|
window.ReactDOM = require('react-dom');
|
||||||
window.moment = require('moment');
|
window.moment = require('moment');
|
||||||
|
|
||||||
const { setup } = require('./js/modules/i18n');
|
const Signal = require('./js/signal');
|
||||||
|
const i18n = require('./js/modules/i18n');
|
||||||
|
const Attachments = require('./app/attachments');
|
||||||
|
|
||||||
const { locale, localeMessages } = window.config;
|
const { locale, localeMessages } = window.config;
|
||||||
window.i18n = setup(locale, localeMessages);
|
window.i18n = i18n.setup(locale, localeMessages);
|
||||||
window.moment.updateLocale(locale, {
|
window.moment.updateLocale(locale, {
|
||||||
relativeTime: {
|
relativeTime: {
|
||||||
s: window.i18n('timestamp_s'),
|
s: window.i18n('timestamp_s'),
|
||||||
|
@ -127,100 +126,16 @@ window.moment.updateLocale(locale, {
|
||||||
});
|
});
|
||||||
window.moment.locale(locale);
|
window.moment.locale(locale);
|
||||||
|
|
||||||
// ES2015+ modules
|
window.Signal = Signal.setup({
|
||||||
const attachmentsPath = Attachments.getPath(app.getPath('userData'));
|
Attachments,
|
||||||
const getAbsoluteAttachmentPath = Attachments.createAbsolutePathGetter(
|
userDataPath: app.getPath('userData'),
|
||||||
attachmentsPath
|
});
|
||||||
);
|
|
||||||
const deleteAttachmentData = Attachments.createDeleter(attachmentsPath);
|
|
||||||
const readAttachmentData = Attachments.createReader(attachmentsPath);
|
|
||||||
const writeNewAttachmentData = Attachments.createWriterForNew(attachmentsPath);
|
|
||||||
const writeExistingAttachmentData = Attachments.createWriterForExisting(
|
|
||||||
attachmentsPath
|
|
||||||
);
|
|
||||||
|
|
||||||
const loadAttachmentData = Attachment.loadData(readAttachmentData);
|
// Pulling these in separately since they access filesystem, electron
|
||||||
|
|
||||||
// Injected context functions to keep `Message` agnostic from Electron:
|
|
||||||
const upgradeSchemaContext = {
|
|
||||||
writeNewAttachmentData,
|
|
||||||
};
|
|
||||||
const upgradeMessageSchema = message =>
|
|
||||||
Message.upgradeSchema(message, upgradeSchemaContext);
|
|
||||||
|
|
||||||
const {
|
|
||||||
getPlaceholderMigrations,
|
|
||||||
} = require('./js/modules/migrations/get_placeholder_migrations');
|
|
||||||
const { IdleDetector } = require('./js/modules/idle_detector');
|
|
||||||
|
|
||||||
window.Signal = {};
|
|
||||||
window.Signal.Backbone = require('./ts/backbone');
|
|
||||||
window.Signal.Backup = require('./js/modules/backup');
|
window.Signal.Backup = require('./js/modules/backup');
|
||||||
window.Signal.Crypto = require('./js/modules/crypto');
|
|
||||||
window.Signal.Database = require('./js/modules/database');
|
|
||||||
window.Signal.Debug = require('./js/modules/debug');
|
window.Signal.Debug = require('./js/modules/debug');
|
||||||
window.Signal.HTML = require('./ts/html');
|
|
||||||
window.Signal.Logs = require('./js/modules/logs');
|
window.Signal.Logs = require('./js/modules/logs');
|
||||||
|
|
||||||
// React components
|
|
||||||
const { Lightbox } = require('./ts/components/Lightbox');
|
|
||||||
const { LightboxGallery } = require('./ts/components/LightboxGallery');
|
|
||||||
const {
|
|
||||||
MediaGallery,
|
|
||||||
} = require('./ts/components/conversation/media-gallery/MediaGallery');
|
|
||||||
const { Quote } = require('./ts/components/conversation/Quote');
|
|
||||||
|
|
||||||
const MediaGalleryMessage = require('./ts/components/conversation/media-gallery/types/Message');
|
|
||||||
|
|
||||||
window.Signal.Components = {
|
|
||||||
Lightbox,
|
|
||||||
LightboxGallery,
|
|
||||||
MediaGallery,
|
|
||||||
Types: {
|
|
||||||
Message: MediaGalleryMessage,
|
|
||||||
},
|
|
||||||
Quote,
|
|
||||||
};
|
|
||||||
|
|
||||||
window.Signal.Migrations = {};
|
|
||||||
window.Signal.Migrations.deleteAttachmentData = Attachment.deleteData(
|
|
||||||
deleteAttachmentData
|
|
||||||
);
|
|
||||||
window.Signal.Migrations.getPlaceholderMigrations = getPlaceholderMigrations;
|
|
||||||
window.Signal.Migrations.writeMessageAttachments = Message.createAttachmentDataWriter(
|
|
||||||
writeExistingAttachmentData
|
|
||||||
);
|
|
||||||
window.Signal.Migrations.getAbsoluteAttachmentPath = getAbsoluteAttachmentPath;
|
|
||||||
window.Signal.Migrations.loadAttachmentData = loadAttachmentData;
|
|
||||||
window.Signal.Migrations.loadMessage = Message.createAttachmentLoader(
|
|
||||||
loadAttachmentData
|
|
||||||
);
|
|
||||||
window.Signal.Migrations.Migrations0DatabaseWithAttachmentData = require('./js/modules/migrations/migrations_0_database_with_attachment_data');
|
|
||||||
window.Signal.Migrations.Migrations1DatabaseWithoutAttachmentData = require('./js/modules/migrations/migrations_1_database_without_attachment_data');
|
|
||||||
|
|
||||||
window.Signal.Migrations.upgradeMessageSchema = upgradeMessageSchema;
|
|
||||||
window.Signal.Notifications = require('./ts/notifications');
|
|
||||||
window.Signal.OS = require('./ts/OS');
|
|
||||||
window.Signal.Settings = require('./js/modules/settings');
|
|
||||||
window.Signal.Startup = require('./js/modules/startup');
|
|
||||||
|
|
||||||
window.Signal.Types = {};
|
|
||||||
window.Signal.Types.Attachment = Attachment;
|
|
||||||
window.Signal.Types.Conversation = require('./ts/types/Conversation');
|
|
||||||
window.Signal.Types.Errors = require('./js/modules/types/errors');
|
|
||||||
|
|
||||||
window.Signal.Types.Message = Message;
|
|
||||||
window.Signal.Types.MIME = require('./ts/types/MIME');
|
|
||||||
window.Signal.Types.Settings = require('./ts/types/Settings');
|
|
||||||
window.Signal.Util = require('./ts/util');
|
|
||||||
|
|
||||||
window.Signal.Views = {};
|
|
||||||
window.Signal.Views.Initialization = require('./js/modules/views/initialization');
|
|
||||||
|
|
||||||
window.Signal.Workflow = {};
|
|
||||||
window.Signal.Workflow.IdleDetector = IdleDetector;
|
|
||||||
window.Signal.Workflow.MessageDataMigrator = require('./js/modules/messages_data_migrator');
|
|
||||||
|
|
||||||
// We pull this in last, because the native module involved appears to be sensitive to
|
// We pull this in last, because the native module involved appears to be sensitive to
|
||||||
// /tmp mounted as noexec on Linux.
|
// /tmp mounted as noexec on Linux.
|
||||||
require('./js/spell_check');
|
require('./js/spell_check');
|
||||||
|
@ -233,7 +148,7 @@ if (window.config.environment === 'test') {
|
||||||
tmp: require('tmp'),
|
tmp: require('tmp'),
|
||||||
path: require('path'),
|
path: require('path'),
|
||||||
basePath: __dirname,
|
basePath: __dirname,
|
||||||
attachmentsPath,
|
attachmentsPath: window.Signal.Migrations.attachmentsPath,
|
||||||
};
|
};
|
||||||
/* eslint-enable global-require, import/no-extraneous-dependencies */
|
/* eslint-enable global-require, import/no-extraneous-dependencies */
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,6 +84,73 @@ message DataMessage {
|
||||||
repeated QuotedAttachment attachments = 4;
|
repeated QuotedAttachment attachments = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message Contact {
|
||||||
|
message Name {
|
||||||
|
optional string givenName = 1;
|
||||||
|
optional string familyName = 2;
|
||||||
|
optional string prefix = 3;
|
||||||
|
optional string suffix = 4;
|
||||||
|
optional string middleName = 5;
|
||||||
|
optional string displayName = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Phone {
|
||||||
|
enum Type {
|
||||||
|
HOME = 1;
|
||||||
|
MOBILE = 2;
|
||||||
|
WORK = 3;
|
||||||
|
CUSTOM = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional string value = 1;
|
||||||
|
optional Type type = 2;
|
||||||
|
optional string label = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Email {
|
||||||
|
enum Type {
|
||||||
|
HOME = 1;
|
||||||
|
MOBILE = 2;
|
||||||
|
WORK = 3;
|
||||||
|
CUSTOM = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional string value = 1;
|
||||||
|
optional Type type = 2;
|
||||||
|
optional string label = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PostalAddress {
|
||||||
|
enum Type {
|
||||||
|
HOME = 1;
|
||||||
|
WORK = 2;
|
||||||
|
CUSTOM = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional Type type = 1;
|
||||||
|
optional string label = 2;
|
||||||
|
optional string street = 3;
|
||||||
|
optional string pobox = 4;
|
||||||
|
optional string neighborhood = 5;
|
||||||
|
optional string city = 6;
|
||||||
|
optional string region = 7;
|
||||||
|
optional string postcode = 8;
|
||||||
|
optional string country = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Avatar {
|
||||||
|
optional AttachmentPointer avatar = 1;
|
||||||
|
optional bool isProfile = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional Name name = 1;
|
||||||
|
repeated Phone number = 3;
|
||||||
|
repeated Email email = 4;
|
||||||
|
repeated PostalAddress address = 5;
|
||||||
|
optional Avatar avatar = 6;
|
||||||
|
optional string organization = 7;
|
||||||
|
}
|
||||||
|
|
||||||
optional string body = 1;
|
optional string body = 1;
|
||||||
repeated AttachmentPointer attachments = 2;
|
repeated AttachmentPointer attachments = 2;
|
||||||
optional GroupContext group = 3;
|
optional GroupContext group = 3;
|
||||||
|
@ -92,6 +159,7 @@ message DataMessage {
|
||||||
optional bytes profileKey = 6;
|
optional bytes profileKey = 6;
|
||||||
optional uint64 timestamp = 7;
|
optional uint64 timestamp = 7;
|
||||||
optional Quote quote = 8;
|
optional Quote quote = 8;
|
||||||
|
repeated Contact contact = 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
message NullMessage {
|
message NullMessage {
|
||||||
|
|
|
@ -526,6 +526,9 @@ span.status {
|
||||||
.quote-wrapper + .content {
|
.quote-wrapper + .content {
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
}
|
}
|
||||||
|
.contact-wrapper + .content {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -740,6 +743,221 @@ span.status {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.embedded-contact {
|
||||||
|
margin-top: -9px;
|
||||||
|
margin-left: -12px;
|
||||||
|
margin-right: -12px;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
button {
|
||||||
|
@include button-reset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.first-line {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
margin: 8px;
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
flex: initial;
|
||||||
|
min-width: 50px;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
object-fit: cover;
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-avatar {
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: gray;
|
||||||
|
color: white;
|
||||||
|
font-size: 25px;
|
||||||
|
line-height: 52px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-container {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-left: 8px;
|
||||||
|
|
||||||
|
.contact-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 300;
|
||||||
|
margin-top: 3px;
|
||||||
|
color: $blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-method {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-message {
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
padding: 11px;
|
||||||
|
border-top: 1px solid $grey_l1_5;
|
||||||
|
border-bottom: 1px solid $grey_l1_5;
|
||||||
|
|
||||||
|
color: $blue;
|
||||||
|
font-weight: 300;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-icon {
|
||||||
|
height: 17px;
|
||||||
|
width: 18px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 5px;
|
||||||
|
@include color-svg('../images/chat-bubble.svg', $blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.incoming .embedded-contact {
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
.text-container .contact-name {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-message {
|
||||||
|
color: white;
|
||||||
|
// We would like to use these border colors for incoming messages, but the version
|
||||||
|
// of Chromium in our Electron version doesn't render these appropriately. Both
|
||||||
|
// borders disappear for some reason, and it seems to have to do with transparency.
|
||||||
|
// border-top: 1px solid rgba(255, 255, 255, 0.5);
|
||||||
|
// border-bottom: 1px solid rgba(255, 255, 255, 0.5);
|
||||||
|
|
||||||
|
.bubble-icon {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group .incoming .embedded-contact {
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-detail {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 300px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
|
||||||
|
button {
|
||||||
|
@include button-reset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
height: 80px;
|
||||||
|
width: 80px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
display: inline-block;
|
||||||
|
object-fit: cover;
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.default-avatar {
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: gray;
|
||||||
|
color: white;
|
||||||
|
font-size: 50px;
|
||||||
|
line-height: 82px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-name {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-method {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-message {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: $blue;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 6px;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
// TODO: border
|
||||||
|
// TODO: gradient
|
||||||
|
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-icon {
|
||||||
|
height: 17px;
|
||||||
|
width: 18px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 5px;
|
||||||
|
@include color-svg('../images/chat-bubble.svg', white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.additional-contact {
|
||||||
|
text-align: left;
|
||||||
|
border-top: 1px solid $grey_l1_5;
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 8px;
|
||||||
|
|
||||||
|
.type {
|
||||||
|
color: rgba(0, 0, 0, 0.5);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation .contact-detail {
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
.quoted-message {
|
.quoted-message {
|
||||||
@include message-replies-colors;
|
@include message-replies-colors;
|
||||||
@include twenty-percent-colors;
|
@include twenty-percent-colors;
|
||||||
|
|
|
@ -191,7 +191,6 @@ input.search {
|
||||||
.last-message {
|
.last-message {
|
||||||
margin: 6px 0 0;
|
margin: 6px 0 0;
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
font-weight: 300;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.gutter .timestamp {
|
.gutter .timestamp {
|
||||||
|
|
|
@ -118,6 +118,57 @@ $ios-border-color: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.embedded-contact {
|
||||||
|
margin: 0;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
.first-line {
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.text-container {
|
||||||
|
.contact-name {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-message {
|
||||||
|
font-weight: normal;
|
||||||
|
border-bottom: none;
|
||||||
|
margin-top: 0;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
.bubble-icon {
|
||||||
|
@include color-svg('../images/chat-bubble-outline.svg', white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.incoming .embedded-contact {
|
||||||
|
.first-line {
|
||||||
|
.text-container {
|
||||||
|
.contact-name {
|
||||||
|
color: $blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
color: black;
|
||||||
|
|
||||||
|
.send-message {
|
||||||
|
color: $blue;
|
||||||
|
|
||||||
|
.bubble-icon {
|
||||||
|
@include color-svg('../images/chat-bubble-outline.svg', $blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.embedded-contact + .content {
|
||||||
|
border-bottom: 1px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
.quoted-message {
|
.quoted-message {
|
||||||
// Not ideal, but necessary to override the specificity of the android theme color
|
// Not ideal, but necessary to override the specificity of the android theme color
|
||||||
// classes used in conversations.scss
|
// classes used in conversations.scss
|
||||||
|
@ -378,10 +429,7 @@ $ios-border-color: rgba(0, 0, 0, 0.1);
|
||||||
// bubble wider than an attached image, and we need a background color on the bottom
|
// bubble wider than an attached image, and we need a background color on the bottom
|
||||||
// section if the image doesn't cover it all.
|
// section if the image doesn't cover it all.
|
||||||
.outgoing .tail-wrapper {
|
.outgoing .tail-wrapper {
|
||||||
.attachments {
|
.inner-bubble {
|
||||||
background-color: $blue;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
background-color: $blue;
|
background-color: $blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,19 @@
|
||||||
|
@mixin button-reset {
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
@mixin color-svg($svg, $color) {
|
@mixin color-svg($svg, $color) {
|
||||||
-webkit-mask: url($svg) no-repeat center;
|
-webkit-mask: url($svg) no-repeat center;
|
||||||
-webkit-mask-size: 100%;
|
-webkit-mask-size: 100%;
|
||||||
background-color: $color;
|
background-color: $color;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin header-icon-white($svg) {
|
@mixin header-icon-white($svg) {
|
||||||
@include color-svg($svg, rgba(255, 255, 255, 0.8));
|
@include color-svg($svg, rgba(255, 255, 255, 0.8));
|
||||||
&:focus,
|
&:focus,
|
||||||
|
@ -17,6 +28,7 @@
|
||||||
@include color-svg($svg, black);
|
@include color-svg($svg, black);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin avatar-colors {
|
@mixin avatar-colors {
|
||||||
&.red {
|
&.red {
|
||||||
background-color: $material_red;
|
background-color: $material_red;
|
||||||
|
|
|
@ -22,6 +22,11 @@ $z-index-modal: 100;
|
||||||
font-family: 'Roboto';
|
font-family: 'Roboto';
|
||||||
src: url('../fonts/Roboto-Regular.ttf') format('truetype');
|
src: url('../fonts/Roboto-Regular.ttf') format('truetype');
|
||||||
}
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
src: url('../fonts/Roboto-Medium.ttf') format('truetype');
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Roboto';
|
font-family: 'Roboto';
|
||||||
src: url('../fonts/Roboto-Italic.ttf') format('truetype');
|
src: url('../fonts/Roboto-Italic.ttf') format('truetype');
|
||||||
|
|
|
@ -239,6 +239,51 @@ $text-dark_l2: darken($text-dark, 30%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.embedded-contact {
|
||||||
|
.first-line {
|
||||||
|
.image-container {
|
||||||
|
.default-avatar {
|
||||||
|
background-color: gray;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-container .contact-name {
|
||||||
|
color: $blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-message {
|
||||||
|
color: $blue;
|
||||||
|
border-top: 1px solid $grey;
|
||||||
|
border-bottom: 1px solid $grey;
|
||||||
|
|
||||||
|
.bubble-icon {
|
||||||
|
@include color-svg('../images/chat-bubble.svg', $blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.incoming .embedded-contact {
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
.text-container .contact-name {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-message {
|
||||||
|
color: white;
|
||||||
|
// Note: would like to use transparency here, but Chromium in Electron doesn't
|
||||||
|
// render the borders when they are transparent.
|
||||||
|
border-top: 1px solid $grey_l1_5;
|
||||||
|
border-bottom: 1px solid $grey_l1_5;
|
||||||
|
|
||||||
|
.bubble-icon {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.outgoing .quoted-message {
|
.outgoing .quoted-message {
|
||||||
background: rgba(255, 255, 255, 0.38);
|
background: rgba(255, 255, 255, 0.38);
|
||||||
|
|
||||||
|
|
|
@ -283,6 +283,7 @@ describe('Backup', () => {
|
||||||
|
|
||||||
const OUR_NUMBER = '+12025550000';
|
const OUR_NUMBER = '+12025550000';
|
||||||
const CONTACT_ONE_NUMBER = '+12025550001';
|
const CONTACT_ONE_NUMBER = '+12025550001';
|
||||||
|
const CONTACT_TWO_NUMBER = '+12025550002';
|
||||||
|
|
||||||
async function wrappedLoadAttachment(attachment) {
|
async function wrappedLoadAttachment(attachment) {
|
||||||
return _.omit(await loadAttachmentData(attachment), ['path']);
|
return _.omit(await loadAttachmentData(attachment), ['path']);
|
||||||
|
@ -356,18 +357,31 @@ describe('Backup', () => {
|
||||||
return wrappedLoadAttachment(thumbnail);
|
return wrappedLoadAttachment(thumbnail);
|
||||||
});
|
});
|
||||||
|
|
||||||
const promises = (message.attachments || []).map(attachment =>
|
|
||||||
wrappedLoadAttachment(attachment)
|
|
||||||
);
|
|
||||||
|
|
||||||
return Object.assign({}, await loadThumbnails(message), {
|
return Object.assign({}, await loadThumbnails(message), {
|
||||||
attachments: await Promise.all(promises),
|
contact: await Promise.all(
|
||||||
|
(message.contact || []).map(async contact => {
|
||||||
|
return contact && contact.avatar && contact.avatar.avatar
|
||||||
|
? Object.assign({}, contact, {
|
||||||
|
avatar: Object.assign({}, contact.avatar, {
|
||||||
|
avatar: await wrappedLoadAttachment(
|
||||||
|
contact.avatar.avatar
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
: contact;
|
||||||
|
})
|
||||||
|
),
|
||||||
|
attachments: await Promise.all(
|
||||||
|
(message.attachments || []).map(attachment =>
|
||||||
|
wrappedLoadAttachment(attachment)
|
||||||
|
)
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let backupDir;
|
let backupDir;
|
||||||
try {
|
try {
|
||||||
const ATTACHMENT_COUNT = 2;
|
const ATTACHMENT_COUNT = 3;
|
||||||
const MESSAGE_COUNT = 1;
|
const MESSAGE_COUNT = 1;
|
||||||
const CONVERSATION_COUNT = 1;
|
const CONVERSATION_COUNT = 1;
|
||||||
|
|
||||||
|
@ -473,6 +487,59 @@ describe('Backup', () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
contact: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone Somewhere',
|
||||||
|
},
|
||||||
|
number: [
|
||||||
|
{
|
||||||
|
value: CONTACT_TWO_NUMBER,
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
avatar: {
|
||||||
|
isProfile: false,
|
||||||
|
avatar: {
|
||||||
|
contentType: 'image/png',
|
||||||
|
data: new Uint8Array([
|
||||||
|
3,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
8,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
8,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
8,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
8,
|
||||||
|
]).buffer,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Backup test: Clear all data');
|
console.log('Backup test: Clear all data');
|
||||||
|
@ -494,7 +561,7 @@ describe('Backup', () => {
|
||||||
profileAvatar: {
|
profileAvatar: {
|
||||||
contentType: 'image/jpeg',
|
contentType: 'image/jpeg',
|
||||||
data: new Uint8Array([
|
data: new Uint8Array([
|
||||||
3,
|
4,
|
||||||
2,
|
2,
|
||||||
3,
|
3,
|
||||||
4,
|
4,
|
||||||
|
@ -530,7 +597,7 @@ describe('Backup', () => {
|
||||||
size: 64,
|
size: 64,
|
||||||
},
|
},
|
||||||
profileKey: new Uint8Array([
|
profileKey: new Uint8Array([
|
||||||
4,
|
5,
|
||||||
2,
|
2,
|
||||||
3,
|
3,
|
||||||
4,
|
4,
|
||||||
|
|
338
test/modules/types/contact_test.js
Normal file
338
test/modules/types/contact_test.js
Normal file
|
@ -0,0 +1,338 @@
|
||||||
|
const { assert } = require('chai');
|
||||||
|
const sinon = require('sinon');
|
||||||
|
|
||||||
|
const Contact = require('../../../js/modules/types/contact');
|
||||||
|
const {
|
||||||
|
stringToArrayBuffer,
|
||||||
|
} = require('../../../js/modules/string_to_array_buffer');
|
||||||
|
|
||||||
|
describe('Contact', () => {
|
||||||
|
const NUMBER = '+12025550099';
|
||||||
|
|
||||||
|
describe('parseAndWriteAvatar', () => {
|
||||||
|
it('handles message with no avatar in contact', async () => {
|
||||||
|
const upgradeAttachment = sinon
|
||||||
|
.stub()
|
||||||
|
.throws(new Error("Shouldn't be called"));
|
||||||
|
const upgradeVersion = Contact.parseAndWriteAvatar(upgradeAttachment);
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
body: 'hey there!',
|
||||||
|
contact: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone Somewhere',
|
||||||
|
},
|
||||||
|
number: [
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
value: NUMBER,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const result = await upgradeVersion(message.contact[0], { message });
|
||||||
|
assert.deepEqual(result, message.contact[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes contact avatar if it has no sub-avatar', async () => {
|
||||||
|
const upgradeAttachment = sinon
|
||||||
|
.stub()
|
||||||
|
.throws(new Error("Shouldn't be called"));
|
||||||
|
const upgradeVersion = Contact.parseAndWriteAvatar(upgradeAttachment);
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
body: 'hey there!',
|
||||||
|
contact: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone Somewhere',
|
||||||
|
},
|
||||||
|
number: [
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
value: NUMBER,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
avatar: {
|
||||||
|
isProfile: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone Somewhere',
|
||||||
|
},
|
||||||
|
number: [
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
value: NUMBER,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const result = await upgradeVersion(message.contact[0], { message });
|
||||||
|
assert.deepEqual(result, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('writes avatar to disk', async () => {
|
||||||
|
const upgradeAttachment = async () => {
|
||||||
|
return {
|
||||||
|
path: 'abc/abcdefg',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const upgradeVersion = Contact.parseAndWriteAvatar(upgradeAttachment);
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
body: 'hey there!',
|
||||||
|
contact: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone Somewhere',
|
||||||
|
},
|
||||||
|
number: [
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
value: NUMBER,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
value: 'someone@somewhere.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
address: [
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
street: '5 Somewhere Ave.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
avatar: {
|
||||||
|
otherKey: 'otherValue',
|
||||||
|
avatar: {
|
||||||
|
contentType: 'image/png',
|
||||||
|
data: stringToArrayBuffer('It’s easy if you try'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone Somewhere',
|
||||||
|
},
|
||||||
|
number: [
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
value: NUMBER,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
value: 'someone@somewhere.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
address: [
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
street: '5 Somewhere Ave.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
avatar: {
|
||||||
|
otherKey: 'otherValue',
|
||||||
|
isProfile: false,
|
||||||
|
avatar: {
|
||||||
|
path: 'abc/abcdefg',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await upgradeVersion(message.contact[0], { message });
|
||||||
|
assert.deepEqual(result, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes number element if it ends up with no value', async () => {
|
||||||
|
const upgradeAttachment = sinon
|
||||||
|
.stub()
|
||||||
|
.throws(new Error("Shouldn't be called"));
|
||||||
|
const upgradeVersion = Contact.parseAndWriteAvatar(upgradeAttachment);
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
body: 'hey there!',
|
||||||
|
contact: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone Somewhere',
|
||||||
|
},
|
||||||
|
number: [
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{
|
||||||
|
value: 'someone@somewhere.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone Somewhere',
|
||||||
|
},
|
||||||
|
email: [
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
value: 'someone@somewhere.com',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const result = await upgradeVersion(message.contact[0], { message });
|
||||||
|
assert.deepEqual(result, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drops address if it has no real values', async () => {
|
||||||
|
const upgradeAttachment = sinon
|
||||||
|
.stub()
|
||||||
|
.throws(new Error("Shouldn't be called"));
|
||||||
|
const upgradeVersion = Contact.parseAndWriteAvatar(upgradeAttachment);
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
body: 'hey there!',
|
||||||
|
contact: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone Somewhere',
|
||||||
|
},
|
||||||
|
number: [
|
||||||
|
{
|
||||||
|
value: NUMBER,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
address: [
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone Somewhere',
|
||||||
|
},
|
||||||
|
number: [
|
||||||
|
{
|
||||||
|
value: NUMBER,
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const result = await upgradeVersion(message.contact[0], { message });
|
||||||
|
assert.deepEqual(result, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes invalid elements if no values remain in contact', async () => {
|
||||||
|
const upgradeAttachment = sinon
|
||||||
|
.stub()
|
||||||
|
.throws(new Error("Shouldn't be called"));
|
||||||
|
const upgradeVersion = Contact.parseAndWriteAvatar(upgradeAttachment);
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
body: 'hey there!',
|
||||||
|
source: NUMBER,
|
||||||
|
sourceDevice: '1',
|
||||||
|
sent_at: 1232132,
|
||||||
|
contact: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone Somewhere',
|
||||||
|
},
|
||||||
|
number: [
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone Somewhere',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = await upgradeVersion(message.contact[0], { message });
|
||||||
|
assert.deepEqual(result, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles a contact with just organization', async () => {
|
||||||
|
const upgradeAttachment = sinon
|
||||||
|
.stub()
|
||||||
|
.throws(new Error("Shouldn't be called"));
|
||||||
|
const upgradeVersion = Contact.parseAndWriteAvatar(upgradeAttachment);
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
contact: [
|
||||||
|
{
|
||||||
|
organization: 'Somewhere Consulting',
|
||||||
|
number: [
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
value: NUMBER,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const result = await upgradeVersion(message.contact[0], { message });
|
||||||
|
assert.deepEqual(result, message.contact[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('_validate', () => {
|
||||||
|
it('returns error if contact has no name.displayName or organization', () => {
|
||||||
|
const messageId = 'the-message-id';
|
||||||
|
const contact = {
|
||||||
|
name: {
|
||||||
|
name: 'Someone',
|
||||||
|
},
|
||||||
|
number: [
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
value: NUMBER,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const expected =
|
||||||
|
"Message the-message-id: Contact had neither 'displayName' nor 'organization'";
|
||||||
|
|
||||||
|
const result = Contact._validate(contact, { messageId });
|
||||||
|
assert.deepEqual(result.message, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('logs if no values remain in contact', async () => {
|
||||||
|
const messageId = 'the-message-id';
|
||||||
|
const contact = {
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone Somewhere',
|
||||||
|
},
|
||||||
|
number: [],
|
||||||
|
email: [],
|
||||||
|
};
|
||||||
|
const expected =
|
||||||
|
'Message the-message-id: Contact had no included numbers, email or addresses';
|
||||||
|
|
||||||
|
const result = Contact._validate(contact, { messageId });
|
||||||
|
assert.deepEqual(result.message, expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -63,6 +63,7 @@ describe('Message', () => {
|
||||||
path: 'ab/abcdefghi',
|
path: 'ab/abcdefghi',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
contact: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const writeExistingAttachmentData = attachment => {
|
const writeExistingAttachmentData = attachment => {
|
||||||
|
@ -108,6 +109,56 @@ describe('Message', () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
contact: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeExistingAttachmentData = attachment => {
|
||||||
|
assert.equal(attachment.path, 'ab/abcdefghi');
|
||||||
|
assert.deepEqual(
|
||||||
|
attachment.data,
|
||||||
|
stringToArrayBuffer('It’s easy if you try')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const actual = await Message.createAttachmentDataWriter(
|
||||||
|
writeExistingAttachmentData
|
||||||
|
)(input);
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should process contact avatars', async () => {
|
||||||
|
const input = {
|
||||||
|
body: 'Imagine there is no heaven…',
|
||||||
|
schemaVersion: 4,
|
||||||
|
attachments: [],
|
||||||
|
contact: [
|
||||||
|
{
|
||||||
|
name: 'john',
|
||||||
|
avatar: {
|
||||||
|
isProfile: false,
|
||||||
|
avatar: {
|
||||||
|
path: 'ab/abcdefghi',
|
||||||
|
data: stringToArrayBuffer('It’s easy if you try'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
body: 'Imagine there is no heaven…',
|
||||||
|
schemaVersion: 4,
|
||||||
|
attachments: [],
|
||||||
|
contact: [
|
||||||
|
{
|
||||||
|
name: 'john',
|
||||||
|
avatar: {
|
||||||
|
isProfile: false,
|
||||||
|
avatar: {
|
||||||
|
path: 'ab/abcdefghi',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const writeExistingAttachmentData = attachment => {
|
const writeExistingAttachmentData = attachment => {
|
||||||
|
@ -212,6 +263,7 @@ describe('Message', () => {
|
||||||
hasVisualMediaAttachments: undefined,
|
hasVisualMediaAttachments: undefined,
|
||||||
hasFileAttachments: 1,
|
hasFileAttachments: 1,
|
||||||
schemaVersion: Message.CURRENT_SCHEMA_VERSION,
|
schemaVersion: Message.CURRENT_SCHEMA_VERSION,
|
||||||
|
contact: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const expectedAttachmentData = stringToArrayBuffer(
|
const expectedAttachmentData = stringToArrayBuffer(
|
||||||
|
@ -458,7 +510,7 @@ describe('Message', () => {
|
||||||
assert.deepEqual(result, message);
|
assert.deepEqual(result, message);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('eliminates thumbnails with no data fielkd', async () => {
|
it('eliminates thumbnails with no data field', async () => {
|
||||||
const upgradeAttachment = sinon
|
const upgradeAttachment = sinon
|
||||||
.stub()
|
.stub()
|
||||||
.throws(new Error("Shouldn't be called"));
|
.throws(new Error("Shouldn't be called"));
|
||||||
|
@ -531,4 +583,51 @@ describe('Message', () => {
|
||||||
assert.deepEqual(result, expected);
|
assert.deepEqual(result, expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('_mapContact', () => {
|
||||||
|
it('handles message with no contact field', async () => {
|
||||||
|
const upgradeContact = sinon
|
||||||
|
.stub()
|
||||||
|
.throws(new Error("Shouldn't be called"));
|
||||||
|
const upgradeVersion = Message._mapContact(upgradeContact);
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
body: 'hey there!',
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
body: 'hey there!',
|
||||||
|
contact: [],
|
||||||
|
};
|
||||||
|
const result = await upgradeVersion(message);
|
||||||
|
assert.deepEqual(result, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles one contact', async () => {
|
||||||
|
const upgradeContact = contact => Promise.resolve(contact);
|
||||||
|
const upgradeVersion = Message._mapContact(upgradeContact);
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
body: 'hey there!',
|
||||||
|
contact: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone somewhere',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
body: 'hey there!',
|
||||||
|
contact: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone somewhere',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const result = await upgradeVersion(message);
|
||||||
|
assert.deepEqual(result, expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -48,8 +48,17 @@ window.Signal.Migrations = {
|
||||||
},
|
},
|
||||||
version: 2,
|
version: 2,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
migrate: (transaction, next) => {
|
||||||
|
console.log('migration version 3');
|
||||||
|
transaction.db.createObjectStore('items');
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
version: 3,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
loadAttachmentData: attachment => Promise.resolve(attachment),
|
loadAttachmentData: attachment => Promise.resolve(attachment),
|
||||||
|
getAbsoluteAttachmentPath: path => path,
|
||||||
};
|
};
|
||||||
|
|
||||||
window.Signal.Components = {};
|
window.Signal.Components = {};
|
||||||
|
|
|
@ -5,8 +5,6 @@ const noop = () => {};
|
||||||
<Lightbox
|
<Lightbox
|
||||||
objectURL="https://placekitten.com/800/600"
|
objectURL="https://placekitten.com/800/600"
|
||||||
contentType="image/jpeg"
|
contentType="image/jpeg"
|
||||||
onNext={noop}
|
|
||||||
onPrevious={noop}
|
|
||||||
onSave={noop}
|
onSave={noop}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
@ -1,16 +1,58 @@
|
||||||
```js
|
```js
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
|
|
||||||
const items = [
|
const messages = [
|
||||||
{ objectURL: 'https://placekitten.com/800/600', contentType: 'image/jpeg' },
|
{
|
||||||
{ objectURL: 'https://placekitten.com/900/600', contentType: 'image/jpeg' },
|
objectURL: 'https://placekitten.com/800/600',
|
||||||
{ objectURL: 'https://placekitten.com/980/800', contentType: 'image/jpeg' },
|
attachments: [
|
||||||
{ objectURL: 'https://placekitten.com/656/540', contentType: 'image/jpeg' },
|
{
|
||||||
{ objectURL: 'https://placekitten.com/762/400', contentType: 'image/jpeg' },
|
contentType: 'image/jpeg',
|
||||||
{ objectURL: 'https://placekitten.com/920/620', contentType: 'image/jpeg' },
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
objectURL: 'https://placekitten.com/900/600',
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
contentType: 'image/jpeg',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
objectURL: 'https://placekitten.com/980/800',
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
contentType: 'image/jpeg',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
objectURL: 'https://placekitten.com/656/540',
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
contentType: 'image/jpeg',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
objectURL: 'https://placekitten.com/762/400',
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
contentType: 'image/jpeg',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
objectURL: 'https://placekitten.com/920/620',
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
contentType: 'image/jpeg',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
<div style={{ position: 'relative', width: '100%', height: 500 }}>
|
<div style={{ position: 'relative', width: '100%', height: 500 }}>
|
||||||
<LightboxGallery items={items} onSave={noop} />
|
<LightboxGallery messages={messages} onSave={noop} />
|
||||||
</div>;
|
</div>;
|
||||||
```
|
```
|
||||||
|
|
173
ts/components/conversation/ContactDetail.md
Normal file
173
ts/components/conversation/ContactDetail.md
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
### With all data types
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const contact = {
|
||||||
|
avatar: {
|
||||||
|
avatar: {
|
||||||
|
path: util.gifObjectUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone Somewhere',
|
||||||
|
},
|
||||||
|
number: [
|
||||||
|
{
|
||||||
|
value: '(202) 555-0000',
|
||||||
|
type: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '(202) 555-0001',
|
||||||
|
type: 4,
|
||||||
|
label: 'My favorite custom label',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{
|
||||||
|
value: 'someone@somewhere.com',
|
||||||
|
type: 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
value: 'someone2@somewhere.com',
|
||||||
|
type: 4,
|
||||||
|
label: 'My second-favorite custom label',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
address: [
|
||||||
|
{
|
||||||
|
street: '5 Pike Place',
|
||||||
|
city: 'Seattle',
|
||||||
|
region: 'WA',
|
||||||
|
postcode: '98101',
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
street: '10 Pike Place',
|
||||||
|
pobox: '3242',
|
||||||
|
neighborhood: 'Downtown',
|
||||||
|
city: 'Seattle',
|
||||||
|
region: 'WA',
|
||||||
|
postcode: '98101',
|
||||||
|
country: 'United States',
|
||||||
|
type: 3,
|
||||||
|
label: 'My favorite spot!',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
<ContactDetail
|
||||||
|
contact={contact}
|
||||||
|
hasSignalAccount={true}
|
||||||
|
i18n={util.i18n}
|
||||||
|
onSendMessage={() => console.log('onSendMessage')}
|
||||||
|
/>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### With default avatar
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const contact = {
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone Somewhere',
|
||||||
|
},
|
||||||
|
number: [
|
||||||
|
{
|
||||||
|
value: '(202) 555-0000',
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
<ContactDetail
|
||||||
|
contact={contact}
|
||||||
|
hasSignalAccount={true}
|
||||||
|
i18n={util.i18n}
|
||||||
|
onSendMessage={() => console.log('onSendMessage')}
|
||||||
|
/>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Without a Signal account
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const contact = {
|
||||||
|
avatar: {
|
||||||
|
avatar: {
|
||||||
|
path: util.gifObjectUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone Somewhere',
|
||||||
|
},
|
||||||
|
number: [
|
||||||
|
{
|
||||||
|
value: '(202) 555-0001',
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
<ContactDetail
|
||||||
|
contact={contact}
|
||||||
|
hasSignalAccount={false}
|
||||||
|
i18n={util.i18n}
|
||||||
|
onSendMessage={() => console.log('onSendMessage')}
|
||||||
|
/>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### No phone or email, partial addresses
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const contact = {
|
||||||
|
avatar: {
|
||||||
|
avatar: {
|
||||||
|
path: util.gifObjectUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone Somewhere',
|
||||||
|
},
|
||||||
|
address: [
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
neighborhood: 'Greenwood',
|
||||||
|
region: 'WA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
city: 'Seattle',
|
||||||
|
region: 'WA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 3,
|
||||||
|
label: 'My label',
|
||||||
|
region: 'WA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 1,
|
||||||
|
label: 'My label',
|
||||||
|
postcode: '98101',
|
||||||
|
region: 'WA',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 2,
|
||||||
|
label: 'My label',
|
||||||
|
postcode: '98101',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
<ContactDetail
|
||||||
|
contact={contact}
|
||||||
|
hasSignalAccount={false}
|
||||||
|
i18n={util.i18n}
|
||||||
|
onSendMessage={() => console.log('onSendMessage')}
|
||||||
|
/>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Empty contact
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const contact = {};
|
||||||
|
<ContactDetail
|
||||||
|
contact={contact}
|
||||||
|
hasSignalAccount={false}
|
||||||
|
i18n={util.i18n}
|
||||||
|
onSendMessage={() => console.log('onSendMessage')}
|
||||||
|
/>;
|
||||||
|
```
|
145
ts/components/conversation/ContactDetail.tsx
Normal file
145
ts/components/conversation/ContactDetail.tsx
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AddressType,
|
||||||
|
Contact,
|
||||||
|
ContactType,
|
||||||
|
Email,
|
||||||
|
Phone,
|
||||||
|
PostalAddress,
|
||||||
|
} from '../../types/Contact';
|
||||||
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
|
|
||||||
|
import {
|
||||||
|
renderAvatar,
|
||||||
|
renderContactShorthand,
|
||||||
|
renderName,
|
||||||
|
renderSendMessage,
|
||||||
|
} from './EmbeddedContact';
|
||||||
|
|
||||||
|
type Localizer = (key: string, values?: Array<string>) => string;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
contact: Contact;
|
||||||
|
hasSignalAccount: boolean;
|
||||||
|
i18n: Localizer;
|
||||||
|
onSendMessage: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLabelForContactMethod(method: Phone | Email, i18n: Localizer) {
|
||||||
|
switch (method.type) {
|
||||||
|
case ContactType.CUSTOM:
|
||||||
|
return method.label;
|
||||||
|
case ContactType.HOME:
|
||||||
|
return i18n('home');
|
||||||
|
case ContactType.MOBILE:
|
||||||
|
return i18n('mobile');
|
||||||
|
case ContactType.WORK:
|
||||||
|
return i18n('work');
|
||||||
|
default:
|
||||||
|
return missingCaseError(method.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLabelForAddress(address: PostalAddress, i18n: Localizer) {
|
||||||
|
switch (address.type) {
|
||||||
|
case AddressType.CUSTOM:
|
||||||
|
return address.label;
|
||||||
|
case AddressType.HOME:
|
||||||
|
return i18n('home');
|
||||||
|
case AddressType.WORK:
|
||||||
|
return i18n('work');
|
||||||
|
default:
|
||||||
|
return missingCaseError(address.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ContactDetail extends React.Component<Props, {}> {
|
||||||
|
public renderAdditionalContact(
|
||||||
|
items: Array<Phone | Email> | undefined,
|
||||||
|
i18n: Localizer
|
||||||
|
) {
|
||||||
|
if (!items || items.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.map((item: Phone | Email) => {
|
||||||
|
return (
|
||||||
|
<div key={item.value} className="additional-contact">
|
||||||
|
<div className="type">{getLabelForContactMethod(item, i18n)}</div>
|
||||||
|
{item.value}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public renderAddressLine(value: string | undefined) {
|
||||||
|
if (!value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>{value}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public renderPOBox(poBox: string | undefined, i18n: Localizer) {
|
||||||
|
if (!poBox) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{i18n('poBox')} {poBox}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public renderAddressLineTwo(address: PostalAddress) {
|
||||||
|
if (address.city || address.region || address.postcode) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{address.city} {address.region} {address.postcode}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public renderAddresses(
|
||||||
|
addresses: Array<PostalAddress> | undefined,
|
||||||
|
i18n: Localizer
|
||||||
|
) {
|
||||||
|
if (!addresses || addresses.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return addresses.map((address: PostalAddress, index: number) => {
|
||||||
|
return (
|
||||||
|
<div key={index} className="additional-contact">
|
||||||
|
<div className="type">{getLabelForAddress(address, i18n)}</div>
|
||||||
|
{this.renderAddressLine(address.street)}
|
||||||
|
{this.renderPOBox(address.pobox, i18n)}
|
||||||
|
{this.renderAddressLine(address.neighborhood)}
|
||||||
|
{this.renderAddressLineTwo(address)}
|
||||||
|
{this.renderAddressLine(address.country)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const { contact, hasSignalAccount, i18n, onSendMessage } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="contact-detail">
|
||||||
|
{renderAvatar(contact)}
|
||||||
|
{renderName(contact)}
|
||||||
|
{renderContactShorthand(contact)}
|
||||||
|
{renderSendMessage({ hasSignalAccount, i18n, onSendMessage })}
|
||||||
|
{this.renderAdditionalContact(contact.number, i18n)}
|
||||||
|
{this.renderAdditionalContact(contact.email, i18n)}
|
||||||
|
{this.renderAddresses(contact.address, i18n)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
244
ts/components/conversation/EmbeddedContact.md
Normal file
244
ts/components/conversation/EmbeddedContact.md
Normal file
|
@ -0,0 +1,244 @@
|
||||||
|
### With a contact
|
||||||
|
|
||||||
|
#### Including all data types
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const outgoing = new Whisper.Message({
|
||||||
|
type: 'outgoing',
|
||||||
|
sent_at: Date.now() - 18000000,
|
||||||
|
contact: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone Somewhere',
|
||||||
|
},
|
||||||
|
number: [
|
||||||
|
{
|
||||||
|
value: util.CONTACTS[0].id,
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
avatar: {
|
||||||
|
avatar: {
|
||||||
|
path: util.gifObjectUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const incoming = new Whisper.Message(
|
||||||
|
Object.assign({}, outgoing.attributes, {
|
||||||
|
source: '+12025550011',
|
||||||
|
type: 'incoming',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const View = Whisper.MessageView;
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<util.BackboneWrapper View={View} options={{ model: incoming }} />
|
||||||
|
<util.BackboneWrapper View={View} options={{ model: outgoing }} />
|
||||||
|
</util.ConversationContext>;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### In group conversation
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const outgoing = new Whisper.Message({
|
||||||
|
type: 'outgoing',
|
||||||
|
sent_at: Date.now() - 18000000,
|
||||||
|
contact: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone Somewhere',
|
||||||
|
},
|
||||||
|
number: [
|
||||||
|
{
|
||||||
|
value: util.CONTACTS[0].id,
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
avatar: {
|
||||||
|
avatar: {
|
||||||
|
path: util.gifObjectUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const incoming = new Whisper.Message(
|
||||||
|
Object.assign({}, outgoing.attributes, {
|
||||||
|
source: '+12025550011',
|
||||||
|
type: 'incoming',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const View = Whisper.MessageView;
|
||||||
|
<util.ConversationContext theme={util.theme} type="group">
|
||||||
|
<util.BackboneWrapper View={View} options={{ model: incoming }} />
|
||||||
|
<util.BackboneWrapper View={View} options={{ model: outgoing }} />
|
||||||
|
</util.ConversationContext>;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### If contact has no signal account
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const outgoing = new Whisper.Message({
|
||||||
|
type: 'outgoing',
|
||||||
|
sent_at: Date.now() - 18000000,
|
||||||
|
contact: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone Somewhere',
|
||||||
|
},
|
||||||
|
number: [
|
||||||
|
{
|
||||||
|
value: '+12025551000',
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
avatar: {
|
||||||
|
avatar: {
|
||||||
|
path: util.gifObjectUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const incoming = new Whisper.Message(
|
||||||
|
Object.assign({}, outgoing.attributes, {
|
||||||
|
source: '+12025550011',
|
||||||
|
type: 'incoming',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const View = Whisper.MessageView;
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<util.BackboneWrapper View={View} options={{ model: incoming }} />
|
||||||
|
<util.BackboneWrapper View={View} options={{ model: outgoing }} />
|
||||||
|
</util.ConversationContext>;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### With organization name instead of name
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const outgoing = new Whisper.Message({
|
||||||
|
type: 'outgoing',
|
||||||
|
sent_at: Date.now() - 18000000,
|
||||||
|
contact: [
|
||||||
|
{
|
||||||
|
organization: 'United Somewheres, Inc.',
|
||||||
|
email: [
|
||||||
|
{
|
||||||
|
value: 'someone@somewheres.com',
|
||||||
|
type: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
avatar: {
|
||||||
|
avatar: {
|
||||||
|
path: util.gifObjectUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const incoming = new Whisper.Message(
|
||||||
|
Object.assign({}, outgoing.attributes, {
|
||||||
|
source: '+12025550011',
|
||||||
|
type: 'incoming',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const View = Whisper.MessageView;
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<util.BackboneWrapper View={View} options={{ model: incoming }} />
|
||||||
|
<util.BackboneWrapper View={View} options={{ model: outgoing }} />
|
||||||
|
</util.ConversationContext>;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Default avatar
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const outgoing = new Whisper.Message({
|
||||||
|
type: 'outgoing',
|
||||||
|
sent_at: Date.now() - 18000000,
|
||||||
|
contact: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone Somewhere',
|
||||||
|
},
|
||||||
|
number: [
|
||||||
|
{
|
||||||
|
value: util.CONTACTS[0].id,
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const incoming = new Whisper.Message(
|
||||||
|
Object.assign({}, outgoing.attributes, {
|
||||||
|
source: '+12025550011',
|
||||||
|
type: 'incoming',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const View = Whisper.MessageView;
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<util.BackboneWrapper View={View} options={{ model: incoming }} />
|
||||||
|
<util.BackboneWrapper View={View} options={{ model: outgoing }} />
|
||||||
|
</util.ConversationContext>;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Empty contact
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const outgoing = new Whisper.Message({
|
||||||
|
type: 'outgoing',
|
||||||
|
sent_at: Date.now() - 18000000,
|
||||||
|
contact: [{}],
|
||||||
|
});
|
||||||
|
const incoming = new Whisper.Message(
|
||||||
|
Object.assign({}, outgoing.attributes, {
|
||||||
|
source: '+12025550011',
|
||||||
|
type: 'incoming',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const View = Whisper.MessageView;
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<util.BackboneWrapper View={View} options={{ model: incoming }} />
|
||||||
|
<util.BackboneWrapper View={View} options={{ model: outgoing }} />
|
||||||
|
</util.ConversationContext>;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Contact with caption (cannot currently be sent)
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const outgoing = new Whisper.Message({
|
||||||
|
type: 'outgoing',
|
||||||
|
sent_at: Date.now() - 18000000,
|
||||||
|
body: 'I want to introduce you to Someone...',
|
||||||
|
contact: [
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
displayName: 'Someone Somewhere',
|
||||||
|
},
|
||||||
|
number: [
|
||||||
|
{
|
||||||
|
value: util.CONTACTS[0].id,
|
||||||
|
type: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
avatar: {
|
||||||
|
avatar: {
|
||||||
|
path: util.gifObjectUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const incoming = new Whisper.Message(
|
||||||
|
Object.assign({}, outgoing.attributes, {
|
||||||
|
source: '+12025550011',
|
||||||
|
type: 'incoming',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const View = Whisper.MessageView;
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<util.BackboneWrapper View={View} options={{ model: incoming }} />
|
||||||
|
<util.BackboneWrapper View={View} options={{ model: outgoing }} />
|
||||||
|
</util.ConversationContext>;
|
||||||
|
```
|
102
ts/components/conversation/EmbeddedContact.tsx
Normal file
102
ts/components/conversation/EmbeddedContact.tsx
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Contact, getName } from '../../types/Contact';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
contact: Contact;
|
||||||
|
hasSignalAccount: boolean;
|
||||||
|
i18n: (key: string, values?: Array<string>) => string;
|
||||||
|
onSendMessage: () => void;
|
||||||
|
onOpenContact: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EmbeddedContact extends React.Component<Props, {}> {
|
||||||
|
public render() {
|
||||||
|
const {
|
||||||
|
contact,
|
||||||
|
hasSignalAccount,
|
||||||
|
i18n,
|
||||||
|
onOpenContact,
|
||||||
|
onSendMessage,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="embedded-contact" onClick={onOpenContact}>
|
||||||
|
<div className="first-line">
|
||||||
|
{renderAvatar(contact)}
|
||||||
|
<div className="text-container">
|
||||||
|
{renderName(contact)}
|
||||||
|
{renderContactShorthand(contact)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{renderSendMessage({ hasSignalAccount, i18n, onSendMessage })}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: putting these below the main component so style guide picks up EmbeddedContact
|
||||||
|
|
||||||
|
function getInitials(name: string): string {
|
||||||
|
return name.trim()[0] || '#';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderAvatar(contact: Contact) {
|
||||||
|
const { avatar } = contact;
|
||||||
|
|
||||||
|
const path = avatar && avatar.avatar && avatar.avatar.path;
|
||||||
|
if (!path) {
|
||||||
|
const name = getName(contact);
|
||||||
|
const initials = getInitials(name || '');
|
||||||
|
return (
|
||||||
|
<div className="image-container">
|
||||||
|
<div className="default-avatar">{initials}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="image-container">
|
||||||
|
<img src={path} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderName(contact: Contact) {
|
||||||
|
return <div className="contact-name">{getName(contact)}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderContactShorthand(contact: Contact) {
|
||||||
|
const { number: phoneNumber, email } = contact;
|
||||||
|
const firstNumber = phoneNumber && phoneNumber[0] && phoneNumber[0].value;
|
||||||
|
const firstEmail = email && email[0] && email[0].value;
|
||||||
|
|
||||||
|
return <div className="contact-method">{firstNumber || firstEmail}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderSendMessage(props: {
|
||||||
|
hasSignalAccount: boolean;
|
||||||
|
i18n: (key: string, values?: Array<string>) => string;
|
||||||
|
onSendMessage: () => void;
|
||||||
|
}) {
|
||||||
|
const { hasSignalAccount, i18n, onSendMessage } = props;
|
||||||
|
|
||||||
|
if (!hasSignalAccount) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't want the overall click handler for this element to fire, so we stop
|
||||||
|
// propagation before handing control to the caller's callback.
|
||||||
|
const onClick = (e: React.MouseEvent<{}>): void => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onSendMessage();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="send-message" onClick={onClick}>
|
||||||
|
<button className="inner">
|
||||||
|
<div className="icon bubble-icon" />
|
||||||
|
{i18n('sendMessageToContact')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { padStart, sample } from 'lodash';
|
import { padStart, sample } from 'lodash';
|
||||||
|
import libphonenumber from 'google-libphonenumber';
|
||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
@ -14,14 +15,9 @@ export { _ };
|
||||||
export { ConversationContext } from './ConversationContext';
|
export { ConversationContext } from './ConversationContext';
|
||||||
export { BackboneWrapper } from '../components/utility/BackboneWrapper';
|
export { BackboneWrapper } from '../components/utility/BackboneWrapper';
|
||||||
|
|
||||||
// Here we can make things inside Webpack available to Backbone views like preload.js.
|
// @ts-ignore
|
||||||
|
import * as Signal from '../../js/signal';
|
||||||
import { Quote } from '../components/conversation/Quote';
|
import { SignalService } from '../protobuf';
|
||||||
import * as HTML from '../html';
|
|
||||||
|
|
||||||
import * as Attachment from '../../ts/types/Attachment';
|
|
||||||
import * as MIME from '../../ts/types/MIME';
|
|
||||||
import { SignalService } from '../../ts/protobuf';
|
|
||||||
|
|
||||||
// TypeScript wants two things when you import:
|
// TypeScript wants two things when you import:
|
||||||
// 1) a normal typescript file
|
// 1) a normal typescript file
|
||||||
|
@ -103,14 +99,15 @@ import localeMessages from '../../_locales/en/messages.json';
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { setup } from '../../js/modules/i18n';
|
import { setup } from '../../js/modules/i18n';
|
||||||
import * as Util from '../util';
|
|
||||||
import filesize from 'filesize';
|
import filesize from 'filesize';
|
||||||
|
|
||||||
const i18n = setup(locale, localeMessages);
|
const i18n = setup(locale, localeMessages);
|
||||||
|
|
||||||
export { theme, locale, i18n };
|
parent.filesize = filesize;
|
||||||
|
|
||||||
parent.i18n = i18n;
|
parent.i18n = i18n;
|
||||||
|
parent.React = React;
|
||||||
|
parent.ReactDOM = ReactDOM;
|
||||||
parent.moment = moment;
|
parent.moment = moment;
|
||||||
|
|
||||||
parent.moment.updateLocale(locale, {
|
parent.moment.updateLocale(locale, {
|
||||||
|
@ -122,18 +119,26 @@ parent.moment.updateLocale(locale, {
|
||||||
});
|
});
|
||||||
parent.moment.locale(locale);
|
parent.moment.locale(locale);
|
||||||
|
|
||||||
parent.React = React;
|
export { theme, locale, i18n };
|
||||||
parent.ReactDOM = ReactDOM;
|
|
||||||
|
|
||||||
parent.Signal.HTML = HTML;
|
// Used by signal.js to set up code that deals with message attachments/avatars
|
||||||
parent.Signal.Types.MIME = MIME;
|
const Attachments = {
|
||||||
parent.Signal.Types.Attachment = Attachment;
|
createAbsolutePathGetter: () => () => '/fake/path',
|
||||||
parent.Signal.Components = {
|
createDeleter: () => async () => undefined,
|
||||||
Quote,
|
createReader: () => async () => new ArrayBuffer(10),
|
||||||
|
createWriterForExisting: () => async () => '/fake/path',
|
||||||
|
createWriterForNew: () => async () => ({
|
||||||
|
data: new ArrayBuffer(10),
|
||||||
|
path: '/fake/path',
|
||||||
|
}),
|
||||||
|
getPath: (path: string) => path,
|
||||||
};
|
};
|
||||||
parent.Signal.Util = Util;
|
|
||||||
|
parent.Signal = Signal.setup({
|
||||||
|
Attachments,
|
||||||
|
userDataPath: '/',
|
||||||
|
});
|
||||||
parent.SignalService = SignalService;
|
parent.SignalService = SignalService;
|
||||||
parent.filesize = filesize;
|
|
||||||
|
|
||||||
parent.ConversationController._initialFetchComplete = true;
|
parent.ConversationController._initialFetchComplete = true;
|
||||||
parent.ConversationController._initialPromise = Promise.resolve();
|
parent.ConversationController._initialPromise = Promise.resolve();
|
||||||
|
@ -194,6 +199,20 @@ group.contactCollection.add(CONTACTS[2]);
|
||||||
export { COLORS, CONTACTS, me, group };
|
export { COLORS, CONTACTS, me, group };
|
||||||
|
|
||||||
parent.textsecure.storage.user.getNumber = () => ourNumber;
|
parent.textsecure.storage.user.getNumber = () => ourNumber;
|
||||||
|
parent.textsecure.messaging = {
|
||||||
|
getProfile: async (phoneNumber: string): Promise<boolean> => {
|
||||||
|
if (parent.ConversationController.get(phoneNumber)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('User does not have Signal account');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
parent.libphonenumber = libphonenumber.PhoneNumberUtil.getInstance();
|
||||||
|
parent.libphonenumber.PhoneNumberFormat = libphonenumber.PhoneNumberFormat;
|
||||||
|
|
||||||
|
parent.storage.put('regionCode', 'US');
|
||||||
|
|
||||||
// Telling Lodash to relinquish _ for use by underscore
|
// Telling Lodash to relinquish _ for use by underscore
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
105
ts/types/Contact.ts
Normal file
105
ts/types/Contact.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
// @ts-ignore
|
||||||
|
import Attachments from '../../app/attachments';
|
||||||
|
import { formatPhoneNumber } from '../util/formatPhoneNumber';
|
||||||
|
|
||||||
|
export interface Contact {
|
||||||
|
name: Name;
|
||||||
|
number?: Array<Phone>;
|
||||||
|
email?: Array<Email>;
|
||||||
|
address?: Array<PostalAddress>;
|
||||||
|
avatar?: Avatar;
|
||||||
|
organization?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Name {
|
||||||
|
givenName?: string;
|
||||||
|
familyName?: string;
|
||||||
|
prefix?: string;
|
||||||
|
suffix?: string;
|
||||||
|
middleName?: string;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ContactType {
|
||||||
|
HOME = 1,
|
||||||
|
MOBILE = 2,
|
||||||
|
WORK = 3,
|
||||||
|
CUSTOM = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AddressType {
|
||||||
|
HOME = 1,
|
||||||
|
WORK = 2,
|
||||||
|
CUSTOM = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Phone {
|
||||||
|
value: string;
|
||||||
|
type: ContactType;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Email {
|
||||||
|
value: string;
|
||||||
|
type: ContactType;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PostalAddress {
|
||||||
|
type: AddressType;
|
||||||
|
label?: string;
|
||||||
|
street?: string;
|
||||||
|
pobox?: string;
|
||||||
|
neighborhood?: string;
|
||||||
|
city?: string;
|
||||||
|
region?: string;
|
||||||
|
postcode?: string;
|
||||||
|
country?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Avatar {
|
||||||
|
avatar: Attachment;
|
||||||
|
isProfile: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Attachment {
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function contactSelector(
|
||||||
|
contact: Contact,
|
||||||
|
options: {
|
||||||
|
regionCode: string;
|
||||||
|
getAbsoluteAttachmentPath: (path: string) => string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { regionCode, getAbsoluteAttachmentPath } = options;
|
||||||
|
|
||||||
|
let { avatar } = contact;
|
||||||
|
if (avatar && avatar.avatar && avatar.avatar.path) {
|
||||||
|
avatar = {
|
||||||
|
...avatar,
|
||||||
|
avatar: {
|
||||||
|
...avatar.avatar,
|
||||||
|
path: getAbsoluteAttachmentPath(avatar.avatar.path),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...contact,
|
||||||
|
avatar,
|
||||||
|
number:
|
||||||
|
contact.number &&
|
||||||
|
contact.number.map(item => ({
|
||||||
|
...item,
|
||||||
|
value: formatPhoneNumber(item.value, {
|
||||||
|
ourRegionCode: regionCode,
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getName(contact: Contact): string | null {
|
||||||
|
const { name, organization } = contact;
|
||||||
|
return (name && name.displayName) || organization || null;
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import { Attachment } from './Attachment';
|
import { Attachment } from './Attachment';
|
||||||
|
import { Contact } from './Contact';
|
||||||
import { IndexableBoolean, IndexablePresence } from './IndexedDB';
|
import { IndexableBoolean, IndexablePresence } from './IndexedDB';
|
||||||
|
|
||||||
export type Message = UserMessage | VerifiedChangeMessage;
|
export type Message = UserMessage | VerifiedChangeMessage;
|
||||||
|
@ -21,6 +22,7 @@ export type IncomingMessage = Readonly<
|
||||||
sourceDevice?: number;
|
sourceDevice?: number;
|
||||||
} & SharedMessageProperties &
|
} & SharedMessageProperties &
|
||||||
MessageSchemaVersion5 &
|
MessageSchemaVersion5 &
|
||||||
|
MessageSchemaVersion6 &
|
||||||
ExpirationTimerUpdate
|
ExpirationTimerUpdate
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
@ -81,3 +83,9 @@ type MessageSchemaVersion5 = Partial<
|
||||||
hasFileAttachments: IndexablePresence;
|
hasFileAttachments: IndexablePresence;
|
||||||
}>
|
}>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
type MessageSchemaVersion6 = Partial<
|
||||||
|
Readonly<{
|
||||||
|
contact: Array<Contact>;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
|
22
ts/util/formatPhoneNumber.ts
Normal file
22
ts/util/formatPhoneNumber.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { instance, PhoneNumberFormat } from './libphonenumberInstance';
|
||||||
|
|
||||||
|
export function formatPhoneNumber(
|
||||||
|
phoneNumber: string,
|
||||||
|
options: {
|
||||||
|
ourRegionCode: string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { ourRegionCode } = options;
|
||||||
|
const parsedNumber = instance.parse(phoneNumber);
|
||||||
|
const regionCode = instance.getRegionCodeForNumber(parsedNumber);
|
||||||
|
|
||||||
|
if (ourRegionCode && regionCode === ourRegionCode) {
|
||||||
|
return instance.format(parsedNumber, PhoneNumberFormat.NATIONAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance.format(parsedNumber, PhoneNumberFormat.INTERNATIONAL);
|
||||||
|
} catch (error) {
|
||||||
|
return phoneNumber;
|
||||||
|
}
|
||||||
|
}
|
6
ts/util/libphonenumberInstance.ts
Normal file
6
ts/util/libphonenumberInstance.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import libphonenumber from 'google-libphonenumber';
|
||||||
|
|
||||||
|
const instance = libphonenumber.PhoneNumberUtil.getInstance();
|
||||||
|
const PhoneNumberFormat = libphonenumber.PhoneNumberFormat;
|
||||||
|
|
||||||
|
export { instance, PhoneNumberFormat };
|
|
@ -91,6 +91,10 @@
|
||||||
version "3.6.0"
|
version "3.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/filesize/-/filesize-3.6.0.tgz#5f1a25c7b4e3d5ee2bc63133d374d096b7008c8d"
|
resolved "https://registry.yarnpkg.com/@types/filesize/-/filesize-3.6.0.tgz#5f1a25c7b4e3d5ee2bc63133d374d096b7008c8d"
|
||||||
|
|
||||||
|
"@types/google-libphonenumber@^7.4.14":
|
||||||
|
version "7.4.14"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/google-libphonenumber/-/google-libphonenumber-7.4.14.tgz#3625d7aed0c16df920588428c86f0538bd0612ec"
|
||||||
|
|
||||||
"@types/jquery@^3.3.1":
|
"@types/jquery@^3.3.1":
|
||||||
version "3.3.1"
|
version "3.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.1.tgz#55758d44d422756d6329cbf54e6d41931d7ba28f"
|
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.1.tgz#55758d44d422756d6329cbf54e6d41931d7ba28f"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue