Sort by inbox position to match phone after link

This commit is contained in:
Josh Perez 2020-03-09 17:43:09 -07:00 committed by Scott Nonnenberg
parent 1f5cb9e8af
commit 4830213a12
25 changed files with 707 additions and 1029 deletions

View file

@ -285,6 +285,10 @@
} }
} }
}, },
"messageHistoryUnsynced": {
"message": "For your security, conversation history isn't transferred to new linked devices.",
"description": "Shown in the conversation history when a user links a new device to explain what is not supported."
},
"youMarkedAsVerified": { "youMarkedAsVerified": {
"message": "You marked your Safety Number with $name$ as verified", "message": "You marked your Safety Number with $name$ as verified",
"description": "Shown in the conversation history when the user marks a contact as verified.", "description": "Shown in the conversation history when the user marks a contact as verified.",
@ -1530,6 +1534,10 @@
"message": "Start new conversation…", "message": "Start new conversation…",
"description": "Label underneath number a user enters that is not an existing contact" "description": "Label underneath number a user enters that is not an existing contact"
}, },
"notSupportedSMS": {
"message": "SMS/MMS messages are not supported.",
"description": "Label underneath number informing user that SMS is not supported on desktop"
},
"newPhoneNumber": { "newPhoneNumber": {
"message": "Enter a phone number to add a contact.", "message": "Enter a phone number to add a contact.",
"description": "Placeholder for adding a new number to a contact" "description": "Placeholder for adding a new number to a contact"

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>info-outline-24</title><path d="M12,2.5A9.5,9.5,0,1,1,2.5,12,9.511,9.511,0,0,1,12,2.5M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,7.5A1.5,1.5,0,0,0,13.5,7a1.5,1.5,0,1,0-2.56,1.06A1.435,1.435,0,0,0,12,8.5Zm1,8V10H9.5v1.5h2v5H9V18h6V16.5Z"/></svg>

After

Width:  |  Height:  |  Size: 334 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><title>info-solid-24</title><path d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1ZM10.94,5.939A1.5,1.5,0,0,1,13.5,7a1.5,1.5,0,0,1-2.56,1.06,1.5,1.5,0,0,1,0-2.121ZM15,18H9V16.5h2.5v-5h-2V10H13v6.5h2Z"/></svg>

After

Width:  |  Height:  |  Size: 286 B

View file

@ -1975,6 +1975,7 @@
name: details.name, name: details.name,
color: details.color, color: details.color,
active_at: activeAt, active_at: activeAt,
inbox_position: details.inboxPosition,
}); });
// Update the conversation avatar only if new avatar exists and hash differs // Update the conversation avatar only if new avatar exists and hash differs
@ -2030,6 +2031,14 @@
verifiedEvent.viaContactSync = true; verifiedEvent.viaContactSync = true;
await onVerified(verifiedEvent); await onVerified(verifiedEvent);
} }
const { appView } = window.owsDesktopApp;
if (appView && appView.installView && appView.installView.didLink) {
window.log.info(
'onContactReceived: Adding the message history disclaimer on link'
);
await conversation.addMessageHistoryDisclaimer();
}
} catch (error) { } catch (error) {
window.log.error('onContactReceived error:', Errors.toLogFormat(error)); window.log.error('onContactReceived error:', Errors.toLogFormat(error));
} }
@ -2063,6 +2072,7 @@
members, members,
color: details.color, color: details.color,
type: 'group', type: 'group',
inbox_position: details.inboxPosition,
}; };
if (details.active) { if (details.active) {
@ -2103,6 +2113,13 @@
window.Signal.Data.updateConversation(id, conversation.attributes); window.Signal.Data.updateConversation(id, conversation.attributes);
const { appView } = window.owsDesktopApp;
if (appView && appView.installView && appView.installView.didLink) {
window.log.info(
'onGroupReceived: Adding the message history disclaimer on link'
);
await conversation.addMessageHistoryDisclaimer();
}
const { expireTimer } = details; const { expireTimer } = details;
const isValidExpireTimer = typeof expireTimer === 'number'; const isValidExpireTimer = typeof expireTimer === 'number';
if (!isValidExpireTimer) { if (!isValidExpireTimer) {

View file

@ -385,6 +385,7 @@
const draftText = this.get('draft'); const draftText = this.get('draft');
const shouldShowDraft = const shouldShowDraft =
this.hasDraft() && draftTimestamp && draftTimestamp >= timestamp; this.hasDraft() && draftTimestamp && draftTimestamp >= timestamp;
const inboxPosition = this.get('inbox_position');
const result = { const result = {
id: this.id, id: this.id,
@ -400,6 +401,7 @@
name: this.getName(), name: this.getName(),
profileName: this.getProfileName(), profileName: this.getProfileName(),
timestamp, timestamp,
inboxPosition,
title: this.getTitle(), title: this.getTitle(),
unreadCount: this.get('unreadCount') || 0, unreadCount: this.get('unreadCount') || 0,
@ -1662,6 +1664,38 @@
return message; return message;
}, },
async addMessageHistoryDisclaimer() {
const timestamp = Date.now();
const model = new Whisper.Message({
type: 'message-history-unsynced',
// Even though this isn't reflected to the user, we want to place the last seen
// indicator above it. We set it to 'unread' to trigger that placement.
unread: 1,
conversationId: this.id,
// No type; 'incoming' messages are specially treated by conversation.markRead()
sent_at: timestamp,
received_at: timestamp,
});
if (this.isPrivate()) {
model.set({ destination: this.id });
}
if (model.isOutgoing()) {
model.set({ recipients: this.getRecipients() });
}
const id = await window.Signal.Data.saveMessage(model.attributes, {
Message: Whisper.Message,
});
model.set({ id });
const message = MessageController.register(id, model);
this.addSingleMessage(message);
return message;
},
isSearchable() { isSearchable() {
return !this.get('left'); return !this.get('left');
}, },

View file

@ -134,6 +134,7 @@
!this.isUnsupportedMessage() && !this.isUnsupportedMessage() &&
!this.isExpirationTimerUpdate() && !this.isExpirationTimerUpdate() &&
!this.isKeyChange() && !this.isKeyChange() &&
!this.isMessageHistoryUnsynced() &&
!this.isVerifiedChange() && !this.isVerifiedChange() &&
!this.isGroupUpdate() && !this.isGroupUpdate() &&
!this.isEndSession() !this.isEndSession()
@ -147,6 +148,11 @@
type: 'unsupportedMessage', type: 'unsupportedMessage',
data: this.getPropsForUnsupportedMessage(), data: this.getPropsForUnsupportedMessage(),
}; };
} else if (this.isMessageHistoryUnsynced()) {
return {
type: 'linkNotification',
data: null,
};
} else if (this.isExpirationTimerUpdate()) { } else if (this.isExpirationTimerUpdate()) {
return { return {
type: 'timerNotification', type: 'timerNotification',
@ -343,6 +349,9 @@
isVerifiedChange() { isVerifiedChange() {
return this.get('type') === 'verified-change'; return this.get('type') === 'verified-change';
}, },
isMessageHistoryUnsynced() {
return this.get('type') === 'message-history-unsynced';
},
isGroupUpdate() { isGroupUpdate() {
return !!this.get('group_update'); return !!this.get('group_update');
}, },

View file

@ -32,6 +32,7 @@
initialize(options = {}) { initialize(options = {}) {
window.readyForUpdates(); window.readyForUpdates();
this.didLink = false;
this.selectStep(Steps.SCAN_QR_CODE); this.selectStep(Steps.SCAN_QR_CODE);
this.connect(); this.connect();
this.on('disconnected', this.reconnect); this.on('disconnected', this.reconnect);
@ -179,7 +180,10 @@
this.selectStep(Steps.PROGRESS_BAR); this.selectStep(Steps.PROGRESS_BAR);
const finish = () => resolve(name); const finish = () => {
this.didLink = true;
return resolve(name);
};
// Delete all data from database unless we're in the middle // Delete all data from database unless we're in the middle
// of a re-link, or we are finishing a light import. Without this, // of a re-link, or we are finishing a light import. Without this,

View file

@ -386,6 +386,7 @@ message ContactDetails {
optional bytes profileKey = 6; optional bytes profileKey = 6;
optional bool blocked = 7; optional bool blocked = 7;
optional uint32 expireTimer = 8; optional uint32 expireTimer = 8;
optional uint32 inboxPosition = 10;
} }
message GroupDetails { message GroupDetails {
@ -408,4 +409,5 @@ message GroupDetails {
optional uint32 expireTimer = 6; optional uint32 expireTimer = 6;
optional string color = 7; optional string color = 7;
optional bool blocked = 8; optional bool blocked = 8;
optional uint32 inboxPosition = 10;
} }

View file

@ -2335,6 +2335,36 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
} }
} }
.module-message-unsynced {
padding-bottom: 24px;
text-align: center;
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
}
.module-message-unsynced__icon {
height: 24px;
margin-bottom: 4px;
margin-left: auto;
margin-right: auto;
width: 24px;
@include light-theme {
@include color-svg(
'../images/icons/v2/info-outline-24.svg',
$color-gray-60
);
}
@include dark-theme {
@include color-svg('../images/icons/v2/info-solid-24.svg', $color-gray-25);
}
}
// Module: Verification Notification // Module: Verification Notification
.module-verification-notification { .module-verification-notification {
@ -4729,6 +4759,19 @@ button.module-image__border-overlay:focus {
} }
} }
.module-search-results__sms-not-supported {
font-size: 14px;
padding-top: 12px;
text-align: center;
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
}
.module-search-results__no-results { .module-search-results__no-results {
margin-top: 27px; margin-top: 27px;
padding-left: 1em; padding-left: 1em;

View file

@ -1,983 +0,0 @@
#### With all result types
```jsx
const items = [
{
type: 'conversations-header',
data: undefined,
},
{
type: 'conversation',
data: {
name: 'Everyone 🌆',
conversationType: 'group',
phoneNumber: '(202) 555-0011',
avatarPath: util.landscapeGreenObjectUrl,
lastUpdated: Date.now() - 5 * 60 * 1000,
lastMessage: {
text: 'The rabbit hopped silently in the night.',
status: 'sent',
},
},
},
{
type: 'conversation',
data: {
name: 'Everyone Else 🔥',
conversationType: 'direct',
phoneNumber: '(202) 555-0012',
avatarPath: util.landscapePurpleObjectUrl,
lastUpdated: Date.now() - 5 * 60 * 1000,
lastMessage: {
text: "What's going on?",
status: 'sent',
},
},
},
{
type: 'contacts-header',
data: undefined,
},
{
type: 'contact',
data: {
name: 'The one Everyone',
conversationType: 'direct',
phoneNumber: '(202) 555-0013',
avatarPath: util.gifObjectUrl,
},
},
{
type: 'contact',
data: {
name: 'No likey everyone',
conversationType: 'direct',
phoneNumber: '(202) 555-0014',
color: 'red',
},
},
{
type: 'messages-header',
data: undefined,
},
];
const messages = [
{
from: {
isMe: true,
avatarPath: util.gifObjectUrl,
},
to: {
name: 'Mr. Fire 🔥',
phoneNumber: '(202) 555-0015',
},
id: '1-guid-guid-guid-guid-guid',
conversationId: '(202) 555-0015',
receivedAt: Date.now() - 5 * 60 * 1000,
snippet: '<<left>>Everyone<<right>>! Get in!',
conversationOpenInternal: () => console.log('onClick'),
},
{
from: {
name: 'Jon ❄️',
phoneNumber: '(202) 555-0016',
color: 'green',
},
to: {
isMe: true,
},
id: '2-guid-guid-guid-guid-guid',
conversationId: '(202) 555-0016',
snippet: 'Why is <<left>>everyone<<right>> so frustrated?',
receivedAt: Date.now() - 20 * 60 * 1000,
conversationOpenInternal: () => console.log('onClick'),
},
{
from: {
name: 'Someone',
phoneNumber: '(202) 555-0011',
color: 'green',
avatarPath: util.pngObjectUrl,
},
to: {
name: "Y'all 🌆",
},
id: '3-guid-guid-guid-guid-guid',
conversationId: 'EveryoneGroupID',
snippet: 'Hello, <<left>>everyone<<right>>! Woohooo!',
receivedAt: Date.now() - 24 * 60 * 1000,
conversationOpenInternal: () => console.log('onClick'),
},
{
from: {
isMe: true,
avatarPath: util.gifObjectUrl,
},
to: {
name: "Y'all 🌆",
},
id: '4-guid-guid-guid-guid-guid',
conversationId: 'EveryoneGroupID',
snippet: 'Well, <<left>>everyone<<right>>, happy new year!',
receivedAt: Date.now() - 24 * 60 * 1000,
conversationOpenInternal: () => console.log('onClick'),
},
];
const messageLookup = util._.fromPairs(
util._.map(messages, message => [message.id, message])
);
messages.forEach(message => {
items.push({
type: 'message',
data: message.id,
});
});
<util.LeftPaneContext
theme={util.theme}
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
>
<SearchResults
items={items}
i18n={util.i18n}
openConversationInternal={(...args) =>
console.log('openConversationInternal', args)
}
startNewConversation={(...args) =>
console.log('startNewConversation', args)
}
onStartNewConversation={(...args) =>
console.log('onStartNewConversation', args)
}
renderMessageSearchResult={id => (
<MessageSearchResult
{...messageLookup[id]}
i18n={util.i18n}
openConversationInternal={(...args) =>
console.log('openConversationInternal', args)
}
/>
)}
/>
</util.LeftPaneContext>;
```
#### With 'start new conversation'
```jsx
const items = [
{
type: 'start-new-conversation',
data: undefined,
},
{
type: 'conversations-header',
data: undefined,
},
{
type: 'conversation',
data: {
name: 'Everyone 🌆',
conversationType: 'group',
phoneNumber: '(202) 555-0011',
avatarPath: util.landscapeGreenObjectUrl,
lastUpdated: Date.now() - 5 * 60 * 1000,
lastMessage: {
text: 'The rabbit hopped silently in the night.',
status: 'sent',
},
},
},
{
type: 'conversation',
data: {
name: 'Everyone Else 🔥',
conversationType: 'direct',
phoneNumber: '(202) 555-0012',
avatarPath: util.landscapePurpleObjectUrl,
lastUpdated: Date.now() - 5 * 60 * 1000,
lastMessage: {
text: "What's going on?",
status: 'sent',
},
},
},
{
type: 'contacts-header',
data: undefined,
},
{
type: 'contact',
data: {
name: 'The one Everyone',
conversationType: 'direct',
phoneNumber: '(202) 555-0013',
avatarPath: util.gifObjectUrl,
},
},
{
type: 'contact',
data: {
name: 'No likey everyone',
conversationType: 'direct',
phoneNumber: '(202) 555-0014',
color: 'red',
},
},
{
type: 'messages-header',
data: undefined,
},
];
const messages = [
{
from: {
isMe: true,
avatarPath: util.gifObjectUrl,
},
to: {
name: 'Mr. Fire 🔥',
phoneNumber: '(202) 555-0015',
},
id: '1-guid-guid-guid-guid-guid',
conversationId: '(202) 555-0015',
receivedAt: Date.now() - 5 * 60 * 1000,
snippet: '<<left>>Everyone<<right>>! Get in!',
conversationOpenInternal: () => console.log('onClick'),
},
{
from: {
name: 'Jon ❄️',
phoneNumber: '(202) 555-0016',
color: 'green',
},
to: {
isMe: true,
},
id: '2-guid-guid-guid-guid-guid',
conversationId: '(202) 555-0016',
snippet: 'Why is <<left>>everyone<<right>> so frustrated?',
receivedAt: Date.now() - 20 * 60 * 1000,
conversationOpenInternal: () => console.log('onClick'),
},
{
from: {
name: 'Someone',
phoneNumber: '(202) 555-0011',
color: 'green',
avatarPath: util.pngObjectUrl,
},
to: {
name: "Y'all 🌆",
},
id: '3-guid-guid-guid-guid-guid',
conversationId: 'EveryoneGroupID',
snippet: 'Hello, <<left>>everyone<<right>>! Woohooo!',
receivedAt: Date.now() - 24 * 60 * 1000,
conversationOpenInternal: () => console.log('onClick'),
},
{
from: {
isMe: true,
avatarPath: util.gifObjectUrl,
},
to: {
name: "Y'all 🌆",
},
id: '4-guid-guid-guid-guid-guid',
conversationId: 'EveryoneGroupID',
snippet: 'Well, <<left>>everyone<<right>>, happy new year!',
receivedAt: Date.now() - 24 * 60 * 1000,
conversationOpenInternal: () => console.log('onClick'),
},
];
const messageLookup = util._.fromPairs(
util._.map(messages, message => [message.id, message])
);
messages.forEach(message => {
items.push({
type: 'message',
data: message.id,
});
});
<util.LeftPaneContext
theme={util.theme}
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
>
<SearchResults
items={items}
i18n={util.i18n}
searchTerm="(202) 555-0015"
openConversationInternal={(...args) =>
console.log('openConversationInternal', args)
}
startNewConversation={(...args) =>
console.log('startNewConversation', args)
}
onStartNewConversation={(...args) =>
console.log('onStartNewConversation', args)
}
renderMessageSearchResult={id => (
<MessageSearchResult
{...messageLookup[id]}
i18n={util.i18n}
openConversationInternal={(...args) =>
console.log('openConversationInternal', args)
}
/>
)}
/>
</util.LeftPaneContext>;
```
#### With no conversations
```jsx
const items = [
{
type: 'contacts-header',
data: undefined,
},
{
type: 'contact',
data: {
name: 'The one Everyone',
conversationType: 'direct',
phoneNumber: '(202) 555-0013',
avatarPath: util.gifObjectUrl,
},
},
{
type: 'contact',
data: {
name: 'No likey everyone',
conversationType: 'direct',
phoneNumber: '(202) 555-0014',
color: 'red',
},
},
{
type: 'messages-header',
data: undefined,
},
];
const messages = [
{
from: {
isMe: true,
avatarPath: util.gifObjectUrl,
},
to: {
name: 'Mr. Fire 🔥',
phoneNumber: '(202) 555-0015',
},
id: '1-guid-guid-guid-guid-guid',
conversationId: '(202) 555-0015',
receivedAt: Date.now() - 5 * 60 * 1000,
snippet: '<<left>>Everyone<<right>>! Get in!',
conversationOpenInternal: () => console.log('onClick'),
},
{
from: {
name: 'Jon ❄️',
phoneNumber: '(202) 555-0016',
color: 'green',
},
to: {
isMe: true,
},
id: '2-guid-guid-guid-guid-guid',
conversationId: '(202) 555-0016',
snippet: 'Why is <<left>>everyone<<right>> so frustrated?',
receivedAt: Date.now() - 20 * 60 * 1000,
conversationOpenInternal: () => console.log('onClick'),
},
{
from: {
name: 'Someone',
phoneNumber: '(202) 555-0011',
color: 'green',
avatarPath: util.pngObjectUrl,
},
to: {
name: "Y'all 🌆",
},
id: '3-guid-guid-guid-guid-guid',
conversationId: 'EveryoneGroupID',
snippet: 'Hello, <<left>>everyone<<right>>! Woohooo!',
receivedAt: Date.now() - 24 * 60 * 1000,
conversationOpenInternal: () => console.log('onClick'),
},
{
from: {
isMe: true,
avatarPath: util.gifObjectUrl,
},
to: {
name: "Y'all 🌆",
},
id: '4-guid-guid-guid-guid-guid',
conversationId: 'EveryoneGroupID',
snippet: 'Well, <<left>>everyone<<right>>, happy new year!',
receivedAt: Date.now() - 24 * 60 * 1000,
conversationOpenInternal: () => console.log('onClick'),
},
];
const messageLookup = util._.fromPairs(
util._.map(messages, message => [message.id, message])
);
messages.forEach(message => {
items.push({
type: 'message',
data: message.id,
});
});
<util.LeftPaneContext
theme={util.theme}
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
>
<SearchResults
items={items}
i18n={util.i18n}
openConversationInternal={(...args) =>
console.log('openConversationInternal', args)
}
startNewConversation={(...args) =>
console.log('startNewConversation', args)
}
onStartNewConversation={(...args) =>
console.log('onStartNewConversation', args)
}
renderMessageSearchResult={id => (
<MessageSearchResult
{...messageLookup[id]}
i18n={util.i18n}
openConversationInternal={(...args) =>
console.log('openConversationInternal', args)
}
/>
)}
/>
</util.LeftPaneContext>;
```
#### With no contacts
```jsx
const items = [
{
type: 'conversations-header',
data: undefined,
},
{
type: 'conversation',
data: {
name: 'Everyone 🌆',
conversationType: 'group',
phoneNumber: '(202) 555-0011',
avatarPath: util.landscapeGreenObjectUrl,
lastUpdated: Date.now() - 5 * 60 * 1000,
lastMessage: {
text: 'The rabbit hopped silently in the night.',
status: 'sent',
},
},
},
{
type: 'conversation',
data: {
name: 'Everyone Else 🔥',
conversationType: 'direct',
phoneNumber: '(202) 555-0012',
avatarPath: util.landscapePurpleObjectUrl,
lastUpdated: Date.now() - 5 * 60 * 1000,
lastMessage: {
text: "What's going on?",
status: 'sent',
},
},
},
{
type: 'messages-header',
data: undefined,
},
];
const messages = [
{
from: {
isMe: true,
avatarPath: util.gifObjectUrl,
},
to: {
name: 'Mr. Fire 🔥',
phoneNumber: '(202) 555-0015',
},
id: '1-guid-guid-guid-guid-guid',
conversationId: '(202) 555-0015',
receivedAt: Date.now() - 5 * 60 * 1000,
snippet: '<<left>>Everyone<<right>>! Get in!',
conversationOpenInternal: () => console.log('onClick'),
},
{
from: {
name: 'Jon ❄️',
phoneNumber: '(202) 555-0016',
color: 'green',
},
to: {
isMe: true,
},
id: '2-guid-guid-guid-guid-guid',
conversationId: '(202) 555-0016',
snippet: 'Why is <<left>>everyone<<right>> so frustrated?',
receivedAt: Date.now() - 20 * 60 * 1000,
conversationOpenInternal: () => console.log('onClick'),
},
{
from: {
name: 'Someone',
phoneNumber: '(202) 555-0011',
color: 'green',
avatarPath: util.pngObjectUrl,
},
to: {
name: "Y'all 🌆",
},
id: '3-guid-guid-guid-guid-guid',
conversationId: 'EveryoneGroupID',
snippet: 'Hello, <<left>>everyone<<right>>! Woohooo!',
receivedAt: Date.now() - 24 * 60 * 1000,
conversationOpenInternal: () => console.log('onClick'),
},
{
from: {
isMe: true,
avatarPath: util.gifObjectUrl,
},
to: {
name: "Y'all 🌆",
},
id: '4-guid-guid-guid-guid-guid',
conversationId: 'EveryoneGroupID',
snippet: 'Well, <<left>>everyone<<right>>, happy new year!',
receivedAt: Date.now() - 24 * 60 * 1000,
conversationOpenInternal: () => console.log('onClick'),
},
];
const messageLookup = util._.fromPairs(
util._.map(messages, message => [message.id, message])
);
messages.forEach(message => {
items.push({
type: 'message',
data: message.id,
});
});
<util.LeftPaneContext
theme={util.theme}
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
>
<SearchResults
items={items}
i18n={util.i18n}
openConversationInternal={(...args) =>
console.log('openConversationInternal', args)
}
startNewConversation={(...args) =>
console.log('startNewConversation', args)
}
onStartNewConversation={(...args) =>
console.log('onStartNewConversation', args)
}
renderMessageSearchResult={id => (
<MessageSearchResult
{...messageLookup[id]}
i18n={util.i18n}
openConversationInternal={(...args) =>
console.log('openConversationInternal', args)
}
/>
)}
/>
</util.LeftPaneContext>;
```
#### With no messages
```jsx
const items = [
{
type: 'conversations-header',
data: undefined,
},
{
type: 'conversation',
data: {
name: 'Everyone 🌆',
conversationType: 'group',
phoneNumber: '(202) 555-0011',
avatarPath: util.landscapeGreenObjectUrl,
lastUpdated: Date.now() - 5 * 60 * 1000,
lastMessage: {
text: 'The rabbit hopped silently in the night.',
status: 'sent',
},
},
},
{
type: 'conversation',
data: {
name: 'Everyone Else 🔥',
conversationType: 'direct',
phoneNumber: '(202) 555-0012',
avatarPath: util.landscapePurpleObjectUrl,
lastUpdated: Date.now() - 5 * 60 * 1000,
lastMessage: {
text: "What's going on?",
status: 'sent',
},
},
},
{
type: 'contacts-header',
data: undefined,
},
{
type: 'contact',
data: {
name: 'The one Everyone',
conversationType: 'direct',
phoneNumber: '(202) 555-0013',
avatarPath: util.gifObjectUrl,
},
},
{
type: 'contact',
data: {
name: 'No likey everyone',
conversationType: 'direct',
phoneNumber: '(202) 555-0014',
color: 'red',
},
},
];
<util.LeftPaneContext
theme={util.theme}
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
>
<SearchResults
items={items}
i18n={util.i18n}
openConversationInternal={(...args) =>
console.log('openConversationInternal', args)
}
startNewConversation={(...args) =>
console.log('startNewConversation', args)
}
onStartNewConversation={(...args) =>
console.log('onStartNewConversation', args)
}
/>
</util.LeftPaneContext>;
```
#### With no results at all
```jsx
<util.LeftPaneContext
theme={util.theme}
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
>
<SearchResults
items={[]}
noResults={true}
searchTerm="something"
i18n={util.i18n}
openConversationInternal={(...args) =>
console.log('openConversationInternal', args)
}
startNewConversation={(...args) =>
console.log('startNewConversation', args)
}
onStartNewConversation={(...args) =>
console.log('onStartNewConversation', args)
}
renderMessageSearchResult={id => (
<MessageSearchResult
{...messageLookup[id]}
i18n={util.i18n}
openConversationInternal={(...args) =>
console.log('openConversationInternal', args)
}
/>
)}
/>
</util.LeftPaneContext>
```
#### With no results at all, searching in conversation
```jsx
<util.LeftPaneContext
theme={util.theme}
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
>
<SearchResults
items={[]}
noResults={true}
searchTerm="something"
searchInConversationName="Everyone 🔥"
i18n={util.i18n}
openConversationInternal={(...args) =>
console.log('openConversationInternal', args)
}
startNewConversation={(...args) =>
console.log('startNewConversation', args)
}
onStartNewConversation={(...args) =>
console.log('onStartNewConversation', args)
}
renderMessageSearchResult={id => (
<MessageSearchResult
{...messageLookup[id]}
i18n={util.i18n}
openConversationInternal={(...args) =>
console.log('openConversationInternal', args)
}
/>
)}
/>
</util.LeftPaneContext>
```
#### Searching in conversation but no search term
```jsx
<util.LeftPaneContext
theme={util.theme}
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
>
<SearchResults
items={[]}
noResults={true}
searchTerm=""
searchInConversationName="Everyone 🔥"
i18n={util.i18n}
openConversationInternal={(...args) =>
console.log('openConversationInternal', args)
}
startNewConversation={(...args) =>
console.log('startNewConversation', args)
}
onStartNewConversation={(...args) =>
console.log('onStartNewConversation', args)
}
renderMessageSearchResult={id => (
<MessageSearchResult
{...messageLookup[id]}
i18n={util.i18n}
openConversationInternal={(...args) =>
console.log('openConversationInternal', args)
}
/>
)}
/>
</util.LeftPaneContext>
```
#### With a lot of results
```jsx
const items = [
{
type: 'conversations-header',
data: undefined,
},
{
type: 'conversation',
data: {
name: 'Everyone 🌆',
conversationType: 'group',
phoneNumber: '(202) 555-0011',
avatarPath: util.landscapeGreenObjectUrl,
lastUpdated: Date.now() - 5 * 60 * 1000,
lastMessage: {
text: 'The rabbit hopped silently in the night.',
status: 'sent',
},
},
},
{
type: 'conversation',
data: {
name: 'Everyone Else 🔥',
conversationType: 'direct',
phoneNumber: '(202) 555-0012',
avatarPath: util.landscapePurpleObjectUrl,
lastUpdated: Date.now() - 5 * 60 * 1000,
lastMessage: {
text: "What's going on?",
status: 'sent',
},
},
},
{
type: 'contacts-header',
data: undefined,
},
{
type: 'contact',
data: {
name: 'The one Everyone',
conversationType: 'direct',
phoneNumber: '(202) 555-0013',
avatarPath: util.gifObjectUrl,
},
},
{
type: 'contact',
data: {
name: 'No likey everyone',
conversationType: 'direct',
phoneNumber: '(202) 555-0014',
color: 'red',
},
},
{
type: 'messages-header',
data: undefined,
},
];
const messages = [];
for (let i = 0; i < 100; i += 1) {
messages.push({
from: {
name: 'Mr. Fire 🔥',
phoneNumber: '(202) 555-0015',
avatarPath: util.landscapeGreenObjectUrl,
},
to: {
isMe: true,
},
id: `${i}-guid-guid-guid-guid-guid`,
conversationId: '(202) 555-0015',
receivedAt: Date.now() - 5 * 60 * 1000,
snippet: `${i} <<left>>Everyone<<right>>! Get in!`,
conversationOpenInternal: data => console.log('onClick', data),
});
}
const messageLookup = util._.fromPairs(
util._.map(messages, message => [message.id, message])
);
messages.forEach(message => {
items.push({
type: 'message',
data: message.id,
});
});
<util.LeftPaneContext
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
theme={util.theme}
>
<SearchResults
items={items}
i18n={util.i18n}
openConversationInternal={(...args) =>
console.log('openConversationInternal', args)
}
startNewConversation={(...args) =>
console.log('startNewConversation', args)
}
onStartNewConversation={(...args) =>
console.log('onStartNewConversation', args)
}
renderMessageSearchResult={id => (
<MessageSearchResult
{...messageLookup[id]}
i18n={util.i18n}
openConversationInternal={(...args) =>
console.log('openConversationInternal', args)
}
/>
)}
/>
</util.LeftPaneContext>;
```
#### With just messages and no header
```jsx
const items = [];
const messages = [];
for (let i = 0; i < 10; i += 1) {
messages.push({
from: {
name: 'Mr. Fire 🔥',
phoneNumber: '(202) 555-0015',
avatarPath: util.landscapeGreenObjectUrl,
},
to: {
isMe: true,
},
id: `${i}-guid-guid-guid-guid-guid`,
conversationId: '(202) 555-0015',
receivedAt: Date.now() - 5 * 60 * 1000,
snippet: `${i} <<left>>Everyone<<right>>! Get in!`,
conversationOpenInternal: data => console.log('onClick', data),
});
}
const messageLookup = util._.fromPairs(
util._.map(messages, message => [message.id, message])
);
messages.forEach(message => {
items.push({
type: 'message',
data: message.id,
});
});
<util.LeftPaneContext
gutterStyle={{ height: '500px', display: 'flex', flexDirection: 'row' }}
theme={util.theme}
>
<SearchResults
items={items}
i18n={util.i18n}
openConversationInternal={(...args) =>
console.log('openConversationInternal', args)
}
startNewConversation={(...args) =>
console.log('startNewConversation', args)
}
onStartNewConversation={(...args) =>
console.log('onStartNewConversation', args)
}
renderMessageSearchResult={id => (
<MessageSearchResult
{...messageLookup[id]}
i18n={util.i18n}
openConversationInternal={(...args) =>
console.log('openConversationInternal', args)
}
/>
)}
/>
</util.LeftPaneContext>;
```

View file

@ -0,0 +1,423 @@
import * as React from 'react';
import { SearchResults } from './SearchResults';
import {
MessageSearchResult,
PropsDataType as MessageSearchResultPropsType,
} from './MessageSearchResult';
// @ts-ignore
import { setup as setupI18n } from '../../js/modules/i18n';
// @ts-ignore
import enMessages from '../../_locales/en/messages.json';
import { storiesOf } from '@storybook/react';
//import { boolean, select } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
// @ts-ignore
import gif from '../../fixtures/giphy-GVNvOUpeYmI7e.gif';
// @ts-ignore
import png from '../../fixtures/freepngs-2cd43b_bed7d1327e88454487397574d87b64dc_mv2.png';
// @ts-ignore
import landscapeGreen from '../../fixtures/1000x50-green.jpeg';
// @ts-ignore
import landscapePurple from '../../fixtures/200x50-purple.png';
const i18n = setupI18n('en', enMessages);
function makeObjectUrl(data: ArrayBuffer, contentType: string): string {
const blob = new Blob([data], {
type: contentType,
});
return URL.createObjectURL(blob);
}
// 320x240
const gifObjectUrl = makeObjectUrl(gif, 'image/gif');
// 800×1200
const pngObjectUrl = makeObjectUrl(png, 'image/png');
const landscapeGreenObjectUrl = makeObjectUrl(landscapeGreen, 'image/jpeg');
const landscapePurpleObjectUrl = makeObjectUrl(landscapePurple, 'image/png');
const messageLookup: Map<string, MessageSearchResultPropsType> = new Map();
const CONTACT = 'contact' as 'contact';
const CONTACTS_HEADER = 'contacts-header' as 'contacts-header';
const CONVERSATION = 'conversation' as 'conversation';
const CONVERSATIONS_HEADER = 'conversations-header' as 'conversations-header';
const DIRECT = 'direct' as 'direct';
const GROUP = 'group' as 'group';
const MESSAGE = 'message' as 'message';
const MESSAGES_HEADER = 'messages-header' as 'messages-header';
const SENT = 'sent' as 'sent';
const START_NEW_CONVERSATION = 'start-new-conversation' as 'start-new-conversation';
const SMS_MMS_NOT_SUPPORTED = 'sms-mms-not-supported-text' as 'sms-mms-not-supported-text';
// tslint:disable-next-line no-backbone-get-set-outside-model
messageLookup.set('1-guid-guid-guid-guid-guid', {
id: '1-guid-guid-guid-guid-guid',
conversationId: '(202) 555-0015',
sentAt: Date.now() - 5 * 60 * 1000,
snippet: '<<left>>Everyone<<right>>! Get in!',
from: {
phoneNumber: '(202) 555-0020',
isMe: true,
avatarPath: gifObjectUrl,
},
to: {
phoneNumber: '(202) 555-0015',
name: 'Mr. Fire 🔥',
},
});
// tslint:disable-next-line no-backbone-get-set-outside-model
messageLookup.set('2-guid-guid-guid-guid-guid', {
id: '2-guid-guid-guid-guid-guid',
conversationId: '(202) 555-0016',
sentAt: Date.now() - 20 * 60 * 1000,
snippet: 'Why is <<left>>everyone<<right>> so frustrated?',
from: {
phoneNumber: '(202) 555-0016',
name: 'Jon ❄️',
color: 'green',
},
to: {
phoneNumber: '(202) 555-0020',
isMe: true,
},
});
// tslint:disable-next-line no-backbone-get-set-outside-model
messageLookup.set('3-guid-guid-guid-guid-guid', {
id: '3-guid-guid-guid-guid-guid',
conversationId: 'EveryoneGroupID',
sentAt: Date.now() - 24 * 60 * 1000,
snippet: 'Hello, <<left>>everyone<<right>>! Woohooo!',
from: {
phoneNumber: '(202) 555-0011',
name: 'Someone',
color: 'green',
avatarPath: pngObjectUrl,
},
to: {
phoneNumber: '(202) 555-0016',
name: "Y'all 🌆",
},
});
// tslint:disable-next-line no-backbone-get-set-outside-model
messageLookup.set('4-guid-guid-guid-guid-guid', {
id: '4-guid-guid-guid-guid-guid',
conversationId: 'EveryoneGroupID',
sentAt: Date.now() - 24 * 60 * 1000,
snippet: 'Well, <<left>>everyone<<right>>, happy new year!',
from: {
phoneNumber: '(202) 555-0020',
isMe: true,
avatarPath: gifObjectUrl,
},
to: {
phoneNumber: '(202) 555-0016',
name: "Y'all 🌆",
},
});
const defaultProps = {
discussionsLoading: false,
items: [],
i18n,
messagesLoading: false,
noResults: false,
openConversationInternal: action('open-conversation-internal'),
regionCode: 'US',
renderMessageSearchResult(id: string): JSX.Element {
const messageProps = messageLookup.get(id) as MessageSearchResultPropsType;
return (
<MessageSearchResult
{...messageProps}
i18n={i18n}
openConversationInternal={action(
'MessageSearchResult-open-conversation-internal'
)}
/>
);
},
searchConversationName: undefined,
searchTerm: '1234567890',
selectedConversationId: undefined,
selectedMessageId: undefined,
startNewConversation: action('start-new-conversation'),
};
const conversations = [
{
type: CONVERSATION,
data: {
id: '+12025550011',
phoneNumber: '(202) 555-0011',
name: 'Everyone 🌆',
type: GROUP,
avatarPath: landscapeGreenObjectUrl,
isMe: false,
lastUpdated: Date.now() - 5 * 60 * 1000,
unreadCount: 0,
isSelected: false,
lastMessage: {
text: 'The rabbit hopped silently in the night.',
status: SENT,
},
},
},
{
type: CONVERSATION,
data: {
id: '+12025550012',
phoneNumber: '(202) 555-0012',
name: 'Everyone Else 🔥',
type: DIRECT,
avatarPath: landscapePurpleObjectUrl,
isMe: false,
lastUpdated: Date.now() - 5 * 60 * 1000,
unreadCount: 0,
isSelected: false,
lastMessage: {
text: "What's going on?",
status: SENT,
},
},
},
];
const contacts = [
{
type: CONTACT,
data: {
id: '+12025550013',
phoneNumber: '(202) 555-0013',
name: 'The one Everyone',
type: DIRECT,
avatarPath: gifObjectUrl,
isMe: false,
lastUpdated: Date.now() - 10 * 60 * 1000,
unreadCount: 0,
isSelected: false,
},
},
{
type: CONTACT,
data: {
id: '+12025550014',
phoneNumber: '(202) 555-0014',
name: 'No likey everyone',
type: DIRECT,
color: 'red',
isMe: false,
lastUpdated: Date.now() - 11 * 60 * 1000,
unreadCount: 0,
isSelected: false,
},
},
];
const messages = [
{
type: MESSAGE,
data: '1-guid-guid-guid-guid-guid',
},
{
type: MESSAGE,
data: '2-guid-guid-guid-guid-guid',
},
{
type: MESSAGE,
data: '3-guid-guid-guid-guid-guid',
},
{
type: MESSAGE,
data: '4-guid-guid-guid-guid-guid',
},
];
const messagesMany = Array.from(Array(100), (_, i) => messages[i % 4]);
const permutations = [
{
title: 'SMS/MMS Not Supported Text',
props: {
items: [
{
type: START_NEW_CONVERSATION,
data: undefined,
},
{
type: SMS_MMS_NOT_SUPPORTED,
data: undefined,
},
],
},
},
{
title: 'All Result Types',
props: {
items: [
{
type: CONVERSATIONS_HEADER,
data: undefined,
},
...conversations,
{
type: CONTACTS_HEADER,
data: undefined,
},
...contacts,
{
type: MESSAGES_HEADER,
data: undefined,
},
...messages,
],
},
},
{
title: 'Start new Conversation',
props: {
items: [
{
type: START_NEW_CONVERSATION,
data: undefined,
},
{
type: CONVERSATIONS_HEADER,
data: undefined,
},
...conversations,
{
type: CONTACTS_HEADER,
data: undefined,
},
...contacts,
{
type: MESSAGES_HEADER,
data: undefined,
},
...messages,
],
},
},
{
title: 'No Conversations',
props: {
items: [
{
type: CONTACTS_HEADER,
data: undefined,
},
...contacts,
{
type: MESSAGES_HEADER,
data: undefined,
},
...messages,
],
},
},
{
title: 'No Contacts',
props: {
items: [
{
type: CONVERSATIONS_HEADER,
data: undefined,
},
...conversations,
{
type: MESSAGES_HEADER,
data: undefined,
},
...messages,
],
},
},
{
title: 'No Messages',
props: {
items: [
{
type: CONVERSATIONS_HEADER,
data: undefined,
},
...conversations,
{
type: CONTACTS_HEADER,
data: undefined,
},
...contacts,
],
},
},
{
title: 'No Results',
props: {
noResults: true,
},
},
{
title: 'No Results, Searching in Conversation',
props: {
noResults: true,
searchInConversationName: 'Everyone 🔥',
searchTerm: 'something',
},
},
{
title: 'Searching in Conversation no search term',
props: {
noResults: true,
searchInConversationName: 'Everyone 🔥',
searchTerm: '',
},
},
{
title: 'Lots of results',
props: {
items: [
{
type: CONVERSATIONS_HEADER,
data: undefined,
},
...conversations,
{
type: CONTACTS_HEADER,
data: undefined,
},
...contacts,
{
type: MESSAGES_HEADER,
data: undefined,
},
...messagesMany,
],
},
},
{
title: 'Messages, no header',
props: {
items: messages,
},
},
];
storiesOf('Components/SearchResults', module).add('Iterations', () => {
return permutations.map(({ props, title }) => (
<>
<h3>{title}</h3>
<div className="module-left-pane">
<SearchResults {...defaultProps} {...props} />
</div>
<hr />
</>
));
});

View file

@ -35,6 +35,10 @@ type StartNewConversationType = {
type: 'start-new-conversation'; type: 'start-new-conversation';
data: undefined; data: undefined;
}; };
type NotSupportedSMS = {
type: 'sms-mms-not-supported-text';
data: undefined;
};
type ConversationHeaderType = { type ConversationHeaderType = {
type: 'conversations-header'; type: 'conversations-header';
data: undefined; data: undefined;
@ -66,6 +70,7 @@ type SpinnerType = {
export type SearchResultRowType = export type SearchResultRowType =
| StartNewConversationType | StartNewConversationType
| NotSupportedSMS
| ConversationHeaderType | ConversationHeaderType
| ContactsHeaderType | ContactsHeaderType
| MessagesHeaderType | MessagesHeaderType
@ -368,6 +373,12 @@ export class SearchResults extends React.Component<PropsType, StateType> {
onClick={this.handleStartNewConversation} onClick={this.handleStartNewConversation}
/> />
); );
} else if (row.type === 'sms-mms-not-supported-text') {
return (
<div className="module-search-results__sms-not-supported">
{i18n('notSupportedSMS')}
</div>
);
} else if (row.type === 'conversations-header') { } else if (row.type === 'conversations-header') {
return ( return (
<div <div

View file

@ -167,6 +167,10 @@ window.itemLookup = {
'Hello there from the new world! And this is multiple lines of text. Lines and lines and lines.', 'Hello there from the new world! And this is multiple lines of text. Lines and lines and lines.',
}, },
}, },
'id-15': {
type: 'linkNotification',
data: null,
},
}; };
window.actions = { window.actions = {

View file

@ -32,6 +32,10 @@ import {
} from './GroupNotification'; } from './GroupNotification';
import { ResetSessionNotification } from './ResetSessionNotification'; import { ResetSessionNotification } from './ResetSessionNotification';
type LinkNotificationType = {
type: 'linkNotification';
data: null;
};
type MessageType = { type MessageType = {
type: 'message'; type: 'message';
data: MessageProps; data: MessageProps;
@ -61,12 +65,13 @@ type ResetSessionNotificationType = {
data: null; data: null;
}; };
export type TimelineItemType = export type TimelineItemType =
| LinkNotificationType
| MessageType | MessageType
| UnsupportedMessageType
| TimerNotificationType
| SafetyNumberNotificationType
| VerificationNotificationType
| ResetSessionNotificationType | ResetSessionNotificationType
| SafetyNumberNotificationType
| TimerNotificationType
| UnsupportedMessageType
| VerificationNotificationType
| GroupNotificationType; | GroupNotificationType;
type PropsLocalType = { type PropsLocalType = {
@ -112,6 +117,13 @@ export class TimelineItem extends React.PureComponent<PropsType> {
notification = ( notification = (
<UnsupportedMessage {...this.props} {...item.data} i18n={i18n} /> <UnsupportedMessage {...this.props} {...item.data} i18n={i18n} />
); );
} else if (item.type === 'linkNotification') {
notification = (
<div className="module-message-unsynced">
<div className="module-message-unsynced__icon" />
{i18n('messageHistoryUnsynced')}
</div>
);
} else if (item.type === 'timerNotification') { } else if (item.type === 'timerNotification') {
notification = ( notification = (
<TimerNotification {...this.props} {...item.data} i18n={i18n} /> <TimerNotification {...this.props} {...item.data} i18n={i18n} />

View file

@ -5,6 +5,7 @@ type TextSecureType = {
user: { user: {
getNumber: () => string; getNumber: () => string;
}; };
get: (item: string) => any;
}; };
messaging: { messaging: {
sendStickerPackSync: ( sendStickerPackSync: (

View file

@ -27,6 +27,7 @@ export type ConversationType = {
isArchived: boolean; isArchived: boolean;
activeAt?: number; activeAt?: number;
timestamp: number; timestamp: number;
inboxPosition: number;
lastMessage?: { lastMessage?: {
status: 'error' | 'sending' | 'sent' | 'delivered' | 'read'; status: 'error' | 'sending' | 'sent' | 'delivered' | 'read';
text: string; text: string;
@ -56,7 +57,13 @@ export type MessageType = {
id: string; id: string;
conversationId: string; conversationId: string;
source: string; source: string;
type: 'incoming' | 'outgoing' | 'group' | 'keychange' | 'verified-change'; type:
| 'incoming'
| 'outgoing'
| 'group'
| 'keychange'
| 'verified-change'
| 'message-history-unsynced';
quote?: { author: string }; quote?: { author: string };
received_at: number; received_at: number;
hasSignalAccount?: boolean; hasSignalAccount?: boolean;

View file

@ -117,6 +117,21 @@ export const _getConversationComparator = (
return rightTimestamp - leftTimestamp; return rightTimestamp - leftTimestamp;
} }
if (
typeof left.inboxPosition === 'number' &&
typeof right.inboxPosition === 'number'
) {
return right.inboxPosition > left.inboxPosition ? -1 : 1;
}
if (typeof left.inboxPosition === 'number' && right.inboxPosition == null) {
return -1;
}
if (typeof right.inboxPosition === 'number' && left.inboxPosition == null) {
return 1;
}
const leftTitle = getConversationTitle(left, { const leftTitle = getConversationTitle(left, {
i18n, i18n,
ourRegionCode, ourRegionCode,

View file

@ -1,6 +1,7 @@
import memoizee from 'memoizee'; import memoizee from 'memoizee';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { getSearchResultsProps } from '../../shims/Whisper'; import { getSearchResultsProps } from '../../shims/Whisper';
import { instance } from '../../util/libphonenumberInstance';
import { StateType } from '../reducer'; import { StateType } from '../reducer';
@ -20,7 +21,7 @@ import {
} from '../../components/SearchResults'; } from '../../components/SearchResults';
import { PropsDataType as MessageSearchResultPropsDataType } from '../../components/MessageSearchResult'; import { PropsDataType as MessageSearchResultPropsDataType } from '../../components/MessageSearchResult';
import { getRegionCode, getUserNumber } from './user'; import { getRegionCode, getUserAgent, getUserNumber } from './user';
import { import {
GetConversationByIdType, GetConversationByIdType,
getConversationLookup, getConversationLookup,
@ -72,6 +73,7 @@ export const getSearchResults = createSelector(
[ [
getSearch, getSearch,
getRegionCode, getRegionCode,
getUserAgent,
getConversationLookup, getConversationLookup,
getSelectedConversation, getSelectedConversation,
getSelectedMessage, getSelectedMessage,
@ -79,6 +81,7 @@ export const getSearchResults = createSelector(
( (
state: SearchStateType, state: SearchStateType,
regionCode: string, regionCode: string,
userAgent: string,
lookup: ConversationLookupType, lookup: ConversationLookupType,
selectedConversationId?: string, selectedConversationId?: string,
selectedMessageId?: string selectedMessageId?: string
@ -114,6 +117,17 @@ export const getSearchResults = createSelector(
type: 'start-new-conversation', type: 'start-new-conversation',
data: undefined, data: undefined,
}); });
const isIOS = userAgent === 'OWI';
const parsedNumber = instance.parse(state.query, regionCode);
const isValidNumber = instance.isValidNumber(parsedNumber);
if (!isIOS && isValidNumber) {
items.push({
type: 'sms-mms-not-supported-text',
data: undefined,
});
}
} }
if (haveConversations) { if (haveConversations) {

View file

@ -4,9 +4,12 @@ import { LocalizerType } from '../../types/Util';
import { StateType } from '../reducer'; import { StateType } from '../reducer';
import { UserStateType } from '../ducks/user'; import { UserStateType } from '../ducks/user';
import { ItemsStateType } from '../ducks/items';
export const getUser = (state: StateType): UserStateType => state.user; export const getUser = (state: StateType): UserStateType => state.user;
export const getItems = (state: StateType): ItemsStateType => state.items;
export const getUserNumber = createSelector( export const getUserNumber = createSelector(
getUser, getUser,
(state: UserStateType): string => state.ourNumber (state: UserStateType): string => state.ourNumber
@ -27,6 +30,11 @@ export const getUserUuid = createSelector(
(state: UserStateType): string => state.ourUuid (state: UserStateType): string => state.ourUuid
); );
export const getUserAgent = createSelector(
getItems,
(state: ItemsStateType): string => state.userAgent
);
export const getIntl = createSelector( export const getIntl = createSelector(
getUser, getUser,
(state: UserStateType): LocalizerType => state.i18n (state: UserStateType): LocalizerType => state.i18n

View file

@ -17,6 +17,7 @@ describe('state/selectors/conversations', () => {
activeAt: Date.now(), activeAt: Date.now(),
name: 'No timestamp', name: 'No timestamp',
timestamp: 0, timestamp: 0,
inboxPosition: 0,
phoneNumber: 'notused', phoneNumber: 'notused',
isArchived: false, isArchived: false,
@ -36,6 +37,7 @@ describe('state/selectors/conversations', () => {
activeAt: Date.now(), activeAt: Date.now(),
name: 'B', name: 'B',
timestamp: 20, timestamp: 20,
inboxPosition: 21,
phoneNumber: 'notused', phoneNumber: 'notused',
isArchived: false, isArchived: false,
@ -55,6 +57,7 @@ describe('state/selectors/conversations', () => {
activeAt: Date.now(), activeAt: Date.now(),
name: 'C', name: 'C',
timestamp: 20, timestamp: 20,
inboxPosition: 22,
phoneNumber: 'notused', phoneNumber: 'notused',
isArchived: false, isArchived: false,
@ -74,6 +77,7 @@ describe('state/selectors/conversations', () => {
activeAt: Date.now(), activeAt: Date.now(),
name: 'Á', name: 'Á',
timestamp: 20, timestamp: 20,
inboxPosition: 20,
phoneNumber: 'notused', phoneNumber: 'notused',
isArchived: false, isArchived: false,
@ -93,6 +97,7 @@ describe('state/selectors/conversations', () => {
activeAt: Date.now(), activeAt: Date.now(),
name: 'First!', name: 'First!',
timestamp: 30, timestamp: 30,
inboxPosition: 30,
phoneNumber: 'notused', phoneNumber: 'notused',
isArchived: false, isArchived: false,

View file

@ -3,6 +3,7 @@ import { assert } from 'chai';
import * as Conversation from '../../types/Conversation'; import * as Conversation from '../../types/Conversation';
import { import {
IncomingMessage, IncomingMessage,
MessageHistoryUnsyncedMessage,
OutgoingMessage, OutgoingMessage,
VerifiedChangeMessage, VerifiedChangeMessage,
} from '../../types/Message'; } from '../../types/Message';
@ -44,6 +45,30 @@ describe('Conversation', () => {
assert.deepEqual(actual, expected); assert.deepEqual(actual, expected);
}); });
}); });
context('for message history unsynced message', () => {
it('should skip update', () => {
const input = {
currentTimestamp: 555,
lastMessage: {
type: 'message-history-unsynced',
conversationId: 'foo',
sent_at: 666,
timestamp: 666,
} as MessageHistoryUnsyncedMessage,
lastMessageNotificationText: 'xoxoxoxo',
};
const expected = {
lastMessage: 'xoxoxoxo',
lastMessageStatus: null,
timestamp: 555,
};
const actual = Conversation.createLastMessageUpdate(input);
assert.deepEqual(actual, expected);
});
});
context('for verified change message', () => { context('for verified change message', () => {
it('should skip update', () => { it('should skip update', () => {
const input = { const input = {

View file

@ -26,13 +26,16 @@ export const createLastMessageUpdate = ({
} }
const { type, expirationTimerUpdate } = lastMessage; const { type, expirationTimerUpdate } = lastMessage;
const isMessageHistoryUnsynced = type === 'message-history-unsynced';
const isVerifiedChangeMessage = type === 'verified-change'; const isVerifiedChangeMessage = type === 'verified-change';
const isExpireTimerUpdateFromSync = Boolean( const isExpireTimerUpdateFromSync = Boolean(
expirationTimerUpdate && expirationTimerUpdate.fromSync expirationTimerUpdate && expirationTimerUpdate.fromSync
); );
const shouldUpdateTimestamp = Boolean( const shouldUpdateTimestamp = Boolean(
!isVerifiedChangeMessage && !isExpireTimerUpdateFromSync !isMessageHistoryUnsynced &&
!isVerifiedChangeMessage &&
!isExpireTimerUpdateFromSync
); );
const newTimestamp = shouldUpdateTimestamp const newTimestamp = shouldUpdateTimestamp
? lastMessage.sent_at ? lastMessage.sent_at

View file

@ -2,7 +2,10 @@ import { Attachment } from './Attachment';
import { ContactType } from './Contact'; import { ContactType } from './Contact';
import { IndexableBoolean, IndexablePresence } from './IndexedDB'; import { IndexableBoolean, IndexablePresence } from './IndexedDB';
export type Message = UserMessage | VerifiedChangeMessage; export type Message =
| UserMessage
| VerifiedChangeMessage
| MessageHistoryUnsyncedMessage;
export type UserMessage = IncomingMessage | OutgoingMessage; export type UserMessage = IncomingMessage | OutgoingMessage;
export type IncomingMessage = Readonly< export type IncomingMessage = Readonly<
@ -65,6 +68,14 @@ export type VerifiedChangeMessage = Readonly<
ExpirationTimerUpdate ExpirationTimerUpdate
>; >;
export type MessageHistoryUnsyncedMessage = Readonly<
{
type: 'message-history-unsynced';
} & SharedMessageProperties &
MessageSchemaVersion5 &
ExpirationTimerUpdate
>;
type SharedMessageProperties = Readonly<{ type SharedMessageProperties = Readonly<{
conversationId: string; conversationId: string;
sent_at: number; sent_at: number;

View file

@ -16,6 +16,9 @@ export const initializeAttachmentMetadata = async (
if (message.type === 'verified-change') { if (message.type === 'verified-change') {
return message; return message;
} }
if (message.type === 'message-history-unsynced') {
return message;
}
if (message.messageTimer || message.isViewOnce) { if (message.messageTimer || message.isViewOnce) {
return message; return message;
} }

View file

@ -570,7 +570,7 @@
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/install_view.js", "path": "js/views/install_view.js",
"line": " this.$('#qr img').remove();", "line": " this.$('#qr img').remove();",
"lineNumber": 135, "lineNumber": 136,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
@ -579,7 +579,7 @@
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/install_view.js", "path": "js/views/install_view.js",
"line": " this.$('#qr .container').show();", "line": " this.$('#qr .container').show();",
"lineNumber": 137, "lineNumber": 138,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
@ -588,7 +588,7 @@
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/install_view.js", "path": "js/views/install_view.js",
"line": " if ($('#qr').length === 0) {", "line": " if ($('#qr').length === 0) {",
"lineNumber": 141, "lineNumber": 142,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
@ -597,25 +597,25 @@
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/install_view.js", "path": "js/views/install_view.js",
"line": " this.$('#qr .container').hide();", "line": " this.$('#qr .container').hide();",
"lineNumber": 146,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/install_view.js",
"line": " this.qr = new QRCode(this.$('#qr')[0]).makeCode(url);",
"lineNumber": 147, "lineNumber": 147,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
}, },
{
"rule": "jQuery-$(",
"path": "js/views/install_view.js",
"line": " this.qr = new QRCode(this.$('#qr')[0]).makeCode(url);",
"lineNumber": 148,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input"
},
{ {
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/install_view.js", "path": "js/views/install_view.js",
"line": " this.$('#qr').addClass('ready');", "line": " this.$('#qr').addClass('ready');",
"lineNumber": 149, "lineNumber": 150,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
@ -624,7 +624,7 @@
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/install_view.js", "path": "js/views/install_view.js",
"line": " this.$(DEVICE_NAME_SELECTOR).val(deviceName || window.getHostName());", "line": " this.$(DEVICE_NAME_SELECTOR).val(deviceName || window.getHostName());",
"lineNumber": 154, "lineNumber": 155,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
@ -633,7 +633,7 @@
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/install_view.js", "path": "js/views/install_view.js",
"line": " this.$('#link-phone').submit();", "line": " this.$('#link-phone').submit();",
"lineNumber": 159, "lineNumber": 160,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
@ -642,7 +642,7 @@
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/install_view.js", "path": "js/views/install_view.js",
"line": " this.$('#link-phone').submit(e => {", "line": " this.$('#link-phone').submit(e => {",
"lineNumber": 169, "lineNumber": 170,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
@ -651,7 +651,7 @@
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/install_view.js", "path": "js/views/install_view.js",
"line": " let name = this.$(DEVICE_NAME_SELECTOR).val();", "line": " let name = this.$(DEVICE_NAME_SELECTOR).val();",
"lineNumber": 173, "lineNumber": 174,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
@ -660,7 +660,7 @@
"rule": "jQuery-$(", "rule": "jQuery-$(",
"path": "js/views/install_view.js", "path": "js/views/install_view.js",
"line": " this.$(DEVICE_NAME_SELECTOR).focus();", "line": " this.$(DEVICE_NAME_SELECTOR).focus();",
"lineNumber": 176, "lineNumber": 177,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z", "updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input" "reasonDetail": "Protected from arbitrary input"
@ -11812,7 +11812,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "ts/shims/textsecure.ts", "path": "ts/shims/textsecure.ts",
"line": " wrap(", "line": " wrap(",
"lineNumber": 63, "lineNumber": 64,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-02-07T19:52:28.522Z" "updated": "2020-02-07T19:52:28.522Z"
} }