New React component: ConversationListItem, installed in left pane

When collecting a conversation's last message, we grab that message's
status as well (if outgoing) and show it.
This commit is contained in:
Scott Nonnenberg 2018-07-17 20:25:55 -07:00
parent 7e2d7b5e60
commit 675e34fc8d
17 changed files with 713 additions and 303 deletions

View file

@ -78,6 +78,18 @@
window.getInboxCollection = () => inboxCollection;
window.ConversationController = {
markAsSelected(toSelect) {
conversations.each(conversation => {
const current = conversation.isSelected || false;
const newValue = conversation.id === toSelect.id;
// eslint-disable-next-line no-param-reassign
conversation.isSelected = newValue;
if (current !== newValue) {
conversation.trigger('change');
}
});
},
get(id) {
if (!this._initialFetchComplete) {
throw new Error(

View file

@ -26,6 +26,7 @@
Errors,
Message,
VisualAttachment,
PhoneNumber,
} = window.Signal.Types;
const { upgradeMessageSchema, loadAttachmentData } = window.Signal.Migrations;
@ -110,6 +111,17 @@
this.messageCollection.on('change:errors', this.handleMessageError, this);
this.messageCollection.on('send-error', this.onMessageError, this);
const debouncedUpdateLastMessage = _.debounce(
this.updateLastMessage.bind(this),
1000
);
this.listenTo(
this.messageCollection,
'add remove',
debouncedUpdateLastMessage
);
this.listenTo(this.model, 'newmessage', debouncedUpdateLastMessage);
this.on('change:avatar', this.updateAvatarUrl);
this.on('change:profileAvatar', this.updateAvatarUrl);
this.on('change:profileKey', this.onChangeProfileKey);
@ -119,6 +131,7 @@
this.on('newmessage', this.addSingleMessage);
this.on('delivered', this.updateMessage);
this.on('read', this.updateMessage);
this.on('sent', this.updateLastMessage);
this.on('expired', this.onExpired);
this.listenTo(
this.messageCollection,
@ -158,6 +171,7 @@
// Used to update existing messages when updated from out-of-band db access,
// like read and delivery receipts.
updateMessage(message) {
this.updateLastMessage();
this.messageCollection.add(message, { merge: true });
},
@ -168,6 +182,43 @@
return model;
},
format() {
const { format } = PhoneNumber;
const regionCode = storage.get('regionCode');
const avatar = this.getAvatar();
const color = this.getColor();
return {
phoneNumber: format(this.id, {
ourRegionCode: regionCode,
}),
color,
avatarPath: avatar ? avatar.url : null,
name: this.getName(),
profileName: this.getProfileName(),
title: this.getTitle(),
};
},
getPropsForListItem() {
const result = {
...this.format(),
lastUpdated: this.get('timestamp'),
hasUnread: Boolean(this.get('unreadCount')),
isSelected: this.isSelected,
lastMessage: {
status: this.get('lastMessageStatus'),
text: this.get('lastMessage'),
},
onClick: () => this.trigger('select', this),
};
return result;
},
onMessageError() {
this.updateVerified();
},
@ -850,6 +901,7 @@
active_at: now,
timestamp: now,
lastMessage: message.getNotificationText(),
lastMessageStatus: 'sending',
});
const conversationType = this.get('type');
@ -889,10 +941,14 @@
const lastMessage = collection.at(0);
const lastMessageJSON = lastMessage ? lastMessage.toJSON() : null;
const lastMessageStatus = lastMessage
? lastMessage.getMessagePropStatus()
: null;
const lastMessageUpdate = Conversation.createLastMessageUpdate({
currentLastMessageText: this.get('lastMessage') || null,
currentTimestamp: this.get('timestamp') || null,
lastMessage: lastMessageJSON,
lastMessageStatus,
lastMessageNotificationText: lastMessage
? lastMessage.getNotificationText()
: null,

View file

@ -422,6 +422,10 @@
};
},
getMessagePropStatus() {
if (!this.isOutgoing()) {
return null;
}
if (this.hasErrors()) {
return 'error';
}
@ -763,6 +767,7 @@
sent: true,
expirationStartTimestamp: now,
});
this.trigger('sent', this);
this.sendSyncMessage();
})
.catch(result => {

View file

@ -19,6 +19,9 @@ const { ContactName } = require('../../ts/components/conversation/ContactName');
const {
ConversationHeader,
} = require('../../ts/components/conversation/ConversationHeader');
const {
ConversationListItem,
} = require('../../ts/components/ConversationListItem');
const {
EmbeddedContact,
} = require('../../ts/components/conversation/EmbeddedContact');
@ -151,6 +154,7 @@ exports.setup = (options = {}) => {
ContactListItem,
ContactName,
ConversationHeader,
ConversationListItem,
EmbeddedContact,
Emojify,
GroupNotification,

View file

@ -1,4 +1,4 @@
/* global Whisper, _, extension, Backbone, Mustache */
/* global Whisper, Signal, Backbone */
// eslint-disable-next-line func-names
(function() {
@ -13,118 +13,38 @@
return `conversation-list-item contact ${this.model.cid}`;
},
templateName: 'conversation-preview',
events: {
click: 'select',
},
initialize() {
// auto update
this.listenTo(
this.model,
'change',
_.debounce(this.render.bind(this), 1000)
);
this.listenTo(this.model, 'destroy', this.remove); // auto update
this.listenTo(this.model, 'opened', this.markSelected); // auto update
const updateLastMessage = _.debounce(
this.model.updateLastMessage.bind(this.model),
1000
);
this.listenTo(
this.model.messageCollection,
'add remove',
updateLastMessage
);
this.listenTo(this.model, 'newmessage', updateLastMessage);
extension.windows.onClosed(() => {
this.stopListening();
});
this.timeStampView = new Whisper.TimestampView({ brief: true });
this.listenTo(this.model, 'destroy', this.remove);
this.model.updateLastMessage();
},
markSelected() {
this.$el
.addClass('selected')
.siblings('.selected')
.removeClass('selected');
},
select() {
this.markSelected();
this.$el.trigger('select', this.model);
},
remove() {
if (this.nameView) {
this.nameView.remove();
this.nameView = null;
}
if (this.bodyView) {
this.bodyView.remove();
this.bodyView = null;
if (this.childView) {
this.childView.remove();
this.childView = null;
}
Backbone.View.prototype.remove.call(this);
},
render() {
const lastMessage = this.model.get('lastMessage');
this.$el.html(
Mustache.render(
_.result(this, 'template', ''),
{
last_message: Boolean(lastMessage),
last_message_timestamp: this.model.get('timestamp'),
number: this.model.getNumber(),
avatar: this.model.getAvatar(),
unreadCount: this.model.get('unreadCount'),
},
this.render_partials()
)
);
this.timeStampView.setElement(this.$('.last-timestamp'));
this.timeStampView.update();
if (this.nameView) {
this.nameView.remove();
this.nameView = null;
if (this.childView) {
this.childView.remove();
this.childView = null;
}
this.nameView = new Whisper.ReactWrapperView({
className: 'name-wrapper',
Component: window.Signal.Components.ContactName,
props: {
phoneNumber: this.model.getNumber(),
name: this.model.getName(),
profileName: this.model.getProfileName(),
},
const props = this.model.getPropsForListItem();
this.childView = new Whisper.ReactWrapperView({
className: 'list-item-wrapper',
Component: Signal.Components.ConversationListItem,
props,
});
this.$('.name').append(this.nameView.el);
if (lastMessage) {
if (this.bodyView) {
this.bodyView.remove();
this.bodyView = null;
}
this.bodyView = new Whisper.ReactWrapperView({
className: 'body-wrapper',
Component: window.Signal.Components.MessageBody,
props: {
text: lastMessage,
disableJumbomoji: true,
disableLinks: true,
},
});
this.$('.last-message').append(this.bodyView.el);
}
const update = () =>
this.childView.update(this.model.getPropsForListItem());
const unread = this.model.get('unreadCount');
if (unread > 0) {
this.$el.addClass('unread');
} else {
this.$el.removeClass('unread');
}
this.listenTo(this.model, 'change', update);
this.$el.append(this.childView.el);
return this;
},

View file

@ -107,11 +107,12 @@
const inboxCollection = getInboxCollection();
inboxCollection.on('messageError', () => {
this.listenTo(inboxCollection, 'messageError', () => {
if (this.networkStatusView) {
this.networkStatusView.render();
}
});
this.listenTo(inboxCollection, 'select', this.openConversation);
this.inboxListView = new Whisper.ConversationListView({
el: this.$('.inbox'),
@ -144,11 +145,7 @@
this.searchView.$el.show();
this.inboxListView.$el.hide();
});
this.listenTo(
this.searchView,
'open',
this.openConversation.bind(this, null)
);
this.listenTo(this.searchView, 'open', this.openConversation);
this.networkStatusView = new Whisper.NetworkStatusView();
this.$el
@ -175,7 +172,6 @@
click: 'onClick',
'click #header': 'focusHeader',
'click .conversation': 'focusConversation',
'select .gutter .conversation-list-item': 'openConversation',
'input input.search': 'filterContacts',
},
startConnectionListener() {
@ -250,7 +246,9 @@
input.removeClass('active');
}
},
openConversation(e, conversation) {
openConversation(conversation) {
ConversationController.markAsSelected(conversation);
this.searchView.hideHints();
if (conversation) {
this.conversation_stack.open(