cleanup models with unused events

also, sort message from DB and on redux by sent_at or received_at when
not a public group
This commit is contained in:
Audric Ackermann 2021-02-09 11:40:32 +11:00
parent 6edcb88788
commit ea2c4437a3
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4
13 changed files with 122 additions and 214 deletions

View File

@ -2472,6 +2472,8 @@ async function getUnreadCountByConversation(conversationId) {
}
// Note: Sorting here is necessary for getting the last message (with limit 1)
// be sure to update the sorting order to sort messages on reduxz too (sortMessages
async function getMessagesByConversation(
conversationId,
{ limit = 100, receivedAt = Number.MAX_VALUE, type = '%' } = {}
@ -2482,7 +2484,7 @@ async function getMessagesByConversation(
conversationId = $conversationId AND
received_at < $received_at AND
type LIKE $type
ORDER BY serverTimestamp DESC, serverId DESC, sent_at DESC
ORDER BY serverTimestamp DESC, serverId DESC, sent_at DESC, received_at DESC
LIMIT $limit;
`,
{

View File

@ -24,6 +24,7 @@ export interface LokiAppDotNetServerInterface {
}
export interface LokiPublicChannelAPI {
banUser(source: string): Promise<boolean>;
getModerators: () => Promise<Array<string>>;
serverAPI: any;
deleteMessages(arg0: any[]);

View File

@ -88,11 +88,6 @@
const force = true;
await message.setToExpire(force);
const conversation = message.getConversation();
if (conversation) {
conversation.trigger('expiration-change', message);
}
}
this.remove(receipt);

View File

@ -11,7 +11,7 @@ import {
ReadReceiptMessage,
TypingMessage,
} from '../session/messages/outgoing';
import { ClosedGroupChatMessage } from '../session/messages/outgoing/content/data/group';
import { ClosedGroupChatMessage } from '../session/messages/outgoing/content/data/group/ClosedGroupChatMessage';
import { OpenGroup, PubKey } from '../session/types';
import { ToastUtils, UserUtils } from '../session/utils';
import { BlockedNumberController } from '../util';
@ -20,7 +20,7 @@ import { leaveClosedGroup } from '../session/group';
import { SignalService } from '../protobuf';
import { MessageCollection, MessageModel } from './message';
import * as Data from '../../js/modules/data';
import { MessageAttributesOptionals } from './messageType';
import { MessageAttributesOptionals, MessageModelType } from './messageType';
import autoBind from 'auto-bind';
export interface OurLokiProfile {
@ -160,15 +160,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
this.bouncyUpdateLastMessage.bind(this),
1000
);
// this.listenTo(
// this.messageCollection,
// 'add remove destroy',
// debouncedUpdateLastMessage
// );
// Listening for out-of-band data updates
this.on('delivered', this.updateAndMerge);
this.on('read', this.updateAndMerge);
this.on('expiration-change', this.updateAndMerge);
this.on('expired', this.onExpired);
this.on('ourAvatarChanged', avatar =>
@ -370,21 +362,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
}
}
public async updateAndMerge(message: any) {
await this.updateLastMessage();
const mergeMessage = () => {
const existing = this.messageCollection.get(message.id);
if (!existing) {
return;
}
existing.merge(message.attributes);
};
mergeMessage();
}
public async onExpired(message: any) {
await this.updateLastMessage();
@ -439,16 +416,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
await model.setServerTimestamp(serverTimestamp);
return undefined;
}
public addSingleMessage(
message: MessageAttributesOptionals,
setToExpire = true
) {
const model = this.messageCollection.add(message, { merge: true });
if (setToExpire) {
void model.setToExpire();
}
return model;
}
public format() {
return this.cachedProps;
}
@ -673,17 +641,23 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
conversationId: this.id,
});
}
public async sendMessageJob(message: any) {
public async sendMessageJob(message: MessageModel) {
try {
const uploads = await message.uploadData();
const { id } = message;
const expireTimer = this.get('expireTimer');
const destination = this.id;
const sentAt = message.get('sent_at');
if (!sentAt) {
throw new Error('sendMessageJob() sent_at must be set.');
}
const chatMessage = new ChatMessage({
body: uploads.body,
identifier: id,
timestamp: message.get('sent_at'),
timestamp: sentAt,
attachments: uploads.attachments,
expireTimer,
preview: uploads.preview,
@ -696,7 +670,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
const openGroupParams = {
body: uploads.body,
timestamp: message.get('sent_at'),
timestamp: sentAt,
group: openGroup,
attachments: uploads.attachments,
preview: uploads.preview,
@ -716,7 +690,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
const groupInvitation = message.get('groupInvitation');
const groupInvitMessage = new GroupInvitationMessage({
identifier: id,
timestamp: message.get('sent_at'),
timestamp: sentAt,
serverName: groupInvitation.name,
channelId: groupInvitation.channelId,
serverAddress: groupInvitation.address,
@ -767,6 +741,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
this.clearTypingTimers();
const destination = this.id;
const isPrivate = this.isPrivate();
const expireTimer = this.get('expireTimer');
const recipients = this.getRecipients();
@ -808,28 +783,16 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
const attributes: MessageAttributesOptionals = {
...messageWithSchema,
groupInvitation,
id: window.getGuid(),
conversationId: this.id,
destination: isPrivate ? destination : undefined,
};
const model = this.addSingleMessage(attributes);
MessageController.getInstance().register(model.id, model);
const model = await this.addSingleMessage(attributes);
const id = await model.commit();
model.set({ id });
if (this.isPrivate()) {
model.set({ destination });
}
if (this.isPublic()) {
await model.setServerTimestamp(new Date().getTime());
}
window.Whisper.events.trigger('messageAdded', {
conversationKey: this.id,
messageModel: model,
});
this.set({
lastMessage: model.getNotificationText(),
lastMessageStatus: 'sending',
@ -912,7 +875,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
public async updateExpirationTimer(
providedExpireTimer: any,
providedSource?: string,
receivedAt?: number,
receivedAt?: number, // is set if it comes from outside
options: any = {}
) {
let expireTimer = providedExpireTimer;
@ -936,6 +899,8 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
source,
});
const isOutgoing = Boolean(receivedAt);
source = source || UserUtils.getOurPubKeyStrFromCache();
// When we add a disappearing messages notification to the conversation, we want it
@ -943,9 +908,8 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
const timestamp = (receivedAt || Date.now()) - 1;
this.set({ expireTimer });
await this.commit();
const message = new MessageModel({
const messageAttributes = {
// 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: true,
@ -961,23 +925,14 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
fromGroupUpdate: options.fromGroupUpdate,
},
expireTimer: 0,
type: 'incoming',
});
type: isOutgoing ? 'outgoing' : ('incoming' as MessageModelType),
destination: this.id,
recipients: isOutgoing ? this.getRecipients() : undefined,
};
message.set({ destination: this.id });
if (message.isOutgoing()) {
message.set({ recipients: this.getRecipients() });
}
const id = await message.commit();
message.set({ id });
window.Whisper.events.trigger('messageAdded', {
conversationKey: this.id,
messageModel: message,
});
const message = await this.addSingleMessage(messageAttributes);
// tell the UI this conversation was updated
await this.commit();
// if change was made remotely, don't send it to the number/group
@ -991,7 +946,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
}
const expireUpdate = {
identifier: id,
identifier: message.id,
timestamp,
expireTimer,
profileKey,
@ -1008,13 +963,16 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return message.sendSyncMessageOnly(expirationTimerMessage);
}
if (this.get('type') === 'private') {
if (this.isPrivate()) {
const expirationTimerMessage = new ExpirationTimerUpdateMessage(
expireUpdate
);
const pubkey = new PubKey(this.get('id'));
await getMessageQueue().sendToPubKey(pubkey, expirationTimerMessage);
} else {
window.log.warn(
'TODO: Expiration update for closed groups are to be updated'
);
const expireUpdateForGroup = {
...expireUpdate,
groupId: this.get('id'),
@ -1023,24 +981,12 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
const expirationTimerMessage = new ExpirationTimerUpdateMessage(
expireUpdateForGroup
);
// special case when we are the only member of a closed group
const ourNumber = UserUtils.getOurPubKeyStrFromCache();
if (
this.get('members').length === 1 &&
this.get('members')[0] === ourNumber
) {
return message.sendSyncMessageOnly(expirationTimerMessage);
}
await getMessageQueue().sendToGroup(expirationTimerMessage);
}
return message;
}
public isSearchable() {
return !this.get('left');
}
public async commit() {
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: ConversationModel,
@ -1048,27 +994,33 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
this.trigger('change', this);
}
public async addMessage(messageAttributes: MessageAttributesOptionals) {
public async addSingleMessage(
messageAttributes: MessageAttributesOptionals,
setToExpire = true
) {
const model = new MessageModel(messageAttributes);
const messageId = await model.commit();
model.set({ id: messageId });
if (setToExpire) {
await model.setToExpire();
}
MessageController.getInstance().register(messageId, model);
window.Whisper.events.trigger('messageAdded', {
conversationKey: this.id,
messageModel: model,
});
return model;
}
public async leaveGroup() {
if (this.get('type') !== ConversationType.GROUP) {
window.log.error('Cannot leave a non-group conversation');
return;
}
if (this.isMediumGroup()) {
await leaveClosedGroup(this.id);
} else {
window.log.error('Cannot leave a non-medium group conversation');
throw new Error(
'Legacy group are not supported anymore. You need to create this group again.'
);

View File

@ -11,7 +11,7 @@ import {
DataMessage,
OpenGroupMessage,
} from '../../ts/session/messages/outgoing';
import { ClosedGroupChatMessage } from '../../ts/session/messages/outgoing/content/data/group';
import { ClosedGroupChatMessage } from '../../ts/session/messages/outgoing/content/data/group/ClosedGroupChatMessage';
import { EncryptionType, PubKey } from '../../ts/session/types';
import { ToastUtils, UserUtils } from '../../ts/session/utils';
import {
@ -41,9 +41,6 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
);
}
this.on('destroy', this.onDestroy);
this.on('change:expirationStartTimestamp', this.setToExpire);
this.on('change:expireTimer', this.setToExpire);
// this.on('expired', this.onExpired);
void this.setToExpire();
autoBind(this);
@ -674,7 +671,9 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
? [this.get('source')]
: _.union(
this.get('sent_to') || [],
this.get('recipients') || this.getConversation().getRecipients()
this.get('recipients') ||
this.getConversation()?.getRecipients() ||
[]
);
// This will make the error message for outgoing key errors a bit nicer
@ -750,8 +749,20 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
resolve: async () => {
const source = this.get('source');
const conversation = this.getConversation();
if (!conversation) {
window.log.info(
'cannot ban user, the corresponding conversation was not found.'
);
return;
}
const channelAPI = await conversation.getPublicSendData();
if (!channelAPI) {
window.log.info(
'cannot ban user, the corresponding channelAPI was not found.'
);
return;
}
const success = await channelAPI.banUser(source);
if (success) {
@ -805,7 +816,8 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
const conversation = this.getConversation();
const openGroup =
conversation && conversation.isPublic() && conversation.toOpenGroup();
(conversation && conversation.isPublic() && conversation.toOpenGroup()) ||
undefined;
const { AttachmentUtils } = Utils;
const [attachments, preview, quote] = await Promise.all([
@ -836,9 +848,12 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
await this.commit();
try {
const conversation = this.getConversation();
const intendedRecipients = this.get('recipients') || [];
const successfulRecipients = this.get('sent_to') || [];
const currentRecipients = conversation.getRecipients();
if (!conversation) {
window.log.info(
'cannot retry send message, the corresponding conversation was not found.'
);
return;
}
if (conversation.isPublic()) {
const openGroup = {
@ -858,19 +873,8 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
return getMessageQueue().sendToGroup(openGroupMessage);
}
let recipients = _.intersection(intendedRecipients, currentRecipients);
recipients = recipients.filter(
key => !successfulRecipients.includes(key)
);
if (!recipients.length) {
window.log.warn('retrySend: Nobody to send to!');
return this.commit();
}
const { body, attachments, preview, quote } = await this.uploadData();
const ourNumber = window.storage.get('primaryDevicePubKey');
const ourNumber = UserUtils.getOurPubKeyStrFromCache();
const ourConversation = ConversationController.getInstance().get(
ourNumber
);
@ -893,86 +897,33 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
const chatMessage = new ChatMessage(chatParams);
// Special-case the self-send case - we send only a sync message
if (recipients.length === 1) {
const isOurDevice = UserUtils.isUsFromCache(recipients[0]);
if (isOurDevice) {
return this.sendSyncMessageOnly(chatMessage);
}
}
if (conversation.isPrivate()) {
const [number] = recipients;
const recipientPubKey = new PubKey(number);
return getMessageQueue().sendToPubKey(recipientPubKey, chatMessage);
}
// TODO should we handle medium groups message here too?
// Not sure there is the concept of retrySend for those
const closedGroupChatMessage = new ClosedGroupChatMessage({
identifier: this.id,
chatMessage,
groupId: this.get('conversationId'),
});
// Because this is a partial group send, we send the message with the groupId field set, but individually
// to each recipient listed
return Promise.all(
recipients.map(async r => {
const recipientPubKey = new PubKey(r);
return getMessageQueue().sendToPubKey(
recipientPubKey,
closedGroupChatMessage
);
})
);
} catch (e) {
await this.saveErrors(e);
return null;
}
}
// Called when the user ran into an error with a specific user, wants to send to them
public async resend(number: string) {
const error = this.removeOutgoingErrors(number);
if (!error) {
window.log.warn('resend: requested number was not present in errors');
return null;
}
try {
const { body, attachments, preview, quote } = await this.uploadData();
const chatMessage = new ChatMessage({
identifier: this.id,
body,
timestamp: this.get('sent_at') || Date.now(),
expireTimer: this.get('expireTimer'),
attachments,
preview,
quote,
});
// Special-case the self-send case - we send only a sync message
if (UserUtils.isUsFromCache(number)) {
if (conversation.isMe()) {
return this.sendSyncMessageOnly(chatMessage);
}
const conversation = this.getConversation();
const recipientPubKey = new PubKey(number);
if (conversation.isPrivate()) {
return getMessageQueue().sendToPubKey(recipientPubKey, chatMessage);
return getMessageQueue().sendToPubKey(
PubKey.cast(conversation.id),
chatMessage
);
}
// Here, the convo is neither an open group, a private convo or ourself. It can only be a medium group.
// For a medium group, retry send only means trigger a send again to all recipients
// as they are all polling from the same group swarm pubkey
if (!conversation.isMediumGroup()) {
throw new Error(
'We should only end up with a medium group here. Anything else is an error'
);
}
const closedGroupChatMessage = new ClosedGroupChatMessage({
identifier: this.id,
chatMessage,
groupId: this.get('conversationId'),
});
// resend tries to send the message to that specific user only in the context of a closed group
return getMessageQueue().sendToPubKey(
recipientPubKey,
closedGroupChatMessage
);
return getMessageQueue().sendToGroup(closedGroupChatMessage);
} catch (e) {
await this.saveErrors(e);
return null;
@ -1085,9 +1036,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
await this.commit();
this.getConversation().updateLastMessage();
this.trigger('sent', this);
this.getConversation()?.updateLastMessage();
}
public async handleMessageSentFailure(sentMessage: any, error: any) {
@ -1113,8 +1062,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
});
await this.commit();
this.getConversation().updateLastMessage();
this.trigger('done');
this.getConversation()?.updateLastMessage();
}
public getConversation() {

View File

@ -13,6 +13,8 @@ export type MessageDeliveryStatus =
| 'error';
export interface MessageAttributes {
// the id of the message
// this can have several uses:
id: string;
source: string;
quote?: any;

View File

@ -51,7 +51,7 @@ export class ConversationController {
);
}
// Needed for some model setup which happens during the initial fetch() call below
public getUnsafe(id: string) {
public getUnsafe(id: string): ConversationModel | undefined {
return this.conversations.get(id);
}

View File

@ -194,7 +194,7 @@ export async function addUpdateMessage(
const unread = type === 'incoming';
const message = await convo.addMessage({
const message = await convo.addSingleMessage({
conversationId: convo.get('id'),
type,
sent_at: now,
@ -340,7 +340,7 @@ export async function leaveClosedGroup(groupId: string) {
convo.set({ groupAdmins: admins });
await convo.commit();
const dbMessage = await convo.addMessage({
const dbMessage = await convo.addSingleMessage({
group_update: { left: 'You' },
conversationId: groupId,
type: 'outgoing',

View File

@ -1,4 +1,3 @@
export * from './ClosedGroupChatMessage';
export * from './ClosedGroupEncryptionPairMessage';
export * from './ClosedGroupNewMessage';
export * from './ClosedGroupAddedMembersMessage';

View File

@ -56,7 +56,6 @@ export interface ConversationType {
index?: number;
activeAt?: number;
timestamp: number;
lastMessage?: {
status: 'error' | 'sending' | 'sent' | 'delivered' | 'read';
text: string;
@ -443,15 +442,26 @@ function sortMessages(
isPublic: boolean
): Array<MessageTypeInConvo> {
// we order by serverTimestamp for public convos
// be sure to update the sorting order to fetch messages from the DB too at getMessagesByConversation
if (isPublic) {
return messages.sort(
(a: any, b: any) =>
b.attributes.serverTimestamp - a.attributes.serverTimestamp
);
}
return messages.sort(
(a: any, b: any) => b.attributes.timestamp - a.attributes.timestamp
if (messages.some(n => !n.attributes.sent_at && !n.attributes.received_at)) {
throw new Error('Found some messages without any timestamp set');
}
// for non public convos, we order by sent_at or received_at timestamp.
// we assume that a message has either a sent_at or a received_at field set.
const messagesSorted = messages.sort(
(a: any, b: any) =>
(b.attributes.sent_at || b.attributes.received_at) -
(a.attributes.sent_at || a.attributes.received_at)
);
return messagesSorted;
}
function handleMessageAdded(
@ -488,12 +498,13 @@ function handleMessageChanged(
state: ConversationsStateType,
action: MessageChangedActionType
) {
const { payload } = action;
const messageInStoreIndex = state?.messages?.findIndex(
m => m.id === action.payload.id
m => m.id === payload.id
);
if (messageInStoreIndex >= 0) {
const changedMessage = _.pick(
action.payload as any,
payload as any,
toPickFromMessageModel
) as MessageTypeInConvo;
// we cannot edit the array directly, so slice the first part, insert our edited message, and slice the second part
@ -503,7 +514,10 @@ function handleMessageChanged(
...state.messages.slice(messageInStoreIndex + 1),
];
const convo = state.conversationLookup[payload.get('conversationId')];
const isPublic = convo?.isPublic || false;
// reorder the messages depending on the timestamp (we might have an updated serverTimestamp now)
const sortedMessage = sortMessages(editedMessages, isPublic);
const updatedWithFirstMessageOfSeries = updateFirstMessageOfSeries(
editedMessages
);

View File

@ -142,7 +142,7 @@ export const _getLeftPaneLists = (
}
// Show loading icon while fetching messages
if (conversation.isPublic && !conversation.timestamp) {
if (conversation.isPublic && !conversation.activeAt) {
conversation.lastMessage = {
status: 'sending',
text: '',

View File

@ -13,9 +13,8 @@ describe('state/selectors/conversations', () => {
const data: ConversationLookupType = {
id1: {
id: 'id1',
activeAt: Date.now(),
activeAt: 0,
name: 'No timestamp',
timestamp: 0,
phoneNumber: 'notused',
type: 'direct',
@ -30,9 +29,8 @@ describe('state/selectors/conversations', () => {
},
id2: {
id: 'id2',
activeAt: Date.now(),
activeAt: 20,
name: 'B',
timestamp: 20,
phoneNumber: 'notused',
type: 'direct',
@ -47,9 +45,8 @@ describe('state/selectors/conversations', () => {
},
id3: {
id: 'id3',
activeAt: Date.now(),
activeAt: 20,
name: 'C',
timestamp: 20,
phoneNumber: 'notused',
type: 'direct',
@ -64,9 +61,8 @@ describe('state/selectors/conversations', () => {
},
id4: {
id: 'id4',
activeAt: Date.now(),
activeAt: 20,
name: 'Á',
timestamp: 20,
phoneNumber: 'notused',
type: 'direct',
isMe: false,
@ -80,9 +76,8 @@ describe('state/selectors/conversations', () => {
},
id5: {
id: 'id5',
activeAt: Date.now(),
activeAt: 30,
name: 'First!',
timestamp: 30,
phoneNumber: 'notused',
type: 'direct',
isMe: false,

View File

@ -5,7 +5,7 @@ import {
import { v4 as uuid } from 'uuid';
import { OpenGroup } from '../../../session/types';
import { generateFakePubKey, generateFakePubKeys } from './pubkey';
import { ClosedGroupChatMessage } from '../../../session/messages/outgoing/content/data/group';
import { ClosedGroupChatMessage } from '../../../session/messages/outgoing/content/data/group/ClosedGroupChatMessage';
import { ConversationAttributes } from '../../../models/conversation';
export function generateChatMessage(identifier?: string): ChatMessage {