In iOS theme, join attachment bubble with caption bubble
This commit is contained in:
parent
3a76c3c86e
commit
ae043bf239
9 changed files with 246 additions and 97 deletions
|
@ -277,10 +277,12 @@
|
||||||
<span class='profileName'>{{ profileName }} </span>
|
<span class='profileName'>{{ profileName }} </span>
|
||||||
{{ /profileName }}
|
{{ /profileName }}
|
||||||
</div>
|
</div>
|
||||||
<div class='attachments'></div>
|
<div class='inner-bubble {{ innerBubbleClasses }}'>
|
||||||
<p class='content' dir='auto'>
|
<div class='attachments'></div>
|
||||||
{{ #message }}<span class='body'>{{ message }}</span>{{ /message }}
|
<p class='content' dir='auto'>
|
||||||
</p>
|
{{ #message }}<div class='body'>{{ message }}</div>{{ /message }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div class='meta'>
|
<div class='meta'>
|
||||||
<span class='timestamp' data-timestamp={{ timestamp }}></span>
|
<span class='timestamp' data-timestamp={{ timestamp }}></span>
|
||||||
<span class='status hide'></span>
|
<span class='status hide'></span>
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
];
|
];
|
||||||
|
|
||||||
Whisper.AttachmentView = Backbone.View.extend({
|
Whisper.AttachmentView = Backbone.View.extend({
|
||||||
tagName: 'span',
|
tagName: 'div',
|
||||||
className() {
|
className() {
|
||||||
if (this.isImage()) {
|
if (this.isImage()) {
|
||||||
return 'attachment';
|
return 'attachment';
|
||||||
|
|
|
@ -355,6 +355,24 @@
|
||||||
this.timerView.setElement(this.$('.timer'));
|
this.timerView.setElement(this.$('.timer'));
|
||||||
this.timerView.update();
|
this.timerView.update();
|
||||||
},
|
},
|
||||||
|
isImageWithoutCaption: function() {
|
||||||
|
var attachments = this.model.get('attachments');
|
||||||
|
var body = this.model.get('body');
|
||||||
|
if (!attachments || attachments.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (body && body.trim()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var first = attachments[0];
|
||||||
|
if (first.contentType.startsWith('image/') && first.contentType !== 'image/tiff') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
render: function() {
|
render: function() {
|
||||||
var contact = this.model.isIncoming() ? this.model.getContact() : null;
|
var contact = this.model.isIncoming() ? this.model.getContact() : null;
|
||||||
this.$el.html(
|
this.$el.html(
|
||||||
|
@ -364,6 +382,7 @@
|
||||||
sender: (contact && contact.getTitle()) || '',
|
sender: (contact && contact.getTitle()) || '',
|
||||||
avatar: (contact && contact.getAvatar()),
|
avatar: (contact && contact.getAvatar()),
|
||||||
profileName: (contact && contact.getProfileName()),
|
profileName: (contact && contact.getProfileName()),
|
||||||
|
innerBubbleClasses: this.isImageWithoutCaption() ? '' : 'with-tail',
|
||||||
}, this.render_partials())
|
}, this.render_partials())
|
||||||
);
|
);
|
||||||
this.timeStampView.setElement(this.$('.timestamp'));
|
this.timeStampView.setElement(this.$('.timestamp'));
|
||||||
|
|
|
@ -451,6 +451,7 @@ span.status {
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
|
margin-top: 0.5em;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
@ -591,6 +592,7 @@ span.status {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
|
|
@ -109,11 +109,33 @@ $ios-border-color: rgba(0,0,0,0.1);
|
||||||
|
|
||||||
.attachments .bubbled {
|
.attachments .bubbled {
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
margin-bottom: 0.25em;
|
|
||||||
|
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
padding-top: 0px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-bubble {
|
||||||
|
border-radius: 15px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
.body {
|
||||||
|
margin-top: 0;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px;
|
||||||
|
position: relative;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachments img {
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-bubble.with-tail {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&:before, &:after {
|
&:before, &:after {
|
||||||
content: '';
|
content: '';
|
||||||
|
@ -135,51 +157,27 @@ $ios-border-color: rgba(0,0,0,0.1);
|
||||||
bottom: -3px;
|
bottom: -3px;
|
||||||
background: #eee;
|
background: #eee;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.bubble {
|
|
||||||
.content {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
.body {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 10px;
|
|
||||||
position: relative;
|
|
||||||
word-break: break-word;
|
|
||||||
|
|
||||||
&:before, &:after {
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
border-radius: 20px;
|
|
||||||
position: absolute;
|
|
||||||
width: 10px;
|
|
||||||
}
|
|
||||||
&:before {
|
|
||||||
right: -1px;
|
|
||||||
bottom: -3px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 20px;
|
|
||||||
background: $blue;
|
|
||||||
}
|
|
||||||
&:after {
|
|
||||||
height: 11px;
|
|
||||||
right: -6px;
|
|
||||||
bottom: -3px;
|
|
||||||
background: #eee;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.content, .attachments img {
|
|
||||||
border-radius: 15px;
|
|
||||||
}
|
|
||||||
.attachments img {
|
.attachments img {
|
||||||
background-color: white;
|
border-bottom-left-radius: 0px;
|
||||||
}
|
border-bottom-right-radius: 0px;
|
||||||
.meta {
|
|
||||||
clear: both;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.incoming .bubbled {
|
.meta {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outgoing .inner-bubble.with-tail {
|
||||||
|
background-color: $blue;
|
||||||
|
max-width: 100%;
|
||||||
|
&, .body, a {
|
||||||
|
@include invert-text-color;
|
||||||
|
}
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.incoming .inner-bubble.with-tail {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
color: black;
|
color: black;
|
||||||
float: left;
|
float: left;
|
||||||
|
@ -194,31 +192,6 @@ $ios-border-color: rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.incoming .content {
|
|
||||||
background-color: white;
|
|
||||||
color: black;
|
|
||||||
float: left;
|
|
||||||
.body {
|
|
||||||
&:before {
|
|
||||||
left: -1px;
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
&:after {
|
|
||||||
left: -6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.outgoing {
|
|
||||||
.content, .attachments .bubbled {
|
|
||||||
background-color: $blue;
|
|
||||||
max-width: 100%;
|
|
||||||
&, .body, a {
|
|
||||||
@include invert-text-color;
|
|
||||||
}
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.outgoing .attachments .fileView .icon {
|
.outgoing .attachments .fileView .icon {
|
||||||
@include color-svg('../images/file.svg', white);
|
@include color-svg('../images/file.svg', white);
|
||||||
&.audio {
|
&.audio {
|
||||||
|
@ -236,7 +209,6 @@ $ios-border-color: rgba(0,0,0,0.1);
|
||||||
a {
|
a {
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
}
|
}
|
||||||
margin-bottom: 1px;
|
|
||||||
}
|
}
|
||||||
.hourglass {
|
.hourglass {
|
||||||
@include hourglass(#999);
|
@include hourglass(#999);
|
||||||
|
|
|
@ -206,14 +206,22 @@
|
||||||
<script type='text/x-tmpl-mustache' id='message'>
|
<script type='text/x-tmpl-mustache' id='message'>
|
||||||
{{> avatar }}
|
{{> avatar }}
|
||||||
<div class='bubble {{ avatar.color }}'>
|
<div class='bubble {{ avatar.color }}'>
|
||||||
<div class='sender' dir='auto'>{{ sender }}</div>
|
<div class='sender' dir='auto'>
|
||||||
<div class='attachments'></div>
|
{{ sender }}
|
||||||
<p class='content' dir='auto'>
|
{{ #profileName }}
|
||||||
{{ #message }}<span class='body'>{{ message }}</span>{{ /message }}
|
<span class='profileName'>{{ profileName }} </span>
|
||||||
</p>
|
{{ /profileName }}
|
||||||
|
</div>
|
||||||
|
<div class='inner-bubble {{ innerBubbleClasses }}'>
|
||||||
|
<div class='attachments'></div>
|
||||||
|
<p class='content' dir='auto'>
|
||||||
|
{{ #message }}<div class='body'>{{ message }}</div>{{ /message }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div class='meta'>
|
<div class='meta'>
|
||||||
<span class='timestamp' data-timestamp={{ timestamp }}></span>
|
<span class='timestamp' data-timestamp={{ timestamp }}></span>
|
||||||
<span class='status hide'></span>
|
<span class='status hide'></span>
|
||||||
|
<span class='timer'></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -32,10 +32,12 @@ window.Whisper.View.Templates = {
|
||||||
<span class='profileName'>{{ profileName }} </span>
|
<span class='profileName'>{{ profileName }} </span>
|
||||||
{{ /profileName }}
|
{{ /profileName }}
|
||||||
</div>
|
</div>
|
||||||
<div class='attachments'></div>
|
<div class='inner-bubble {{ innerBubbleClasses }}'>
|
||||||
<p class='content' dir='auto'>
|
<div class='attachments'></div>
|
||||||
{{ #message }}<span class='body'>{{ message }}</span>{{ /message }}
|
<p class='content' dir='auto'>
|
||||||
</p>
|
{{ #message }}<div class='body'>{{ message }}</div>{{ /message }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div class='meta'>
|
<div class='meta'>
|
||||||
<span class='timestamp' data-timestamp={{ timestamp }}></span>
|
<span class='timestamp' data-timestamp={{ timestamp }}></span>
|
||||||
<span class='status hide'></span>
|
<span class='status hide'></span>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
Placeholder:
|
Placeholder component:
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
<util.ConversationContext theme={util.theme}>
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
@ -7,9 +7,36 @@ Placeholder:
|
||||||
</util.ConversationContext>
|
</util.ConversationContext>
|
||||||
```
|
```
|
||||||
|
|
||||||
## With an attachment
|
## MessageView (Backbone)
|
||||||
|
|
||||||
### Image
|
### Plain messages
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const outgoing = new Whisper.Message({
|
||||||
|
type: 'outgoing',
|
||||||
|
body: 'How are you doing this fine day?',
|
||||||
|
sent_at: Date.now() - 18000,
|
||||||
|
});
|
||||||
|
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
|
||||||
|
source: '+12025550100',
|
||||||
|
type: 'incoming',
|
||||||
|
}));
|
||||||
|
const View = Whisper.MessageView;
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<util.BackboneWrapper
|
||||||
|
View={View}
|
||||||
|
options={{ model: incoming }}
|
||||||
|
/>
|
||||||
|
<util.BackboneWrapper
|
||||||
|
View={View}
|
||||||
|
options={{ model: outgoing }}
|
||||||
|
/>
|
||||||
|
</util.ConversationContext>
|
||||||
|
```
|
||||||
|
|
||||||
|
### With an attachment
|
||||||
|
|
||||||
|
#### Image
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
const outgoing = new Whisper.Message({
|
const outgoing = new Whisper.Message({
|
||||||
|
@ -39,12 +66,41 @@ const View = Whisper.MessageView;
|
||||||
</util.ConversationContext>
|
</util.ConversationContext>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Video
|
#### Image, no caption
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
const outgoing = new Whisper.Message({
|
const outgoing = new Whisper.Message({
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
body: "Beatiful, isn't it?",
|
sent_at: Date.now() - 18000000,
|
||||||
|
attachments: [{
|
||||||
|
data: util.gif,
|
||||||
|
fileName: 'pi.gif',
|
||||||
|
contentType: 'image/gif',
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
|
||||||
|
source: '+12025550100',
|
||||||
|
type: 'incoming',
|
||||||
|
}));
|
||||||
|
const View = Whisper.MessageView;
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<util.BackboneWrapper
|
||||||
|
View={View}
|
||||||
|
options={{ model: incoming }}
|
||||||
|
/>
|
||||||
|
<util.BackboneWrapper
|
||||||
|
View={View}
|
||||||
|
options={{ model: outgoing }}
|
||||||
|
/>
|
||||||
|
</util.ConversationContext>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Video
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const outgoing = new Whisper.Message({
|
||||||
|
type: 'outgoing',
|
||||||
|
body: "Beautiful, isn't it?",
|
||||||
sent_at: Date.now() - 10000,
|
sent_at: Date.now() - 10000,
|
||||||
attachments: [{
|
attachments: [{
|
||||||
data: util.mp4,
|
data: util.mp4,
|
||||||
|
@ -69,7 +125,36 @@ const View = Whisper.MessageView;
|
||||||
</util.ConversationContext>
|
</util.ConversationContext>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Audio
|
#### Video, no caption
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const outgoing = new Whisper.Message({
|
||||||
|
type: 'outgoing',
|
||||||
|
sent_at: Date.now() - 10000,
|
||||||
|
attachments: [{
|
||||||
|
data: util.mp4,
|
||||||
|
fileName: 'freezing_bubble.mp4',
|
||||||
|
contentType: 'video/mp4',
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
|
||||||
|
source: '+12025550100',
|
||||||
|
type: 'incoming',
|
||||||
|
}));
|
||||||
|
const View = Whisper.MessageView;
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<util.BackboneWrapper
|
||||||
|
View={View}
|
||||||
|
options={{ model: incoming }}
|
||||||
|
/>
|
||||||
|
<util.BackboneWrapper
|
||||||
|
View={View}
|
||||||
|
options={{ model: outgoing }}
|
||||||
|
/>
|
||||||
|
</util.ConversationContext>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Audio
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
const outgoing = new Whisper.Message({
|
const outgoing = new Whisper.Message({
|
||||||
|
@ -99,12 +184,40 @@ const View = Whisper.MessageView;
|
||||||
</util.ConversationContext>
|
</util.ConversationContext>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Voice message
|
#### Audio, no caption
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const outgoing = new Whisper.Message({
|
||||||
|
type: 'outgoing',
|
||||||
|
sent_at: Date.now() - 15000,
|
||||||
|
attachments: [{
|
||||||
|
data: util.mp3,
|
||||||
|
fileName: 'agnus_dei.mp3',
|
||||||
|
contentType: 'audio/mp3',
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
|
||||||
|
source: '+12025550100',
|
||||||
|
type: 'incoming',
|
||||||
|
}));
|
||||||
|
const View = Whisper.MessageView;
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<util.BackboneWrapper
|
||||||
|
View={View}
|
||||||
|
options={{ model: incoming }}
|
||||||
|
/>
|
||||||
|
<util.BackboneWrapper
|
||||||
|
View={View}
|
||||||
|
options={{ model: outgoing }}
|
||||||
|
/>
|
||||||
|
</util.ConversationContext>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Voice message
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
const outgoing = new Whisper.Message({
|
const outgoing = new Whisper.Message({
|
||||||
type: 'outgoing',
|
type: 'outgoing',
|
||||||
body: 'This is a nice song',
|
|
||||||
sent_at: Date.now() - 15000,
|
sent_at: Date.now() - 15000,
|
||||||
attachments: [{
|
attachments: [{
|
||||||
flags: textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE,
|
flags: textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE,
|
||||||
|
@ -130,7 +243,7 @@ const View = Whisper.MessageView;
|
||||||
</util.ConversationContext>
|
</util.ConversationContext>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Other file type
|
#### Other file type
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
const outgoing = new Whisper.Message({
|
const outgoing = new Whisper.Message({
|
||||||
|
@ -159,3 +272,32 @@ const View = Whisper.MessageView;
|
||||||
/>
|
/>
|
||||||
</util.ConversationContext>
|
</util.ConversationContext>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Other file type, no caption
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const outgoing = new Whisper.Message({
|
||||||
|
type: 'outgoing',
|
||||||
|
sent_at: Date.now() - 15000,
|
||||||
|
attachments: [{
|
||||||
|
data: util.txt,
|
||||||
|
fileName: 'lorum_ipsum.txt',
|
||||||
|
contentType: 'text/plain',
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
const incoming = new Whisper.Message(Object.assign({}, outgoing.attributes, {
|
||||||
|
source: '+12025550100',
|
||||||
|
type: 'incoming',
|
||||||
|
}));
|
||||||
|
const View = Whisper.MessageView;
|
||||||
|
<util.ConversationContext theme={util.theme}>
|
||||||
|
<util.BackboneWrapper
|
||||||
|
View={View}
|
||||||
|
options={{ model: incoming }}
|
||||||
|
/>
|
||||||
|
<util.BackboneWrapper
|
||||||
|
View={View}
|
||||||
|
options={{ model: outgoing }}
|
||||||
|
/>
|
||||||
|
</util.ConversationContext>
|
||||||
|
```
|
||||||
|
|
|
@ -4,7 +4,7 @@ import React from 'react';
|
||||||
/**
|
/**
|
||||||
* A placeholder Message component for now, giving the structure of a plain message with
|
* A placeholder Message component for now, giving the structure of a plain message with
|
||||||
* none of the dynamic functionality. This page will be used to build up our corpus of
|
* none of the dynamic functionality. This page will be used to build up our corpus of
|
||||||
* permutations before start moving all message functionality to React.
|
* permutations before we start moving all message functionality to React.
|
||||||
*/
|
*/
|
||||||
export class Message extends React.Component<{}, {}> {
|
export class Message extends React.Component<{}, {}> {
|
||||||
public render() {
|
public render() {
|
||||||
|
@ -13,12 +13,14 @@ export class Message extends React.Component<{}, {}> {
|
||||||
<span className="avatar" />
|
<span className="avatar" />
|
||||||
<div className="bubble">
|
<div className="bubble">
|
||||||
<div className="sender" dir="auto" />
|
<div className="sender" dir="auto" />
|
||||||
<div className="attachments" />
|
<div className="inner-bubble">
|
||||||
<p className="content" dir="auto">
|
<div className="attachments" />
|
||||||
<span className="body">
|
<p className="content" dir="auto">
|
||||||
Hi there. How are you doing? Feeling pretty good? Awesome.
|
<span className="body">
|
||||||
</span>
|
Hi there. How are you doing? Feeling pretty good? Awesome.
|
||||||
</p>
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div className="meta">
|
<div className="meta">
|
||||||
<span
|
<span
|
||||||
className="timestamp"
|
className="timestamp"
|
||||||
|
|
Loading…
Reference in a new issue