Message Requests improvements

This commit is contained in:
Scott Nonnenberg 2020-08-06 17:50:54 -07:00 committed by GitHub
parent b63291507a
commit 81cb7730a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 302 additions and 263 deletions

View file

@ -1764,6 +1764,10 @@
} }
}, },
"ConversationListItem--message-request": {
"message": "Message Request",
"description": "Preview shown for conversation if the user has not yet accepted an incoming message request"
},
"ConversationListItem--draft-prefix": { "ConversationListItem--draft-prefix": {
"message": "Draft:", "message": "Draft:",
"description": "Prefix shown in italic in conversation view when a draft is saved" "description": "Prefix shown in italic in conversation view when a draft is saved"

View file

@ -1885,6 +1885,11 @@
logger: window.log, logger: window.log,
}); });
// Force a re-fetch here when we've processed our queue. Without this, we won't try
// again for two hours after our first attempt. Which might have been while we were
// offline or didn't have credentials.
window.Signal.RemoteConfig.refreshRemoteConfig();
let interval = setInterval(() => { let interval = setInterval(() => {
const view = window.owsDesktopApp.appView; const view = window.owsDesktopApp.appView;
if (view) { if (view) {

View file

@ -28,7 +28,7 @@
}; };
const { Util } = window.Signal; const { Util } = window.Signal;
const { Conversation, Contact, Message } = window.Signal.Types; const { Contact, Message } = window.Signal.Types;
const { const {
deleteAttachmentData, deleteAttachmentData,
doesAttachmentExist, doesAttachmentExist,
@ -134,12 +134,6 @@
this.updateLastMessage.bind(this), this.updateLastMessage.bind(this),
200 200
); );
this.throttledUpdateSharedGroups =
this.throttledUpdateSharedGroups ||
_.throttle(
this.updateSharedGroups.bind(this),
1000 * 60 * 5 // five minutes
);
this.listenTo( this.listenTo(
this.messageCollection, this.messageCollection,
@ -175,10 +169,11 @@
this.typingPauseTimer = null; this.typingPauseTimer = null;
// Keep props ready // Keep props ready
this.generateProps = () => { const generateProps = () => {
this.cachedProps = this.getProps(); this.cachedProps = this.getProps();
}; };
this.on('change', this.generateProps); this.on('change', generateProps);
generateProps();
}, },
isMe() { isMe() {
@ -451,8 +446,6 @@
getProps() { getProps() {
const color = this.getColor(); const color = this.getColor();
this.throttledUpdateSharedGroups();
const typingValues = _.values(this.contactTypingTimers || {}); const typingValues = _.values(this.contactTypingTimers || {});
const typingMostRecent = _.first(_.sortBy(typingValues, 'timestamp')); const typingMostRecent = _.first(_.sortBy(typingValues, 'timestamp'));
const typingContact = typingMostRecent const typingContact = typingMostRecent
@ -575,15 +568,16 @@
async handleReadAndDownloadAttachments() { async handleReadAndDownloadAttachments() {
let messages; let messages;
do { do {
const first = messages ? messages.first() : null;
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
messages = await window.Signal.Data.getOlderMessagesByConversation( messages = await window.Signal.Data.getOlderMessagesByConversation(
this.get('id'), this.get('id'),
{ {
MessageCollection: Whisper.MessageCollection, MessageCollection: Whisper.MessageCollection,
limit: 100, limit: 100,
receivedAt: messages receivedAt: first ? first.get('received_at') : null,
? messages.first().get('received_at') messageId: first ? first.id : null,
: undefined,
} }
); );
@ -959,13 +953,18 @@
(this.get('messageCountBeforeMessageRequests') || 0) > 0; (this.get('messageCountBeforeMessageRequests') || 0) > 0;
const hasNoMessages = (this.get('messageCount') || 0) === 0; const hasNoMessages = (this.get('messageCount') || 0) === 0;
const isEmptyPrivateConvo = hasNoMessages && this.isPrivate();
const isEmptyWhitelistedGroup =
hasNoMessages && !this.isPrivate() && this.get('profileSharing');
return ( return (
isFromOrAddedByTrustedContact || isFromOrAddedByTrustedContact ||
hasSentMessages || hasSentMessages ||
hasMessagesBeforeMessageRequests || hasMessagesBeforeMessageRequests ||
// an empty conversation is the scenario where we need to rely on // an empty group is the scenario where we need to rely on
// whether the profile has already been shared or not // whether the profile has already been shared or not
(hasNoMessages && this.get('profileSharing')) isEmptyPrivateConvo ||
isEmptyWhitelistedGroup
); );
}, },
@ -1868,37 +1867,40 @@
return; return;
} }
const messages = await window.Signal.Data.getOlderMessagesByConversation( const [previewMessage, activityMessage] = await Promise.all([
this.id, window.Signal.Data.getLastConversationPreview(this.id, {
{ limit: 1, MessageCollection: Whisper.MessageCollection } Message: Whisper.Message,
); }),
window.Signal.Data.getLastConversationActivity(this.id, {
Message: Whisper.Message,
}),
]);
// This is the less-restrictive of these two fetches; if it's falsey, both will be
if (!previewMessage) {
return;
}
const lastMessageModel = messages.at(0);
if ( if (
this.hasDraft() && this.hasDraft() &&
this.get('draftTimestamp') && this.get('draftTimestamp') &&
(!lastMessageModel || previewMessage.get('sent_at') < this.get('draftTimestamp')
lastMessageModel.get('sent_at') < this.get('draftTimestamp'))
) { ) {
return; return;
} }
const lastMessageJSON = lastMessageModel const currentTimestamp = this.get('timestamp') || null;
? lastMessageModel.toJSON() const timestamp = activityMessage
: null; ? activityMessage.sent_at || currentTimestamp
const lastMessageStatusModel = lastMessageModel : currentTimestamp;
? lastMessageModel.getMessagePropStatus()
: null; this.set({
const lastMessageUpdate = Conversation.createLastMessageUpdate({ lastMessage: previewMessage.getNotificationText() || '',
currentTimestamp: this.get('timestamp') || null, lastMessageStatus: previewMessage.getMessagePropStatus() || null,
lastMessage: lastMessageJSON, timestamp,
lastMessageStatus: lastMessageStatusModel, lastMessageDeletedForEveryone: previewMessage.deletedForEveryone,
lastMessageNotificationText: lastMessageModel
? lastMessageModel.getNotificationText()
: null,
}); });
this.set(lastMessageUpdate);
window.Signal.Data.updateConversation(this.attributes); window.Signal.Data.updateConversation(this.attributes);
}, },

View file

@ -1,7 +1,6 @@
/* global crypto, window */ /* global crypto, window */
const { isFunction, isNumber } = require('lodash'); const { isFunction, isNumber } = require('lodash');
const { createLastMessageUpdate } = require('../../../ts/types/Conversation');
const { const {
arrayBufferToBase64, arrayBufferToBase64,
base64ToArrayBuffer, base64ToArrayBuffer,
@ -161,7 +160,7 @@ module.exports = {
arrayBufferToBase64, arrayBufferToBase64,
base64ToArrayBuffer, base64ToArrayBuffer,
computeHash, computeHash,
createLastMessageUpdate,
deleteExternalFiles, deleteExternalFiles,
maybeUpdateAvatar, maybeUpdateAvatar,
maybeUpdateProfileAvatar, maybeUpdateProfileAvatar,

View file

@ -16,6 +16,8 @@
(function() { (function() {
'use strict'; 'use strict';
const FIVE_MINUTES = 1000 * 60 * 5;
window.Whisper = window.Whisper || {}; window.Whisper = window.Whisper || {};
const { Message, MIME, VisualAttachment } = window.Signal.Types; const { Message, MIME, VisualAttachment } = window.Signal.Types;
const { const {
@ -304,9 +306,12 @@
); );
this.model.throttledGetProfiles = this.model.throttledGetProfiles =
this.model.throttledGetProfiles || this.model.throttledGetProfiles ||
_.throttle(this.model.getProfiles.bind(this.model), FIVE_MINUTES);
this.model.throttledUpdateSharedGroups =
this.model.throttledUpdateSharedGroups ||
_.throttle( _.throttle(
this.model.getProfiles.bind(this.model), this.model.updateSharedGroups.bind(this.model),
1000 * 60 * 5 // five minutes FIVE_MINUTES
); );
this.debouncedMaybeGrabLinkPreview = _.debounce( this.debouncedMaybeGrabLinkPreview = _.debounce(
this.maybeGrabLinkPreview.bind(this), this.maybeGrabLinkPreview.bind(this),
@ -720,6 +725,7 @@
showVisualAttachment, showVisualAttachment,
showExpiredIncomingTapToViewToast, showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast, showExpiredOutgoingTapToViewToast,
updateSharedGroups: this.model.throttledUpdateSharedGroups,
}), }),
}); });

View file

@ -3750,6 +3750,17 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
align-items: center; align-items: center;
} }
.module-conversation-list-item__message-request {
@include font-body-2-bold;
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
}
.module-conversation-list-item__message__text { .module-conversation-list-item__message__text {
flex-grow: 1; flex-grow: 1;
flex-shrink: 1; flex-shrink: 1;

View file

@ -636,8 +636,6 @@ export class ConversationController {
await Promise.all( await Promise.all(
this._conversations.map(async conversation => { this._conversations.map(async conversation => {
conversation.generateProps();
if (!conversation.get('lastMessage')) { if (!conversation.get('lastMessage')) {
await conversation.updateLastMessage(); await conversation.updateLastMessage();
} }

View file

@ -42,7 +42,7 @@ export function onChange(key: ConfigKeyType, fn: ConfigListenerType) {
}; };
} }
const refreshRemoteConfig = async () => { export const refreshRemoteConfig = async () => {
const now = Date.now(); const now = Date.now();
const server = getServer(); const server = getServer();
const newConfig = await server.getConfig(); const newConfig = await server.getConfig();

View file

@ -4,6 +4,8 @@
<util.LeftPaneContext theme={util.theme}> <util.LeftPaneContext theme={util.theme}>
<ConversationListItem <ConversationListItem
id="conversationId1" id="conversationId1"
isAccepted
title="Someone 🔥 Somewhere"
name="Someone 🔥 Somewhere" name="Someone 🔥 Somewhere"
type={'direct'} type={'direct'}
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
@ -25,8 +27,10 @@
<util.LeftPaneContext theme={util.theme}> <util.LeftPaneContext theme={util.theme}>
<ConversationListItem <ConversationListItem
id="conversationId1" id="conversationId1"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
type={'direct'} type={'direct'}
title="Mr. Fire🔥"
name="Mr. Fire🔥" name="Mr. Fire🔥"
color="green" color="green"
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
@ -46,9 +50,11 @@
<util.LeftPaneContext theme={util.theme}> <util.LeftPaneContext theme={util.theme}>
<ConversationListItem <ConversationListItem
id="conversationId1" id="conversationId1"
isAccepted
isMe={true} isMe={true}
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
type={'direct'} type={'direct'}
title="Mr. Fire🔥"
name="Mr. Fire🔥" name="Mr. Fire🔥"
color="green" color="green"
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
@ -69,8 +75,10 @@
<div> <div>
<ConversationListItem <ConversationListItem
id="conversationId1" id="conversationId1"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
type={'direct'} type={'direct'}
title="Mr. Fire🔥"
name="Mr. Fire🔥" name="Mr. Fire🔥"
color="green" color="green"
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
@ -83,8 +91,10 @@
/> />
<ConversationListItem <ConversationListItem
id="conversationId2" id="conversationId2"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
type={'direct'} type={'direct'}
title="Mr. Fire🔥"
name="Mr. Fire🔥" name="Mr. Fire🔥"
color="green" color="green"
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
@ -97,8 +107,10 @@
/> />
<ConversationListItem <ConversationListItem
id="conversationId3" id="conversationId3"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
type={'direct'} type={'direct'}
title="Mr. Fire🔥"
name="Mr. Fire🔥" name="Mr. Fire🔥"
color="green" color="green"
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
@ -111,8 +123,10 @@
/> />
<ConversationListItem <ConversationListItem
id="conversationId4" id="conversationId4"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
type={'direct'} type={'direct'}
title="Mr. Fire🔥"
name="Mr. Fire🔥" name="Mr. Fire🔥"
color="green" color="green"
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
@ -125,8 +139,10 @@
/> />
<ConversationListItem <ConversationListItem
id="conversationId5" id="conversationId5"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
type={'direct'} type={'direct'}
title="Mr. Fire🔥"
name="Mr. Fire🔥" name="Mr. Fire🔥"
color="green" color="green"
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
@ -148,7 +164,9 @@
<div> <div>
<ConversationListItem <ConversationListItem
id="conversationId1" id="conversationId1"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'} type={'direct'}
unreadCount={4} unreadCount={4}
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
@ -162,7 +180,51 @@
<div> <div>
<ConversationListItem <ConversationListItem
id="conversationId2" id="conversationId2"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'}
unreadCount={4}
lastUpdated={Date.now() - 5 * 60 * 1000}
typingContact={{
name: 'Someone Here',
}}
lastMessage={{
status: 'read',
}}
onClick={result => console.log('onClick', result)}
i18n={util.i18n}
/>
</div>
</util.LeftPaneContext>
```
#### Message Request
```jsx
<util.LeftPaneContext theme={util.theme}>
<div>
<ConversationListItem
id="conversationId1"
isAccepted={false}
phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'}
unreadCount={4}
lastUpdated={Date.now() - 5 * 60 * 1000}
typingContact={{
name: 'Someone Here',
}}
onClick={result => console.log('onClick', result)}
i18n={util.i18n}
/>
</div>
<div>
<ConversationListItem
id="conversationId2"
isAccepted={false}
phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'} type={'direct'}
unreadCount={4} unreadCount={4}
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
@ -188,7 +250,9 @@
<div> <div>
<ConversationListItem <ConversationListItem
id="conversationId1" id="conversationId1"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'} type={'direct'}
unreadCount={4} unreadCount={4}
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
@ -200,7 +264,9 @@
/> />
<ConversationListItem <ConversationListItem
id="conversationId2" id="conversationId2"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'} type={'direct'}
unreadCount={10} unreadCount={10}
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
@ -212,7 +278,9 @@
/> />
<ConversationListItem <ConversationListItem
id="conversationId3" id="conversationId3"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'} type={'direct'}
unreadCount={250} unreadCount={250}
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
@ -232,7 +300,9 @@
<util.LeftPaneContext theme={util.theme}> <util.LeftPaneContext theme={util.theme}>
<ConversationListItem <ConversationListItem
id="conversationId1" id="conversationId1"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'} type={'direct'}
isSelected={true} isSelected={true}
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
@ -254,7 +324,9 @@ We don't want Jumbomoji or links.
<div> <div>
<ConversationListItem <ConversationListItem
id="conversationId1" id="conversationId1"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'} type={'direct'}
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{ lastMessage={{
@ -265,7 +337,9 @@ We don't want Jumbomoji or links.
/> />
<ConversationListItem <ConversationListItem
id="conversationId2" id="conversationId2"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'} type={'direct'}
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{ lastMessage={{
@ -287,7 +361,9 @@ We only show one line.
<div> <div>
<ConversationListItem <ConversationListItem
id="conversationId1" id="conversationId1"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'} type={'direct'}
name="Long contact name. Esquire. The third. And stuff. And more! And more!" name="Long contact name. Esquire. The third. And stuff. And more! And more!"
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
@ -299,7 +375,9 @@ We only show one line.
/> />
<ConversationListItem <ConversationListItem
id="conversationId2" id="conversationId2"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'} type={'direct'}
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{ lastMessage={{
@ -311,7 +389,9 @@ We only show one line.
/> />
<ConversationListItem <ConversationListItem
id="conversationId3" id="conversationId3"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'} type={'direct'}
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{ lastMessage={{
@ -325,7 +405,9 @@ We only show one line.
<ConversationListItem <ConversationListItem
id="conversationId4" id="conversationId4"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'} type={'direct'}
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
unreadCount={8} unreadCount={8}
@ -338,7 +420,9 @@ We only show one line.
/> />
<ConversationListItem <ConversationListItem
id="conversationId5" id="conversationId5"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'} type={'direct'}
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{ lastMessage={{
@ -350,7 +434,9 @@ We only show one line.
/> />
<ConversationListItem <ConversationListItem
id="conversationId6" id="conversationId6"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'} type={'direct'}
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{ lastMessage={{
@ -374,7 +460,9 @@ On platforms that show scrollbars all the time, this is true all the time.
<div style={{ width: '280px' }}> <div style={{ width: '280px' }}>
<ConversationListItem <ConversationListItem
id="conversationId1" id="conversationId1"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'} type={'direct'}
name="Long contact name. Esquire. The third. And stuff. And more! And more!" name="Long contact name. Esquire. The third. And stuff. And more! And more!"
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
@ -386,7 +474,9 @@ On platforms that show scrollbars all the time, this is true all the time.
/> />
<ConversationListItem <ConversationListItem
id="conversationId2" id="conversationId2"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'} type={'direct'}
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{ lastMessage={{
@ -407,7 +497,9 @@ On platforms that show scrollbars all the time, this is true all the time.
<div> <div>
<ConversationListItem <ConversationListItem
id="conversationId1" id="conversationId1"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'} type={'direct'}
lastUpdated={Date.now() - 5 * 60 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 60 * 1000}
lastMessage={{ lastMessage={{
@ -418,7 +510,9 @@ On platforms that show scrollbars all the time, this is true all the time.
/> />
<ConversationListItem <ConversationListItem
id="conversationId2" id="conversationId2"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'} type={'direct'}
lastUpdated={Date.now() - 24 * 60 * 60 * 1000} lastUpdated={Date.now() - 24 * 60 * 60 * 1000}
lastMessage={{ lastMessage={{
@ -429,7 +523,9 @@ On platforms that show scrollbars all the time, this is true all the time.
/> />
<ConversationListItem <ConversationListItem
id="conversationId3" id="conversationId3"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'} type={'direct'}
lastUpdated={Date.now() - 7 * 24 * 60 * 60 * 1000} lastUpdated={Date.now() - 7 * 24 * 60 * 60 * 1000}
lastMessage={{ lastMessage={{
@ -440,7 +536,9 @@ On platforms that show scrollbars all the time, this is true all the time.
/> />
<ConversationListItem <ConversationListItem
id="conversationId4" id="conversationId4"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'} type={'direct'}
lastUpdated={Date.now() - 365 * 24 * 60 * 60 * 1000} lastUpdated={Date.now() - 365 * 24 * 60 * 60 * 1000}
lastMessage={{ lastMessage={{
@ -460,7 +558,9 @@ On platforms that show scrollbars all the time, this is true all the time.
<div> <div>
<ConversationListItem <ConversationListItem
id="conversationId1" id="conversationId1"
isAccepted
name="John" name="John"
title="John"
type={'direct'} type={'direct'}
lastUpdated={null} lastUpdated={null}
lastMessage={{ lastMessage={{
@ -471,7 +571,9 @@ On platforms that show scrollbars all the time, this is true all the time.
/> />
<ConversationListItem <ConversationListItem
id="conversationId2" id="conversationId2"
isAccepted
name="Missing message" name="Missing message"
title="Missing message"
type={'direct'} type={'direct'}
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{ lastMessage={{
@ -482,7 +584,9 @@ On platforms that show scrollbars all the time, this is true all the time.
/> />
<ConversationListItem <ConversationListItem
id="conversationId3" id="conversationId3"
isAccepted
phoneNumber="(202) 555-0011" phoneNumber="(202) 555-0011"
title="(202) 555-0011"
type={'direct'} type={'direct'}
lastUpdated={Date.now() - 5 * 60 * 1000} lastUpdated={Date.now() - 5 * 60 * 1000}
lastMessage={{ lastMessage={{

View file

@ -26,6 +26,7 @@ export type PropsData = {
unreadCount?: number; unreadCount?: number;
isSelected: boolean; isSelected: boolean;
isAccepted?: boolean;
draftPreview?: string; draftPreview?: string;
shouldShowDraft?: boolean; shouldShowDraft?: boolean;
@ -152,6 +153,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
const { const {
draftPreview, draftPreview,
i18n, i18n,
isAccepted,
lastMessage, lastMessage,
shouldShowDraft, shouldShowDraft,
typingContact, typingContact,
@ -187,7 +189,11 @@ export class ConversationListItem extends React.PureComponent<Props> {
: null : null
)} )}
> >
{typingContact ? ( {!isAccepted ? (
<span className="module-conversation-list-item__message-request">
{i18n('ConversationListItem--message-request')}
</span>
) : typingContact ? (
<TypingAnimation i18n={i18n} /> <TypingAnimation i18n={i18n} />
) : ( ) : (
<> <>

View file

@ -13,6 +13,7 @@ export type Props = {
membersCount?: number; membersCount?: number;
phoneNumber?: string; phoneNumber?: string;
onHeightChange?: () => unknown; onHeightChange?: () => unknown;
updateSharedGroups?: () => unknown;
} & Omit<AvatarProps, 'onClick' | 'size' | 'noteToSelf'>; } & Omit<AvatarProps, 'onClick' | 'size' | 'noteToSelf'>;
const renderMembershipRow = ({ const renderMembershipRow = ({
@ -113,6 +114,7 @@ export const ConversationHero = ({
profileName, profileName,
title, title,
onHeightChange, onHeightChange,
updateSharedGroups,
}: Props) => { }: Props) => {
const firstRenderRef = React.useRef(true); const firstRenderRef = React.useRef(true);
@ -121,6 +123,11 @@ export const ConversationHero = ({
// component may have changed. The cleanup function notifies listeners of // component may have changed. The cleanup function notifies listeners of
// any potential height changes. // any potential height changes.
return () => { return () => {
// Kick off the expensive hydration of the current sharedGroupNames
if (updateSharedGroups) {
updateSharedGroups();
}
if (onHeightChange && !firstRenderRef.current) { if (onHeightChange && !firstRenderRef.current) {
onHeightChange(); onHeightChange();
} else { } else {
@ -135,7 +142,7 @@ export const ConversationHero = ({
`mc-${membersCount}`, `mc-${membersCount}`,
`n-${name}`, `n-${name}`,
`pn-${profileName}`, `pn-${profileName}`,
...sharedGroupNames.map(g => `g-${g}`), sharedGroupNames.map(g => `g-${g}`).join(' '),
]); ]);
const phoneNumberOnly = Boolean( const phoneNumberOnly = Boolean(

View file

@ -50,7 +50,11 @@ type PropsHousekeepingType = {
actions: Object actions: Object
) => JSX.Element; ) => JSX.Element;
renderLastSeenIndicator: (id: string) => JSX.Element; renderLastSeenIndicator: (id: string) => JSX.Element;
renderHeroRow: (id: string, resizeHeroRow: () => unknown) => JSX.Element; renderHeroRow: (
id: string,
resizeHeroRow: () => unknown,
updateSharedGroups: () => unknown
) => JSX.Element;
renderLoadingRow: (id: string) => JSX.Element; renderLoadingRow: (id: string) => JSX.Element;
renderTypingBubble: (id: string) => JSX.Element; renderTypingBubble: (id: string) => JSX.Element;
}; };
@ -70,6 +74,7 @@ type PropsActionsType = {
markMessageRead: (messageId: string) => unknown; markMessageRead: (messageId: string) => unknown;
selectMessage: (messageId: string, conversationId: string) => unknown; selectMessage: (messageId: string, conversationId: string) => unknown;
clearSelectedMessage: () => unknown; clearSelectedMessage: () => unknown;
updateSharedGroups: () => unknown;
} & MessageActionsType & } & MessageActionsType &
SafetyNumberActionsType; SafetyNumberActionsType;
@ -510,6 +515,7 @@ export class Timeline extends React.PureComponent<Props, State> {
renderLoadingRow, renderLoadingRow,
renderLastSeenIndicator, renderLastSeenIndicator,
renderTypingBubble, renderTypingBubble,
updateSharedGroups,
} = this.props; } = this.props;
const styleWithWidth = { const styleWithWidth = {
@ -524,7 +530,7 @@ export class Timeline extends React.PureComponent<Props, State> {
if (haveOldest && row === 0) { if (haveOldest && row === 0) {
rowContents = ( rowContents = (
<div data-row={row} style={styleWithWidth} role="row"> <div data-row={row} style={styleWithWidth} role="row">
{renderHeroRow(id, this.resizeHeroRow)} {renderHeroRow(id, this.resizeHeroRow, updateSharedGroups)}
</div> </div>
); );
} else if (!haveOldest && row === 0) { } else if (!haveOldest && row === 0) {

1
ts/model-types.d.ts vendored
View file

@ -85,7 +85,6 @@ declare class ConversationModelType extends Backbone.Model<
cleanup(): Promise<void>; cleanup(): Promise<void>;
disableProfileSharing(): void; disableProfileSharing(): void;
dropProfileKey(): Promise<void>; dropProfileKey(): Promise<void>;
generateProps(): void;
getAccepted(): boolean; getAccepted(): boolean;
getAvatarPath(): string | undefined; getAvatarPath(): string | undefined;
getColor(): ColorType | undefined; getColor(): ColorType | undefined;

View file

@ -156,6 +156,8 @@ const dataInterface: ClientInterface = {
getTapToViewMessagesNeedingErase, getTapToViewMessagesNeedingErase,
getOlderMessagesByConversation, getOlderMessagesByConversation,
getNewerMessagesByConversation, getNewerMessagesByConversation,
getLastConversationActivity,
getLastConversationPreview,
getMessageMetricsForConversation, getMessageMetricsForConversation,
migrateConversationMessages, migrateConversationMessages,
@ -1022,6 +1024,32 @@ async function getNewerMessagesByConversation(
return new MessageCollection(handleMessageJSON(messages)); return new MessageCollection(handleMessageJSON(messages));
} }
async function getLastConversationActivity(
conversationId: string,
options: {
Message: typeof MessageModelType;
}
): Promise<MessageModelType | undefined> {
const { Message } = options;
const result = await channels.getLastConversationActivity(conversationId);
if (result) {
return new Message(result);
}
return;
}
async function getLastConversationPreview(
conversationId: string,
options: {
Message: typeof MessageModelType;
}
): Promise<MessageModelType | undefined> {
const { Message } = options;
const result = await channels.getLastConversationPreview(conversationId);
if (result) {
return new Message(result);
}
return;
}
async function getMessageMetricsForConversation(conversationId: string) { async function getMessageMetricsForConversation(conversationId: string) {
const result = await channels.getMessageMetricsForConversation( const result = await channels.getMessageMetricsForConversation(
conversationId conversationId

View file

@ -210,6 +210,12 @@ export type ServerInterface = DataInterface & {
conversationId: string, conversationId: string,
options?: { limit?: number; receivedAt?: number } options?: { limit?: number; receivedAt?: number }
) => Promise<Array<MessageTypeUnhydrated>>; ) => Promise<Array<MessageTypeUnhydrated>>;
getLastConversationActivity: (
conversationId: string
) => Promise<MessageType | undefined>;
getLastConversationPreview: (
conversationId: string
) => Promise<MessageType | undefined>;
getNextExpiringMessage: () => Promise<MessageType>; getNextExpiringMessage: () => Promise<MessageType>;
getNextTapToViewMessageToAgeOut: () => Promise<MessageType>; getNextTapToViewMessageToAgeOut: () => Promise<MessageType>;
getOutgoingWithoutExpiresAt: () => Promise<Array<MessageType>>; getOutgoingWithoutExpiresAt: () => Promise<Array<MessageType>>;
@ -308,6 +314,18 @@ export type ClientInterface = DataInterface & {
MessageCollection: typeof MessageModelCollectionType; MessageCollection: typeof MessageModelCollectionType;
} }
) => Promise<MessageModelCollectionType>; ) => Promise<MessageModelCollectionType>;
getLastConversationActivity: (
conversationId: string,
options: {
Message: typeof MessageModelType;
}
) => Promise<MessageModelType | undefined>;
getLastConversationPreview: (
conversationId: string,
options: {
Message: typeof MessageModelType;
}
) => Promise<MessageModelType | undefined>;
getNextExpiringMessage: ({ getNextExpiringMessage: ({
Message, Message,
}: { }: {

View file

@ -132,6 +132,8 @@ const dataInterface: ServerInterface = {
getOlderMessagesByConversation, getOlderMessagesByConversation,
getNewerMessagesByConversation, getNewerMessagesByConversation,
getMessageMetricsForConversation, getMessageMetricsForConversation,
getLastConversationActivity,
getLastConversationPreview,
migrateConversationMessages, migrateConversationMessages,
getUnprocessedCount, getUnprocessedCount,
@ -2749,6 +2751,50 @@ async function getNewestMessageForConversation(conversationId: string) {
return row; return row;
} }
async function getLastConversationActivity(
conversationId: string
): Promise<MessageType | null> {
const db = getInstance();
const row = await db.get(
`SELECT * FROM messages WHERE
conversationId = $conversationId AND
type NOT IN ('profile-change', 'verified-change', 'message-history-unsynced') AND
json_extract(json, '$.expirationTimerUpdate.fromSync') != true
ORDER BY received_at DESC
LIMIT 1;`,
{
$conversationId: conversationId,
}
);
if (!row) {
return null;
}
return jsonToObject(row.json);
}
async function getLastConversationPreview(
conversationId: string
): Promise<MessageType | null> {
const db = getInstance();
const row = await db.get(
`SELECT * FROM messages WHERE
conversationId = $conversationId AND
type NOT IN ('profile-change', 'verified-change', 'message-history-unsynced')
ORDER BY received_at DESC
LIMIT 1;`,
{
$conversationId: conversationId,
}
);
if (!row) {
return null;
}
return jsonToObject(row.json);
}
async function getOldestUnreadMessageForConversation(conversationId: string) { async function getOldestUnreadMessageForConversation(conversationId: string) {
const db = getInstance(); const db = getInstance();
const row = await db.get( const row = await db.get(

View file

@ -68,8 +68,18 @@ function renderEmojiPicker({
function renderLastSeenIndicator(id: string): JSX.Element { function renderLastSeenIndicator(id: string): JSX.Element {
return <FilteredSmartLastSeenIndicator id={id} />; return <FilteredSmartLastSeenIndicator id={id} />;
} }
function renderHeroRow(id: string, onHeightChange: () => unknown): JSX.Element { function renderHeroRow(
return <FilteredSmartHeroRow id={id} onHeightChange={onHeightChange} />; id: string,
onHeightChange: () => unknown,
updateSharedGroups: () => unknown
): JSX.Element {
return (
<FilteredSmartHeroRow
id={id}
onHeightChange={onHeightChange}
updateSharedGroups={updateSharedGroups}
/>
);
} }
function renderLoadingRow(id: string): JSX.Element { function renderLoadingRow(id: string): JSX.Element {
return <FilteredSmartTimelineLoadingRow id={id} />; return <FilteredSmartTimelineLoadingRow id={id} />;

View file

@ -1,152 +0,0 @@
import { assert } from 'chai';
import * as Conversation from '../../types/Conversation';
import {
IncomingMessage,
MessageHistoryUnsyncedMessage,
OutgoingMessage,
ProfileChangeNotificationMessage,
VerifiedChangeMessage,
} from '../../types/Message';
describe('Conversation', () => {
describe('createLastMessageUpdate', () => {
it('should reset last message if conversation has no messages', () => {
const input = {};
const expected = {
lastMessage: '',
lastMessageStatus: null,
timestamp: null,
};
const actual = Conversation.createLastMessageUpdate(input);
assert.deepEqual(actual, expected);
});
context('for regular message', () => {
it('should update last message text and timestamp', () => {
const input = {
currentTimestamp: 555,
lastMessageStatus: 'read',
lastMessage: {
type: 'outgoing',
conversationId: 'foo',
sent_at: 666,
timestamp: 666,
} as OutgoingMessage,
lastMessageNotificationText: 'New outgoing message',
};
const expected = {
lastMessage: 'New outgoing message',
lastMessageStatus: 'read',
lastMessageDeletedForEveryone: undefined,
timestamp: 666,
};
const actual = Conversation.createLastMessageUpdate(input);
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,
lastMessageDeletedForEveryone: undefined,
timestamp: 555,
};
const actual = Conversation.createLastMessageUpdate(input);
assert.deepEqual(actual, expected);
});
});
context('for verified change message', () => {
it('should skip update', () => {
const input = {
currentTimestamp: 555,
lastMessage: {
type: 'verified-change',
conversationId: 'foo',
sent_at: 666,
timestamp: 666,
} as VerifiedChangeMessage,
lastMessageNotificationText: 'Verified Changed',
};
const expected = {
lastMessage: '',
lastMessageStatus: null,
lastMessageDeletedForEveryone: undefined,
timestamp: 555,
};
const actual = Conversation.createLastMessageUpdate(input);
assert.deepEqual(actual, expected);
});
});
context('for expire timer update from sync', () => {
it('should update message but not timestamp (to prevent bump to top)', () => {
const input = {
currentTimestamp: 555,
lastMessage: {
type: 'incoming',
conversationId: 'foo',
sent_at: 666,
timestamp: 666,
expirationTimerUpdate: {
expireTimer: 111,
fromSync: true,
source: '+12223334455',
},
} as IncomingMessage,
lastMessageNotificationText: 'Last message before expired',
};
const expected = {
lastMessage: 'Last message before expired',
lastMessageStatus: null,
lastMessageDeletedForEveryone: undefined,
timestamp: 555,
};
const actual = Conversation.createLastMessageUpdate(input);
assert.deepEqual(actual, expected);
});
});
context('for profile change message', () => {
it('should update message but not timestamp (to prevent bump to top)', () => {
const input = {
currentTimestamp: 555,
lastMessage: {
type: 'profile-change',
conversationId: 'foo',
sent_at: 666,
timestamp: 666,
} as ProfileChangeNotificationMessage,
lastMessageNotificationText: 'John changed their profile name',
};
const expected = {
lastMessage: 'John changed their profile name',
lastMessageStatus: null,
lastMessageDeletedForEveryone: undefined,
timestamp: 555,
};
const actual = Conversation.createLastMessageUpdate(input);
assert.deepEqual(actual, expected);
});
});
});
});

View file

@ -1401,7 +1401,7 @@ class MessageReceiverInner extends EventTarget {
} }
const to = sentMessage.message.group const to = sentMessage.message.group
? `group(${sentMessage.message.group.id.toBinary()})` ? `group(${sentMessage.message.group.id.toBinary()})`
: sentMessage.destination; : sentMessage.destination || sentMessage.destinationUuid;
window.log.info( window.log.info(
'sent message to', 'sent message to',

View file

@ -1,58 +0,0 @@
import { Message } from './Message';
interface ConversationLastMessageUpdate {
lastMessage: string;
lastMessageStatus: string | null;
timestamp: number | null;
lastMessageDeletedForEveryone?: boolean;
}
export const createLastMessageUpdate = ({
currentTimestamp,
lastMessage,
lastMessageStatus,
lastMessageNotificationText,
}: {
currentTimestamp?: number;
lastMessage?: Message;
lastMessageStatus?: string;
lastMessageNotificationText?: string;
}): ConversationLastMessageUpdate => {
if (!lastMessage) {
return {
lastMessage: '',
lastMessageStatus: null,
timestamp: null,
};
}
const { type, expirationTimerUpdate, deletedForEveryone } = lastMessage;
const isMessageHistoryUnsynced = type === 'message-history-unsynced';
const isProfileChangedMessage = type === 'profile-change';
const isVerifiedChangeMessage = type === 'verified-change';
const isExpireTimerUpdateFromSync = Boolean(
expirationTimerUpdate && expirationTimerUpdate.fromSync
);
const shouldUpdateTimestamp = Boolean(
!isMessageHistoryUnsynced &&
!isProfileChangedMessage &&
!isVerifiedChangeMessage &&
!isExpireTimerUpdateFromSync
);
const newTimestamp = shouldUpdateTimestamp
? lastMessage.sent_at
: currentTimestamp;
const shouldUpdateLastMessageText = !isVerifiedChangeMessage;
const newLastMessageText = shouldUpdateLastMessageText
? lastMessageNotificationText
: '';
return {
lastMessage: deletedForEveryone ? '' : newLastMessageText || '',
lastMessageStatus: lastMessageStatus || null,
timestamp: newTimestamp || null,
lastMessageDeletedForEveryone: deletedForEveryone,
};
};

View file

@ -207,7 +207,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "js/models/conversations.js", "path": "js/models/conversations.js",
"line": " await wrap(", "line": " await wrap(",
"lineNumber": 671, "lineNumber": 665,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-06-09T20:26:46.515Z" "updated": "2020-06-09T20:26:46.515Z"
}, },