Note to Self

This commit is contained in:
Scott Nonnenberg 2019-01-30 17:45:58 -08:00
parent 681ca363fe
commit a43a78731a
15 changed files with 411 additions and 148 deletions

View file

@ -1544,6 +1544,10 @@
"message": "Dark",
"description": "Label text for dark theme"
},
"noteToSelf": {
"message": "Note to Self",
"description": "Name for the conversation with your own phone number"
},
"hideMenuBar": {
"message": "Hide menu bar",
"description": "Label text for menu bar visibility setting"

1
images/note-28.svg Normal file
View file

@ -0,0 +1 @@
<svg id="Export" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28"><title>note-28</title><path d="M21,3H7A2,2,0,0,0,5,5V23a2,2,0,0,0,2,2H21a2,2,0,0,0,2-2V5A2,2,0,0,0,21,3ZM17,19.5H7V18H17ZM21,16H7V14.5H21Zm0-3.5H7V11H21ZM21,9H7V7.5H21Z"/></svg>

After

Width:  |  Height:  |  Size: 249 B

View file

@ -312,6 +312,7 @@
const result = {
...this.format(),
isMe: this.isMe(),
conversationType: this.isPrivate() ? 'direct' : 'group',
lastUpdated: this.get('timestamp'),
@ -908,6 +909,25 @@
return null;
}
const attachmentsWithData = await Promise.all(
messageWithSchema.attachments.map(loadAttachmentData)
);
// Special-case the self-send case - we send only a sync message
if (this.isMe()) {
const dataMessage = await textsecure.messaging.getMessageProto(
destination,
body,
attachmentsWithData,
quote,
preview,
now,
expireTimer,
profileKey
);
return message.sendSyncMessageOnly(dataMessage);
}
const conversationType = this.get('type');
const sendFunction = (() => {
switch (conversationType) {
@ -922,10 +942,6 @@
}
})();
const attachmentsWithData = await Promise.all(
messageWithSchema.attachments.map(loadAttachmentData)
);
const options = this.getSendOptions();
return message.send(
this.wrapSend(

View file

@ -736,10 +736,25 @@
const quoteWithData = await loadQuoteData(this.get('quote'));
const previewWithData = await loadPreviewData(this.get('preview'));
const conversation = this.getConversation();
const options = conversation.getSendOptions();
// Special-case the self-send case - we send only a sync message
if (numbers.length === 1 && numbers[0] === this.OUR_NUMBER) {
const [number] = numbers;
const dataMessage = await textsecure.messaging.getMessageProto(
number,
this.get('body'),
attachmentsWithData,
quoteWithData,
previewWithData,
this.get('sent_at'),
this.get('expireTimer'),
profileKey
);
return this.sendSyncMessageOnly(dataMessage);
}
let promise;
const conversation = this.getConversation();
const options = conversation.getSendOptions();
if (conversation.isPrivate()) {
const [number] = numbers;
@ -794,18 +809,21 @@
// One caller today: ConversationView.forceSend()
async resend(number) {
const error = this.removeOutgoingErrors(number);
if (error) {
const profileKey = null;
const attachmentsWithData = await Promise.all(
(this.get('attachments') || []).map(loadAttachmentData)
);
const quoteWithData = await loadQuoteData(this.get('quote'));
const previewWithData = await loadPreviewData(this.get('preview'));
if (!error) {
window.log.warn('resend: requested number was not present in errors');
return null;
}
const { wrap, sendOptions } = ConversationController.prepareForSend(
number
);
const promise = textsecure.messaging.sendMessageToNumber(
const profileKey = null;
const attachmentsWithData = await Promise.all(
(this.get('attachments') || []).map(loadAttachmentData)
);
const quoteWithData = await loadQuoteData(this.get('quote'));
const previewWithData = await loadPreviewData(this.get('preview'));
// Special-case the self-send case - we send only a sync message
if (number === this.OUR_NUMBER) {
const dataMessage = await textsecure.messaging.getMessageProto(
number,
this.get('body'),
attachmentsWithData,
@ -813,12 +831,27 @@
previewWithData,
this.get('sent_at'),
this.get('expireTimer'),
profileKey,
sendOptions
profileKey
);
this.send(wrap(promise));
return this.sendSyncMessageOnly(dataMessage);
}
const { wrap, sendOptions } = ConversationController.prepareForSend(
number
);
const promise = textsecure.messaging.sendMessageToNumber(
number,
this.get('body'),
attachmentsWithData,
quoteWithData,
previewWithData,
this.get('sent_at'),
this.get('expireTimer'),
profileKey,
sendOptions
);
return this.send(wrap(promise));
},
removeOutgoingErrors(number) {
const errors = _.partition(
@ -912,7 +945,7 @@
this.trigger('done');
// This is used by sendSyncMessage, then set to null
if (result.dataMessage) {
if (!this.get('synced') && result.dataMessage) {
this.set({ dataMessage: result.dataMessage });
}
@ -1013,6 +1046,35 @@
return false;
},
async sendSyncMessageOnly(dataMessage) {
this.set({ dataMessage });
try {
await this.sendSyncMessage();
this.set({
delivered_to: [this.OUR_NUMBER],
read_by: [this.OUR_NUMBER],
});
} catch (result) {
const errors = (result && result.errors) || [
new Error('Unknown error'),
];
this.set({ errors });
} finally {
await window.Signal.Data.saveMessage(this.attributes, {
Message: Whisper.Message,
});
this.trigger('done');
const errors = this.get('errors');
if (errors) {
this.trigger('send-error', errors);
} else {
this.trigger('sent');
}
}
},
sendSyncMessage() {
const ourNumber = textsecure.storage.user.getNumber();
const { wrap, sendOptions } = ConversationController.prepareForSend(
@ -1021,7 +1083,7 @@
);
this.syncPromise = this.syncPromise || Promise.resolve();
this.syncPromise = this.syncPromise.then(() => {
const next = () => {
const dataMessage = this.get('dataMessage');
if (this.get('synced') || !dataMessage) {
return Promise.resolve();
@ -1036,16 +1098,20 @@
this.get('unidentifiedDeliveries'),
sendOptions
)
).then(() => {
).then(result => {
this.set({
synced: true,
dataMessage: null,
});
return window.Signal.Data.saveMessage(this.attributes, {
Message: Whisper.Message,
});
}).then(() => result);
});
});
};
this.syncPromise = this.syncPromise.then(next, next);
return this.syncPromise;
},
async saveErrors(providedErrors) {
@ -1312,6 +1378,14 @@
});
}
// A sync'd message to ourself is automatically considered read and delivered
if (conversation.isMe()) {
message.set({
read_by: conversation.getRecipients(),
delivered_to: conversation.getRecipients(),
});
}
message.set({ recipients: conversation.getRecipients() });
}

View file

@ -1,6 +1,4 @@
/* global ConversationController: false */
/* global i18n: false */
/* global Whisper: false */
/* global ConversationController, i18n, textsecure, Whisper */
// eslint-disable-next-line func-names
(function() {
@ -81,9 +79,19 @@
/* eslint-disable more/no-then */
this.pending = this.pending.then(() =>
this.typeahead.search(query).then(() => {
this.typeahead_view.collection.reset(
this.typeahead.filter(isSearchable)
);
let results = this.typeahead.filter(isSearchable);
const noteToSelf = i18n('noteToSelf');
if (noteToSelf.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
const ourNumber = textsecure.storage.user.getNumber();
const conversation = ConversationController.get(ourNumber);
if (conversation) {
// ensure that we don't have duplicates in our results
results = results.filter(item => item.id !== ourNumber);
results.unshift(conversation);
}
}
this.typeahead_view.collection.reset(results);
})
);
/* eslint-enable more/no-then */

View file

@ -759,6 +759,37 @@ MessageSender.prototype = {
});
},
async getMessageProto(
number,
body,
attachments,
quote,
preview,
timestamp,
expireTimer,
profileKey
) {
const attributes = {
recipients: [number],
body,
timestamp,
attachments,
quote,
preview,
expireTimer,
profileKey,
};
const message = new Message(attributes);
await Promise.all([
this.uploadAttachments(message),
this.uploadThumbnails(message),
this.uploadLinkPreviews(message),
]);
return message.toArrayBuffer();
},
sendMessageToNumber(
number,
messageText,
@ -1110,6 +1141,7 @@ textsecure.MessageSender = function MessageSenderWrapper(
this.sendReadReceipts = sender.sendReadReceipts.bind(sender);
this.makeProxiedRequest = sender.makeProxiedRequest.bind(sender);
this.getProxiedSize = sender.getProxiedSize.bind(sender);
this.getMessageProto = sender.getMessageProto.bind(sender);
};
textsecure.MessageSender.prototype = {

View file

@ -1348,7 +1348,7 @@
}
.module-conversation-header__title {
margin-left: 8px;
margin-left: 6px;
min-width: 0;
font-size: 16px;
@ -1356,8 +1356,8 @@
font-weight: 300;
color: $color-gray-90;
// width of avatar (28px) and our 8px left margin
max-width: calc(100% - 36px);
// width of avatar (28px) and our 6px left margin
max-width: calc(100% - 34px);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@ -2036,6 +2036,12 @@
width: 42px;
}
.module-avatar__icon--note-to-self {
width: 70%;
height: 70%;
@include color-svg('../images/note-28.svg', $color-white);
}
.module-avatar--no-image {
background-color: $color-conversation-grey;
}

View file

@ -1098,6 +1098,10 @@ body.dark-theme {
color: $color-dark-05;
}
.module-conversation-header__note-to-self {
color: $color-dark-05;
}
.module-conversation-header__title__verified-icon {
@include color-svg('../images/verified-check.svg', $color-dark-05);
}
@ -1262,11 +1266,15 @@ body.dark-theme {
}
.module-avatar__icon--group {
@include color-svg('../images/profile-group.svg', $color-gray-05);
background-color: $color-gray-05;
}
.module-avatar__icon--direct {
@include color-svg('../images/profile-individual.svg', $color-gray-05);
background-color: $color-gray-05;
}
.module-avatar__icon--note-to-self {
background-color: $color-gray-05;
}
.module-avatar--no-image {

View file

@ -63,6 +63,45 @@
</util.ConversationContext>
```
### Note to self
```jsx
<util.ConversationContext theme={util.theme} ios={util.ios}>
<Avatar
size={80}
color="pink"
noteToSelf={true}
phoneNumber="(555) 353-3433"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={48}
color="pink"
noteToSelf={true}
phoneNumber="(555) 353-3433"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={36}
color="pink"
noteToSelf={true}
phoneNumber="(555) 353-3433"
conversationType="direct"
i18n={util.i18n}
/>
<Avatar
size={28}
color="pink"
noteToSelf={true}
phoneNumber="(555) 353-3433"
conversationType="direct"
i18n={util.i18n}
/>
</util.ConversationContext>
```
### All colors
```jsx

View file

@ -9,6 +9,7 @@ interface Props {
color?: string;
conversationType: 'group' | 'direct';
i18n: Localizer;
noteToSelf?: boolean;
name?: string;
phoneNumber?: string;
profileName?: string;
@ -63,11 +64,23 @@ export class Avatar extends React.Component<Props, State> {
}
public renderNoImage() {
const { conversationType, name, size } = this.props;
const { conversationType, name, noteToSelf, size } = this.props;
const initials = getInitials(name);
const isGroup = conversationType === 'group';
if (noteToSelf) {
return (
<div
className={classNames(
'module-avatar__icon',
'module-avatar__icon--note-to-self',
`module-avatar__icon--${size}`
)}
/>
);
}
if (!isGroup && initials) {
return (
<div
@ -93,10 +106,10 @@ export class Avatar extends React.Component<Props, State> {
}
public render() {
const { avatarPath, color, size } = this.props;
const { avatarPath, color, size, noteToSelf } = this.props;
const { imageBroken } = this.state;
const hasImage = avatarPath && !imageBroken;
const hasImage = !noteToSelf && avatarPath && !imageBroken;
if (size !== 28 && size !== 36 && size !== 48 && size !== 80) {
throw new Error(`Size ${size} is not supported!`);

View file

@ -38,6 +38,27 @@
</util.LeftPaneContext>
```
#### Conversation with yourself
```jsx
<util.LeftPaneContext theme={util.theme}>
<ConversationListItem
isMe={true}
phoneNumber="(202) 555-0011"
conversationType={'direct'}
name="Mr. Fire🔥"
color="green"
lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{
text: 'Just a second',
status: 'read',
}}
onClick={() => console.log('onClick')}
i18n={util.i18n}
/>
</util.LeftPaneContext>
```
#### All types of status
```jsx

View file

@ -16,6 +16,7 @@ interface Props {
color?: string;
conversationType: 'group' | 'direct';
avatarPath?: string;
isMe: boolean;
lastUpdated: number;
unreadCount: number;
@ -38,6 +39,7 @@ export class ConversationListItem extends React.Component<Props> {
color,
conversationType,
i18n,
isMe,
name,
phoneNumber,
profileName,
@ -48,6 +50,7 @@ export class ConversationListItem extends React.Component<Props> {
<Avatar
avatarPath={avatarPath}
color={color}
noteToSelf={isMe}
conversationType={conversationType}
i18n={i18n}
name={name}
@ -78,6 +81,7 @@ export class ConversationListItem extends React.Component<Props> {
const {
unreadCount,
i18n,
isMe,
lastUpdated,
name,
phoneNumber,
@ -94,12 +98,16 @@ export class ConversationListItem extends React.Component<Props> {
: null
)}
>
<ContactName
phoneNumber={phoneNumber}
name={name}
profileName={profileName}
i18n={i18n}
/>
{isMe ? (
i18n('noteToSelf')
) : (
<ContactName
phoneNumber={phoneNumber}
name={name}
profileName={profileName}
i18n={i18n}
/>
)}
</div>
<div
className={classNames(

View file

@ -5,100 +5,112 @@ Note the five items in gear menu, and the second-level menu with disappearing me
#### With name and profile, verified
```jsx
<ConversationHeader
i18n={util.i18n}
color="red"
isVerified={true}
avatarPath={util.gifObjectUrl}
name="Someone 🔥 Somewhere"
phoneNumber="(202) 555-0001"
id="1"
profileName="🔥Flames🔥"
onSetDisappearingMessages={seconds =>
console.log('onSetDisappearingMessages', seconds)
}
onDeleteMessages={() => console.log('onDeleteMessages')}
onResetSession={() => console.log('onResetSession')}
onShowSafetyNumber={() => console.log('onShowSafetyNumber')}
onShowAllMedia={() => console.log('onShowAllMedia')}
onShowGroupMembers={() => console.log('onShowGroupMembers')}
onGoBack={() => console.log('onGoBack')}
/>
<util.ConversationContext theme={util.theme}>
<ConversationHeader
i18n={util.i18n}
color="red"
isVerified={true}
avatarPath={util.gifObjectUrl}
name="Someone 🔥 Somewhere"
phoneNumber="(202) 555-0001"
id="1"
profileName="🔥Flames🔥"
onSetDisappearingMessages={seconds =>
console.log('onSetDisappearingMessages', seconds)
}
onDeleteMessages={() => console.log('onDeleteMessages')}
onResetSession={() => console.log('onResetSession')}
onShowSafetyNumber={() => console.log('onShowSafetyNumber')}
onShowAllMedia={() => console.log('onShowAllMedia')}
onShowGroupMembers={() => console.log('onShowGroupMembers')}
onGoBack={() => console.log('onGoBack')}
/>
</util.ConversationContext>
```
#### With name, not verified, no avatar
```jsx
<ConversationHeader
i18n={util.i18n}
color="blue"
isVerified={false}
name="Someone 🔥 Somewhere"
phoneNumber="(202) 555-0002"
id="2"
/>
<util.ConversationContext theme={util.theme}>
<ConversationHeader
i18n={util.i18n}
color="blue"
isVerified={false}
name="Someone 🔥 Somewhere"
phoneNumber="(202) 555-0002"
id="2"
/>
</util.ConversationContext>
```
#### Profile, no name
```jsx
<ConversationHeader
i18n={util.i18n}
color="teal"
isVerified={false}
phoneNumber="(202) 555-0003"
id="3"
profileName="🔥Flames🔥"
/>
<util.ConversationContext theme={util.theme}>
<ConversationHeader
i18n={util.i18n}
color="teal"
isVerified={false}
phoneNumber="(202) 555-0003"
id="3"
profileName="🔥Flames🔥"
/>
</util.ConversationContext>
```
#### No name, no profile, no color
```jsx
<ConversationHeader i18n={util.i18n} phoneNumber="(202) 555-0011" id="11" />
<util.ConversationContext theme={util.theme}>
<ConversationHeader i18n={util.i18n} phoneNumber="(202) 555-0011" id="11" />
</util.ConversationContext>
```
### With back button
```jsx
<ConversationHeader
showBackButton={true}
color="deep_orange"
i18n={util.i18n}
phoneNumber="(202) 555-0004"
id="4"
/>
<util.ConversationContext theme={util.theme}>
<ConversationHeader
showBackButton={true}
color="deep_orange"
i18n={util.i18n}
phoneNumber="(202) 555-0004"
id="4"
/>
</util.ConversationContext>
```
### Disappearing messages set
```jsx
<ConversationHeader
color="indigo"
i18n={util.i18n}
phoneNumber="(202) 555-0005"
id="5"
expirationSettingName="10 seconds"
timerOptions={[
{
name: 'off',
value: 0,
},
{
name: '10 seconds',
value: 10,
},
]}
onSetDisappearingMessages={seconds =>
console.log('onSetDisappearingMessages', seconds)
}
onDeleteMessages={() => console.log('onDeleteMessages')}
onResetSession={() => console.log('onResetSession')}
onShowSafetyNumber={() => console.log('onShowSafetyNumber')}
onShowAllMedia={() => console.log('onShowAllMedia')}
onShowGroupMembers={() => console.log('onShowGroupMembers')}
onGoBack={() => console.log('onGoBack')}
/>
<util.ConversationContext theme={util.theme}>
<ConversationHeader
color="indigo"
i18n={util.i18n}
phoneNumber="(202) 555-0005"
id="5"
expirationSettingName="10 seconds"
timerOptions={[
{
name: 'off',
value: 0,
},
{
name: '10 seconds',
value: 10,
},
]}
onSetDisappearingMessages={seconds =>
console.log('onSetDisappearingMessages', seconds)
}
onDeleteMessages={() => console.log('onDeleteMessages')}
onResetSession={() => console.log('onResetSession')}
onShowSafetyNumber={() => console.log('onShowSafetyNumber')}
onShowAllMedia={() => console.log('onShowAllMedia')}
onShowGroupMembers={() => console.log('onShowGroupMembers')}
onGoBack={() => console.log('onGoBack')}
/>
</util.ConversationContext>
```
### In a group
@ -106,34 +118,38 @@ Note the five items in gear menu, and the second-level menu with disappearing me
Note that the menu should includes 'Show Members' instead of 'Show Safety Number'
```jsx
<ConversationHeader
i18n={util.i18n}
color="green"
phoneNumber="(202) 555-0006"
id="6"
isGroup={true}
onSetDisappearingMessages={seconds =>
console.log('onSetDisappearingMessages', seconds)
}
onDeleteMessages={() => console.log('onDeleteMessages')}
onResetSession={() => console.log('onResetSession')}
onShowSafetyNumber={() => console.log('onShowSafetyNumber')}
onShowAllMedia={() => console.log('onShowAllMedia')}
onShowGroupMembers={() => console.log('onShowGroupMembers')}
onGoBack={() => console.log('onGoBack')}
/>
<util.ConversationContext theme={util.theme}>
<ConversationHeader
i18n={util.i18n}
color="green"
phoneNumber="(202) 555-0006"
id="6"
isGroup={true}
onSetDisappearingMessages={seconds =>
console.log('onSetDisappearingMessages', seconds)
}
onDeleteMessages={() => console.log('onDeleteMessages')}
onResetSession={() => console.log('onResetSession')}
onShowSafetyNumber={() => console.log('onShowSafetyNumber')}
onShowAllMedia={() => console.log('onShowAllMedia')}
onShowGroupMembers={() => console.log('onShowGroupMembers')}
onGoBack={() => console.log('onGoBack')}
/>
</util.ConversationContext>
```
### In chat with yourself
Note that the menu should not have a 'Show Safety Number' entry.
This is the 'Note to self' conversation. Note that the menu should not have a 'Show Safety Number' entry.
```jsx
<ConversationHeader
color="cyan"
i18n={util.i18n}
phoneNumber="(202) 555-0007"
id="7"
isMe={true}
/>
<util.ConversationContext theme={util.theme}>
<ConversationHeader
color="cyan"
i18n={util.i18n}
phoneNumber="(202) 555-0007"
id="7"
isMe={true}
/>
</util.ConversationContext>
```

View file

@ -84,7 +84,22 @@ export class ConversationHeader extends React.Component<Props> {
}
public renderTitle() {
const { name, phoneNumber, i18n, profileName, isVerified } = this.props;
const {
name,
phoneNumber,
i18n,
isMe,
profileName,
isVerified,
} = this.props;
if (isMe) {
return (
<div className="module-conversation-header__title">
{i18n('noteToSelf')}
</div>
);
}
return (
<div className="module-conversation-header__title">
@ -113,6 +128,7 @@ export class ConversationHeader extends React.Component<Props> {
color,
i18n,
isGroup,
isMe,
name,
phoneNumber,
profileName,
@ -125,6 +141,7 @@ export class ConversationHeader extends React.Component<Props> {
color={color}
conversationType={isGroup ? 'group' : 'direct'}
i18n={i18n}
noteToSelf={isMe}
name={name}
phoneNumber={phoneNumber}
profileName={profileName}

View file

@ -206,8 +206,8 @@
{
"rule": "jQuery-wrap(",
"path": "js/models/messages.js",
"line": " this.send(wrap(promise));",
"lineNumber": 820,
"line": " return this.send(wrap(promise));",
"lineNumber": 854,
"reasonCategory": "falseMatch",
"updated": "2018-10-05T23:12:28.961Z"
},
@ -215,7 +215,7 @@
"rule": "jQuery-wrap(",
"path": "js/models/messages.js",
"line": " return wrap(",
"lineNumber": 1029,
"lineNumber": 1091,
"reasonCategory": "falseMatch",
"updated": "2018-10-05T23:12:28.961Z"
},
@ -527,7 +527,7 @@
"rule": "jQuery-$(",
"path": "js/views/conversation_search_view.js",
"line": " this.$new_contact = this.$('.new-contact');",
"lineNumber": 42,
"lineNumber": 40,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input"
@ -536,7 +536,7 @@
"rule": "jQuery-append(",
"path": "js/views/conversation_search_view.js",
"line": " this.$el.append(this.typeahead_view.el);",
"lineNumber": 59,
"lineNumber": 57,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
@ -545,7 +545,7 @@
"rule": "jQuery-$(",
"path": "js/views/conversation_search_view.js",
"line": " this.new_contact_view.$('.number').text(i18n('invalidNumberError'));",
"lineNumber": 111,
"lineNumber": 119,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input"
@ -554,7 +554,7 @@
"rule": "jQuery-insertAfter(",
"path": "js/views/conversation_search_view.js",
"line": " this.hintView.$el.insertAfter(this.$input);",
"lineNumber": 147,
"lineNumber": 155,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
@ -6215,4 +6215,4 @@
"updated": "2018-09-17T20:50:40.689Z",
"reasonDetail": "Hard-coded value"
}
]
]