Support for iOS theme
This commit is contained in:
parent
644bc9e6fb
commit
087dd0f758
7 changed files with 226 additions and 19 deletions
|
@ -428,6 +428,24 @@
|
||||||
"selectAContact": {
|
"selectAContact": {
|
||||||
"message": "Select a contact or group to start chatting."
|
"message": "Select a contact or group to start chatting."
|
||||||
},
|
},
|
||||||
|
"replyingToYourself": {
|
||||||
|
"message": "Replying to Yourself",
|
||||||
|
"description": "Shown in iOS theme when you quote yourself"
|
||||||
|
},
|
||||||
|
"replyingToYou": {
|
||||||
|
"message": "Replying to You",
|
||||||
|
"description": "Shown in iOS theme when someone else quotes a message from you"
|
||||||
|
},
|
||||||
|
"replyingTo": {
|
||||||
|
"message": "Replying to $name`$",
|
||||||
|
"description": "Shown in iOS theme when you or someone quotes to a message which is not from you",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "John"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"audio": {
|
"audio": {
|
||||||
"message": "Audio",
|
"message": "Audio",
|
||||||
"description": "Shown in a quotation of a message containing an audio attachment if no text was originally provided with that attachment"
|
"description": "Shown in a quotation of a message containing an audio attachment if no text was originally provided with that attachment"
|
||||||
|
|
|
@ -400,29 +400,30 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const OUR_NUMBER = textsecure.storage.user.getNumber();
|
||||||
const { author } = quote;
|
const { author } = quote;
|
||||||
const contact = ConversationController.get(author);
|
const contact = ConversationController.get(author);
|
||||||
|
|
||||||
const authorTitle = contact ? contact.getTitle() : author;
|
const authorTitle = contact ? contact.getTitle() : author;
|
||||||
const authorProfileName = contact ? contact.getProfileName() : null;
|
const authorProfileName = contact ? contact.getProfileName() : null;
|
||||||
const authorColor = contact ? contact.getColor() : 'grey';
|
const authorColor = contact ? contact.getColor() : 'grey';
|
||||||
|
const isFromMe = contact ? contact.id === OUR_NUMBER : false;
|
||||||
const isIncoming = this.model.isIncoming();
|
const isIncoming = this.model.isIncoming();
|
||||||
const quoterContact = this.model.getContact();
|
|
||||||
const quoterAuthorColor = quoterContact ? quoterContact.getColor() : null;
|
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
authorTitle,
|
attachments: quote.attachments && quote.attachments.map(processAttachment),
|
||||||
authorProfileName,
|
|
||||||
authorColor,
|
authorColor,
|
||||||
|
authorProfileName,
|
||||||
|
authorTitle,
|
||||||
|
isFromMe,
|
||||||
isIncoming,
|
isIncoming,
|
||||||
quoterAuthorColor,
|
onClick: () => {
|
||||||
openQuotedMessage: () => {
|
|
||||||
const { quotedMessage } = this.model;
|
const { quotedMessage } = this.model;
|
||||||
if (quotedMessage) {
|
if (quotedMessage) {
|
||||||
this.trigger('scroll-to-message', { id: quotedMessage.id });
|
this.trigger('scroll-to-message', { id: quotedMessage.id });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
text: quote.text,
|
text: quote.text,
|
||||||
attachments: quote.attachments && quote.attachments.map(processAttachment),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!this.replyView) {
|
if (!this.replyView) {
|
||||||
|
|
|
@ -379,11 +379,7 @@ li.entry .error-icon-container {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-list .outgoing .bubble .quote {
|
.message-list .outgoing .bubble .quote, .private .message-list .incoming .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;
|
margin-top: $android-bubble-quote-padding - $android-bubble-padding-vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -479,7 +475,7 @@ span.status {
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
|
|
||||||
// Accent color border:
|
// Accent color border:
|
||||||
border-left-width: 3;
|
border-left-width: 3px;
|
||||||
border-left-style: solid;
|
border-left-style: solid;
|
||||||
|
|
||||||
.primary {
|
.primary {
|
||||||
|
@ -489,6 +485,12 @@ span.status {
|
||||||
padding-top: 6px;
|
padding-top: 6px;
|
||||||
padding-bottom: 6px;
|
padding-bottom: 6px;
|
||||||
|
|
||||||
|
// Will turn on in the iOS theme. This extra element is necessary because the iOS
|
||||||
|
// theme requires text that isn't used at all in the Android Theme
|
||||||
|
.ios-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.author {
|
.author {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 0.3em;
|
margin-bottom: 0.3em;
|
||||||
|
|
|
@ -106,6 +106,130 @@ $ios-border-color: rgba(0,0,0,0.1);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-list {
|
||||||
|
.quote {
|
||||||
|
border-top-left-radius: 15px;
|
||||||
|
border-top-right-radius: 15px;
|
||||||
|
border-bottom-left-radius: 0px;
|
||||||
|
border-bottom-right-radius: 0px;
|
||||||
|
|
||||||
|
// Not ideal, but necessary to override the specificity of the android theme color
|
||||||
|
// classes used in conversations.scss
|
||||||
|
background-color: white !important;
|
||||||
|
border: 1px solid lightgray !important;
|
||||||
|
border-bottom: none !important;
|
||||||
|
|
||||||
|
margin-top: 0px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
margin-left: 0px;
|
||||||
|
margin-right: 0px;
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.text,
|
||||||
|
.filename-label,
|
||||||
|
.type-label {
|
||||||
|
border-left: 2px solid lightgray;
|
||||||
|
padding: 5px;
|
||||||
|
padding-left: 7px;
|
||||||
|
// Without this smaller bottom padding, text beyond four lines still shows up!
|
||||||
|
padding-bottom: 2px;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ios-label {
|
||||||
|
display: block;
|
||||||
|
color: lightgray;
|
||||||
|
font-size: smaller;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-container {
|
||||||
|
height: 61px;
|
||||||
|
width: 61px;
|
||||||
|
|
||||||
|
.circle-background {
|
||||||
|
left: 12px;
|
||||||
|
right: 12px;
|
||||||
|
top: 12px;
|
||||||
|
bottom: 12px;
|
||||||
|
|
||||||
|
background-color: $blue !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
left: 18px;
|
||||||
|
right: 18px;
|
||||||
|
top: 18px;
|
||||||
|
bottom: 18px;
|
||||||
|
|
||||||
|
background-color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.from-me {
|
||||||
|
.primary {
|
||||||
|
.text,
|
||||||
|
.filename-label,
|
||||||
|
.type-label {
|
||||||
|
border-left: 2px solid $blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.incoming {
|
||||||
|
.bubble {
|
||||||
|
.quote {
|
||||||
|
background-color: lightgray !important;
|
||||||
|
border-left: none;
|
||||||
|
|
||||||
|
.ios-label {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
.text,
|
||||||
|
.filename-label,
|
||||||
|
.type-label {
|
||||||
|
border-left: 2px solid white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble {
|
||||||
|
.quote.from-me {
|
||||||
|
.primary {
|
||||||
|
.text,
|
||||||
|
.filename-label,
|
||||||
|
.type-label {
|
||||||
|
border-left: 2px solid $blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.outgoing .bubble .quote,
|
||||||
|
.private .message-list .incoming .bubble .quote {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.outgoing .bubble .quote .icon-container .circle-background {
|
||||||
|
background-color: lightgray !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.attachments .bubbled {
|
.attachments .bubbled {
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
|
|
|
@ -34,6 +34,39 @@ const View = Whisper.MessageView;
|
||||||
</util.ConversationContext>
|
</util.ConversationContext>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 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;
|
||||||
|
<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
|
#### In a group conversation
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
|
|
|
@ -11,9 +11,9 @@ interface Props {
|
||||||
authorProfileName?: string;
|
authorProfileName?: string;
|
||||||
authorTitle: string;
|
authorTitle: string;
|
||||||
i18n: (key: string, values?: Array<string>) => string;
|
i18n: (key: string, values?: Array<string>) => string;
|
||||||
|
isFromMe: string;
|
||||||
isIncoming: boolean;
|
isIncoming: boolean;
|
||||||
openQuotedMessage?: () => void;
|
onClick?: () => void;
|
||||||
quoterAuthorColor?: string;
|
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,10 +68,10 @@ export class Quote extends React.Component<Props, {}> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderIcon(icon: string) {
|
public renderIcon(icon: string) {
|
||||||
const { authorColor, isIncoming, quoterAuthorColor } = this.props;
|
const { authorColor, isIncoming } = this.props;
|
||||||
|
|
||||||
const backgroundColor = isIncoming ? 'white' : authorColor;
|
const backgroundColor = isIncoming ? 'white' : authorColor;
|
||||||
const iconColor = isIncoming ? quoterAuthorColor : 'white';
|
const iconColor = isIncoming ? authorColor : 'white';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="icon-container">
|
<div className="icon-container">
|
||||||
|
@ -138,12 +138,28 @@ export class Quote extends React.Component<Props, {}> {
|
||||||
return <div className="filename-label">{fileName}</div>;
|
return <div className="filename-label">{fileName}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 <div className='ios-label'>{label}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const {
|
const {
|
||||||
authorTitle,
|
authorTitle,
|
||||||
authorProfileName,
|
authorProfileName,
|
||||||
authorColor,
|
authorColor,
|
||||||
openQuotedMessage,
|
onClick,
|
||||||
|
isFromMe,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!validateQuote(this.props)) {
|
if (!validateQuote(this.props)) {
|
||||||
|
@ -155,8 +171,13 @@ export class Quote extends React.Component<Props, {}> {
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onClick={openQuotedMessage} className={classnames(authorColor, 'quote')} >
|
<div onClick={onClick} className={classnames(
|
||||||
|
authorColor,
|
||||||
|
'quote',
|
||||||
|
isFromMe ? 'from-me' : null
|
||||||
|
)} >
|
||||||
<div className="primary">
|
<div className="primary">
|
||||||
|
{this.renderIOSLabel()}
|
||||||
<div className={classnames(authorColor, 'author')}>
|
<div className={classnames(authorColor, 'author')}>
|
||||||
{authorTitle}{' '}{authorProfileElement}
|
{authorTitle}{' '}{authorProfileElement}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -141,9 +141,17 @@ const CONTACTS = COLORS.map((color, index) => {
|
||||||
return parent.ConversationController.dangerouslyCreateAndAdd(contact);
|
return parent.ConversationController.dangerouslyCreateAndAdd(contact);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const me = parent.ConversationController.dangerouslyCreateAndAdd({
|
||||||
|
id: ourNumber,
|
||||||
|
name: 'Me!',
|
||||||
|
type: 'private',
|
||||||
|
color: 'light_blue',
|
||||||
|
});
|
||||||
|
|
||||||
export {
|
export {
|
||||||
COLORS,
|
COLORS,
|
||||||
CONTACTS,
|
CONTACTS,
|
||||||
|
me,
|
||||||
};
|
};
|
||||||
|
|
||||||
parent.textsecure.storage.user.getNumber = () => ourNumber;
|
parent.textsecure.storage.user.getNumber = () => ourNumber;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue