-
-
- Hi there. How are you doing? Feeling pretty good? Awesome.
-
-
+
+ );
+ }
+
+ public renderMetadata() {
+ const {
+ collapseMetadata,
+ color,
+ direction,
+ i18n,
+ status,
+ timestamp,
+ text,
+ attachment,
+ } = this.props;
+
+ if (collapseMetadata) {
+ return null;
+ }
+ // We're not showing metadata on top of videos since they still have native controls
+ if (!text && isVideo(attachment)) {
+ return null;
+ }
+
+ const withImageNoCaption = !text && isImage(attachment);
+
+ return (
+
+
+ {formatRelativeTime(timestamp, { i18n, extended: true })}
+
+ {this.renderTimer()}
+
+ {direction === 'outgoing' ? (
+
+ ) : null}
+
+ );
+ }
+
+ public renderAuthor() {
+ const {
+ authorName,
+ conversationType,
+ direction,
+ i18n,
+ authorPhoneNumber,
+ authorProfileName,
+ } = this.props;
+
+ const title = authorName ? authorName : authorPhoneNumber;
+
+ if (direction !== 'incoming' || conversationType !== 'group' || !title) {
+ return null;
+ }
+
+ const profileElement =
+ authorProfileName && !authorName ? (
+
+ ~
+
+ ) : null;
+
+ return (
+
+ {profileElement}
+
+ );
+ }
+
+ public renderAttachment() {
+ const {
+ i18n,
+ attachment,
+ text,
+ collapseMetadata,
+ conversationType,
+ direction,
+ quote,
+ onClickAttachment,
+ } = this.props;
+
+ if (!attachment) {
+ return null;
+ }
+
+ const withCaption = Boolean(text);
+ // For attachments which aren't full-frame
+ const withContentBelow = withCaption || !collapseMetadata;
+ const withContentAbove =
+ quote || (conversationType === 'group' && direction === 'incoming');
+
+ if (isImage(attachment)) {
+ return (
+
+
+ {!withCaption && !collapseMetadata ? (
+
+ ) : null}
+
+ );
+ } else if (isVideo(attachment)) {
+ return (
+
+
+
+ );
+ } else if (isAudio(attachment)) {
+ return (
+
+
+
+ );
+ } else {
+ const { fileName, fileSize, contentType } = attachment;
+ const extension = getExtension({ contentType, fileName });
+
+ return (
+
+
+ {extension ? (
+
+ {extension}
+
+ ) : null}
+
+
+
+ {fileName}
+
+
+ {fileSize}
-
-
- 1 minute ago
-
-
-
-
+
+ );
+ }
+ }
+
+ public renderQuote() {
+ const {
+ color,
+ conversationType,
+ direction,
+ i18n,
+ onClickQuote,
+ quote,
+ } = this.props;
+
+ if (!quote) {
+ return null;
+ }
+
+ const authorTitle = quote.authorName
+ ? quote.authorName
+ : quote.authorPhoneNumber;
+ const authorProfileName = !quote.authorName
+ ? quote.authorProfileName
+ : undefined;
+ const withContentAbove =
+ conversationType === 'group' && direction === 'incoming';
+
+ return (
+
+ );
+ }
+
+ public renderEmbeddedContact() {
+ const {
+ collapseMetadata,
+ contactHasSignalAccount,
+ contacts,
+ conversationType,
+ direction,
+ i18n,
+ onClickContact,
+ onSendMessageToContact,
+ text,
+ } = this.props;
+ const first = contacts && contacts[0];
+
+ if (!first) {
+ return null;
+ }
+
+ const withCaption = Boolean(text);
+ const withContentAbove =
+ conversationType === 'group' && direction === 'incoming';
+ const withContentBelow = withCaption || !collapseMetadata;
+
+ return (
+
+ );
+ }
+
+ public renderSendMessageButton() {
+ const {
+ contactHasSignalAccount,
+ contacts,
+ i18n,
+ onSendMessageToContact,
+ } = this.props;
+ const first = contacts && contacts[0];
+
+ if (!first || !contactHasSignalAccount) {
+ return null;
+ }
+
+ return (
+
+ {i18n('sendMessageToContact')}
+
+ );
+ }
+
+ public renderAvatar() {
+ const {
+ authorName,
+ authorPhoneNumber,
+ authorProfileName,
+ authorAvatarPath,
+ collapseMetadata,
+ color,
+ conversationType,
+ direction,
+ i18n,
+ } = this.props;
+
+ const title = `${authorName || authorPhoneNumber}${
+ !authorName && authorProfileName ? ` ~${authorProfileName}` : ''
+ }`;
+
+ if (
+ collapseMetadata ||
+ conversationType !== 'group' ||
+ direction === 'outgoing'
+ ) {
+ return;
+ }
+
+ if (!authorAvatarPath) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ );
+ }
+
+ public renderText() {
+ const { text, i18n, direction } = this.props;
+
+ if (!text) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+ }
+
+ public render() {
+ const {
+ attachment,
+ color,
+ conversationType,
+ direction,
+ id,
+ quote,
+ text,
+ } = this.props;
+
+ const imageAndNothingElse =
+ !text && isImage(attachment) && conversationType !== 'group' && !quote;
+
+ return (
+
+
+ {this.renderAuthor()}
+ {this.renderQuote()}
+ {this.renderAttachment()}
+ {this.renderEmbeddedContact()}
+ {this.renderText()}
+ {this.renderMetadata()}
+ {this.renderSendMessageButton()}
+ {this.renderAvatar()}
);
diff --git a/ts/components/conversation/Notification.md b/ts/components/conversation/Notification.md
new file mode 100644
index 000000000..3ec261087
--- /dev/null
+++ b/ts/components/conversation/Notification.md
@@ -0,0 +1,151 @@
+### Timer change
+
+```jsx
+const fromOther = new Whisper.Message({
+ type: 'incoming',
+ flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
+ source: '+12025550003',
+ sent_at: Date.now() - 200000,
+ expireTimer: 120,
+ expirationStartTimestamp: Date.now() - 1000,
+ expirationTimerUpdate: {
+ source: '+12025550003',
+ },
+});
+const fromUpdate = new Whisper.Message({
+ type: 'incoming',
+ flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
+ source: util.ourNumber,
+ sent_at: Date.now() - 200000,
+ expireTimer: 120,
+ expirationStartTimestamp: Date.now() - 1000,
+ expirationTimerUpdate: {
+ fromSync: true,
+ source: util.ourNumber,
+ },
+});
+const fromMe = new Whisper.Message({
+ type: 'incoming',
+ flags: textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
+ source: util.ourNumber,
+ sent_at: Date.now() - 200000,
+ expireTimer: 120,
+ expirationStartTimestamp: Date.now() - 1000,
+ expirationTimerUpdate: {
+ source: util.ourNumber,
+ },
+});
+const View = Whisper.ExpirationTimerUpdateView;
+
+
+
+
+ console.log('onClick')} />
+ ;
+```
+
+### Safety number change
+
+```js
+const incoming = new Whisper.Message({
+ type: 'keychange',
+ sent_at: Date.now() - 200000,
+ key_changed: '+12025550003',
+});
+const View = Whisper.KeyChangeView;
+
+
+ ;
+```
+
+### Marking as verified
+
+```js
+const fromPrimary = new Whisper.Message({
+ type: 'verified-change',
+ sent_at: Date.now() - 200000,
+ verifiedChanged: '+12025550003',
+ verified: true,
+});
+const local = new Whisper.Message({
+ type: 'verified-change',
+ sent_at: Date.now() - 200000,
+ verifiedChanged: '+12025550003',
+ local: true,
+ verified: true,
+});
+
+const View = Whisper.VerifiedChangeView;
+
+
+
+ ;
+```
+
+### Marking as not verified
+
+```js
+const fromPrimary = new Whisper.Message({
+ type: 'verified-change',
+ sent_at: Date.now() - 200000,
+ verifiedChanged: '+12025550003',
+});
+const local = new Whisper.Message({
+ type: 'verified-change',
+ sent_at: Date.now() - 200000,
+ verifiedChanged: '+12025550003',
+ local: true,
+});
+
+const View = Whisper.VerifiedChangeView;
+
+
+
+ ;
+```
+
+### Group update
+
+```js
+const outgoing = new Whisper.Message({
+ type: 'outgoing',
+ sent_at: Date.now() - 200000,
+ group_update: {
+ joined: ['+12025550007', '+12025550008', '+12025550009'],
+ },
+});
+const incoming = new Whisper.Message(
+ Object.assign({}, outgoing.attributes, {
+ source: '+12025550003',
+ type: 'incoming',
+ })
+);
+
+const View = Whisper.MessageView;
+
+
+
+ ;
+```
+
+### End session
+
+```js
+const outgoing = new Whisper.Message({
+ type: 'outgoing',
+ sent_at: Date.now() - 200000,
+ flags: textsecure.protobuf.DataMessage.Flags.END_SESSION,
+});
+const incoming = new Whisper.Message(
+ Object.assign({}, outgoing.attributes, {
+ source: '+12025550003',
+ type: 'incoming',
+ })
+);
+
+const View = Whisper.MessageView;
+
+
+
+ ;
+```
diff --git a/ts/components/conversation/Notification.tsx b/ts/components/conversation/Notification.tsx
new file mode 100644
index 000000000..eeaa3d7ec
--- /dev/null
+++ b/ts/components/conversation/Notification.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import classnames from 'classnames';
+
+interface Props {
+ type: string;
+ onClick: () => void;
+}
+
+export class Notification extends React.Component
{
+ public renderContents() {
+ const { type } = this.props;
+
+ return Notification of type {type} ;
+ }
+
+ public render() {
+ const { onClick } = this.props;
+
+ return (
+
+ {this.renderContents()}
+
+ );
+ }
+}
diff --git a/ts/components/conversation/Quote.md b/ts/components/conversation/Quote.md
index 5ab2f5896..b3308a05e 100644
--- a/ts/components/conversation/Quote.md
+++ b/ts/components/conversation/Quote.md
@@ -3,560 +3,679 @@
#### Plain text
```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: '+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;
-
-
- ;
+ console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ />
+
```
#### With emoji
```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: '+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;
-
-
- ;
+ console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ />
+
```
#### Replies to you or yourself
```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: util.ourNumber,
- id: Date.now() - 1000,
- },
-});
-const incoming = new Whisper.Message(
- Object.assign({}, outgoing.attributes, {
- source: '+12025550011',
- type: 'incoming',
- quote: Object.assign({}, outgoing.attributes.quote, {
- author: util.ourNumber,
- }),
- })
-);
-const View = Whisper.MessageView;
-
-
- ;
+ console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ />
+
```
#### 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;
-
-
- ;
+ console.log('onClickQuote')}
+ authorAvatarPath={util.gifObjectUrl}
+ />
+ console.log('onClickQuote')}
+ authorAvatarPath={util.gifObjectUrl}
+ />
+
```
#### A lot of text in quotation
```jsx
-const outgoing = new Whisper.Message({
- type: 'outgoing',
- body: 'Woo, otters!',
- 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;
-
-
- ;
+ console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ />
+
```
#### A lot of text in quotation, with icon
```jsx
-const outgoing = new Whisper.Message({
- type: 'outgoing',
- body: 'Woo, otters!',
- 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,
- attachments: [
- {
- contentType: 'text/plain',
- fileName: 'lorum_ipsum.txt',
- },
- ],
- },
-});
-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;
-
-
- ;
+ console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ />
+
```
#### A lot of text in quotation, with image
```jsx
-const thumbnail = {
- objectUrl: util.gifObjectUrl,
-};
-const outgoing = new Whisper.Message({
- type: 'outgoing',
- body: 'Woo, otters!',
- 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,
- attachments: [
- {
- contentType: 'image/gif',
- fileName: 'pi.gif',
- thumbnail: {
- contentType: 'image/gif',
- },
- },
- ],
- },
-});
-const incoming = new Whisper.Message(
- Object.assign({}, outgoing.attributes, {
- source: '+12025550011',
- type: 'incoming',
- quote: Object.assign({}, outgoing.attributes.quote, {
- author: '+12025550005',
- }),
- })
-);
-
-outgoing.quoteThumbnail = thumbnail;
-incoming.quoteThumbnail = thumbnail;
-
-const View = Whisper.MessageView;
-
-
- ;
+ console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ />
+
```
#### Image with caption
```jsx
-const thumbnail = {
- objectUrl: 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: '+12025550011',
- id: Date.now() - 1000,
- attachments: [
- {
- contentType: 'image/gif',
- fileName: 'pi.gif',
- thumbnail: {
- contentType: 'image/gif',
- },
- },
- ],
- },
-});
-const incoming = new Whisper.Message(
- Object.assign({}, outgoing.attributes, {
- source: '+12025550011',
- type: 'incoming',
- quote: Object.assign({}, outgoing.attributes.quote, {
- author: '+12025550005',
- }),
- })
-);
-
-outgoing.quoteThumbnail = thumbnail;
-incoming.quoteThumbnail = thumbnail;
-
-const View = Whisper.MessageView;
-
-
- ;
+ console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ />
+
```
#### Image
```jsx
-const thumbnail = {
- objectUrl: 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: '+12025550011',
- id: Date.now() - 1000,
- attachments: [
- {
- contentType: 'image/gif',
- fileName: 'pi.gif',
- thumbnail: {
- contentType: 'image/gif',
- },
- },
- ],
- },
-});
-const incoming = new Whisper.Message(
- Object.assign({}, outgoing.attributes, {
- source: '+12025550011',
- type: 'incoming',
- quote: Object.assign({}, outgoing.attributes.quote, {
- author: '+12025550005',
- }),
- })
-);
-
-outgoing.quoteThumbnail = thumbnail;
-incoming.quoteThumbnail = thumbnail;
-
-const View = Whisper.MessageView;
-
-
- ;
+ console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ />
+
```
#### 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;
-
-
- ;
+ console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ />
+
```
#### Video with caption
```jsx
-const thumbnail = {
- objectUrl: 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: '+12025550011',
- text: 'Check out this video I found!',
- id: Date.now() - 1000,
- attachments: [
- {
- contentType: 'video/mp4',
- fileName: 'freezing_bubble.mp4',
- thumbnail: {
- contentType: 'image/gif',
- },
- },
- ],
- },
-});
-const incoming = new Whisper.Message(
- Object.assign({}, outgoing.attributes, {
- source: '+12025550011',
- type: 'incoming',
- quote: Object.assign({}, outgoing.attributes.quote, {
- author: '+12025550005',
- }),
- })
-);
-
-outgoing.quoteThumbnail = thumbnail;
-incoming.quoteThumbnail = thumbnail;
-
-const View = Whisper.MessageView;
-
-
- ;
+ console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ />
+
```
#### Video
```jsx
-const thumbnail = {
- objectUrl: util.gifObjectUrl,
-};
-
-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',
- thumbnail: {
- contentType: 'image/gif',
- data: util.gif,
- },
- },
- ],
- },
-});
-const incoming = new Whisper.Message(
- Object.assign({}, outgoing.attributes, {
- source: '+12025550011',
- type: 'incoming',
- quote: Object.assign({}, outgoing.attributes.quote, {
- author: '+12025550005',
- }),
- })
-);
-
-outgoing.quoteThumbnail = thumbnail;
-incoming.quoteThumbnail = thumbnail;
-
-const View = Whisper.MessageView;
-
-
- ;
+ console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ />
+
```
#### 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;
-
-
- ;
+ console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ />
+
```
#### Audio with caption
```jsx
-const outgoing = new Whisper.Message({
- type: 'outgoing',
- body: 'I really like it!',
- sent_at: Date.now() - 18000000,
- quote: {
- author: '+12025550011',
- text: 'Check out this beautiful song!',
- id: Date.now() - 1000,
- attachments: [
- {
- contentType: 'audio/mp3',
- fileName: 'agnus_dei.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;
-
-
- ;
+ console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ />
+
```
#### Audio
```jsx
-const outgoing = new Whisper.Message({
- type: 'outgoing',
- body: 'I really like it!',
- sent_at: Date.now() - 18000000,
- quote: {
- author: '+12025550011',
- id: Date.now() - 1000,
- attachments: [
- {
- contentType: 'audio/mp3',
- fileName: 'agnus_dei.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;
-
-
- ;
+ console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ />
+
```
#### Voice message
@@ -571,8 +690,6 @@ const outgoing = new Whisper.Message({
id: Date.now() - 1000,
attachments: [
{
- // proposed as of afternoon of 4/6 in Quoted Replies group
- flags: SignalService.AttachmentPointer.Flags.VOICE_MESSAGE,
contentType: 'audio/mp3',
fileName: 'agnus_dei.mp4',
},
@@ -590,78 +707,132 @@ const incoming = new Whisper.Message(
);
const View = Whisper.MessageView;
-
-
+ console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ />
;
```
#### Other file type with caption
```jsx
-const outgoing = new Whisper.Message({
- type: 'outgoing',
- body: "I can't read latin.",
- sent_at: Date.now() - 18000000,
- quote: {
- author: '+12025550011',
- text: 'This is my manifesto. Tell me what you think!',
- id: Date.now() - 1000,
- attachments: [
- {
- contentType: 'text/plain',
- fileName: 'lorum_ipsum.txt',
- },
- ],
- },
-});
-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;
-
-
- ;
+ console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ />
+
```
#### Other file type
```jsx
-const outgoing = new Whisper.Message({
- type: 'outgoing',
- body: "Sorry, I can't read latin!",
- sent_at: Date.now() - 18000000,
- quote: {
- author: '+12025550011',
- id: Date.now() - 1000,
- attachments: [
- {
- contentType: 'text/plain',
- fileName: 'lorum_ipsum.txt',
- },
- ],
- },
-});
-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;
-
-
- ;
+ console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ />
+
```
### With a quotation, including attachment
@@ -669,240 +840,269 @@ const View = Whisper.MessageView;
#### Quote, image attachment, and caption
```jsx
-const outgoing = new Whisper.Message({
- type: 'outgoing',
- body: 'Like pi or so?',
- sent_at: Date.now() - 18000000,
- quote: {
- text: 'How many ferrets do you have?',
- author: '+12025550011',
- id: Date.now() - 1000,
- },
- attachments: [
- {
- data: util.gif,
+
+
-
-
- ;
+ }}
+ onClickQuote={() => console.log('onClickQuote')}
+ text="About six"
+ i18n={util.i18n}
+ quote={{
+ text: 'How many ferrets do you have?',
+ attachments: [],
+ authorPhoneNumber: '(202) 555-0011',
+ }}
+ onClickQuote={() => console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ color="green"
+ text="About six"
+ i18n={util.i18n}
+ quote={{
+ text: 'How many ferrets do you have?',
+ attachments: [],
+ authorPhoneNumber: '(202) 555-0011',
+ }}
+ onClickQuote={() => console.log('onClickQuote')}
+ />
+
```
#### Quote, image attachment
```jsx
-const outgoing = new Whisper.Message({
- type: 'outgoing',
- sent_at: Date.now() - 18000000,
- quote: {
- text: 'How many ferrets do you have?',
- author: '+12025550011',
- id: Date.now() - 1000,
- },
- attachments: [
- {
- data: util.gif,
+
+
-
-
- ;
+ }}
+ onClickQuote={() => console.log('onClickQuote')}
+ i18n={util.i18n}
+ quote={{
+ text: 'How many ferrets do you have?',
+ attachments: [],
+ authorPhoneNumber: '(202) 555-0011',
+ }}
+ onClickQuote={() => console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ color="green"
+ i18n={util.i18n}
+ quote={{
+ text: 'How many ferrets do you have?',
+ attachments: [],
+ authorPhoneNumber: '(202) 555-0011',
+ }}
+ onClickQuote={() => console.log('onClickQuote')}
+ />
+
```
#### Quote, portrait image attachment
```jsx
-const outgoing = new Whisper.Message({
- type: 'outgoing',
- sent_at: Date.now() - 18000000,
- quote: {
- text: 'How many ferrets do you have?',
- author: '+12025550011',
- id: Date.now() - 1000,
- },
- attachments: [
- {
- data: util.portraitYellow,
+
+
-
-
- ;
+ }}
+ onClickQuote={() => console.log('onClickQuote')}
+ i18n={util.i18n}
+ quote={{
+ text: 'How many ferrets do you have?',
+ attachments: [],
+ authorPhoneNumber: '(202) 555-0011',
+ }}
+ onClickQuote={() => console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ color="green"
+ i18n={util.i18n}
+ quote={{
+ text: 'How many ferrets do you have?',
+ attachments: [],
+ authorPhoneNumber: '(202) 555-0011',
+ }}
+ onClickQuote={() => console.log('onClickQuote')}
+ />
+
```
#### Quote, video attachment
```jsx
-const outgoing = new Whisper.Message({
- type: 'outgoing',
- sent_at: Date.now() - 18000000,
- quote: {
- text: 'How many ferrets do you have?',
- author: '+12025550011',
- id: Date.now() - 1000,
- },
- attachments: [
- {
- data: util.mp4,
+
+
-
-
- ;
+ }}
+ onClickQuote={() => console.log('onClickQuote')}
+ i18n={util.i18n}
+ quote={{
+ text: 'How many ferrets do you have?',
+ attachments: [],
+ authorPhoneNumber: '(202) 555-0011',
+ }}
+ onClickQuote={() => console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ color="green"
+ i18n={util.i18n}
+ quote={{
+ text: 'How many ferrets do you have?',
+ attachments: [],
+ authorPhoneNumber: '(202) 555-0011',
+ }}
+ onClickQuote={() => console.log('onClickQuote')}
+ />
+
```
#### Quote, audio attachment
```jsx
-const outgoing = new Whisper.Message({
- type: 'outgoing',
- sent_at: Date.now() - 18000000,
- quote: {
- text: 'How many ferrets do you have?',
- author: '+12025550011',
- id: Date.now() - 1000,
- },
- attachments: [
- {
- data: util.mp3,
+
+
-
-
- ;
+ }}
+ onClickQuote={() => console.log('onClickQuote')}
+ i18n={util.i18n}
+ quote={{
+ text: 'How many ferrets do you have?',
+ attachments: [],
+ authorPhoneNumber: '(202) 555-0011',
+ }}
+ onClickQuote={() => console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ color="green"
+ i18n={util.i18n}
+ quote={{
+ text: 'How many ferrets do you have?',
+ attachments: [],
+ authorPhoneNumber: '(202) 555-0011',
+ }}
+ onClickQuote={() => console.log('onClickQuote')}
+ />
+
```
#### Quote, file attachment
```jsx
-const outgoing = new Whisper.Message({
- type: 'outgoing',
- sent_at: Date.now() - 18000000,
- quote: {
- text: 'How many ferrets do you have?',
- author: '+12025550011',
- id: Date.now() - 1000,
- },
- attachments: [
- {
- data: util.txt,
+
+
-
-
- ;
-```
-
-#### Quote, but no message
-
-```jsx
-const outgoing = new Whisper.Message({
- type: 'outgoing',
- sent_at: Date.now() - 18000000,
- quote: {
- text: 'How many ferrets do you have?',
- 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;
-
-
-
- ;
+ fileSize: '3.05 KB',
+ }}
+ onClickQuote={() => console.log('onClickQuote')}
+ i18n={util.i18n}
+ quote={{
+ text: 'How many ferrets do you have?',
+ attachments: [],
+ authorPhoneNumber: '(202) 555-0011',
+ }}
+ onClickQuote={() => console.log('onClickQuote')}
+ />
+ console.log('onClickQuote')}
+ color="green"
+ i18n={util.i18n}
+ quote={{
+ text: 'How many ferrets do you have?',
+ attachments: [],
+ authorPhoneNumber: '(202) 555-0011',
+ }}
+ onClickQuote={() => console.log('onClickQuote')}
+ />
+
```
### In bottom bar
diff --git a/ts/components/conversation/Quote.tsx b/ts/components/conversation/Quote.tsx
index bb86cf9c4..368efe6c6 100644
--- a/ts/components/conversation/Quote.tsx
+++ b/ts/components/conversation/Quote.tsx
@@ -1,7 +1,7 @@
// tslint:disable:react-this-binding-issue
import React from 'react';
-import classNames from 'classnames';
+import classnames from 'classnames';
import * as MIME from '../../../ts/types/MIME';
import * as GoogleChrome from '../../../ts/util/GoogleChrome';
@@ -12,18 +12,19 @@ import { Localizer } from '../../types/Util';
interface Props {
attachments: Array;
- authorColor: string;
+ color: string;
authorProfileName?: string;
authorTitle: string;
i18n: Localizer;
- isFromMe: string;
+ isFromMe: boolean;
isIncoming: boolean;
+ withContentAbove: boolean;
onClick?: () => void;
onClose?: () => void;
text: string;
}
-interface QuotedAttachment {
+export interface QuotedAttachment {
contentType: MIME.MIMEType;
fileName: string;
/** Not included in protobuf */
@@ -57,32 +58,93 @@ function getObjectUrl(thumbnail: Attachment | undefined): string | null {
return null;
}
+function getTypeLabel({
+ i18n,
+ contentType,
+ isVoiceMessage,
+}: {
+ i18n: Localizer;
+ contentType: MIME.MIMEType;
+ isVoiceMessage: boolean;
+}): string | null {
+ if (GoogleChrome.isVideoTypeSupported(contentType)) {
+ return i18n('video');
+ }
+ if (GoogleChrome.isImageTypeSupported(contentType)) {
+ return i18n('photo');
+ }
+ if (MIME.isAudio(contentType) && isVoiceMessage) {
+ return i18n('voiceMessage');
+ }
+ if (MIME.isAudio(contentType)) {
+ return i18n('audio');
+ }
+
+ return null;
+}
+
export class Quote extends React.Component {
public renderImage(url: string, i18n: Localizer, icon?: string) {
const iconElement = icon ? (
-
+
) : null;
return (
-
-
-
- {iconElement}
-
+
+
+ {iconElement}
);
}
public renderIcon(icon: string) {
- const { authorColor, isIncoming } = this.props;
+ return (
+
+ );
+ }
- const backgroundColor = isIncoming ? 'white' : authorColor;
- const iconColor = isIncoming ? authorColor : 'white';
+ public renderGenericFile() {
+ const { attachments } = this.props;
+
+ if (!attachments || !attachments.length) {
+ return;
+ }
+
+ const first = attachments[0];
+ const { fileName, contentType } = first;
+ const isGenericFile =
+ !GoogleChrome.isVideoTypeSupported(contentType) &&
+ !GoogleChrome.isImageTypeSupported(contentType) &&
+ !MIME.isAudio(contentType);
+
+ if (!isGenericFile) {
+ return null;
+ }
return (
-
-
-
+
);
}
@@ -111,7 +173,7 @@ export class Quote extends React.Component
{
return this.renderIcon('microphone');
}
- return this.renderIcon('file');
+ return null;
}
public renderText() {
@@ -119,7 +181,7 @@ export class Quote extends React.Component {
if (text) {
return (
-
+
);
@@ -130,43 +192,16 @@ export class Quote extends React.Component
{
}
const first = attachments[0];
- const { contentType, fileName, isVoiceMessage } = first;
+ const { contentType, isVoiceMessage } = first;
- if (GoogleChrome.isVideoTypeSupported(contentType)) {
- return {i18n('video')}
;
- }
- if (GoogleChrome.isImageTypeSupported(contentType)) {
- return {i18n('photo')}
;
- }
- if (MIME.isAudio(contentType) && isVoiceMessage) {
- return {i18n('voiceMessage')}
;
- }
- if (MIME.isAudio(contentType)) {
- return {i18n('audio')}
;
+ const typeLabel = getTypeLabel({ i18n, contentType, isVoiceMessage });
+ if (typeLabel) {
+ return (
+ {typeLabel}
+ );
}
- return {fileName}
;
- }
-
- public renderIOSLabel() {
- const {
- i18n,
- isIncoming,
- isFromMe,
- authorTitle,
- authorProfileName,
- } = this.props;
-
- const profileString = authorProfileName ? ` ~${authorProfileName}` : '';
- const authorName = `${authorTitle}${profileString}`;
-
- const label = isFromMe
- ? isIncoming
- ? i18n('replyingToYou')
- : i18n('replyingToYourself')
- : i18n('replyingTo', [authorName]);
-
- return {label}
;
+ return null;
}
public renderClose() {
@@ -185,29 +220,27 @@ export class Quote extends React.Component {
// We need the container to give us the flexibility to implement the iOS design.
return (
-
-
+
);
}
public renderAuthor() {
- const {
- authorColor,
- authorProfileName,
- authorTitle,
- i18n,
- isFromMe,
- } = this.props;
+ const { authorProfileName, authorTitle, i18n, isFromMe } = this.props;
const authorProfileElement = authorProfileName ? (
-
+
~
) : null;
return (
-
+
{isFromMe ? (
i18n('you')
) : (
@@ -220,24 +253,27 @@ export class Quote extends React.Component
{
}
public render() {
- const { authorColor, onClick, isFromMe } = this.props;
+ const { color, isIncoming, onClick, withContentAbove } = this.props;
if (!validateQuote(this.props)) {
return null;
}
- const classes = classNames(
- authorColor,
- 'quoted-message',
- isFromMe ? 'from-me' : null,
- !onClick ? 'no-click' : null
- );
-
return (
-
-
- {this.renderIOSLabel()}
+
+
{this.renderAuthor()}
+ {this.renderGenericFile()}
{this.renderText()}
{this.renderIconContainer()}
diff --git a/ts/styleguide/StyleGuideUtil.ts b/ts/styleguide/StyleGuideUtil.ts
index da98bb5b4..912862661 100644
--- a/ts/styleguide/StyleGuideUtil.ts
+++ b/ts/styleguide/StyleGuideUtil.ts
@@ -35,6 +35,9 @@ const txtObjectUrl = makeObjectUrl(txt, 'text/plain');
// @ts-ignore
import mp4 from '../../fixtures/pixabay-Soap-Bubble-7141.mp4';
const mp4ObjectUrl = makeObjectUrl(mp4, 'video/mp4');
+// @ts-ignore
+import png from '../../fixtures/freepngs-2cd43b_bed7d1327e88454487397574d87b64dc_mv2.png';
+const pngObjectUrl = makeObjectUrl(png, 'image/png');
// @ts-ignore
import landscapeGreen from '../../fixtures/1000x50-green.jpeg';
@@ -70,6 +73,8 @@ export {
gifObjectUrl,
mp4,
mp4ObjectUrl,
+ png,
+ pngObjectUrl,
txt,
txtObjectUrl,
landscapeGreen,
diff --git a/ts/types/Util.ts b/ts/types/Util.ts
index 9819b5a85..abe06251b 100644
--- a/ts/types/Util.ts
+++ b/ts/types/Util.ts
@@ -3,6 +3,6 @@ export type RenderTextCallback = (
text: string;
key: number;
}
-) => JSX.Element;
+) => JSX.Element | string;
export type Localizer = (key: string, values?: Array
) => string;
diff --git a/ts/util/formatRelativeTime.ts b/ts/util/formatRelativeTime.ts
new file mode 100644
index 000000000..7da339ccb
--- /dev/null
+++ b/ts/util/formatRelativeTime.ts
@@ -0,0 +1,53 @@
+import moment from 'moment';
+import { Localizer } from '../types/Util';
+
+const getExtendedFormats = (i18n: Localizer) => ({
+ y: 'lll',
+ M: `${i18n('timestampFormat_M') || 'MMM D'} LT`,
+ d: 'ddd LT',
+});
+const getShortFormats = (i18n: Localizer) => ({
+ y: 'll',
+ M: i18n('timestampFormat_M') || 'MMM D',
+ d: 'ddd',
+});
+
+function isToday(timestamp: moment.Moment) {
+ const today = moment().format('ddd');
+ const targetDay = moment(timestamp).format('ddd');
+
+ return today === targetDay;
+}
+
+function isYear(timestamp: moment.Moment) {
+ const year = moment().format('YYYY');
+ const targetYear = moment(timestamp).format('YYYY');
+
+ return year === targetYear;
+}
+
+export function formatRelativeTime(
+ rawTimestamp: number | Date,
+ options: { extended: boolean; i18n: Localizer }
+) {
+ const { extended, i18n } = options;
+
+ const formats = extended ? getExtendedFormats(i18n) : getShortFormats(i18n);
+ const timestamp = moment(rawTimestamp);
+ const now = moment();
+ const diff = moment.duration(now.diff(timestamp));
+
+ if (diff.years() >= 1 || !isYear(timestamp)) {
+ return timestamp.format(formats.y);
+ } else if (diff.months() >= 1 || diff.days() > 6) {
+ return timestamp.format(formats.M);
+ } else if (diff.days() >= 1 || !isToday(timestamp)) {
+ return timestamp.format(formats.d);
+ } else if (diff.hours() >= 1) {
+ return i18n('hoursAgo', [String(diff.hours())]);
+ } else if (diff.minutes() >= 1) {
+ return i18n('minutesAgo', [String(diff.minutes())]);
+ }
+
+ return i18n('justNow');
+}
diff --git a/ts/util/migrateColor.ts b/ts/util/migrateColor.ts
new file mode 100644
index 000000000..689a0caa8
--- /dev/null
+++ b/ts/util/migrateColor.ts
@@ -0,0 +1,70 @@
+// import { missingCaseError } from './missingCaseError';
+
+type OldColor =
+ | 'amber'
+ | 'blue'
+ | 'blue_grey'
+ | 'cyan'
+ | 'deep_orange'
+ | 'deep_purple'
+ | 'green'
+ | 'grey'
+ | 'indigo'
+ | 'light_blue'
+ | 'light_green'
+ | 'orange'
+ | 'pink'
+ | 'purple'
+ | 'red'
+ | 'teal';
+
+type NewColor =
+ | 'blue'
+ | 'cyan'
+ | 'deep_orange'
+ | 'grey'
+ | 'green'
+ | 'indigo'
+ | 'pink'
+ | 'purple'
+ | 'red'
+ | 'teal';
+
+export function migrateColor(color: OldColor): NewColor {
+ switch (color) {
+ // These colors no longer exist
+ case 'amber':
+ case 'orange':
+ return 'red';
+
+ case 'blue_grey':
+ case 'light_blue':
+ return 'blue';
+
+ case 'deep_purple':
+ return 'purple';
+
+ case 'light_green':
+ return 'teal';
+
+ // These can stay as they are
+ case 'blue':
+ case 'cyan':
+ case 'deep_orange':
+ case 'green':
+ case 'grey':
+ case 'indigo':
+ case 'pink':
+ case 'purple':
+ case 'red':
+ case 'teal':
+ return color;
+
+ // Can uncomment this to ensure that we've covered all potential cases
+ // default:
+ // throw missingCaseError(color);
+
+ default:
+ return 'grey';
+ }
+}