Fixed examples in Quote.md, rough Android visuals

This commit is contained in:
Scott Nonnenberg 2018-04-09 18:31:52 -07:00
parent 6653123671
commit 21bf02c94d
No known key found for this signature in database
GPG key ID: 5F82280C35134661
15 changed files with 408 additions and 59 deletions

View file

@ -428,6 +428,18 @@
"selectAContact": {
"message": "Select a contact or group to start chatting."
},
"audio": {
"message": "Audio",
"description": "Shown in a quotation of a message containing an audio attachment if no text was originally provided with that attachment"
},
"video": {
"message": "Video",
"description": "Shown in a quotation of a message containing a video if no text was originally provided with that video"
},
"photo": {
"message": "Photo",
"description": "Shown in a quotation of a message containing a photo if no text was originally provided with that image"
},
"ok": {
"message": "OK"
},

View file

@ -278,6 +278,7 @@
{{ /profileName }}
</div>
<div class='inner-bubble {{ innerBubbleClasses }}'>
<div class='quote-wrapper'></div>
<div class='attachments'></div>
<p class='content' dir='auto'>
{{ #message }}<div class='body'>{{ message }}</div>{{ /message }}

1
images/play.svg Normal file
View 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.14V19.14L19,12.14L8,5.14Z" /></svg>

After

Width:  |  Height:  |  Size: 325 B

View file

@ -1,2 +1,10 @@
exports.isJPEG = mimeType =>
mimeType === 'image/jpeg';
exports.isVideo = mimeType =>
mimeType.startsWith('video/') && mimeType !== 'video/wmv';
exports.isImage = mimeType =>
mimeType.startsWith('image/') && mimeType !== 'image/tiff';
exports.isAudio = mimeType => mimeType.startsWith('audio/');

View file

@ -136,7 +136,8 @@
return this.model.contentType.startsWith('audio/');
},
isVideo() {
return this.model.contentType.startsWith('video/');
const type = this.model.contentType;
return type.startsWith('video/') && type !== 'image/wmv';
},
isImage() {
const type = this.model.contentType;

View file

@ -235,7 +235,6 @@
// Failsafe: if in the background, animation events don't fire
setTimeout(this.remove.bind(this), 1000);
},
/* jshint ignore:start */
onUnload() {
if (this.avatarView) {
this.avatarView.remove();
@ -252,6 +251,9 @@
if (this.timeStampView) {
this.timeStampView.remove();
}
if (this.replyView) {
this.replyView.remove();
}
// NOTE: We have to do this in the background (`then` instead of `await`)
// as our tests rely on `onUnload` synchronously removing the view from
@ -265,7 +267,6 @@
this.remove();
},
/* jshint ignore:end */
onDestroy() {
if (this.$el.hasClass('expired')) {
return;
@ -359,6 +360,53 @@
this.timerView.setElement(this.$('.timer'));
this.timerView.update();
},
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 quote = this.model.get('quote');
if (!quote) {
return;
}
const props = {
authorName: 'someone',
authorColor: 'indigo',
text: quote.text,
attachments: quote.attachments && quote.attachments.map(processAttachment),
};
if (!this.replyView) {
this.replyView = new Whisper.ReactWrapperView({
el: this.$('.quote-wrapper'),
Component: window.Signal.Components.Quote,
props,
});
} else {
this.replyView.update(props);
}
},
isImageWithoutCaption() {
const attachments = this.model.get('attachments');
const body = this.model.get('body');
@ -406,6 +454,7 @@
this.renderRead();
this.renderErrors();
this.renderExpiring();
this.renderReply();
// NOTE: We have to do this in the background (`then` instead of `await`)

View file

@ -94,6 +94,7 @@
},
"devDependencies": {
"@types/chai": "^4.1.2",
"@types/classnames": "^2.2.3",
"@types/lodash": "^4.14.106",
"@types/mocha": "^5.0.0",
"@types/qs": "^6.5.1",

View file

@ -450,6 +450,75 @@ span.status {
max-width: calc(100% - 45px - #{$error-icon-size}); // avatar size + padding + error-icon size
}
.quote {
@include message-replies-colors;
display: flex;
flex-direction: row;
align-items: stretch;
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;
// Accent color border:
border-left-width: 3;
border-left-style: solid;
.primary {
flex-grow: 1;
padding-left: 10px;
padding-right: 10px;
padding-top: 6px;
padding-bottom: 6px;
.author {
font-weight: bold;
margin-bottom: 0.3em;
}
.text {
white-space: pre-wrap;
}
.type-label {
font-style: italic;
font-size: 12px;
}
.filename-label {
font-size: 12px;
}
}
.icon-container {
flex: initial;
min-width: 48px;
@include aspect-ratio(1, 1);
.inner {
border: 1px red solid;
max-height: 48px;
max-width: 48px;
&.file {
@include color-svg('../images/file.svg', $grey_d);
}
&.microphone {
@include color-svg('../images/microphone.svg', $grey_d);
}
&.play {
@include color-svg('../images/play.svg', $grey_d);
}
}
}
}
.body {
margin-top: 0.5em;
white-space: pre-wrap;

View file

@ -1,3 +1,20 @@
@mixin aspect-ratio($width, $height) {
position: relative;
&:before {
display: block;
content: "";
width: 100%;
padding-top: ($height / $width) * 100%;
}
> .inner {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
}
@mixin color-svg($svg, $color) {
-webkit-mask: url($svg) no-repeat center;
-webkit-mask-size: 100%;
@ -53,6 +70,47 @@
&.grey { background-color: #666666 ; }
&.default { background-color: $blue ; }
}
// TODO: Deduplicate these! Can SASS functions generate property names?
@mixin message-replies-colors {
&.red { border-left-color: $material_red ; }
&.pink { border-left-color: $material_pink ; }
&.purple { border-left-color: $material_purple ; }
&.deep_purple { border-left-color: $material_deep_purple ; }
&.indigo { border-left-color: $material_indigo ; }
&.blue { border-left-color: $material_blue ; }
&.light_blue { border-left-color: $material_light_blue ; }
&.cyan { border-left-color: $material_cyan ; }
&.teal { border-left-color: $material_teal ; }
&.green { border-left-color: $material_green ; }
&.light_green { border-left-color: $material_light_green ; }
&.orange { border-left-color: $material_orange ; }
&.deep_orange { border-left-color: $material_deep_orange ; }
&.amber { border-left-color: $material_amber ; }
&.blue_grey { border-left-color: $material_blue_grey ; }
&.grey { border-left-color: #999999 ; }
&.default { border-left-color: $blue ; }
}
@mixin dark-message-replies-colors {
&.red { border-left-color: $dark_material_red ; }
&.pink { border-left-color: $dark_material_pink ; }
&.purple { border-left-color: $dark_material_purple ; }
&.deep_purple { border-left-color: $dark_material_deep_purple ; }
&.indigo { border-left-color: $dark_material_indigo ; }
&.blue { border-left-color: $dark_material_blue ; }
&.light_blue { border-left-color: $dark_material_light_blue ; }
&.cyan { border-left-color: $dark_material_cyan ; }
&.teal { border-left-color: $dark_material_teal ; }
&.green { border-left-color: $dark_material_green ; }
&.light_green { border-left-color: $dark_material_light_green ; }
&.orange { border-left-color: $dark_material_orange ; }
&.deep_orange { border-left-color: $dark_material_deep_orange ; }
&.amber { border-left-color: $dark_material_amber ; }
&.blue_grey { border-left-color: $dark_material_blue_grey ; }
&.grey { border-left-color: #666666 ; }
&.default { border-left-color: $blue ; }
}
@mixin invert-text-color {
color: white;

View file

@ -82,3 +82,8 @@ $dark_material_orange: #F57C00;
$dark_material_deep_orange: #E64A19;
$dark_material_amber: #FFA000;
$dark_material_blue_grey: #455A64;
// Android
$android-bubble-padding-horizontal: 12px;
$android-bubble-padding-vertical: 9px;
$android-bubble-quote-padding: 4px;

View file

@ -213,6 +213,7 @@
{{ /profileName }}
</div>
<div class='inner-bubble {{ innerBubbleClasses }}'>
<div class='quote-wrapper'></div>
<div class='attachments'></div>
<p class='content' dir='auto'>
{{ #message }}<div class='body'>{{ message }}</div>{{ /message }}

View file

@ -33,6 +33,7 @@ window.Whisper.View.Templates = {
{{ /profileName }}
</div>
<div class='inner-bubble {{ innerBubbleClasses }}'>
<div class='quote-wrapper'></div>
<div class='attachments'></div>
<p class='content' dir='auto'>
{{ #message }}<div class='body'>{{ message }}</div>{{ /message }}

View file

@ -45,14 +45,16 @@ const outgoing = new Whisper.Message({
text: 'I am pretty confused about Pi.',
author: '+12025550100',
id: Date.now() - 1000,
attachments: {
contentType: 'image/gif',
fileName: 'pi.gif',
thumbnail: {
attachments: [
{
contentType: 'image/gif',
data: util.gif,
}
}
fileName: 'pi.gif',
thumbnail: {
contentType: 'image/gif',
data: util.gif,
},
},
],
},
});
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
@ -85,14 +87,16 @@ const outgoing = new Whisper.Message({
quote: {
author: '+12025550100',
id: Date.now() - 1000,
attachments: {
contentType: 'image/gif',
fileName: 'pi.gif',
thumbnail: {
attachments: [
{
contentType: 'image/gif',
data: util.gif,
}
}
fileName: 'pi.gif',
thumbnail: {
contentType: 'image/gif',
data: util.gif,
},
},
],
},
});
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
@ -126,14 +130,16 @@ const outgoing = new Whisper.Message({
author: '+12025550100',
text: 'Check out this video I found!',
id: Date.now() - 1000,
attachments: {
contentType: 'video/mp4',
fileName: 'freezing_bubble.mp4',
thumbnail: {
contentType: 'image/gif',
data: util.gif,
}
}
attachments: [
{
contentType: 'video/mp4',
fileName: 'freezing_bubble.mp4',
thumbnail: {
contentType: 'image/gif',
data: util.gif,
},
},
],
},
});
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
@ -166,14 +172,16 @@ const outgoing = new Whisper.Message({
quote: {
author: '+12025550100',
id: Date.now() - 1000,
attachments: {
contentType: 'video/mp4',
fileName: 'freezing_bubble.mp4',
thumbnail: {
contentType: 'image/gif',
data: util.gif,
}
}
attachments: [
{
contentType: 'video/mp4',
fileName: 'freezing_bubble.mp4',
thumbnail: {
contentType: 'image/gif',
data: util.gif,
}
},
],
},
});
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
@ -207,10 +215,12 @@ const outgoing = new Whisper.Message({
author: '+12025550100',
text: 'Check out this beautiful song!',
id: Date.now() - 1000,
attachments: {
contentType: 'audio/mp3',
fileName: 'agnus_dei.mp4',
}
attachments: [
{
contentType: 'audio/mp3',
fileName: 'agnus_dei.mp4',
},
],
},
});
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
@ -243,10 +253,12 @@ const outgoing = new Whisper.Message({
quote: {
author: '+12025550100',
id: Date.now() - 1000,
attachments: {
contentType: 'audio/mp3',
fileName: 'agnus_dei.mp4',
}
attachments: [
{
contentType: 'audio/mp3',
fileName: 'agnus_dei.mp4',
},
],
},
});
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
@ -279,12 +291,14 @@ const outgoing = new Whisper.Message({
quote: {
author: '+12025550100',
id: Date.now() - 1000,
attachments: {
// proposed as of afternoon of 4/6 in Quoted Replies group
flags: textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE,
contentType: 'audio/mp3',
fileName: 'agnus_dei.mp4',
}
attachments: [
{
// proposed as of afternoon of 4/6 in Quoted Replies group
flags: textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE,
contentType: 'audio/mp3',
fileName: 'agnus_dei.mp4',
},
],
},
});
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
@ -318,10 +332,12 @@ const outgoing = new Whisper.Message({
author: '+12025550100',
text: 'This is my manifesto. Tell me what you think!',
id: Date.now() - 1000,
attachments: {
contentType: 'text/plain',
fileName: 'lorum_ipsum.txt',
}
attachments: [
{
contentType: 'text/plain',
fileName: 'lorum_ipsum.txt',
},
],
},
});
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
@ -354,10 +370,12 @@ const outgoing = new Whisper.Message({
quote: {
author: '+12025550100',
id: Date.now() - 1000,
attachments: {
contentType: 'text/plain',
fileName: 'lorum_ipsum.txt',
}
attachments: [
{
contentType: 'text/plain',
fileName: 'lorum_ipsum.txt',
},
],
},
});
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {

View file

@ -1,14 +1,134 @@
import React from 'react';
import classnames from 'classnames';
// @ts-ignore
import Mime from '../../../js/modules/types/mime';
interface Props { name: string; }
interface Props {
i18n: (key: string, values?: Array<string>) => string;
authorName: string;
authorColor: string;
attachments: Array<QuotedAttachment>;
text: string;
}
interface State { count: number; }
interface QuotedAttachment {
fileName: string;
contentType: string;
isVoiceMessage: boolean;
objectUrl: string;
thumbnail: {
contentType: string;
data: ArrayBuffer;
}
}
function validateQuote(quote: Props): boolean {
if (quote.text) {
return true;
}
if (quote.attachments && quote.attachments.length > 0) {
return true;
}
return false;
}
function getContentType(attachments: Array<QuotedAttachment>): string | null {
if (!attachments || attachments.length === 0) {
return null;
}
const first = attachments[0];
return first.contentType;
}
export class Quote extends React.Component<Props, {}> {
public renderIcon(first: QuotedAttachment) {
const contentType = first.contentType;
const objectUrl = first.objectUrl;
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 renderIconContainer() {
const { attachments } = this.props;
if (!attachments || attachments.length === 0) {
return null;
}
const first = attachments[0];
return <div className='icon-container'>
{this.renderIcon(first)}
</div>
}
public renderText() {
const { i18n, text, attachments } = this.props;
if (text) {
return <div className='text'>{text}</div>;
}
if (!attachments || attachments.length === 0) {
return null;
}
const contentType = getContentType(attachments);
const first = attachments[0];
const fileName = first.fileName;
console.log(contentType);
if (Mime.isVideo(contentType)) {
return <div className='type-label'>{i18n('video')}</div>;
} else if (Mime.isImage(contentType)) {
return <div className='type-label'>{i18n('photo')}</div>;
} else if (Mime.isAudio(contentType) && first.isVoiceMessage) {
return <div className='type-label'>{i18n('voiceMessage')}</div>;
} else if (Mime.isAudio(contentType)) {
console.log(first);
return <div className='type-label'>{i18n('audio')}</div>;
}
return <div className='filename-label'>{fileName}</div>;
}
export class Reply extends React.Component<Props, State> {
public render() {
const { authorName, authorColor } = this.props;
if (!validateQuote(this.props)) {
return null;
}
return (
<div>Placeholder</div>
<div className={classnames(authorColor, 'quote')} >
<div className="primary">
<div className="author">{authorName}</div>
{this.renderText()}
</div>
{this.renderIconContainer()}
</div>
);
}
}

View file

@ -40,6 +40,10 @@
version "4.1.2"
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.1.2.tgz#f1af664769cfb50af805431c407425ed619daa21"
"@types/classnames@^2.2.3":
version "2.2.3"
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.3.tgz#3f0ff6873da793870e20a260cada55982f38a9e5"
"@types/lodash@^4.14.106":
version "4.14.106"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.106.tgz#6093e9a02aa567ddecfe9afadca89e53e5dce4dd"