Message Requests improvements
This commit is contained in:
parent
b63291507a
commit
81cb7730a5
21 changed files with 302 additions and 263 deletions
|
@ -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": {
|
||||
"message": "Draft:",
|
||||
"description": "Prefix shown in italic in conversation view when a draft is saved"
|
||||
|
|
|
@ -1885,6 +1885,11 @@
|
|||
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(() => {
|
||||
const view = window.owsDesktopApp.appView;
|
||||
if (view) {
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
};
|
||||
|
||||
const { Util } = window.Signal;
|
||||
const { Conversation, Contact, Message } = window.Signal.Types;
|
||||
const { Contact, Message } = window.Signal.Types;
|
||||
const {
|
||||
deleteAttachmentData,
|
||||
doesAttachmentExist,
|
||||
|
@ -134,12 +134,6 @@
|
|||
this.updateLastMessage.bind(this),
|
||||
200
|
||||
);
|
||||
this.throttledUpdateSharedGroups =
|
||||
this.throttledUpdateSharedGroups ||
|
||||
_.throttle(
|
||||
this.updateSharedGroups.bind(this),
|
||||
1000 * 60 * 5 // five minutes
|
||||
);
|
||||
|
||||
this.listenTo(
|
||||
this.messageCollection,
|
||||
|
@ -175,10 +169,11 @@
|
|||
this.typingPauseTimer = null;
|
||||
|
||||
// Keep props ready
|
||||
this.generateProps = () => {
|
||||
const generateProps = () => {
|
||||
this.cachedProps = this.getProps();
|
||||
};
|
||||
this.on('change', this.generateProps);
|
||||
this.on('change', generateProps);
|
||||
generateProps();
|
||||
},
|
||||
|
||||
isMe() {
|
||||
|
@ -451,8 +446,6 @@
|
|||
getProps() {
|
||||
const color = this.getColor();
|
||||
|
||||
this.throttledUpdateSharedGroups();
|
||||
|
||||
const typingValues = _.values(this.contactTypingTimers || {});
|
||||
const typingMostRecent = _.first(_.sortBy(typingValues, 'timestamp'));
|
||||
const typingContact = typingMostRecent
|
||||
|
@ -575,15 +568,16 @@
|
|||
async handleReadAndDownloadAttachments() {
|
||||
let messages;
|
||||
do {
|
||||
const first = messages ? messages.first() : null;
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
messages = await window.Signal.Data.getOlderMessagesByConversation(
|
||||
this.get('id'),
|
||||
{
|
||||
MessageCollection: Whisper.MessageCollection,
|
||||
limit: 100,
|
||||
receivedAt: messages
|
||||
? messages.first().get('received_at')
|
||||
: undefined,
|
||||
receivedAt: first ? first.get('received_at') : null,
|
||||
messageId: first ? first.id : null,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -959,13 +953,18 @@
|
|||
(this.get('messageCountBeforeMessageRequests') || 0) > 0;
|
||||
const hasNoMessages = (this.get('messageCount') || 0) === 0;
|
||||
|
||||
const isEmptyPrivateConvo = hasNoMessages && this.isPrivate();
|
||||
const isEmptyWhitelistedGroup =
|
||||
hasNoMessages && !this.isPrivate() && this.get('profileSharing');
|
||||
|
||||
return (
|
||||
isFromOrAddedByTrustedContact ||
|
||||
hasSentMessages ||
|
||||
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
|
||||
(hasNoMessages && this.get('profileSharing'))
|
||||
isEmptyPrivateConvo ||
|
||||
isEmptyWhitelistedGroup
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -1868,37 +1867,40 @@
|
|||
return;
|
||||
}
|
||||
|
||||
const messages = await window.Signal.Data.getOlderMessagesByConversation(
|
||||
this.id,
|
||||
{ limit: 1, MessageCollection: Whisper.MessageCollection }
|
||||
);
|
||||
const [previewMessage, activityMessage] = await Promise.all([
|
||||
window.Signal.Data.getLastConversationPreview(this.id, {
|
||||
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 (
|
||||
this.hasDraft() &&
|
||||
this.get('draftTimestamp') &&
|
||||
(!lastMessageModel ||
|
||||
lastMessageModel.get('sent_at') < this.get('draftTimestamp'))
|
||||
previewMessage.get('sent_at') < this.get('draftTimestamp')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastMessageJSON = lastMessageModel
|
||||
? lastMessageModel.toJSON()
|
||||
: null;
|
||||
const lastMessageStatusModel = lastMessageModel
|
||||
? lastMessageModel.getMessagePropStatus()
|
||||
: null;
|
||||
const lastMessageUpdate = Conversation.createLastMessageUpdate({
|
||||
currentTimestamp: this.get('timestamp') || null,
|
||||
lastMessage: lastMessageJSON,
|
||||
lastMessageStatus: lastMessageStatusModel,
|
||||
lastMessageNotificationText: lastMessageModel
|
||||
? lastMessageModel.getNotificationText()
|
||||
: null,
|
||||
const currentTimestamp = this.get('timestamp') || null;
|
||||
const timestamp = activityMessage
|
||||
? activityMessage.sent_at || currentTimestamp
|
||||
: currentTimestamp;
|
||||
|
||||
this.set({
|
||||
lastMessage: previewMessage.getNotificationText() || '',
|
||||
lastMessageStatus: previewMessage.getMessagePropStatus() || null,
|
||||
timestamp,
|
||||
lastMessageDeletedForEveryone: previewMessage.deletedForEveryone,
|
||||
});
|
||||
|
||||
this.set(lastMessageUpdate);
|
||||
window.Signal.Data.updateConversation(this.attributes);
|
||||
},
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/* global crypto, window */
|
||||
|
||||
const { isFunction, isNumber } = require('lodash');
|
||||
const { createLastMessageUpdate } = require('../../../ts/types/Conversation');
|
||||
const {
|
||||
arrayBufferToBase64,
|
||||
base64ToArrayBuffer,
|
||||
|
@ -161,7 +160,7 @@ module.exports = {
|
|||
arrayBufferToBase64,
|
||||
base64ToArrayBuffer,
|
||||
computeHash,
|
||||
createLastMessageUpdate,
|
||||
|
||||
deleteExternalFiles,
|
||||
maybeUpdateAvatar,
|
||||
maybeUpdateProfileAvatar,
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
(function() {
|
||||
'use strict';
|
||||
|
||||
const FIVE_MINUTES = 1000 * 60 * 5;
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
const { Message, MIME, VisualAttachment } = window.Signal.Types;
|
||||
const {
|
||||
|
@ -304,9 +306,12 @@
|
|||
);
|
||||
this.model.throttledGetProfiles =
|
||||
this.model.throttledGetProfiles ||
|
||||
_.throttle(this.model.getProfiles.bind(this.model), FIVE_MINUTES);
|
||||
this.model.throttledUpdateSharedGroups =
|
||||
this.model.throttledUpdateSharedGroups ||
|
||||
_.throttle(
|
||||
this.model.getProfiles.bind(this.model),
|
||||
1000 * 60 * 5 // five minutes
|
||||
this.model.updateSharedGroups.bind(this.model),
|
||||
FIVE_MINUTES
|
||||
);
|
||||
this.debouncedMaybeGrabLinkPreview = _.debounce(
|
||||
this.maybeGrabLinkPreview.bind(this),
|
||||
|
@ -720,6 +725,7 @@
|
|||
showVisualAttachment,
|
||||
showExpiredIncomingTapToViewToast,
|
||||
showExpiredOutgoingTapToViewToast,
|
||||
updateSharedGroups: this.model.throttledUpdateSharedGroups,
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
@ -3750,6 +3750,17 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
|
|||
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 {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
|
|
@ -636,8 +636,6 @@ export class ConversationController {
|
|||
|
||||
await Promise.all(
|
||||
this._conversations.map(async conversation => {
|
||||
conversation.generateProps();
|
||||
|
||||
if (!conversation.get('lastMessage')) {
|
||||
await conversation.updateLastMessage();
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ export function onChange(key: ConfigKeyType, fn: ConfigListenerType) {
|
|||
};
|
||||
}
|
||||
|
||||
const refreshRemoteConfig = async () => {
|
||||
export const refreshRemoteConfig = async () => {
|
||||
const now = Date.now();
|
||||
const server = getServer();
|
||||
const newConfig = await server.getConfig();
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
<util.LeftPaneContext theme={util.theme}>
|
||||
<ConversationListItem
|
||||
id="conversationId1"
|
||||
isAccepted
|
||||
title="Someone 🔥 Somewhere"
|
||||
name="Someone 🔥 Somewhere"
|
||||
type={'direct'}
|
||||
phoneNumber="(202) 555-0011"
|
||||
|
@ -25,8 +27,10 @@
|
|||
<util.LeftPaneContext theme={util.theme}>
|
||||
<ConversationListItem
|
||||
id="conversationId1"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
type={'direct'}
|
||||
title="Mr. Fire🔥"
|
||||
name="Mr. Fire🔥"
|
||||
color="green"
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
|
@ -46,9 +50,11 @@
|
|||
<util.LeftPaneContext theme={util.theme}>
|
||||
<ConversationListItem
|
||||
id="conversationId1"
|
||||
isAccepted
|
||||
isMe={true}
|
||||
phoneNumber="(202) 555-0011"
|
||||
type={'direct'}
|
||||
title="Mr. Fire🔥"
|
||||
name="Mr. Fire🔥"
|
||||
color="green"
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
|
@ -69,8 +75,10 @@
|
|||
<div>
|
||||
<ConversationListItem
|
||||
id="conversationId1"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
type={'direct'}
|
||||
title="Mr. Fire🔥"
|
||||
name="Mr. Fire🔥"
|
||||
color="green"
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
|
@ -83,8 +91,10 @@
|
|||
/>
|
||||
<ConversationListItem
|
||||
id="conversationId2"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
type={'direct'}
|
||||
title="Mr. Fire🔥"
|
||||
name="Mr. Fire🔥"
|
||||
color="green"
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
|
@ -97,8 +107,10 @@
|
|||
/>
|
||||
<ConversationListItem
|
||||
id="conversationId3"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
type={'direct'}
|
||||
title="Mr. Fire🔥"
|
||||
name="Mr. Fire🔥"
|
||||
color="green"
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
|
@ -111,8 +123,10 @@
|
|||
/>
|
||||
<ConversationListItem
|
||||
id="conversationId4"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
type={'direct'}
|
||||
title="Mr. Fire🔥"
|
||||
name="Mr. Fire🔥"
|
||||
color="green"
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
|
@ -125,8 +139,10 @@
|
|||
/>
|
||||
<ConversationListItem
|
||||
id="conversationId5"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
type={'direct'}
|
||||
title="Mr. Fire🔥"
|
||||
name="Mr. Fire🔥"
|
||||
color="green"
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
|
@ -148,7 +164,9 @@
|
|||
<div>
|
||||
<ConversationListItem
|
||||
id="conversationId1"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
title="(202) 555-0011"
|
||||
type={'direct'}
|
||||
unreadCount={4}
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
|
@ -162,7 +180,51 @@
|
|||
<div>
|
||||
<ConversationListItem
|
||||
id="conversationId2"
|
||||
isAccepted
|
||||
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'}
|
||||
unreadCount={4}
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
|
@ -188,7 +250,9 @@
|
|||
<div>
|
||||
<ConversationListItem
|
||||
id="conversationId1"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
title="(202) 555-0011"
|
||||
type={'direct'}
|
||||
unreadCount={4}
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
|
@ -200,7 +264,9 @@
|
|||
/>
|
||||
<ConversationListItem
|
||||
id="conversationId2"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
title="(202) 555-0011"
|
||||
type={'direct'}
|
||||
unreadCount={10}
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
|
@ -212,7 +278,9 @@
|
|||
/>
|
||||
<ConversationListItem
|
||||
id="conversationId3"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
title="(202) 555-0011"
|
||||
type={'direct'}
|
||||
unreadCount={250}
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
|
@ -232,7 +300,9 @@
|
|||
<util.LeftPaneContext theme={util.theme}>
|
||||
<ConversationListItem
|
||||
id="conversationId1"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
title="(202) 555-0011"
|
||||
type={'direct'}
|
||||
isSelected={true}
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
|
@ -254,7 +324,9 @@ We don't want Jumbomoji or links.
|
|||
<div>
|
||||
<ConversationListItem
|
||||
id="conversationId1"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
title="(202) 555-0011"
|
||||
type={'direct'}
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
lastMessage={{
|
||||
|
@ -265,7 +337,9 @@ We don't want Jumbomoji or links.
|
|||
/>
|
||||
<ConversationListItem
|
||||
id="conversationId2"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
title="(202) 555-0011"
|
||||
type={'direct'}
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
lastMessage={{
|
||||
|
@ -287,7 +361,9 @@ We only show one line.
|
|||
<div>
|
||||
<ConversationListItem
|
||||
id="conversationId1"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
title="(202) 555-0011"
|
||||
type={'direct'}
|
||||
name="Long contact name. Esquire. The third. And stuff. And more! And more!"
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
|
@ -299,7 +375,9 @@ We only show one line.
|
|||
/>
|
||||
<ConversationListItem
|
||||
id="conversationId2"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
title="(202) 555-0011"
|
||||
type={'direct'}
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
lastMessage={{
|
||||
|
@ -311,7 +389,9 @@ We only show one line.
|
|||
/>
|
||||
<ConversationListItem
|
||||
id="conversationId3"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
title="(202) 555-0011"
|
||||
type={'direct'}
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
lastMessage={{
|
||||
|
@ -325,7 +405,9 @@ We only show one line.
|
|||
|
||||
<ConversationListItem
|
||||
id="conversationId4"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
title="(202) 555-0011"
|
||||
type={'direct'}
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
unreadCount={8}
|
||||
|
@ -338,7 +420,9 @@ We only show one line.
|
|||
/>
|
||||
<ConversationListItem
|
||||
id="conversationId5"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
title="(202) 555-0011"
|
||||
type={'direct'}
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
lastMessage={{
|
||||
|
@ -350,7 +434,9 @@ We only show one line.
|
|||
/>
|
||||
<ConversationListItem
|
||||
id="conversationId6"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
title="(202) 555-0011"
|
||||
type={'direct'}
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
lastMessage={{
|
||||
|
@ -374,7 +460,9 @@ On platforms that show scrollbars all the time, this is true all the time.
|
|||
<div style={{ width: '280px' }}>
|
||||
<ConversationListItem
|
||||
id="conversationId1"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
title="(202) 555-0011"
|
||||
type={'direct'}
|
||||
name="Long contact name. Esquire. The third. And stuff. And more! And more!"
|
||||
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
|
||||
id="conversationId2"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
title="(202) 555-0011"
|
||||
type={'direct'}
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
lastMessage={{
|
||||
|
@ -407,7 +497,9 @@ On platforms that show scrollbars all the time, this is true all the time.
|
|||
<div>
|
||||
<ConversationListItem
|
||||
id="conversationId1"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
title="(202) 555-0011"
|
||||
type={'direct'}
|
||||
lastUpdated={Date.now() - 5 * 60 * 60 * 1000}
|
||||
lastMessage={{
|
||||
|
@ -418,7 +510,9 @@ On platforms that show scrollbars all the time, this is true all the time.
|
|||
/>
|
||||
<ConversationListItem
|
||||
id="conversationId2"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
title="(202) 555-0011"
|
||||
type={'direct'}
|
||||
lastUpdated={Date.now() - 24 * 60 * 60 * 1000}
|
||||
lastMessage={{
|
||||
|
@ -429,7 +523,9 @@ On platforms that show scrollbars all the time, this is true all the time.
|
|||
/>
|
||||
<ConversationListItem
|
||||
id="conversationId3"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
title="(202) 555-0011"
|
||||
type={'direct'}
|
||||
lastUpdated={Date.now() - 7 * 24 * 60 * 60 * 1000}
|
||||
lastMessage={{
|
||||
|
@ -440,7 +536,9 @@ On platforms that show scrollbars all the time, this is true all the time.
|
|||
/>
|
||||
<ConversationListItem
|
||||
id="conversationId4"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
title="(202) 555-0011"
|
||||
type={'direct'}
|
||||
lastUpdated={Date.now() - 365 * 24 * 60 * 60 * 1000}
|
||||
lastMessage={{
|
||||
|
@ -460,7 +558,9 @@ On platforms that show scrollbars all the time, this is true all the time.
|
|||
<div>
|
||||
<ConversationListItem
|
||||
id="conversationId1"
|
||||
isAccepted
|
||||
name="John"
|
||||
title="John"
|
||||
type={'direct'}
|
||||
lastUpdated={null}
|
||||
lastMessage={{
|
||||
|
@ -471,7 +571,9 @@ On platforms that show scrollbars all the time, this is true all the time.
|
|||
/>
|
||||
<ConversationListItem
|
||||
id="conversationId2"
|
||||
isAccepted
|
||||
name="Missing message"
|
||||
title="Missing message"
|
||||
type={'direct'}
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
lastMessage={{
|
||||
|
@ -482,7 +584,9 @@ On platforms that show scrollbars all the time, this is true all the time.
|
|||
/>
|
||||
<ConversationListItem
|
||||
id="conversationId3"
|
||||
isAccepted
|
||||
phoneNumber="(202) 555-0011"
|
||||
title="(202) 555-0011"
|
||||
type={'direct'}
|
||||
lastUpdated={Date.now() - 5 * 60 * 1000}
|
||||
lastMessage={{
|
||||
|
|
|
@ -26,6 +26,7 @@ export type PropsData = {
|
|||
unreadCount?: number;
|
||||
isSelected: boolean;
|
||||
|
||||
isAccepted?: boolean;
|
||||
draftPreview?: string;
|
||||
shouldShowDraft?: boolean;
|
||||
|
||||
|
@ -152,6 +153,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
|||
const {
|
||||
draftPreview,
|
||||
i18n,
|
||||
isAccepted,
|
||||
lastMessage,
|
||||
shouldShowDraft,
|
||||
typingContact,
|
||||
|
@ -187,7 +189,11 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
|||
: null
|
||||
)}
|
||||
>
|
||||
{typingContact ? (
|
||||
{!isAccepted ? (
|
||||
<span className="module-conversation-list-item__message-request">
|
||||
{i18n('ConversationListItem--message-request')}
|
||||
</span>
|
||||
) : typingContact ? (
|
||||
<TypingAnimation i18n={i18n} />
|
||||
) : (
|
||||
<>
|
||||
|
|
|
@ -13,6 +13,7 @@ export type Props = {
|
|||
membersCount?: number;
|
||||
phoneNumber?: string;
|
||||
onHeightChange?: () => unknown;
|
||||
updateSharedGroups?: () => unknown;
|
||||
} & Omit<AvatarProps, 'onClick' | 'size' | 'noteToSelf'>;
|
||||
|
||||
const renderMembershipRow = ({
|
||||
|
@ -113,6 +114,7 @@ export const ConversationHero = ({
|
|||
profileName,
|
||||
title,
|
||||
onHeightChange,
|
||||
updateSharedGroups,
|
||||
}: Props) => {
|
||||
const firstRenderRef = React.useRef(true);
|
||||
|
||||
|
@ -121,6 +123,11 @@ export const ConversationHero = ({
|
|||
// component may have changed. The cleanup function notifies listeners of
|
||||
// any potential height changes.
|
||||
return () => {
|
||||
// Kick off the expensive hydration of the current sharedGroupNames
|
||||
if (updateSharedGroups) {
|
||||
updateSharedGroups();
|
||||
}
|
||||
|
||||
if (onHeightChange && !firstRenderRef.current) {
|
||||
onHeightChange();
|
||||
} else {
|
||||
|
@ -135,7 +142,7 @@ export const ConversationHero = ({
|
|||
`mc-${membersCount}`,
|
||||
`n-${name}`,
|
||||
`pn-${profileName}`,
|
||||
...sharedGroupNames.map(g => `g-${g}`),
|
||||
sharedGroupNames.map(g => `g-${g}`).join(' '),
|
||||
]);
|
||||
|
||||
const phoneNumberOnly = Boolean(
|
||||
|
|
|
@ -50,7 +50,11 @@ type PropsHousekeepingType = {
|
|||
actions: Object
|
||||
) => 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;
|
||||
renderTypingBubble: (id: string) => JSX.Element;
|
||||
};
|
||||
|
@ -70,6 +74,7 @@ type PropsActionsType = {
|
|||
markMessageRead: (messageId: string) => unknown;
|
||||
selectMessage: (messageId: string, conversationId: string) => unknown;
|
||||
clearSelectedMessage: () => unknown;
|
||||
updateSharedGroups: () => unknown;
|
||||
} & MessageActionsType &
|
||||
SafetyNumberActionsType;
|
||||
|
||||
|
@ -510,6 +515,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
|||
renderLoadingRow,
|
||||
renderLastSeenIndicator,
|
||||
renderTypingBubble,
|
||||
updateSharedGroups,
|
||||
} = this.props;
|
||||
|
||||
const styleWithWidth = {
|
||||
|
@ -524,7 +530,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
|||
if (haveOldest && row === 0) {
|
||||
rowContents = (
|
||||
<div data-row={row} style={styleWithWidth} role="row">
|
||||
{renderHeroRow(id, this.resizeHeroRow)}
|
||||
{renderHeroRow(id, this.resizeHeroRow, updateSharedGroups)}
|
||||
</div>
|
||||
);
|
||||
} else if (!haveOldest && row === 0) {
|
||||
|
|
1
ts/model-types.d.ts
vendored
1
ts/model-types.d.ts
vendored
|
@ -85,7 +85,6 @@ declare class ConversationModelType extends Backbone.Model<
|
|||
cleanup(): Promise<void>;
|
||||
disableProfileSharing(): void;
|
||||
dropProfileKey(): Promise<void>;
|
||||
generateProps(): void;
|
||||
getAccepted(): boolean;
|
||||
getAvatarPath(): string | undefined;
|
||||
getColor(): ColorType | undefined;
|
||||
|
|
|
@ -156,6 +156,8 @@ const dataInterface: ClientInterface = {
|
|||
getTapToViewMessagesNeedingErase,
|
||||
getOlderMessagesByConversation,
|
||||
getNewerMessagesByConversation,
|
||||
getLastConversationActivity,
|
||||
getLastConversationPreview,
|
||||
getMessageMetricsForConversation,
|
||||
migrateConversationMessages,
|
||||
|
||||
|
@ -1022,6 +1024,32 @@ async function getNewerMessagesByConversation(
|
|||
|
||||
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) {
|
||||
const result = await channels.getMessageMetricsForConversation(
|
||||
conversationId
|
||||
|
|
|
@ -210,6 +210,12 @@ export type ServerInterface = DataInterface & {
|
|||
conversationId: string,
|
||||
options?: { limit?: number; receivedAt?: number }
|
||||
) => Promise<Array<MessageTypeUnhydrated>>;
|
||||
getLastConversationActivity: (
|
||||
conversationId: string
|
||||
) => Promise<MessageType | undefined>;
|
||||
getLastConversationPreview: (
|
||||
conversationId: string
|
||||
) => Promise<MessageType | undefined>;
|
||||
getNextExpiringMessage: () => Promise<MessageType>;
|
||||
getNextTapToViewMessageToAgeOut: () => Promise<MessageType>;
|
||||
getOutgoingWithoutExpiresAt: () => Promise<Array<MessageType>>;
|
||||
|
@ -308,6 +314,18 @@ export type ClientInterface = DataInterface & {
|
|||
MessageCollection: typeof MessageModelCollectionType;
|
||||
}
|
||||
) => Promise<MessageModelCollectionType>;
|
||||
getLastConversationActivity: (
|
||||
conversationId: string,
|
||||
options: {
|
||||
Message: typeof MessageModelType;
|
||||
}
|
||||
) => Promise<MessageModelType | undefined>;
|
||||
getLastConversationPreview: (
|
||||
conversationId: string,
|
||||
options: {
|
||||
Message: typeof MessageModelType;
|
||||
}
|
||||
) => Promise<MessageModelType | undefined>;
|
||||
getNextExpiringMessage: ({
|
||||
Message,
|
||||
}: {
|
||||
|
|
|
@ -132,6 +132,8 @@ const dataInterface: ServerInterface = {
|
|||
getOlderMessagesByConversation,
|
||||
getNewerMessagesByConversation,
|
||||
getMessageMetricsForConversation,
|
||||
getLastConversationActivity,
|
||||
getLastConversationPreview,
|
||||
migrateConversationMessages,
|
||||
|
||||
getUnprocessedCount,
|
||||
|
@ -2749,6 +2751,50 @@ async function getNewestMessageForConversation(conversationId: string) {
|
|||
|
||||
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) {
|
||||
const db = getInstance();
|
||||
const row = await db.get(
|
||||
|
|
|
@ -68,8 +68,18 @@ function renderEmojiPicker({
|
|||
function renderLastSeenIndicator(id: string): JSX.Element {
|
||||
return <FilteredSmartLastSeenIndicator id={id} />;
|
||||
}
|
||||
function renderHeroRow(id: string, onHeightChange: () => unknown): JSX.Element {
|
||||
return <FilteredSmartHeroRow id={id} onHeightChange={onHeightChange} />;
|
||||
function renderHeroRow(
|
||||
id: string,
|
||||
onHeightChange: () => unknown,
|
||||
updateSharedGroups: () => unknown
|
||||
): JSX.Element {
|
||||
return (
|
||||
<FilteredSmartHeroRow
|
||||
id={id}
|
||||
onHeightChange={onHeightChange}
|
||||
updateSharedGroups={updateSharedGroups}
|
||||
/>
|
||||
);
|
||||
}
|
||||
function renderLoadingRow(id: string): JSX.Element {
|
||||
return <FilteredSmartTimelineLoadingRow id={id} />;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1401,7 +1401,7 @@ class MessageReceiverInner extends EventTarget {
|
|||
}
|
||||
const to = sentMessage.message.group
|
||||
? `group(${sentMessage.message.group.id.toBinary()})`
|
||||
: sentMessage.destination;
|
||||
: sentMessage.destination || sentMessage.destinationUuid;
|
||||
|
||||
window.log.info(
|
||||
'sent message to',
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -207,7 +207,7 @@
|
|||
"rule": "jQuery-wrap(",
|
||||
"path": "js/models/conversations.js",
|
||||
"line": " await wrap(",
|
||||
"lineNumber": 671,
|
||||
"lineNumber": 665,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-06-09T20:26:46.515Z"
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue