commit
787d023557
40 changed files with 2424 additions and 160 deletions
|
@ -15,6 +15,7 @@ test/test.js
|
|||
ts/**/*.js
|
||||
ts/protobuf/*.d.ts
|
||||
ts/protobuf/*.js
|
||||
stylesheets/manifest.css
|
||||
|
||||
# Third-party files
|
||||
components/**
|
||||
|
@ -37,3 +38,4 @@ _locales/**
|
|||
|
||||
# Symlink into third-party `components`:
|
||||
stylesheets/_intlTelInput.scss
|
||||
|
||||
|
|
|
@ -460,6 +460,26 @@
|
|||
"selectAContact": {
|
||||
"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": {
|
||||
"message": "Reply to Message",
|
||||
"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();
|
||||
}
|
||||
});
|
||||
Whisper.events.on('showConversation', function(conversation) {
|
||||
if (appView) {
|
||||
appView.openConversation(conversation);
|
||||
}
|
||||
});
|
||||
|
||||
Whisper.Notifications.on('click', function(conversation) {
|
||||
showWindow();
|
||||
if (conversation) {
|
||||
|
|
|
@ -711,13 +711,21 @@
|
|||
},
|
||||
|
||||
async makeQuote(quotedMessage) {
|
||||
const { getName } = Signal.Types.Contact;
|
||||
const contact = quotedMessage.getContact();
|
||||
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 {
|
||||
author: contact.id,
|
||||
id: quotedMessage.get('sent_at'),
|
||||
text: quotedMessage.get('body'),
|
||||
text: body || embeddedContactName,
|
||||
attachments: await Promise.all(
|
||||
(attachments || []).map(async attachment => {
|
||||
const { contentType } = attachment;
|
||||
|
|
|
@ -588,6 +588,7 @@
|
|||
message.set({
|
||||
attachments: dataMessage.attachments,
|
||||
body: dataMessage.body,
|
||||
contact: dataMessage.contact,
|
||||
conversationId: conversation.id,
|
||||
decrypted_at: now,
|
||||
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 = {}) {
|
||||
const { key, newKey, filename, dir } = options;
|
||||
|
||||
|
@ -714,6 +772,21 @@ async function exportConversation(db, conversation, options) {
|
|||
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;
|
||||
cursor.continue();
|
||||
} else {
|
||||
|
@ -870,27 +943,44 @@ function getDirContents(dir) {
|
|||
});
|
||||
}
|
||||
|
||||
function loadAttachments(dir, getName, options) {
|
||||
async function loadAttachments(dir, getName, options) {
|
||||
options = options || {};
|
||||
const { message } = options;
|
||||
|
||||
const attachmentPromises = _.map(message.attachments, (attachment, index) => {
|
||||
const name = getName(message, index, attachment);
|
||||
return readAttachment(dir, attachment, name, options);
|
||||
});
|
||||
await Promise.all(
|
||||
_.map(message.attachments, (attachment, index) => {
|
||||
const name = getName(message, index, attachment);
|
||||
return readAttachment(dir, attachment, name, options);
|
||||
})
|
||||
);
|
||||
|
||||
const quoteAttachments = message.quote && message.quote.attachments;
|
||||
const thumbnailPromises = _.map(quoteAttachments, (attachment, index) => {
|
||||
const thumbnail = attachment && attachment.thumbnail;
|
||||
if (!thumbnail) {
|
||||
return null;
|
||||
}
|
||||
await Promise.all(
|
||||
_.map(quoteAttachments, (attachment, index) => {
|
||||
const thumbnail = attachment && attachment.thumbnail;
|
||||
if (!thumbnail) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = `${getName(message, index, thumbnail)}-thumbnail`;
|
||||
return readAttachment(dir, thumbnail, name, options);
|
||||
});
|
||||
const name = `${getName(message, index)}-thumbnail`;
|
||||
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) {
|
||||
|
|
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 Contact = require('./contact');
|
||||
const Attachment = require('./attachment');
|
||||
const Errors = require('./errors');
|
||||
const SchemaVersion = require('./schema_version');
|
||||
|
@ -29,6 +30,8 @@ const PRIVATE = 'private';
|
|||
// - `hasAttachments?: 1 | 0`
|
||||
// - `hasVisualMediaAttachments?: 1 | undefined` (for media gallery ‘Media’ 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;
|
||||
|
||||
|
@ -37,7 +40,7 @@ const INITIAL_SCHEMA_VERSION = 0;
|
|||
// 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
|
||||
// how we do database migrations:
|
||||
exports.CURRENT_SCHEMA_VERSION = 5;
|
||||
exports.CURRENT_SCHEMA_VERSION = 6;
|
||||
|
||||
// Public API
|
||||
exports.GROUP = GROUP;
|
||||
|
@ -154,6 +157,20 @@ exports._mapAttachments = upgradeAttachment => async (message, context) => {
|
|||
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) ->
|
||||
// (Message, Context) ->
|
||||
// Promise Message
|
||||
|
@ -214,6 +231,13 @@ const toVersion4 = exports._withSchemaVersion(
|
|||
);
|
||||
const toVersion5 = exports._withSchemaVersion(5, initializeAttachmentMetadata);
|
||||
|
||||
const toVersion6 = exports._withSchemaVersion(
|
||||
6,
|
||||
exports._mapContact(
|
||||
Contact.parseAndWriteAvatar(Attachment.migrateDataToFileSystem)
|
||||
)
|
||||
);
|
||||
|
||||
// UpgradeStep
|
||||
exports.upgradeSchema = async (rawMessage, { writeNewAttachmentData } = {}) => {
|
||||
if (!isFunction(writeNewAttachmentData)) {
|
||||
|
@ -228,6 +252,7 @@ exports.upgradeSchema = async (rawMessage, { writeNewAttachmentData } = {}) => {
|
|||
toVersion3,
|
||||
toVersion4,
|
||||
toVersion5,
|
||||
toVersion6,
|
||||
];
|
||||
|
||||
for (let i = 0, max = versions.length; i < max; i += 1) {
|
||||
|
@ -269,10 +294,11 @@ exports.createAttachmentDataWriter = writeExistingAttachmentData => {
|
|||
|
||||
const message = exports.initializeSchemaVersion(rawMessage);
|
||||
|
||||
const { attachments, quote } = message;
|
||||
const { attachments, quote, contact } = message;
|
||||
const hasFilesToWrite =
|
||||
(quote && quote.attachments && quote.attachments.length > 0) ||
|
||||
(attachments && attachments.length > 0);
|
||||
(attachments && attachments.length > 0) ||
|
||||
(contact && contact.length > 0);
|
||||
|
||||
if (!hasFilesToWrite) {
|
||||
return message;
|
||||
|
@ -318,10 +344,26 @@ exports.createAttachmentDataWriter = writeExistingAttachmentData => {
|
|||
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(
|
||||
{},
|
||||
await writeThumbnails(message),
|
||||
{
|
||||
contact: await Promise.all((contact || []).map(writeContactAvatar)),
|
||||
attachments: await Promise.all(
|
||||
(attachments || []).map(async 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',
|
||||
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.model.updateVerified.bind(this.model),
|
||||
|
@ -996,6 +1006,41 @@
|
|||
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) {
|
||||
this.panels = this.panels || [];
|
||||
if (this.panels.length > 0) {
|
||||
|
@ -1199,7 +1244,6 @@
|
|||
|
||||
if (message) {
|
||||
const quote = await this.model.makeQuote(this.quotedMessage);
|
||||
console.log({ quote });
|
||||
this.quote = quote;
|
||||
|
||||
this.focusMessageFieldAndClearDisabled();
|
||||
|
|
|
@ -5,13 +5,17 @@
|
|||
/* global emoji_util: false */
|
||||
/* global Mustache: false */
|
||||
/* global $: false */
|
||||
/* global storage: false */
|
||||
/* global Signal: false */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const { Signal } = window;
|
||||
const { loadAttachmentData } = window.Signal.Migrations;
|
||||
const {
|
||||
loadAttachmentData,
|
||||
getAbsoluteAttachmentPath,
|
||||
} = window.Signal.Migrations;
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
|
@ -290,6 +294,9 @@
|
|||
if (this.quoteView) {
|
||||
this.quoteView.remove();
|
||||
}
|
||||
if (this.contactView) {
|
||||
this.contactView.remove();
|
||||
}
|
||||
|
||||
// NOTE: We have to do this in the background (`then` instead of `await`)
|
||||
// as our tests rely on `onUnload` synchronously removing the view from
|
||||
|
@ -436,6 +443,73 @@
|
|||
});
|
||||
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() {
|
||||
const attachments = this.model.get('attachments');
|
||||
const body = this.model.get('body');
|
||||
|
@ -458,7 +532,10 @@
|
|||
const attachments = this.model.get('attachments');
|
||||
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() {
|
||||
const body = this.model.get('body');
|
||||
|
@ -525,6 +602,7 @@
|
|||
this.renderErrors();
|
||||
this.renderExpiring();
|
||||
this.renderQuote();
|
||||
this.renderContact();
|
||||
|
||||
// NOTE: We have to do this in the background (`then` instead of `await`)
|
||||
// as our code / Backbone seems to rely on `render` synchronously returning
|
||||
|
|
|
@ -1065,6 +1065,14 @@ MessageReceiver.prototype.extend({
|
|||
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) {
|
||||
decrypted.quote.id = decrypted.quote.id.toNumber();
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^0.8.0",
|
||||
"@types/google-libphonenumber": "^7.4.14",
|
||||
"archiver": "^2.1.1",
|
||||
"blob-util": "^1.3.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 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 { app } = electron.remote;
|
||||
|
@ -114,10 +111,12 @@ window.React = require('react');
|
|||
window.ReactDOM = require('react-dom');
|
||||
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;
|
||||
window.i18n = setup(locale, localeMessages);
|
||||
window.i18n = i18n.setup(locale, localeMessages);
|
||||
window.moment.updateLocale(locale, {
|
||||
relativeTime: {
|
||||
s: window.i18n('timestamp_s'),
|
||||
|
@ -127,100 +126,16 @@ window.moment.updateLocale(locale, {
|
|||
});
|
||||
window.moment.locale(locale);
|
||||
|
||||
// ES2015+ modules
|
||||
const attachmentsPath = Attachments.getPath(app.getPath('userData'));
|
||||
const getAbsoluteAttachmentPath = Attachments.createAbsolutePathGetter(
|
||||
attachmentsPath
|
||||
);
|
||||
const deleteAttachmentData = Attachments.createDeleter(attachmentsPath);
|
||||
const readAttachmentData = Attachments.createReader(attachmentsPath);
|
||||
const writeNewAttachmentData = Attachments.createWriterForNew(attachmentsPath);
|
||||
const writeExistingAttachmentData = Attachments.createWriterForExisting(
|
||||
attachmentsPath
|
||||
);
|
||||
window.Signal = Signal.setup({
|
||||
Attachments,
|
||||
userDataPath: app.getPath('userData'),
|
||||
});
|
||||
|
||||
const loadAttachmentData = Attachment.loadData(readAttachmentData);
|
||||
|
||||
// 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');
|
||||
// Pulling these in separately since they access filesystem, electron
|
||||
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.HTML = require('./ts/html');
|
||||
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
|
||||
// /tmp mounted as noexec on Linux.
|
||||
require('./js/spell_check');
|
||||
|
@ -233,7 +148,7 @@ if (window.config.environment === 'test') {
|
|||
tmp: require('tmp'),
|
||||
path: require('path'),
|
||||
basePath: __dirname,
|
||||
attachmentsPath,
|
||||
attachmentsPath: window.Signal.Migrations.attachmentsPath,
|
||||
};
|
||||
/* eslint-enable global-require, import/no-extraneous-dependencies */
|
||||
}
|
||||
|
|
|
@ -84,6 +84,73 @@ message DataMessage {
|
|||
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;
|
||||
repeated AttachmentPointer attachments = 2;
|
||||
optional GroupContext group = 3;
|
||||
|
@ -92,6 +159,7 @@ message DataMessage {
|
|||
optional bytes profileKey = 6;
|
||||
optional uint64 timestamp = 7;
|
||||
optional Quote quote = 8;
|
||||
repeated Contact contact = 9;
|
||||
}
|
||||
|
||||
message NullMessage {
|
||||
|
|
|
@ -526,6 +526,9 @@ span.status {
|
|||
.quote-wrapper + .content {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
.contact-wrapper + .content {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
p {
|
||||
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 {
|
||||
@include message-replies-colors;
|
||||
@include twenty-percent-colors;
|
||||
|
|
|
@ -191,7 +191,6 @@ input.search {
|
|||
.last-message {
|
||||
margin: 6px 0 0;
|
||||
font-size: $font-size-small;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.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 {
|
||||
// Not ideal, but necessary to override the specificity of the android theme color
|
||||
// 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
|
||||
// section if the image doesn't cover it all.
|
||||
.outgoing .tail-wrapper {
|
||||
.attachments {
|
||||
background-color: $blue;
|
||||
}
|
||||
.content {
|
||||
.inner-bubble {
|
||||
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) {
|
||||
-webkit-mask: url($svg) no-repeat center;
|
||||
-webkit-mask-size: 100%;
|
||||
background-color: $color;
|
||||
}
|
||||
|
||||
@mixin header-icon-white($svg) {
|
||||
@include color-svg($svg, rgba(255, 255, 255, 0.8));
|
||||
&:focus,
|
||||
|
@ -17,6 +28,7 @@
|
|||
@include color-svg($svg, black);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin avatar-colors {
|
||||
&.red {
|
||||
background-color: $material_red;
|
||||
|
|
|
@ -22,6 +22,11 @@ $z-index-modal: 100;
|
|||
font-family: 'Roboto';
|
||||
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-family: 'Roboto';
|
||||
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 {
|
||||
background: rgba(255, 255, 255, 0.38);
|
||||
|
||||
|
|
|
@ -283,6 +283,7 @@ describe('Backup', () => {
|
|||
|
||||
const OUR_NUMBER = '+12025550000';
|
||||
const CONTACT_ONE_NUMBER = '+12025550001';
|
||||
const CONTACT_TWO_NUMBER = '+12025550002';
|
||||
|
||||
async function wrappedLoadAttachment(attachment) {
|
||||
return _.omit(await loadAttachmentData(attachment), ['path']);
|
||||
|
@ -356,18 +357,31 @@ describe('Backup', () => {
|
|||
return wrappedLoadAttachment(thumbnail);
|
||||
});
|
||||
|
||||
const promises = (message.attachments || []).map(attachment =>
|
||||
wrappedLoadAttachment(attachment)
|
||||
);
|
||||
|
||||
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;
|
||||
try {
|
||||
const ATTACHMENT_COUNT = 2;
|
||||
const ATTACHMENT_COUNT = 3;
|
||||
const MESSAGE_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');
|
||||
|
@ -494,7 +561,7 @@ describe('Backup', () => {
|
|||
profileAvatar: {
|
||||
contentType: 'image/jpeg',
|
||||
data: new Uint8Array([
|
||||
3,
|
||||
4,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
|
@ -530,7 +597,7 @@ describe('Backup', () => {
|
|||
size: 64,
|
||||
},
|
||||
profileKey: new Uint8Array([
|
||||
4,
|
||||
5,
|
||||
2,
|
||||
3,
|
||||
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',
|
||||
},
|
||||
],
|
||||
contact: [],
|
||||
};
|
||||
|
||||
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 => {
|
||||
|
@ -212,6 +263,7 @@ describe('Message', () => {
|
|||
hasVisualMediaAttachments: undefined,
|
||||
hasFileAttachments: 1,
|
||||
schemaVersion: Message.CURRENT_SCHEMA_VERSION,
|
||||
contact: [],
|
||||
};
|
||||
|
||||
const expectedAttachmentData = stringToArrayBuffer(
|
||||
|
@ -458,7 +510,7 @@ describe('Message', () => {
|
|||
assert.deepEqual(result, message);
|
||||
});
|
||||
|
||||
it('eliminates thumbnails with no data fielkd', async () => {
|
||||
it('eliminates thumbnails with no data field', async () => {
|
||||
const upgradeAttachment = sinon
|
||||
.stub()
|
||||
.throws(new Error("Shouldn't be called"));
|
||||
|
@ -531,4 +583,51 @@ describe('Message', () => {
|
|||
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,
|
||||
},
|
||||
{
|
||||
migrate: (transaction, next) => {
|
||||
console.log('migration version 3');
|
||||
transaction.db.createObjectStore('items');
|
||||
next();
|
||||
},
|
||||
version: 3,
|
||||
},
|
||||
],
|
||||
loadAttachmentData: attachment => Promise.resolve(attachment),
|
||||
getAbsoluteAttachmentPath: path => path,
|
||||
};
|
||||
|
||||
window.Signal.Components = {};
|
||||
|
|
|
@ -5,8 +5,6 @@ const noop = () => {};
|
|||
<Lightbox
|
||||
objectURL="https://placekitten.com/800/600"
|
||||
contentType="image/jpeg"
|
||||
onNext={noop}
|
||||
onPrevious={noop}
|
||||
onSave={noop}
|
||||
/>
|
||||
</div>;
|
||||
|
|
|
@ -1,16 +1,58 @@
|
|||
```js
|
||||
const noop = () => {};
|
||||
|
||||
const items = [
|
||||
{ objectURL: 'https://placekitten.com/800/600', contentType: 'image/jpeg' },
|
||||
{ objectURL: 'https://placekitten.com/900/600', contentType: 'image/jpeg' },
|
||||
{ objectURL: 'https://placekitten.com/980/800', contentType: 'image/jpeg' },
|
||||
{ objectURL: 'https://placekitten.com/656/540', contentType: 'image/jpeg' },
|
||||
{ objectURL: 'https://placekitten.com/762/400', contentType: 'image/jpeg' },
|
||||
{ objectURL: 'https://placekitten.com/920/620', contentType: 'image/jpeg' },
|
||||
const messages = [
|
||||
{
|
||||
objectURL: 'https://placekitten.com/800/600',
|
||||
attachments: [
|
||||
{
|
||||
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 }}>
|
||||
<LightboxGallery items={items} onSave={noop} />
|
||||
<LightboxGallery messages={messages} onSave={noop} />
|
||||
</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 ReactDOM from 'react-dom';
|
||||
import { padStart, sample } from 'lodash';
|
||||
import libphonenumber from 'google-libphonenumber';
|
||||
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
@ -14,14 +15,9 @@ export { _ };
|
|||
export { ConversationContext } from './ConversationContext';
|
||||
export { BackboneWrapper } from '../components/utility/BackboneWrapper';
|
||||
|
||||
// Here we can make things inside Webpack available to Backbone views like preload.js.
|
||||
|
||||
import { Quote } from '../components/conversation/Quote';
|
||||
import * as HTML from '../html';
|
||||
|
||||
import * as Attachment from '../../ts/types/Attachment';
|
||||
import * as MIME from '../../ts/types/MIME';
|
||||
import { SignalService } from '../../ts/protobuf';
|
||||
// @ts-ignore
|
||||
import * as Signal from '../../js/signal';
|
||||
import { SignalService } from '../protobuf';
|
||||
|
||||
// TypeScript wants two things when you import:
|
||||
// 1) a normal typescript file
|
||||
|
@ -103,14 +99,15 @@ import localeMessages from '../../_locales/en/messages.json';
|
|||
|
||||
// @ts-ignore
|
||||
import { setup } from '../../js/modules/i18n';
|
||||
import * as Util from '../util';
|
||||
import filesize from 'filesize';
|
||||
|
||||
const i18n = setup(locale, localeMessages);
|
||||
|
||||
export { theme, locale, i18n };
|
||||
parent.filesize = filesize;
|
||||
|
||||
parent.i18n = i18n;
|
||||
parent.React = React;
|
||||
parent.ReactDOM = ReactDOM;
|
||||
parent.moment = moment;
|
||||
|
||||
parent.moment.updateLocale(locale, {
|
||||
|
@ -122,18 +119,26 @@ parent.moment.updateLocale(locale, {
|
|||
});
|
||||
parent.moment.locale(locale);
|
||||
|
||||
parent.React = React;
|
||||
parent.ReactDOM = ReactDOM;
|
||||
export { theme, locale, i18n };
|
||||
|
||||
parent.Signal.HTML = HTML;
|
||||
parent.Signal.Types.MIME = MIME;
|
||||
parent.Signal.Types.Attachment = Attachment;
|
||||
parent.Signal.Components = {
|
||||
Quote,
|
||||
// Used by signal.js to set up code that deals with message attachments/avatars
|
||||
const Attachments = {
|
||||
createAbsolutePathGetter: () => () => '/fake/path',
|
||||
createDeleter: () => async () => undefined,
|
||||
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.filesize = filesize;
|
||||
|
||||
parent.ConversationController._initialFetchComplete = true;
|
||||
parent.ConversationController._initialPromise = Promise.resolve();
|
||||
|
@ -194,6 +199,20 @@ group.contactCollection.add(CONTACTS[2]);
|
|||
export { COLORS, CONTACTS, me, group };
|
||||
|
||||
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
|
||||
// @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 { Contact } from './Contact';
|
||||
import { IndexableBoolean, IndexablePresence } from './IndexedDB';
|
||||
|
||||
export type Message = UserMessage | VerifiedChangeMessage;
|
||||
|
@ -21,6 +22,7 @@ export type IncomingMessage = Readonly<
|
|||
sourceDevice?: number;
|
||||
} & SharedMessageProperties &
|
||||
MessageSchemaVersion5 &
|
||||
MessageSchemaVersion6 &
|
||||
ExpirationTimerUpdate
|
||||
>;
|
||||
|
||||
|
@ -81,3 +83,9 @@ type MessageSchemaVersion5 = Partial<
|
|||
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"
|
||||
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":
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.1.tgz#55758d44d422756d6329cbf54e6d41931d7ba28f"
|
||||
|
|
Loading…
Reference in a new issue