Merge pull request #1423 from Bilb/fix-conversation-message-list

Fix conversation message list
pull/1435/head
Audric Ackermann 5 years ago committed by GitHub
commit 62e637857b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -325,6 +325,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
messageProps.i18n = window.i18n; messageProps.i18n = window.i18n;
messageProps.selected = selected; messageProps.selected = selected;
messageProps.firstMessageOfSeries = firstMessageOfSeries; messageProps.firstMessageOfSeries = firstMessageOfSeries;
messageProps.multiSelectMode = multiSelectMode; messageProps.multiSelectMode = multiSelectMode;
messageProps.onSelectMessage = this.props.selectMessage; messageProps.onSelectMessage = this.props.selectMessage;
messageProps.onDeleteMessage = this.props.deleteMessage; messageProps.onDeleteMessage = this.props.deleteMessage;

@ -1,336 +1,339 @@
import { import {
ConversationAttributes, ConversationAttributes,
ConversationModel, ConversationModel,
} from '../../../js/models/conversations'; } from '../../../js/models/conversations';
import { BlockedNumberController } from '../../util'; import { BlockedNumberController } from '../../util';
// It's not only data from the db which is stored on the MessageController entries, we could fetch this again. What we cannot fetch from the db and which is stored here is all listeners a particular messages is linked to for instance. We will be able to get rid of this once we don't use backbone models at all // It's not only data from the db which is stored on the MessageController entries, we could fetch this again. What we cannot fetch from the db and which is stored here is all listeners a particular messages is linked to for instance. We will be able to get rid of this once we don't use backbone models at all
export class ConversationController { export class ConversationController {
private static instance: ConversationController | null; private static instance: ConversationController | null;
private readonly conversations: any; private readonly conversations: any;
private _initialFetchComplete: boolean = false; private _initialFetchComplete: boolean = false;
private _initialPromise?: Promise<any>; private _initialPromise?: Promise<any>;
private constructor() { private constructor() {
this.conversations = new window.Whisper.ConversationCollection(); this.conversations = new window.Whisper.ConversationCollection();
}
public static getInstance() {
if (ConversationController.instance) {
return ConversationController.instance;
} }
ConversationController.instance = new ConversationController();
public static getInstance() { return ConversationController.instance;
if (ConversationController.instance) { }
return ConversationController.instance;
} public get(id: string): ConversationModel {
ConversationController.instance = new ConversationController(); if (!this._initialFetchComplete) {
return ConversationController.instance; throw new Error(
'ConversationController.get() needs complete initial fetch'
);
} }
public get(id: string): ConversationModel { return this.conversations.get(id);
if (!this._initialFetchComplete) { }
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
}
return this.conversations.get(id); public getOrThrow(id: string) {
if (!this._initialFetchComplete) {
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
} }
public getOrThrow(id: string) { const convo = this.conversations.get(id);
if (!this._initialFetchComplete) {
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
}
const convo = this.conversations.get(id); if (convo) {
return convo;
}
throw new Error(
`Conversation ${id} does not exist on ConversationController.get()`
);
}
// Needed for some model setup which happens during the initial fetch() call below
public getUnsafe(id: string) {
return this.conversations.get(id);
}
public dangerouslyCreateAndAdd(attributes: ConversationAttributes) {
return this.conversations.add(attributes);
}
public getOrCreate(id: string, type: string) {
if (typeof id !== 'string') {
throw new TypeError("'id' must be a string");
}
if (convo) { if (type !== 'private' && type !== 'group') {
return convo; throw new TypeError(
} `'type' must be 'private' or 'group'; got: '${type}'`
throw new Error( );
`Conversation ${id} does not exist on ConversationController.get()`
);
} }
// Needed for some model setup which happens during the initial fetch() call below
public getUnsafe(id: string) { if (!this._initialFetchComplete) {
return this.conversations.get(id); throw new Error(
'ConversationController.get() needs complete initial fetch'
);
} }
public dangerouslyCreateAndAdd(attributes: ConversationAttributes) { let conversation = this.conversations.get(id);
return this.conversations.add(attributes); if (conversation) {
return conversation;
} }
public getOrCreate(id: string, type: string) { conversation = this.conversations.add({
if (typeof id !== 'string') { id,
throw new TypeError("'id' must be a string"); type,
} version: 2,
} as any);
if (type !== 'private' && type !== 'group') {
throw new TypeError( const create = async () => {
`'type' must be 'private' or 'group'; got: '${type}'` if (!conversation.isValid()) {
); const validationError = conversation.validationError || {};
} window.log.error(
'Contact is not valid. Not saving, but adding to collection:',
if (!this._initialFetchComplete) { conversation.idForLogging(),
throw new Error( validationError.stack
'ConversationController.get() needs complete initial fetch' );
);
}
let conversation = this.conversations.get(id);
if (conversation) {
return conversation;
}
conversation = this.conversations.add({
id,
type,
version: 2,
} as any);
const create = async () => {
if (!conversation.isValid()) {
const validationError = conversation.validationError || {};
window.log.error(
'Contact is not valid. Not saving, but adding to collection:',
conversation.idForLogging(),
validationError.stack
);
return conversation;
}
try {
await window.Signal.Data.saveConversation(conversation.attributes, {
Conversation: window.Whisper.Conversation,
});
} catch (error) {
window.log.error(
'Conversation save failed! ',
id,
type,
'Error:',
error && error.stack ? error.stack : error
);
throw error;
}
return conversation;
};
conversation.initialPromise = create();
conversation.initialPromise.then(async () => {
if (!conversation.isPublic() && !conversation.isRss()) {
await Promise.all([
conversation.updateProfileAvatar(),
// NOTE: we request snodes updating the cache, but ignore the result
window.SnodePool.getSnodesFor(id),
]);
}
if (window.inboxStore) {
conversation.on('change', this.updateReduxConvoChanged);
window.inboxStore.dispatch(
window.actionsCreators.conversationAdded(
conversation.id,
conversation.getProps()
)
);
}
});
return conversation; return conversation;
} }
public getContactProfileNameOrShortenedPubKey(pubKey: string): string { try {
const conversation = ConversationController.getInstance().get(pubKey); await window.Signal.Data.saveConversation(conversation.attributes, {
if (!conversation) { Conversation: window.Whisper.Conversation,
return pubKey; });
} } catch (error) {
return conversation.getContactProfileNameOrShortenedPubKey(); window.log.error(
'Conversation save failed! ',
id,
type,
'Error:',
error && error.stack ? error.stack : error
);
throw error;
}
return conversation;
};
conversation.initialPromise = create();
conversation.initialPromise.then(async () => {
if (window.inboxStore) {
conversation.on('change', this.updateReduxConvoChanged);
window.inboxStore.dispatch(
window.actionsCreators.conversationAdded(
conversation.id,
conversation.getProps()
)
);
}
if (!conversation.isPublic() && !conversation.isRss()) {
await Promise.all([
conversation.updateProfileAvatar(),
// NOTE: we request snodes updating the cache, but ignore the result
window.SnodePool.getSnodesFor(id),
]);
}
});
return conversation;
}
public getContactProfileNameOrShortenedPubKey(pubKey: string): string {
const conversation = ConversationController.getInstance().get(pubKey);
if (!conversation) {
return pubKey;
} }
return conversation.getContactProfileNameOrShortenedPubKey();
}
public getContactProfileNameOrFullPubKey(pubKey: string): string { public getContactProfileNameOrFullPubKey(pubKey: string): string {
const conversation = this.conversations.get(pubKey); const conversation = this.conversations.get(pubKey);
if (!conversation) { if (!conversation) {
return pubKey; return pubKey;
}
return conversation.getContactProfileNameOrFullPubKey();
} }
return conversation.getContactProfileNameOrFullPubKey();
}
public isMediumGroup(hexEncodedGroupPublicKey: string): boolean { public isMediumGroup(hexEncodedGroupPublicKey: string): boolean {
const convo = this.conversations.get(hexEncodedGroupPublicKey); const convo = this.conversations.get(hexEncodedGroupPublicKey);
if (convo) { if (convo) {
return convo.isMediumGroup(); return convo.isMediumGroup();
}
return false;
} }
return false;
public async getOrCreateAndWait(id: any, type: string) { }
const initialPromise = this._initialPromise !== undefined ? this._initialPromise : Promise.resolve();
return initialPromise.then(() => { public async getOrCreateAndWait(id: any, type: string) {
if (!id) { const initialPromise =
return Promise.reject( this._initialPromise !== undefined
new Error('getOrCreateAndWait: invalid id passed.') ? this._initialPromise
); : Promise.resolve();
} return initialPromise.then(() => {
const pubkey = id && id.key ? id.key : id; if (!id) {
const conversation = this.getOrCreate(pubkey, type); return Promise.reject(
new Error('getOrCreateAndWait: invalid id passed.')
if (conversation) { );
return conversation.initialPromise.then(() => conversation); }
} const pubkey = id && id.key ? id.key : id;
const conversation = this.getOrCreate(pubkey, type);
return Promise.reject(
new Error('getOrCreateAndWait: did not get conversation') if (conversation) {
); return conversation.initialPromise.then(() => conversation);
}); }
return Promise.reject(
new Error('getOrCreateAndWait: did not get conversation')
);
});
}
public async getAllGroupsInvolvingId(id: String) {
const groups = await window.Signal.Data.getAllGroupsInvolvingId(id, {
ConversationCollection: window.Whisper.ConversationCollection,
});
return groups.map((group: any) => this.conversations.add(group));
}
public async deleteContact(id: string) {
if (typeof id !== 'string') {
throw new TypeError("'id' must be a string");
} }
public async getAllGroupsInvolvingId(id: String) { if (!this._initialFetchComplete) {
const groups = await window.Signal.Data.getAllGroupsInvolvingId(id, { throw new Error(
ConversationCollection: window.Whisper.ConversationCollection, 'ConversationController.get() needs complete initial fetch'
}); );
return groups.map((group: any) => this.conversations.add(group));
} }
public async deleteContact(id: string) { const conversation = this.conversations.get(id);
if (typeof id !== 'string') { if (!conversation) {
throw new TypeError("'id' must be a string"); return;
}
if (!this._initialFetchComplete) {
throw new Error(
'ConversationController.get() needs complete initial fetch'
);
}
const conversation = this.conversations.get(id);
if (!conversation) {
return;
}
// Close group leaving
if (conversation.isClosedGroup()) {
await conversation.leaveGroup();
} else if (conversation.isPublic()) {
const channelAPI = await conversation.getPublicSendData();
if (channelAPI === null) {
window.log.warn(`Could not get API for public conversation ${id}`);
} else {
channelAPI.serverAPI.partChannel(channelAPI.channelId);
}
} else if (conversation.isPrivate()) {
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds(
id
);
await Promise.all(
deviceIds.map((deviceId: string) => {
const address = new window.libsignal.SignalProtocolAddress(
id,
deviceId
);
const sessionCipher = new window.libsignal.SessionCipher(
window.textsecure.storage.protocol,
address
);
return sessionCipher.deleteAllSessionsForDevice();
})
);
}
await conversation.destroyMessages();
await window.Signal.Data.removeConversation(id, {
Conversation: window.Whisper.Conversation,
});
conversation.off('change', this.updateReduxConvoChanged);
this.conversations.remove(conversation);
if (window.inboxStore) {
window.inboxStore.dispatch(
window.actionsCreators.conversationRemoved(conversation.id)
);
}
} }
public getConversations(): Array<ConversationModel> { // Close group leaving
return Array.from(this.conversations.models.values()); if (conversation.isClosedGroup()) {
await conversation.leaveGroup();
} else if (conversation.isPublic()) {
const channelAPI = await conversation.getPublicSendData();
if (channelAPI === null) {
window.log.warn(`Could not get API for public conversation ${id}`);
} else {
channelAPI.serverAPI.partChannel(channelAPI.channelId);
}
} else if (conversation.isPrivate()) {
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds(
id
);
await Promise.all(
deviceIds.map((deviceId: string) => {
const address = new window.libsignal.SignalProtocolAddress(
id,
deviceId
);
const sessionCipher = new window.libsignal.SessionCipher(
window.textsecure.storage.protocol,
address
);
return sessionCipher.deleteAllSessionsForDevice();
})
);
} }
public async load() { await conversation.destroyMessages();
window.log.info('ConversationController: starting initial fetch');
await window.Signal.Data.removeConversation(id, {
if (this.conversations.length) { Conversation: window.Whisper.Conversation,
throw new Error('ConversationController: Already loaded!'); });
} conversation.off('change', this.updateReduxConvoChanged);
this.conversations.remove(conversation);
const load = async () => { if (window.inboxStore) {
try { window.inboxStore.dispatch(
const collection = await window.Signal.Data.getAllConversations({ window.actionsCreators.conversationRemoved(conversation.id)
ConversationCollection: window.Whisper.ConversationCollection, );
});
this.conversations.add(collection.models);
this._initialFetchComplete = true;
const promises: any = [];
this.conversations.forEach((conversation: ConversationModel) => {
if (!conversation.get('lastMessage')) {
// tslint:disable-next-line: no-void-expression
promises.push(conversation.updateLastMessage());
}
promises.concat([
conversation.updateProfileName(),
conversation.updateProfileAvatar(),
]);
});
this.conversations.forEach((conversation: ConversationModel) => {
// register for change event on each conversation, and forward to redux
conversation.on('change', this.updateReduxConvoChanged);
});
await Promise.all(promises);
// Remove any unused images
window.profileImages.removeImagesNotInArray(
this.conversations.map((c: any) => c.id)
);
window.log.info('ConversationController: done with initial fetch');
} catch (error) {
window.log.error(
'ConversationController: initial fetch failed',
error && error.stack ? error.stack : error
);
throw error;
}
};
await BlockedNumberController.load();
this._initialPromise = load();
return this._initialPromise;
} }
}
public loadPromise() { public getConversations(): Array<ConversationModel> {
return this._initialPromise; return Array.from(this.conversations.models.values());
} }
public reset() {
this._initialPromise = Promise.resolve(); public async load() {
this._initialFetchComplete = false; window.log.info('ConversationController: starting initial fetch');
if (window.inboxStore) {
this.conversations.forEach((convo: ConversationModel) => if (this.conversations.length) {
convo.off('change', this.updateReduxConvoChanged) throw new Error('ConversationController: Already loaded!');
);
window.inboxStore.dispatch(
window.actionsCreators.removeAllConversations()
);
}
this.conversations.reset([]);
} }
private updateReduxConvoChanged(convo: ConversationModel) { const load = async () => {
if (window.inboxStore) { try {
window.inboxStore.dispatch( const collection = await window.Signal.Data.getAllConversations({
window.actionsCreators.conversationChanged(convo.id, convo.getProps()) ConversationCollection: window.Whisper.ConversationCollection,
); });
}
this.conversations.add(collection.models);
this._initialFetchComplete = true;
const promises: any = [];
this.conversations.forEach((conversation: ConversationModel) => {
if (!conversation.get('lastMessage')) {
// tslint:disable-next-line: no-void-expression
promises.push(conversation.updateLastMessage());
}
promises.concat([
conversation.updateProfileName(),
conversation.updateProfileAvatar(),
]);
});
this.conversations.forEach((conversation: ConversationModel) => {
// register for change event on each conversation, and forward to redux
conversation.on('change', this.updateReduxConvoChanged);
});
await Promise.all(promises);
// Remove any unused images
window.profileImages.removeImagesNotInArray(
this.conversations.map((c: any) => c.id)
);
window.log.info('ConversationController: done with initial fetch');
} catch (error) {
window.log.error(
'ConversationController: initial fetch failed',
error && error.stack ? error.stack : error
);
throw error;
}
};
await BlockedNumberController.load();
this._initialPromise = load();
return this._initialPromise;
}
public loadPromise() {
return this._initialPromise;
}
public reset() {
this._initialPromise = Promise.resolve();
this._initialFetchComplete = false;
if (window.inboxStore) {
this.conversations.forEach((convo: ConversationModel) =>
convo.off('change', this.updateReduxConvoChanged)
);
window.inboxStore.dispatch(
window.actionsCreators.removeAllConversations()
);
}
this.conversations.reset([]);
}
private updateReduxConvoChanged(convo: ConversationModel) {
if (window.inboxStore) {
window.inboxStore.dispatch(
window.actionsCreators.conversationChanged(convo.id, convo.getProps())
);
} }
}
} }

@ -4,6 +4,7 @@ import { Constants } from '../../session';
import { createAsyncThunk } from '@reduxjs/toolkit'; import { createAsyncThunk } from '@reduxjs/toolkit';
import { MessageModel } from '../../../js/models/messages'; import { MessageModel } from '../../../js/models/messages';
import { ConversationController } from '../../session/conversations'; import { ConversationController } from '../../session/conversations';
import { StateType } from '../reducer';
// State // State
@ -122,11 +123,18 @@ async function getMessages(
// Set first member of series here. // Set first member of series here.
const messageModels = messageSet.models; const messageModels = messageSet.models;
const isPublic = conversation.isPublic();
const sortedMessage = sortMessages(messageModels, isPublic);
// no need to do that `firstMessageOfSeries` on a private chat // no need to do that `firstMessageOfSeries` on a private chat
if (conversation.isPrivate()) { if (conversation.isPrivate()) {
return messageModels; return sortedMessage;
} }
return updateFirstMessageOfSeries(sortedMessage);
}
const updateFirstMessageOfSeries = (messageModels: Array<any>) => {
// messages are got from the more recent to the oldest, so we need to check if // messages are got from the more recent to the oldest, so we need to check if
// the next messages in the list is still the same author. // the next messages in the list is still the same author.
// The message is the first of the series if the next message is not from the same author // The message is the first of the series if the next message is not from the same author
@ -138,13 +146,13 @@ async function getMessages(
i < messageModels.length - 1 i < messageModels.length - 1
? messageModels[i + 1].propsForMessage?.authorPhoneNumber ? messageModels[i + 1].propsForMessage?.authorPhoneNumber
: undefined; : undefined;
if (i > 0 && currentSender === nextSender) { if (i >= 0 && currentSender === nextSender) {
firstMessageOfSeries = false; firstMessageOfSeries = false;
} }
messageModels[i].firstMessageOfSeries = firstMessageOfSeries; messageModels[i].firstMessageOfSeries = firstMessageOfSeries;
} }
return messageModels; return messageModels;
} };
const fetchMessagesForConversation = createAsyncThunk( const fetchMessagesForConversation = createAsyncThunk(
'messages/fetchByConversationKey', 'messages/fetchByConversationKey',
@ -422,6 +430,138 @@ function getEmptyState(): ConversationsStateType {
}; };
} }
function sortMessages(
messages: Array<MessageTypeInConvo>,
isPublic: boolean
): Array<MessageTypeInConvo> {
// we order by serverTimestamp for public convos
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
);
}
function handleMessageAdded(
state: ConversationsStateType,
action: MessageAddedActionType
) {
const { messages } = state;
const { conversationKey, messageModel } = action.payload;
if (conversationKey === state.selectedConversation) {
const addedMessage = _.pick(
messageModel as any,
toPickFromMessageModel
) as MessageTypeInConvo;
const messagesWithNewMessage = [...messages, addedMessage];
const convo = state.conversationLookup[state.selectedConversation];
const isPublic = convo?.isPublic || false;
if (convo) {
const sortedMessage = sortMessages(messagesWithNewMessage, isPublic);
const updatedWithFirstMessageOfSeries = updateFirstMessageOfSeries(
sortedMessage
);
return {
...state,
messages: updatedWithFirstMessageOfSeries,
};
}
}
return state;
}
function handleMessageChanged(
state: ConversationsStateType,
action: MessageChangedActionType
) {
const messageInStoreIndex = state?.messages?.findIndex(
m => m.id === action.payload.id
);
if (messageInStoreIndex >= 0) {
const changedMessage = _.pick(
action.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
const editedMessages = [
...state.messages.slice(0, messageInStoreIndex),
changedMessage,
...state.messages.slice(messageInStoreIndex + 1),
];
// reorder the messages depending on the timestamp (we might have an updated serverTimestamp now)
const updatedWithFirstMessageOfSeries = updateFirstMessageOfSeries(
editedMessages
);
return {
...state,
messages: updatedWithFirstMessageOfSeries,
};
}
return state;
}
function handleMessageExpiredOrDeleted(
state: ConversationsStateType,
action: MessageDeletedActionType | MessageExpiredActionType
) {
const { conversationKey, messageId } = action.payload;
if (conversationKey === state.selectedConversation) {
// search if we find this message id.
// we might have not loaded yet, so this case might not happen
const messageInStoreIndex = state?.messages.findIndex(
m => m.id === messageId
);
if (messageInStoreIndex >= 0) {
// we cannot edit the array directly, so slice the first part, and slice the second part,
// keeping the index removed out
const editedMessages = [
...state.messages.slice(0, messageInStoreIndex),
...state.messages.slice(messageInStoreIndex + 1),
];
const updatedWithFirstMessageOfSeries = updateFirstMessageOfSeries(
editedMessages
);
// FIXME two other thing we have to do:
// * update the last message text if the message deleted was the last one
// * update the unread count of the convo if the message was the one counted as an unread
return {
...state,
messages: updatedWithFirstMessageOfSeries,
};
}
return state;
}
return state;
}
function handleConversationReset(
state: ConversationsStateType,
action: ConversationResetActionType
) {
const { conversationKey } = action.payload;
if (conversationKey === state.selectedConversation) {
// just empty the list of messages
return {
...state,
messages: [],
};
}
return state;
}
// tslint:disable: cyclomatic-complexity // tslint:disable: cyclomatic-complexity
// tslint:disable: max-func-body-length // tslint:disable: max-func-body-length
export function reducer( export function reducer(
@ -492,6 +632,7 @@ export function reducer(
const { id } = payload; const { id } = payload;
const oldSelectedConversation = state.selectedConversation; const oldSelectedConversation = state.selectedConversation;
const newSelectedConversation = id; const newSelectedConversation = id;
if (newSelectedConversation !== oldSelectedConversation) { if (newSelectedConversation !== oldSelectedConversation) {
// empty the message list // empty the message list
return { return {
@ -505,6 +646,8 @@ export function reducer(
selectedConversation: id, selectedConversation: id,
}; };
} }
// this is called once the messages are loaded from the db for the currently selected conversation
if (action.type === fetchMessagesForConversation.fulfilled.type) { if (action.type === fetchMessagesForConversation.fulfilled.type) {
const { messages, conversationKey } = action.payload as any; const { messages, conversationKey } = action.payload as any;
// double check that this update is for the shown convo // double check that this update is for the shown convo
@ -521,102 +664,18 @@ export function reducer(
} }
if (action.type === 'MESSAGE_CHANGED') { if (action.type === 'MESSAGE_CHANGED') {
const messageInStoreIndex = state?.messages?.findIndex( return handleMessageChanged(state, action);
m => m.id === action.payload.id
);
if (messageInStoreIndex >= 0) {
const changedMessage = _.pick(
action.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
const editedMessages = [
...state.messages.slice(0, messageInStoreIndex),
changedMessage,
...state.messages.slice(messageInStoreIndex + 1),
];
return {
...state,
messages: editedMessages,
};
}
return state;
} }
if (action.type === 'MESSAGE_ADDED') { if (action.type === 'MESSAGE_ADDED') {
const { conversationKey, messageModel } = action.payload; return handleMessageAdded(state, action);
if (conversationKey === state.selectedConversation) {
const { messages } = state;
const addedMessage = _.pick(
messageModel as any,
toPickFromMessageModel
) as MessageTypeInConvo;
const messagesWithNewMessage = [...messages, addedMessage];
const convo = state.conversationLookup[state.selectedConversation];
const isPublic = convo?.isPublic;
if (convo && isPublic) {
return {
...state,
messages: messagesWithNewMessage.sort(
(a: any, b: any) =>
b.attributes.serverTimestamp - a.attributes.serverTimestamp
),
};
}
if (convo) {
return {
...state,
messages: messagesWithNewMessage.sort(
(a, b) => b.attributes.timestamp - a.attributes.timestamp
),
};
}
}
return state;
} }
if (action.type === 'MESSAGE_EXPIRED' || action.type === 'MESSAGE_DELETED') { if (action.type === 'MESSAGE_EXPIRED' || action.type === 'MESSAGE_DELETED') {
const { conversationKey, messageId } = action.payload; return handleMessageExpiredOrDeleted(state, action);
if (conversationKey === state.selectedConversation) {
// search if we find this message id.
// we might have not loaded yet, so this case might not happen
const messageInStoreIndex = state?.messages.findIndex(
m => m.id === messageId
);
if (messageInStoreIndex >= 0) {
// we cannot edit the array directly, so slice the first part, and slice the second part,
// keeping the index removed out
const editedMessages = [
...state.messages.slice(0, messageInStoreIndex),
...state.messages.slice(messageInStoreIndex + 1),
];
// FIXME two other thing we have to do:
// * update the last message text if the message deleted was the last one
// * update the unread count of the convo if the message was one one counted as an unread
return {
...state,
messages: editedMessages,
};
}
return state;
}
return state;
} }
if (action.type === 'CONVERSATION_RESET') { if (action.type === 'CONVERSATION_RESET') {
const { conversationKey } = action.payload; return handleConversationReset(state, action);
if (conversationKey === state.selectedConversation) {
// just empty the list of messages
return {
...state,
messages: [],
};
}
return state;
} }
return state; return state;

@ -9,7 +9,6 @@ const mapStateToProps = (state: StateType) => {
(conversationKey && (conversationKey &&
state.conversations.conversationLookup[conversationKey]) || state.conversations.conversationLookup[conversationKey]) ||
null; null;
return { return {
conversation, conversation,
conversationKey, conversationKey,

Loading…
Cancel
Save