Full support for quotations in Android theme
This commit is contained in:
parent
47a3acd5c9
commit
1cc0633786
13 changed files with 734 additions and 128 deletions
1
images/image.svg
Normal file
1
images/image.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M8.5,13.5L11,16.5L14.5,12L19,18H5M21,19V5C21,3.89 20.1,3 19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19Z" /></svg>
|
After Width: | Height: | Size: 410 B |
|
@ -17,7 +17,7 @@
|
|||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
const { Message } = window.Signal.Types;
|
||||
const { Message, MIME } = window.Signal.Types;
|
||||
const { upgradeMessageSchema, loadAttachmentData } = window.Signal.Migrations;
|
||||
|
||||
// TODO: Factor out private and group subclasses of Conversation
|
||||
|
@ -1027,15 +1027,173 @@
|
|||
});
|
||||
},
|
||||
|
||||
fetchMessages() {
|
||||
if (!this.id) {
|
||||
return Promise.reject(new Error('This conversation has no id!'));
|
||||
makeKey(author, id) {
|
||||
return `${author}-${id}`;
|
||||
},
|
||||
doMessagesMatch(left, right) {
|
||||
if (left.get('source') !== right.get('source')) {
|
||||
return false;
|
||||
}
|
||||
return this.messageCollection.fetchConversation(
|
||||
if (left.get('sent_at') !== right.get('sent_at')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
needData(attachments) {
|
||||
if (!attachments || attachments.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const first = attachments[0];
|
||||
const { thumbnail, contentType } = first;
|
||||
|
||||
return thumbnail || MIME.isVideo(contentType) || MIME.isImage(contentType);
|
||||
},
|
||||
forceRender(message) {
|
||||
message.trigger('change', message);
|
||||
},
|
||||
makeObjectUrl(data, contentType) {
|
||||
const blob = new Blob([data], {
|
||||
type: contentType,
|
||||
});
|
||||
return URL.createObjectURL(blob);
|
||||
},
|
||||
makeMessagesLookup(messages) {
|
||||
return messages.reduce((acc, message) => {
|
||||
const { source, sent_at: sentAt } = message.attributes;
|
||||
const key = this.makeKey(source, sentAt);
|
||||
|
||||
acc[key] = message;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
},
|
||||
async loadQuotedMessageFromDatabase(message) {
|
||||
const { quote } = message.attributes;
|
||||
const { attachments, id } = quote;
|
||||
const first = attachments[0];
|
||||
|
||||
// Maybe in the future we could try to pull the thumbnail from a video ourselves,
|
||||
// but for now we will rely on incoming thumbnails only.
|
||||
if (!MIME.isImage(first.contentType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const collection = new Whisper.MessageCollection();
|
||||
await collection.fetchSentAt(id);
|
||||
const queryMessage = collection.find(m => this.doMessagesMatch(message, m));
|
||||
|
||||
if (!queryMessage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const queryAttachments = queryMessage.attachments || [];
|
||||
if (queryAttachments.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const queryFirst = queryAttachments[0];
|
||||
queryMessage.attachments[0] = await loadAttachmentData(queryFirst);
|
||||
|
||||
// Note: it would be nice to take the full-size image and downsample it into
|
||||
// a true thumbnail here.
|
||||
// Note: if the attachment is a video, then this object URL won't make any sense
|
||||
// when we try to use it in an img tag.
|
||||
queryMessage.updateImageUrl();
|
||||
|
||||
// We need to differentiate between messages we load from database and those already
|
||||
// in memory. More cleanup needs to happen on messages from the database because
|
||||
// they aren't tracked any other way.
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
message.quotedMessageFromDatabase = queryMessage;
|
||||
|
||||
this.forceRender(message);
|
||||
return true;
|
||||
},
|
||||
async loadQuoteThumbnail(message) {
|
||||
const { quote } = message.attributes;
|
||||
const { attachments } = quote;
|
||||
const first = attachments[0];
|
||||
const { thumbnail } = first;
|
||||
|
||||
if (!thumbnail) {
|
||||
return false;
|
||||
}
|
||||
const thumbnailWithData = await loadAttachmentData(thumbnail);
|
||||
thumbnailWithData.objectUrl = this.makeObjectUrl(
|
||||
thumbnailWithData.data,
|
||||
thumbnailWithData.contentType
|
||||
);
|
||||
|
||||
// If we update this data in place, there's the risk that this data could be
|
||||
// saved back to the database
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
message.quoteThumbnail = thumbnailWithData;
|
||||
|
||||
this.forceRender(message);
|
||||
return true;
|
||||
},
|
||||
|
||||
async processQuotes(messages) {
|
||||
const lookup = this.makeMessagesLookup(messages);
|
||||
|
||||
const promises = messages.map(async (message) => {
|
||||
const { quote } = message.attributes;
|
||||
if (!quote) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { attachments } = quote;
|
||||
if (!this.needData(attachments)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We've already gone through this method once for this message
|
||||
if (message.quoteIsProcessed) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
message.quoteIsProcessed = true;
|
||||
|
||||
// First, check to see if we've already loaded the target message into memory
|
||||
const { author, id } = quote;
|
||||
const key = this.makeKey(author, id);
|
||||
const quotedMessage = lookup[key];
|
||||
|
||||
if (quotedMessage) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
message.quotedMessage = quotedMessage;
|
||||
this.forceRender(message);
|
||||
return;
|
||||
}
|
||||
|
||||
// Then go to the database for the real referenced attachment
|
||||
const loaded = await this.loadQuotedMessageFromDatabase(message, id);
|
||||
if (loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Finally, use the provided thumbnail
|
||||
await this.loadQuoteThumbnail(message, quote);
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
},
|
||||
|
||||
async fetchMessages() {
|
||||
if (!this.id) {
|
||||
throw new Error('This conversation has no id!');
|
||||
}
|
||||
|
||||
await this.messageCollection.fetchConversation(
|
||||
this.id,
|
||||
null,
|
||||
this.get('unreadCount')
|
||||
);
|
||||
|
||||
// We kick this process off, but don't wait for it. If async updates happen on a
|
||||
// given Message, 'change' will be triggered
|
||||
this.processQuotes(this.messageCollection);
|
||||
},
|
||||
|
||||
hasMember(number) {
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
this.on('destroy', this.onDestroy);
|
||||
this.on('change:expirationStartTimestamp', this.setToExpire);
|
||||
this.on('change:expireTimer', this.setToExpire);
|
||||
this.on('unload', this.revokeImageUrl);
|
||||
this.on('unload', this.unload);
|
||||
this.setToExpire();
|
||||
},
|
||||
idForLogging() {
|
||||
|
@ -174,6 +174,20 @@
|
|||
this.imageUrl = null;
|
||||
}
|
||||
},
|
||||
unload() {
|
||||
if (this.quoteThumbnail) {
|
||||
URL.revokeObjectURL(this.quoteThumbnail.objectUrl);
|
||||
this.quoteThumbnail = null;
|
||||
}
|
||||
if (this.quotedMessageFromDatabase) {
|
||||
this.quotedMessageFromDatabase.unload();
|
||||
this.quotedMessageFromDatabase = null;
|
||||
}
|
||||
if (this.quotedMessage) {
|
||||
this.quotedMessage = null;
|
||||
}
|
||||
this.revokeImageUrl();
|
||||
},
|
||||
revokeImageUrl() {
|
||||
if (this.imageUrl) {
|
||||
URL.revokeObjectURL(this.imageUrl);
|
||||
|
|
|
@ -114,6 +114,7 @@
|
|||
this.listenTo(this.model, 'expired', this.onExpired);
|
||||
this.listenTo(this.model, 'prune', this.onPrune);
|
||||
this.listenTo(this.model.messageCollection, 'expired', this.onExpiredCollection);
|
||||
this.listenTo(this.model.messageCollection, 'scroll-to-message', this.scrollToMessage);
|
||||
|
||||
this.lazyUpdateVerified = _.debounce(
|
||||
this.model.updateVerified.bind(this.model),
|
||||
|
@ -529,6 +530,22 @@
|
|||
}
|
||||
},
|
||||
|
||||
scrollToMessage: function(providedOptions) {
|
||||
const options = providedOptions || options;
|
||||
const { id } = options;
|
||||
|
||||
if (id) {
|
||||
return;
|
||||
}
|
||||
|
||||
const el = this.$(`#${id}`);
|
||||
if (!el || el.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
el.scrollIntoView();
|
||||
},
|
||||
|
||||
scrollToBottom: function() {
|
||||
// If we're above the last seen indicator, we should scroll there instead
|
||||
// Note: if we don't end up at the bottom of the conversation, button will not go away!
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
/* global _: false */
|
||||
/* global emoji_util: false */
|
||||
/* global Mustache: false */
|
||||
/* global ConversationController: false */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
|
@ -360,44 +361,74 @@
|
|||
this.timerView.setElement(this.$('.timer'));
|
||||
this.timerView.update();
|
||||
},
|
||||
getQuoteObjectUrl() {
|
||||
// Potential sources of objectUrl, as provided in Conversation.processQuotes
|
||||
// 1. model.quotedMessage.imageUrl
|
||||
// 2. model.quoteThumbnail.objectUrl
|
||||
|
||||
if (this.model.quotedMessageFromDatabase) {
|
||||
return this.model.quotedMessageFromDatabase.imageUrl;
|
||||
}
|
||||
if (this.model.quotedMessage) {
|
||||
return this.model.quotedMessage.imageUrl;
|
||||
}
|
||||
if (this.model.quoteThumbnail) {
|
||||
return this.model.quoteThumbnail.objectUrl;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
renderReply() {
|
||||
const VOICE_MESSAGE_FLAG =
|
||||
textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE;
|
||||
function addVoiceMessageFlag(attachment) {
|
||||
return Object.assign({}, attachment, {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
isVoiceMessage: attachment.flags & VOICE_MESSAGE_FLAG,
|
||||
});
|
||||
}
|
||||
function getObjectUrl(attachment) {
|
||||
if (!attachment || attachment.objectUrl) {
|
||||
return attachment;
|
||||
}
|
||||
|
||||
const blob = new Blob([attachment.data], {
|
||||
type: attachment.contentType,
|
||||
});
|
||||
return Object.assign({}, attachment, {
|
||||
objectUrl: URL.createObjectURL(blob),
|
||||
});
|
||||
}
|
||||
function processAttachment(attachment) {
|
||||
return getObjectUrl(addVoiceMessageFlag(attachment));
|
||||
}
|
||||
|
||||
const VOICE_FLAG = textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE;
|
||||
const objectUrl = this.getQuoteObjectUrl();
|
||||
const quote = this.model.get('quote');
|
||||
if (!quote) {
|
||||
return;
|
||||
}
|
||||
|
||||
function processAttachment(attachment) {
|
||||
const thumbnail = !attachment.thumbnail
|
||||
? null
|
||||
: Object.assign({}, attachment.thumbnail, {
|
||||
objectUrl,
|
||||
});
|
||||
|
||||
return Object.assign({}, attachment, {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
isVoiceMessage: attachment.flags & VOICE_FLAG,
|
||||
thumbnail,
|
||||
});
|
||||
}
|
||||
|
||||
const { author } = quote;
|
||||
const contact = ConversationController.get(author);
|
||||
const authorTitle = contact ? contact.getTitle() : author;
|
||||
const authorProfileName = contact ? contact.getProfileName() : null;
|
||||
const authorColor = contact ? contact.getColor() : 'grey';
|
||||
const isIncoming = this.model.isIncoming();
|
||||
const quoterContact = this.model.getContact();
|
||||
const quoterAuthorColor = quoterContact ? quoterContact.getColor() : null;
|
||||
|
||||
const props = {
|
||||
authorName: 'someone',
|
||||
authorColor: 'indigo',
|
||||
authorTitle,
|
||||
authorProfileName,
|
||||
authorColor,
|
||||
isIncoming,
|
||||
quoterAuthorColor,
|
||||
openQuotedMessage: () => {
|
||||
const { quotedMessage } = this.model;
|
||||
if (quotedMessage) {
|
||||
this.trigger('scroll-to-message', { id: quotedMessage.id });
|
||||
}
|
||||
},
|
||||
text: quote.text,
|
||||
attachments: quote.attachments && quote.attachments.map(processAttachment),
|
||||
};
|
||||
|
||||
if (!this.replyView) {
|
||||
if (contact) {
|
||||
this.listenTo(contact, 'change:color', this.renderReply);
|
||||
}
|
||||
this.replyView = new Whisper.ReactWrapperView({
|
||||
el: this.$('.quote-wrapper'),
|
||||
Component: window.Signal.Components.Quote,
|
||||
|
|
|
@ -27,6 +27,9 @@ module.exports = {
|
|||
// Exposes necessary utilities in the global scope for all readme code snippets
|
||||
util: 'ts/styleguide/StyleGuideUtil',
|
||||
},
|
||||
contextDependencies: [
|
||||
path.join(__dirname, 'ts/test'),
|
||||
],
|
||||
// We don't want one long, single page
|
||||
pagePerSection: true,
|
||||
// Expose entire repository to the styleguidist server, primarily for stylesheets
|
||||
|
|
|
@ -379,6 +379,14 @@ li.entry .error-icon-container {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.message-list .outgoing .bubble .quote {
|
||||
margin-top: $android-bubble-quote-padding - $android-bubble-padding-vertical;
|
||||
}
|
||||
|
||||
.private .message-list .incoming .bubble .quote {
|
||||
margin-top: $android-bubble-quote-padding - $android-bubble-padding-vertical;
|
||||
}
|
||||
|
||||
.sender {
|
||||
font-size: smaller;
|
||||
opacity: 0.8;
|
||||
|
@ -435,6 +443,8 @@ span.status {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.bubble {
|
||||
position: relative;
|
||||
left: -2px;
|
||||
|
@ -452,16 +462,18 @@ span.status {
|
|||
|
||||
.quote {
|
||||
@include message-replies-colors;
|
||||
@include twenty-percent-colors;
|
||||
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
overflow: hidden;
|
||||
|
||||
border-radius: 2px;
|
||||
background-color: #eee;
|
||||
position: relative;
|
||||
|
||||
margin-top: $android-bubble-quote-padding - $android-bubble-padding-vertical;
|
||||
margin-right: $android-bubble-quote-padding - $android-bubble-padding-horizontal;
|
||||
margin-left: $android-bubble-quote-padding - $android-bubble-padding-horizontal;
|
||||
margin-bottom: 0.5em;
|
||||
|
@ -480,10 +492,25 @@ span.status {
|
|||
.author {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.3em;
|
||||
@include text-colors;
|
||||
|
||||
.profile-name {
|
||||
font-size: smaller;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
white-space: pre-wrap;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
// Note: -webkit-line-clamp doesn't work for RTL text, and it forces you to use
|
||||
// ... as the truncation indicator. That's not a solution that works well for
|
||||
// all languages. More resources:
|
||||
// - http://hackingui.com/front-end/a-pure-css-solution-for-multiline-text-truncation/
|
||||
// - https://medium.com/mofed/css-line-clamp-the-good-the-bad-and-the-straight-up-broken-865413f16e5
|
||||
}
|
||||
|
||||
.type-label {
|
||||
|
@ -498,22 +525,53 @@ span.status {
|
|||
|
||||
.icon-container {
|
||||
flex: initial;
|
||||
min-width: 48px;
|
||||
@include aspect-ratio(1, 1);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
position: relative;
|
||||
|
||||
.inner {
|
||||
border: 1px red solid;
|
||||
max-height: 48px;
|
||||
max-width: 48px;
|
||||
.circle-background {
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
right: 6px;
|
||||
top: 6px;
|
||||
bottom: 6px;
|
||||
|
||||
border-radius: 50%;
|
||||
@include avatar-colors;
|
||||
&.white {
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
top: 12px;
|
||||
bottom: 12px;
|
||||
|
||||
&.file {
|
||||
@include color-svg('../images/file.svg', $grey_d);
|
||||
@include color-svg('../images/file.svg', white);
|
||||
}
|
||||
&.image {
|
||||
@include color-svg('../images/image.svg', white);
|
||||
}
|
||||
&.microphone {
|
||||
@include color-svg('../images/microphone.svg', $grey_d);
|
||||
@include color-svg('../images/microphone.svg', white);
|
||||
}
|
||||
&.play {
|
||||
@include color-svg('../images/play.svg', $grey_d);
|
||||
@include color-svg('../images/play.svg', white);
|
||||
}
|
||||
|
||||
@include avatar-colors;
|
||||
}
|
||||
|
||||
.inner {
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -579,6 +637,13 @@ span.status {
|
|||
.avatar, .bubble {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
.quote {
|
||||
background-color: rgba(white, 0.6);
|
||||
border-left-color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.outgoing {
|
||||
|
|
|
@ -67,8 +67,46 @@
|
|||
&.deep_orange { background-color: $dark_material_deep_orange ; }
|
||||
&.amber { background-color: $dark_material_amber ; }
|
||||
&.blue_grey { background-color: $dark_material_blue_grey ; }
|
||||
&.grey { background-color: #666666 ; }
|
||||
&.default { background-color: $blue ; }
|
||||
&.grey { background-color: #666666 ; }
|
||||
&.default { background-color: $blue ; }
|
||||
}
|
||||
@mixin twenty-percent-colors {
|
||||
&.red { background-color: rgba($dark_material_red, 0.2) ; }
|
||||
&.pink { background-color: rgba($dark_material_pink, 0.2) ; }
|
||||
&.purple { background-color: rgba($dark_material_purple, 0.2) ; }
|
||||
&.deep_purple { background-color: rgba($dark_material_deep_purple, 0.2) ; }
|
||||
&.indigo { background-color: rgba($dark_material_indigo, 0.2) ; }
|
||||
&.blue { background-color: rgba($dark_material_blue, 0.2) ; }
|
||||
&.light_blue { background-color: rgba($dark_material_light_blue, 0.2) ; }
|
||||
&.cyan { background-color: rgba($dark_material_cyan, 0.2) ; }
|
||||
&.teal { background-color: rgba($dark_material_teal, 0.2) ; }
|
||||
&.green { background-color: rgba($dark_material_green, 0.2) ; }
|
||||
&.light_green { background-color: rgba($dark_material_light_green, 0.2) ; }
|
||||
&.orange { background-color: rgba($dark_material_orange, 0.2) ; }
|
||||
&.deep_orange { background-color: rgba($dark_material_deep_orange, 0.2) ; }
|
||||
&.amber { background-color: rgba($dark_material_amber, 0.2) ; }
|
||||
&.blue_grey { background-color: rgba($dark_material_blue_grey, 0.2) ; }
|
||||
&.grey { background-color: rgba(#666666, 0.2) ; }
|
||||
&.default { background-color: rgba($blue, 0.2) ; }
|
||||
}
|
||||
@mixin text-colors {
|
||||
&.red { color: $material_red ; }
|
||||
&.pink { color: $material_pink ; }
|
||||
&.purple { color: $material_purple ; }
|
||||
&.deep_purple { color: $material_deep_purple ; }
|
||||
&.indigo { color: $material_indigo ; }
|
||||
&.blue { color: $material_blue ; }
|
||||
&.light_blue { color: $material_light_blue ; }
|
||||
&.cyan { color: $material_cyan ; }
|
||||
&.teal { color: $material_teal ; }
|
||||
&.green { color: $material_green ; }
|
||||
&.light_green { color: $material_light_green ; }
|
||||
&.orange { color: $material_orange ; }
|
||||
&.deep_orange { color: $material_deep_orange ; }
|
||||
&.amber { color: $material_amber ; }
|
||||
&.blue_grey { color: $material_blue_grey ; }
|
||||
&.grey { color: #999999 ; }
|
||||
&.default { color: $blue ; }
|
||||
}
|
||||
|
||||
// TODO: Deduplicate these! Can SASS functions generate property names?
|
||||
|
|
|
@ -13,7 +13,7 @@ export class Message extends React.Component<{}, {}> {
|
|||
<span className="avatar" />
|
||||
<div className="bubble">
|
||||
<div className="sender" dir="auto" />
|
||||
<div className="inner-bubble">
|
||||
<div className="inner-bubble with-tail">
|
||||
<div className="attachments" />
|
||||
<p className="content" dir="auto">
|
||||
<span className="body">
|
||||
|
|
|
@ -10,15 +10,85 @@ const outgoing = new Whisper.Message({
|
|||
sent_at: Date.now() - 18000000,
|
||||
quote: {
|
||||
text: 'How many ferrets do you have?',
|
||||
author: '+12025550100',
|
||||
author: '+12025550011',
|
||||
id: Date.now() - 1000,
|
||||
},
|
||||
});
|
||||
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
|
||||
source: '+12025550100',
|
||||
source: '+12025550011',
|
||||
type: 'incoming',
|
||||
quote: Object.assign({}, outgoing.attributes.quote, {
|
||||
author: '+12025550200',
|
||||
author: '+12025550005',
|
||||
}),
|
||||
}));
|
||||
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 a group conversation
|
||||
|
||||
```jsx
|
||||
const outgoing = new Whisper.Message({
|
||||
type: 'outgoing',
|
||||
body: 'About six',
|
||||
sent_at: Date.now() - 18000000,
|
||||
quote: {
|
||||
text: 'How many ferrets do you have?',
|
||||
author: '+12025550010',
|
||||
id: Date.now() - 1000,
|
||||
},
|
||||
});
|
||||
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
|
||||
source: '+12025550007',
|
||||
type: 'incoming',
|
||||
quote: Object.assign({}, outgoing.attributes.quote, {
|
||||
author: '+12025550002',
|
||||
}),
|
||||
}));
|
||||
const View = Whisper.MessageView;
|
||||
<util.ConversationContext theme={util.theme} conversationType="group">
|
||||
<util.BackboneWrapper
|
||||
View={View}
|
||||
options={{ model: incoming }}
|
||||
/>
|
||||
<util.BackboneWrapper
|
||||
View={View}
|
||||
options={{ model: outgoing }}
|
||||
/>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
#### A lot of text in quotation
|
||||
|
||||
```jsx
|
||||
const outgoing = new Whisper.Message({
|
||||
type: 'outgoing',
|
||||
body: 'About six',
|
||||
sent_at: Date.now() - 18000000,
|
||||
quote: {
|
||||
text:
|
||||
'I have lots of things to say. First, I enjoy otters. Second best are cats. ' +
|
||||
'After that, probably dogs. And then, you know, reptiles of all types. ' +
|
||||
'Then birds. They are dinosaurs, after all. Then cephalapods, because they are ' +
|
||||
'really smart.',
|
||||
author: '+12025550011',
|
||||
id: Date.now() - 1000,
|
||||
},
|
||||
});
|
||||
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
|
||||
source: '+12025550011',
|
||||
type: 'incoming',
|
||||
quote: Object.assign({}, outgoing.attributes.quote, {
|
||||
author: '+12025550005',
|
||||
}),
|
||||
}));
|
||||
const View = Whisper.MessageView;
|
||||
|
@ -37,13 +107,17 @@ const View = Whisper.MessageView;
|
|||
#### Image with caption
|
||||
|
||||
```jsx
|
||||
const quotedMessage = {
|
||||
imageUrl: util.gifObjectUrl,
|
||||
id: '3234-23423-2342',
|
||||
};
|
||||
const outgoing = new Whisper.Message({
|
||||
type: 'outgoing',
|
||||
body: "Totally, it's a pretty unintuitive concept.",
|
||||
sent_at: Date.now() - 18000000,
|
||||
quote: {
|
||||
text: 'I am pretty confused about Pi.',
|
||||
author: '+12025550100',
|
||||
author: '+12025550011',
|
||||
id: Date.now() - 1000,
|
||||
attachments: [
|
||||
{
|
||||
|
@ -51,19 +125,22 @@ const outgoing = new Whisper.Message({
|
|||
fileName: 'pi.gif',
|
||||
thumbnail: {
|
||||
contentType: 'image/gif',
|
||||
data: util.gif,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
|
||||
source: '+12025550100',
|
||||
source: '+12025550011',
|
||||
type: 'incoming',
|
||||
quote: Object.assign({}, outgoing.attributes.quote, {
|
||||
author: '+12025550200',
|
||||
author: '+12025550005',
|
||||
}),
|
||||
}));
|
||||
|
||||
outgoing.quotedMessage = quotedMessage;
|
||||
incoming.quotedMessage = quotedMessage;
|
||||
|
||||
const View = Whisper.MessageView;
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<util.BackboneWrapper
|
||||
|
@ -80,12 +157,16 @@ const View = Whisper.MessageView;
|
|||
#### Image
|
||||
|
||||
```jsx
|
||||
const quotedMessage = {
|
||||
imageUrl: util.gifObjectUrl,
|
||||
};
|
||||
|
||||
const outgoing = new Whisper.Message({
|
||||
type: 'outgoing',
|
||||
body: "Yeah, pi. Tough to wrap your head around.",
|
||||
sent_at: Date.now() - 18000000,
|
||||
quote: {
|
||||
author: '+12025550100',
|
||||
author: '+12025550011',
|
||||
id: Date.now() - 1000,
|
||||
attachments: [
|
||||
{
|
||||
|
@ -93,19 +174,61 @@ const outgoing = new Whisper.Message({
|
|||
fileName: 'pi.gif',
|
||||
thumbnail: {
|
||||
contentType: 'image/gif',
|
||||
data: util.gif,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
|
||||
source: '+12025550100',
|
||||
source: '+12025550011',
|
||||
type: 'incoming',
|
||||
quote: Object.assign({}, outgoing.attributes.quote, {
|
||||
author: '+12025550200',
|
||||
author: '+12025550005',
|
||||
}),
|
||||
}));
|
||||
|
||||
outgoing.quotedMessage = quotedMessage;
|
||||
incoming.quotedMessage = quotedMessage;
|
||||
|
||||
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>
|
||||
```
|
||||
|
||||
#### Image with no thumbnail
|
||||
|
||||
```jsx
|
||||
const outgoing = new Whisper.Message({
|
||||
type: 'outgoing',
|
||||
body: "Yeah, pi. Tough to wrap your head around.",
|
||||
sent_at: Date.now() - 18000000,
|
||||
quote: {
|
||||
author: '+12025550011',
|
||||
id: Date.now() - 1000,
|
||||
attachments: [
|
||||
{
|
||||
contentType: 'image/gif',
|
||||
fileName: 'pi.gif',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
|
||||
source: '+12025550011',
|
||||
type: 'incoming',
|
||||
quote: Object.assign({}, outgoing.attributes.quote, {
|
||||
author: '+12025550005',
|
||||
}),
|
||||
}));
|
||||
|
||||
const View = Whisper.MessageView;
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<util.BackboneWrapper
|
||||
|
@ -122,12 +245,16 @@ const View = Whisper.MessageView;
|
|||
#### Video with caption
|
||||
|
||||
```jsx
|
||||
const quotedMessage = {
|
||||
imageUrl: util.gifObjectUrl,
|
||||
};
|
||||
|
||||
const outgoing = new Whisper.Message({
|
||||
type: 'outgoing',
|
||||
body: "Sweet the way the video sneaks up on you!",
|
||||
sent_at: Date.now() - 18000000,
|
||||
quote: {
|
||||
author: '+12025550100',
|
||||
author: '+12025550011',
|
||||
text: 'Check out this video I found!',
|
||||
id: Date.now() - 1000,
|
||||
attachments: [
|
||||
|
@ -136,19 +263,22 @@ const outgoing = new Whisper.Message({
|
|||
fileName: 'freezing_bubble.mp4',
|
||||
thumbnail: {
|
||||
contentType: 'image/gif',
|
||||
data: util.gif,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
|
||||
source: '+12025550100',
|
||||
source: '+12025550011',
|
||||
type: 'incoming',
|
||||
quote: Object.assign({}, outgoing.attributes.quote, {
|
||||
author: '+12025550200',
|
||||
author: '+12025550005',
|
||||
}),
|
||||
}));
|
||||
|
||||
outgoing.quotedMessage = quotedMessage;
|
||||
incoming.quotedMessage = quotedMessage;
|
||||
|
||||
const View = Whisper.MessageView;
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<util.BackboneWrapper
|
||||
|
@ -165,12 +295,16 @@ const View = Whisper.MessageView;
|
|||
#### Video
|
||||
|
||||
```jsx
|
||||
const quotedMessage = {
|
||||
imageUrl: util.gifObjectUrl,
|
||||
};
|
||||
|
||||
const outgoing = new Whisper.Message({
|
||||
type: 'outgoing',
|
||||
body: "Awesome!",
|
||||
sent_at: Date.now() - 18000000,
|
||||
quote: {
|
||||
author: '+12025550100',
|
||||
author: '+12025550011',
|
||||
id: Date.now() - 1000,
|
||||
attachments: [
|
||||
{
|
||||
|
@ -185,12 +319,55 @@ const outgoing = new Whisper.Message({
|
|||
},
|
||||
});
|
||||
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
|
||||
source: '+12025550100',
|
||||
source: '+12025550011',
|
||||
type: 'incoming',
|
||||
quote: Object.assign({}, outgoing.attributes.quote, {
|
||||
author: '+12025550200',
|
||||
author: '+12025550005',
|
||||
}),
|
||||
}));
|
||||
|
||||
outgoing.quotedMessage = quotedMessage;
|
||||
incoming.quotedMessage = quotedMessage;
|
||||
|
||||
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>
|
||||
```
|
||||
|
||||
#### Video with no thumbnail
|
||||
|
||||
```jsx
|
||||
const outgoing = new Whisper.Message({
|
||||
type: 'outgoing',
|
||||
body: "Awesome!",
|
||||
sent_at: Date.now() - 18000000,
|
||||
quote: {
|
||||
author: '+12025550011',
|
||||
id: Date.now() - 1000,
|
||||
attachments: [
|
||||
{
|
||||
contentType: 'video/mp4',
|
||||
fileName: 'freezing_bubble.mp4',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
|
||||
source: '+12025550011',
|
||||
type: 'incoming',
|
||||
quote: Object.assign({}, outgoing.attributes.quote, {
|
||||
author: '+12025550005',
|
||||
}),
|
||||
}));
|
||||
|
||||
const View = Whisper.MessageView;
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<util.BackboneWrapper
|
||||
|
@ -212,7 +389,7 @@ const outgoing = new Whisper.Message({
|
|||
body: 'I really like it!',
|
||||
sent_at: Date.now() - 18000000,
|
||||
quote: {
|
||||
author: '+12025550100',
|
||||
author: '+12025550011',
|
||||
text: 'Check out this beautiful song!',
|
||||
id: Date.now() - 1000,
|
||||
attachments: [
|
||||
|
@ -224,10 +401,10 @@ const outgoing = new Whisper.Message({
|
|||
},
|
||||
});
|
||||
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
|
||||
source: '+12025550100',
|
||||
source: '+12025550011',
|
||||
type: 'incoming',
|
||||
quote: Object.assign({}, outgoing.attributes.quote, {
|
||||
author: '+12025550200',
|
||||
author: '+12025550005',
|
||||
}),
|
||||
}));
|
||||
const View = Whisper.MessageView;
|
||||
|
@ -251,7 +428,7 @@ const outgoing = new Whisper.Message({
|
|||
body: 'I really like it!',
|
||||
sent_at: Date.now() - 18000000,
|
||||
quote: {
|
||||
author: '+12025550100',
|
||||
author: '+12025550011',
|
||||
id: Date.now() - 1000,
|
||||
attachments: [
|
||||
{
|
||||
|
@ -262,10 +439,10 @@ const outgoing = new Whisper.Message({
|
|||
},
|
||||
});
|
||||
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
|
||||
source: '+12025550100',
|
||||
source: '+12025550011',
|
||||
type: 'incoming',
|
||||
quote: Object.assign({}, outgoing.attributes.quote, {
|
||||
author: '+12025550200',
|
||||
author: '+12025550005',
|
||||
}),
|
||||
}));
|
||||
const View = Whisper.MessageView;
|
||||
|
@ -289,7 +466,7 @@ const outgoing = new Whisper.Message({
|
|||
body: 'I really like it!',
|
||||
sent_at: Date.now() - 18000000,
|
||||
quote: {
|
||||
author: '+12025550100',
|
||||
author: '+12025550011',
|
||||
id: Date.now() - 1000,
|
||||
attachments: [
|
||||
{
|
||||
|
@ -302,10 +479,10 @@ const outgoing = new Whisper.Message({
|
|||
},
|
||||
});
|
||||
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
|
||||
source: '+12025550100',
|
||||
source: '+12025550011',
|
||||
type: 'incoming',
|
||||
quote: Object.assign({}, outgoing.attributes.quote, {
|
||||
author: '+12025550200',
|
||||
author: '+12025550005',
|
||||
}),
|
||||
}));
|
||||
const View = Whisper.MessageView;
|
||||
|
@ -329,7 +506,7 @@ const outgoing = new Whisper.Message({
|
|||
body: "I can't read latin.",
|
||||
sent_at: Date.now() - 18000000,
|
||||
quote: {
|
||||
author: '+12025550100',
|
||||
author: '+12025550011',
|
||||
text: 'This is my manifesto. Tell me what you think!',
|
||||
id: Date.now() - 1000,
|
||||
attachments: [
|
||||
|
@ -341,10 +518,10 @@ const outgoing = new Whisper.Message({
|
|||
},
|
||||
});
|
||||
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
|
||||
source: '+12025550100',
|
||||
source: '+12025550011',
|
||||
type: 'incoming',
|
||||
quote: Object.assign({}, outgoing.attributes.quote, {
|
||||
author: '+12025550200',
|
||||
author: '+12025550005',
|
||||
}),
|
||||
}));
|
||||
const View = Whisper.MessageView;
|
||||
|
@ -368,7 +545,7 @@ const outgoing = new Whisper.Message({
|
|||
body: "Sorry, I can't read latin!",
|
||||
sent_at: Date.now() - 18000000,
|
||||
quote: {
|
||||
author: '+12025550100',
|
||||
author: '+12025550011',
|
||||
id: Date.now() - 1000,
|
||||
attachments: [
|
||||
{
|
||||
|
@ -379,10 +556,10 @@ const outgoing = new Whisper.Message({
|
|||
},
|
||||
});
|
||||
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
|
||||
source: '+12025550100',
|
||||
source: '+12025550011',
|
||||
type: 'incoming',
|
||||
quote: Object.assign({}, outgoing.attributes.quote, {
|
||||
author: '+12025550200',
|
||||
author: '+12025550005',
|
||||
}),
|
||||
}));
|
||||
const View = Whisper.MessageView;
|
||||
|
|
|
@ -7,21 +7,28 @@ import Mime from '../../../js/modules/types/mime';
|
|||
|
||||
interface Props {
|
||||
i18n: (key: string, values?: Array<string>) => string;
|
||||
authorName: string;
|
||||
authorTitle: string;
|
||||
authorProfileName?: string;
|
||||
authorColor: string;
|
||||
attachments: Array<QuotedAttachment>;
|
||||
text: string;
|
||||
attachments: Array<QuotedAttachment>;
|
||||
openQuotedMessage?: () => void;
|
||||
quoterAuthorColor?: string,
|
||||
isIncoming: boolean,
|
||||
}
|
||||
|
||||
interface QuotedAttachment {
|
||||
fileName: string;
|
||||
contentType: string;
|
||||
thumbnail?: Attachment,
|
||||
/* Not included in protobuf */
|
||||
isVoiceMessage: boolean;
|
||||
objectUrl: string;
|
||||
thumbnail: {
|
||||
contentType: string;
|
||||
data: ArrayBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
interface Attachment {
|
||||
contentType: string;
|
||||
/* Not included in protobuf, and is loaded asynchronously */
|
||||
objectUrl?: string;
|
||||
}
|
||||
|
||||
function validateQuote(quote: Props): boolean {
|
||||
|
@ -36,51 +43,68 @@ function validateQuote(quote: Props): boolean {
|
|||
return false;
|
||||
}
|
||||
|
||||
function getContentType(attachments: Array<QuotedAttachment>): string | null {
|
||||
if (!attachments || attachments.length === 0) {
|
||||
return null;
|
||||
function getObjectUrl(thumbnail: Attachment | undefined): string | null {
|
||||
if (thumbnail && thumbnail.objectUrl) {
|
||||
return thumbnail.objectUrl;
|
||||
}
|
||||
|
||||
const first = attachments[0];
|
||||
return first.contentType;
|
||||
return null;
|
||||
}
|
||||
|
||||
export class Quote extends React.Component<Props, {}> {
|
||||
public renderIcon(first: QuotedAttachment) {
|
||||
const contentType = first.contentType;
|
||||
const objectUrl = first.objectUrl;
|
||||
public renderImage(url: string, icon?: string) {
|
||||
return (
|
||||
<div className="icon-container">
|
||||
<div className="inner">
|
||||
<img src={url} />
|
||||
{icon
|
||||
? <div className={classnames('icon', icon)}></div>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (Mime.isVideo(contentType)) {
|
||||
// Render play icon on top of thumbnail
|
||||
// We'd have to generate our own thumbnail from a local video??
|
||||
return <div className='inner play'>Video</div>;
|
||||
} else if (Mime.isImage(contentType)) {
|
||||
if (objectUrl) {
|
||||
return <div className='inner'><img src={objectUrl} /></div>;
|
||||
} else {
|
||||
return <div className='inner'>Loading Widget</div>
|
||||
}
|
||||
} else if (Mime.isAudio(contentType)) {
|
||||
// Show microphone inner in circle
|
||||
return <div className='inner microphone'>Audio</div>;
|
||||
} else {
|
||||
// Show file icon
|
||||
return <div className='inner file'>File</div>;
|
||||
}
|
||||
public renderIcon(icon: string) {
|
||||
const { authorColor, isIncoming, quoterAuthorColor } = this.props;
|
||||
|
||||
const backgroundColor = isIncoming ? 'white' : authorColor;
|
||||
const iconColor = isIncoming ? quoterAuthorColor : 'white';
|
||||
|
||||
return (
|
||||
<div className='icon-container'>
|
||||
<div className={classnames('circle-background', backgroundColor)}></div>
|
||||
<div className={classnames('icon', icon, iconColor)}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public renderIconContainer() {
|
||||
const { attachments } = this.props;
|
||||
|
||||
if (!attachments || attachments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const first = attachments[0];
|
||||
const { contentType, thumbnail } = first;
|
||||
const objectUrl = getObjectUrl(thumbnail);
|
||||
|
||||
return <div className='icon-container'>
|
||||
{this.renderIcon(first)}
|
||||
</div>
|
||||
if (Mime.isVideo(contentType)) {
|
||||
return objectUrl
|
||||
? this.renderImage(objectUrl, 'play')
|
||||
: this.renderIcon('play');
|
||||
}
|
||||
if (Mime.isImage(contentType)) {
|
||||
return objectUrl
|
||||
? this.renderImage(objectUrl)
|
||||
: this.renderIcon('image');
|
||||
}
|
||||
if (Mime.isAudio(contentType)) {
|
||||
return this.renderIcon('microphone');
|
||||
}
|
||||
|
||||
return this.renderIcon('file');
|
||||
}
|
||||
|
||||
public renderText() {
|
||||
|
@ -94,20 +118,19 @@ export class Quote extends React.Component<Props, {}> {
|
|||
return null;
|
||||
}
|
||||
|
||||
const contentType = getContentType(attachments);
|
||||
const first = attachments[0];
|
||||
const fileName = first.fileName;
|
||||
|
||||
console.log(contentType);
|
||||
const { contentType, fileName, isVoiceMessage } = first;
|
||||
|
||||
if (Mime.isVideo(contentType)) {
|
||||
return <div className='type-label'>{i18n('video')}</div>;
|
||||
} else if (Mime.isImage(contentType)) {
|
||||
}
|
||||
if (Mime.isImage(contentType)) {
|
||||
return <div className='type-label'>{i18n('photo')}</div>;
|
||||
} else if (Mime.isAudio(contentType) && first.isVoiceMessage) {
|
||||
}
|
||||
if (Mime.isAudio(contentType) && isVoiceMessage) {
|
||||
return <div className='type-label'>{i18n('voiceMessage')}</div>;
|
||||
} else if (Mime.isAudio(contentType)) {
|
||||
console.log(first);
|
||||
}
|
||||
if (Mime.isAudio(contentType)) {
|
||||
return <div className='type-label'>{i18n('audio')}</div>;
|
||||
}
|
||||
|
||||
|
@ -115,16 +138,27 @@ export class Quote extends React.Component<Props, {}> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const { authorName, authorColor } = this.props;
|
||||
const {
|
||||
authorTitle,
|
||||
authorProfileName,
|
||||
authorColor,
|
||||
openQuotedMessage,
|
||||
} = this.props;
|
||||
|
||||
if (!validateQuote(this.props)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classnames(authorColor, 'quote')} >
|
||||
<div onClick={openQuotedMessage} className={classnames(authorColor, 'quote')} >
|
||||
<div className="primary">
|
||||
<div className="author">{authorName}</div>
|
||||
<div className={classnames(authorColor, 'author')}>
|
||||
{authorTitle}{' '}
|
||||
{authorProfileName
|
||||
? <span className='profile-name'>~{authorProfileName}</span>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
{this.renderText()}
|
||||
</div>
|
||||
{this.renderIconContainer()}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
|
||||
interface Props {
|
||||
|
@ -6,6 +7,7 @@ interface Props {
|
|||
* Corresponds to the theme setting in the app, and the class added to the root element.
|
||||
*/
|
||||
theme: 'ios' | 'android' | 'android-dark';
|
||||
conversationType: 'private' | 'group';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -14,11 +16,11 @@ interface Props {
|
|||
*/
|
||||
export class ConversationContext extends React.Component<Props, {}> {
|
||||
public render() {
|
||||
const { theme } = this.props;
|
||||
const { theme, conversationType } = this.props;
|
||||
|
||||
return (
|
||||
<div className={theme}>
|
||||
<div className="conversation">
|
||||
<div className={theme || 'android'}>
|
||||
<div className={classnames('conversation', conversationType || 'private')}>
|
||||
<div className="discussion-container" style={{padding: '0.5em'}}>
|
||||
<ul className="message-list">
|
||||
{this.props.children}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import moment from 'moment';
|
||||
import qs from 'qs';
|
||||
|
||||
import { sample, padStart } from 'lodash';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
|
@ -23,18 +24,36 @@ import { Quote } from '../components/conversation/Quote';
|
|||
|
||||
// @ts-ignore
|
||||
import gif from '../../fixtures/giphy-GVNvOUpeYmI7e.gif';
|
||||
const gifObjectUrl = makeObjectUrl(gif, 'image/gif');
|
||||
// @ts-ignore
|
||||
import mp3 from '../../fixtures/incompetech-com-Agnus-Dei-X.mp3';
|
||||
const mp3ObjectUrl = makeObjectUrl(mp3, 'audio/mp3');
|
||||
// @ts-ignore
|
||||
import txt from '../../fixtures/lorem-ipsum.txt';
|
||||
const txtObjectUrl = makeObjectUrl(txt, 'text/plain');
|
||||
// @ts-ignore
|
||||
import mp4 from '../../fixtures/pixabay-Soap-Bubble-7141.mp4';
|
||||
const mp4ObjectUrl = makeObjectUrl(mp4, 'video/mp4');
|
||||
|
||||
function makeObjectUrl(data: ArrayBuffer, contentType: string): string {
|
||||
const blob = new Blob([data], {
|
||||
type: contentType,
|
||||
});
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
const ourNumber = '+12025559999';
|
||||
|
||||
export {
|
||||
mp3,
|
||||
mp3ObjectUrl,
|
||||
gif,
|
||||
gifObjectUrl,
|
||||
mp4,
|
||||
mp4ObjectUrl,
|
||||
txt,
|
||||
txtObjectUrl,
|
||||
ourNumber
|
||||
};
|
||||
|
||||
|
||||
|
@ -82,3 +101,50 @@ parent.Signal.Components = {
|
|||
|
||||
parent.ConversationController._initialFetchComplete = true;
|
||||
parent.ConversationController._initialPromise = Promise.resolve();
|
||||
|
||||
|
||||
const COLORS = [
|
||||
'red',
|
||||
'pink',
|
||||
'purple',
|
||||
'deep_purple',
|
||||
'indigo',
|
||||
'blue',
|
||||
'light_blue',
|
||||
'cyan',
|
||||
'teal',
|
||||
'green',
|
||||
'light_green',
|
||||
'orange',
|
||||
'deep_orange',
|
||||
'amber',
|
||||
'blue_grey',
|
||||
'grey',
|
||||
'default',
|
||||
];
|
||||
|
||||
const CONTACTS = COLORS.map((color, index) => {
|
||||
const title = `${sample(['Mr.', 'Mrs.', 'Ms.', 'Unknown'])} ${color}`;
|
||||
const key = sample(['name', 'profileName']) as string;
|
||||
const id = `+1202555${padStart(index.toString(), 4, '0')}`;
|
||||
|
||||
const contact = {
|
||||
color,
|
||||
[key]: title,
|
||||
id,
|
||||
type: 'private',
|
||||
};
|
||||
|
||||
return parent.ConversationController.dangerouslyCreateAndAdd(contact);
|
||||
});
|
||||
|
||||
export {
|
||||
COLORS,
|
||||
CONTACTS,
|
||||
}
|
||||
|
||||
parent.textsecure.storage.user.getNumber = () => ourNumber;
|
||||
|
||||
// Telling Lodash to relinquish _ for use by underscore
|
||||
// @ts-ignore
|
||||
_.noConflict();
|
||||
|
|
Loading…
Add table
Reference in a new issue