Full support for quotations in Android theme

This commit is contained in:
Scott Nonnenberg 2018-04-11 23:55:32 -07:00
parent 47a3acd5c9
commit 1cc0633786
No known key found for this signature in database
GPG key ID: 5F82280C35134661
13 changed files with 734 additions and 128 deletions

View file

@ -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">

View file

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

View file

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

View file

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

View file

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