From e45ce43e019d9b1e830f590f014c939de0cd9b9f Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Thu, 12 Nov 2020 12:18:50 +1100 Subject: [PATCH] store messages currently loaded in convo on redux --- js/models/messages.js | 55 +++--- js/modules/signal.js | 2 + js/views/inbox_view.js | 5 + js/views/message_view.js | 95 ----------- package.json | 1 + stylesheets/_conversation.scss | 2 +- .../conversation/SessionConversation.tsx | 159 +++--------------- .../SessionConversationMessagesList.tsx | 12 +- ts/state/actions.ts | 4 +- ts/state/createStore.ts | 15 +- ts/state/ducks/conversations.ts | 30 +--- ts/state/ducks/messages.ts | 117 ++++++++++++- ts/state/reducer.ts | 7 +- ts/state/smart/SessionConversation.tsx | 46 ++--- yarn.lock | 22 ++- 15 files changed, 233 insertions(+), 339 deletions(-) delete mode 100644 js/views/message_view.js diff --git a/js/models/messages.js b/js/models/messages.js index 444d3f80e..567f55d68 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -79,9 +79,8 @@ this.propsForSearchResult = this.getPropsForSearchResult(); this.propsForMessage = this.getPropsForMessage(); } + Whisper.events.trigger('messageChanged', this); }; - const triggerChange = () => this.trigger('change'); - this.on('change', generateProps); const applicableConversationChanges = @@ -93,7 +92,7 @@ // trigger a change event on this component. // this will call generateProps and refresh the Message.tsx component with new props - this.listenTo(conversation, 'disable:input', triggerChange); + this.listenTo(conversation, 'disable:input', () => this.trigger('change')); if (fromContact) { this.listenTo( fromContact, @@ -661,8 +660,8 @@ contact.number && contact.number[0] && contact.number[0].value; const onSendMessage = firstNumber ? () => { - this.trigger('open-conversation', firstNumber); - } + this.trigger('open-conversation', firstNumber); + } : null; const onClick = async () => { // First let's be sure that the signal account check is complete. @@ -693,8 +692,8 @@ !path && !objectUrl ? null : Object.assign({}, attachment.thumbnail || {}, { - objectUrl: path || objectUrl, - }); + objectUrl: path || objectUrl, + }); return Object.assign({}, attachment, { isVoiceMessage: Signal.Types.Attachment.isVoiceMessage(attachment), @@ -747,13 +746,13 @@ const onClick = noClick ? null : event => { - event.stopPropagation(); - this.trigger('scroll-to-message', { - author, - id, - referencedMessageNotFound, - }); - }; + event.stopPropagation(); + this.trigger('scroll-to-message', { + author, + id, + referencedMessageNotFound, + }); + }; const firstAttachment = quote.attachments && quote.attachments[0]; @@ -788,15 +787,15 @@ url: path ? getAbsoluteAttachmentPath(path) : null, screenshot: screenshot ? { - ...screenshot, - url: getAbsoluteAttachmentPath(screenshot.path), - } + ...screenshot, + url: getAbsoluteAttachmentPath(screenshot.path), + } : null, thumbnail: thumbnail ? { - ...thumbnail, - url: getAbsoluteAttachmentPath(thumbnail.path), - } + ...thumbnail, + url: getAbsoluteAttachmentPath(thumbnail.path), + } : null, }; }, @@ -825,9 +824,9 @@ const phoneNumbers = this.isIncoming() ? [this.get('source')] : _.union( - this.get('sent_to') || [], - this.get('recipients') || this.getConversation().getRecipients() - ); + this.get('sent_to') || [], + this.get('recipients') || this.getConversation().getRecipients() + ); // This will make the error message for outgoing key errors a bit nicer const allErrors = (this.get('errors') || []).map(error => { @@ -944,7 +943,6 @@ } else { convo.removeMessageSelection(this); } - this.trigger('change'); }, @@ -1278,8 +1276,8 @@ */ const hasBodyOrAttachments = Boolean( dataMessage && - (dataMessage.body || - (dataMessage.attachments && dataMessage.attachments.length)) + (dataMessage.body || + (dataMessage.attachments && dataMessage.attachments.length)) ); const shouldNotifyPushServer = hasBodyOrAttachments && isSessionOrClosedMessage; @@ -1351,7 +1349,6 @@ // unidentifiedDeliveries: result.unidentifiedDeliveries, }); await this.commit(); - this.trigger('change', this); this.getConversation().updateLastMessage(); @@ -1442,7 +1439,7 @@ return null; }, async setCalculatingPoW() { - if (this.calculatingPoW) { + if (this.get('calculatingPoW')) { return; } @@ -1616,6 +1613,8 @@ forceSave, Message: Whisper.Message, }); + console.warn('case commit') + this.trigger('change'); return id; }, diff --git a/js/modules/signal.js b/js/modules/signal.js index 705bc015d..c70c9b17d 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -114,6 +114,7 @@ const { const { createStore } = require('../../ts/state/createStore'); const conversationsDuck = require('../../ts/state/ducks/conversations'); const userDuck = require('../../ts/state/ducks/user'); +const messagesDuck = require('../../ts/state/ducks/messages'); // Migrations const { @@ -293,6 +294,7 @@ exports.setup = (options = {}) => { const Ducks = { conversations: conversationsDuck, user: userDuck, + messages: messagesDuck, }; const State = { bindActionCreators, diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index fcfa39c94..24ef179f8 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -143,6 +143,10 @@ Signal.State.Ducks.user.actions, this.store.dispatch ); + const { messageChanged } = Signal.State.bindActionCreators( + Signal.State.Ducks.messages.actions, + this.store.dispatch + ); this.openConversationAction = openConversationExternal; @@ -175,6 +179,7 @@ .events.addListener('fail', this.handleMessageSentFailure); Whisper.events.on('messageExpired', messageExpired); + Whisper.events.on('messageChanged', messageChanged); Whisper.events.on('userChanged', userChanged); // Finally, add it to the DOM diff --git a/js/views/message_view.js b/js/views/message_view.js deleted file mode 100644 index 0dbeeff96..000000000 --- a/js/views/message_view.js +++ /dev/null @@ -1,95 +0,0 @@ -/* global Whisper: false */ - -// eslint-disable-next-line func-names -(function() { - 'use strict'; - - window.Whisper = window.Whisper || {}; - - Whisper.MessageView = Whisper.View.extend({ - tagName: 'li', - id() { - return this.model.id; - }, - initialize() { - this.listenTo(this.model, 'change', this.onChange); - this.listenTo(this.model, 'destroy', this.onDestroy); - this.listenTo(this.model, 'unload', this.onUnload); - this.listenTo(this.model, 'expired', this.onExpired); - }, - onChange() { - this.addId(); - }, - addId() { - // The ID is important for other items inserting themselves into the DOM. Because - // of ReactWrapperView and this view, there are two layers of DOM elements - // between the parent and the elements returned by the React component, so this is - // necessary. - const { id } = this.model; - this.$el.attr('id', id); - }, - onExpired() { - setTimeout(() => this.onUnload(), 1000); - }, - onUnload() { - if (this.childView) { - this.childView.remove(); - } - - this.remove(); - }, - onDestroy() { - this.onUnload(); - }, - getRenderInfo() { - return null; - }, - render() { - this.addId(); - - if (this.childView) { - this.childView.remove(); - this.childView = null; - } - - const { Component, props } = this.getRenderInfo(); - this.childView = new Whisper.ReactWrapperView({ - className: 'message-wrapper', - Component, - props, - }); - - const update = () => { - const info = this.getRenderInfo(); - this.childView.update(info.props); - }; - - this.listenTo(this.model, 'change', update); - this.listenTo(this.model, 'expired', update); - - const applicableConversationChanges = - 'change:color change:name change:number change:profileName change:profileAvatar'; - - this.conversation = this.model.getConversation(); - this.listenTo(this.conversation, applicableConversationChanges, update); - - this.fromContact = this.model.getIncomingContact(); - if (this.fromContact) { - this.listenTo(this.fromContact, applicableConversationChanges, update); - } - - this.quotedContact = this.model.getQuoteContact(); - if (this.quotedContact) { - this.listenTo( - this.quotedContact, - applicableConversationChanges, - update - ); - } - - this.$el.append(this.childView.el); - - return this; - }, - }); -})(); diff --git a/package.json b/package.json index bf228e07f..494b3d30b 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ }, "dependencies": { "@journeyapps/sqlcipher": "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#b10f232fac62ba7f8775c9e086bb5558fe7d948b", + "@reduxjs/toolkit": "^1.4.0", "@sindresorhus/is": "0.8.0", "@types/dompurify": "^2.0.0", "@types/emoji-mart": "^2.11.3", diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss index b87137734..4c6057196 100644 --- a/stylesheets/_conversation.scss +++ b/stylesheets/_conversation.scss @@ -4,7 +4,7 @@ overflow-y: auto; } -.message-container{ +.message-container { list-style: none; li { diff --git a/ts/components/session/conversation/SessionConversation.tsx b/ts/components/session/conversation/SessionConversation.tsx index 24bebc188..5a82d61fe 100644 --- a/ts/components/session/conversation/SessionConversation.tsx +++ b/ts/components/session/conversation/SessionConversation.tsx @@ -41,7 +41,6 @@ interface State { sendingProgressStatus: -1 | 0 | 1 | 2; unreadCount: number; - initialFetchComplete: boolean; selectedMessages: Array; isScrolledToBottom: boolean; displayScrollToBottomButton: boolean; @@ -93,8 +92,6 @@ export class SessionConversation extends React.Component { prevSendingProgress: 0, sendingProgressStatus: 0, unreadCount, - initialFetchComplete: false, - messages: [], selectedMessages: [], isScrolledToBottom: !unreadCount, displayScrollToBottomButton: false, @@ -131,8 +128,6 @@ export class SessionConversation extends React.Component { this.replyToMessage = this.replyToMessage.bind(this); this.onClickAttachment = this.onClickAttachment.bind(this); this.downloadAttachment = this.downloadAttachment.bind(this); - this.refreshMessages = this.refreshMessages.bind(this); - this.getMessages = this.getMessages.bind(this); // Keyboard navigation this.onKeyDown = this.onKeyDown.bind(this); @@ -159,15 +154,6 @@ export class SessionConversation extends React.Component { } public componentWillUnmount() { - const { conversationKey } = this.props; - try { - const conversationModel = window.ConversationController.getOrThrow( - conversationKey - ); - conversationModel.off('change', this.refreshMessages); - } catch (e) { - window.log.error(e); - } const div = this.messageContainerRef.current; div?.removeEventListener('dragenter', this.handleDragIn); div?.removeEventListener('dragleave', this.handleDragOut); @@ -175,34 +161,8 @@ export class SessionConversation extends React.Component { div?.removeEventListener('drop', this.handleDrop); } - public componentDidUpdate(prevProps: Props, prevState: State) { - const { conversationKey: oldKey } = prevProps; - try { - const oldConversationModel = window.ConversationController.getOrThrow( - oldKey - ); - oldConversationModel.off('change', this.refreshMessages); - } catch (e) { - window.log.warn(e); - } - try { - const { conversationKey: newKey } = this.props; - const newConversationModel = window.ConversationController.getOrThrow( - newKey - ); - newConversationModel.on('change', this.refreshMessages); - } catch (e) { - window.log.warn(e); - } - } public componentDidMount() { - // reload as much messages as we had before the change. - const { conversationKey } = this.props; - const conversationModel = window.ConversationController.getOrThrow( - conversationKey - ); - conversationModel.on('change', this.refreshMessages); // Pause thread to wait for rendering to complete setTimeout(() => { const div = this.messageContainerRef.current; @@ -225,7 +185,6 @@ export class SessionConversation extends React.Component { selectedMessages, isDraggingFile, stagedAttachments, - initialFetchComplete, } = this.state; const selectionMode = !!selectedMessages.length; @@ -285,11 +244,7 @@ export class SessionConversation extends React.Component { {lightBoxOptions?.media && this.renderLightBox(lightBoxOptions)}
- {initialFetchComplete && ( - - )} + {showRecordingView && (
@@ -342,14 +297,7 @@ export class SessionConversation extends React.Component { // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ public async loadInitialMessages() { - // Grabs the initial set of messages and adds them to our conversation model. - // After the inital fetch, all new messages are automatically added from onNewMessage - // in the conversation model. - // The only time we need to call getMessages() is to grab more messages on scroll. - if (this.state.initialFetchComplete) { - return; - } - const { conversationKey, conversation } = this.props; + const { conversationKey } = this.props; const conversationModel = window.ConversationController.getOrThrow( conversationKey ); @@ -358,67 +306,11 @@ export class SessionConversation extends React.Component { Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT, unreadCount ); - if (conversation) { - return this.getMessages(messagesToFetch, () => { - this.setState({ initialFetchComplete: true }); - }); - } - } - - // tslint:disable-next-line: no-empty - public async getMessages(numMessages?: number, callback: any = () => {}) { - const { unreadCount } = this.state; - const { conversationKey, conversation } = this.props; - let msgCount = - numMessages || - Number(Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT) + unreadCount; - msgCount = - msgCount > Constants.CONVERSATION.MAX_MESSAGE_FETCH_COUNT - ? Constants.CONVERSATION.MAX_MESSAGE_FETCH_COUNT - : msgCount; - - if (msgCount < Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT) { - msgCount = Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT; - } - if (!conversation) { - // no valid conversation, early return - return; - } - - const messageSet = await window.Signal.Data.getMessagesByConversation( + this.props.actions.fetchMessagesForConversation({ conversationKey, - { limit: msgCount, MessageCollection: window.Whisper.MessageCollection } - ); - - // Set first member of series here. - const messageModels = messageSet.models; - - const messages = []; - // no need to do that `firstMessageOfSeries` on a private chat - if (conversation.type === 'direct') { - this.setState({ messages: messageSet.models }, callback); - return; - } - - // 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 message is the first of the series if the next message is not from the same authori - for (let i = 0; i < messageModels.length; i++) { - // Handle firstMessageOfSeries for conditional avatar rendering - let firstMessageOfSeries = true; - const currentSender = messageModels[i].propsForMessage?.authorPhoneNumber; - const nextSender = - i < messageModels.length - 1 - ? messageModels[i + 1].propsForMessage?.authorPhoneNumber - : undefined; - if (i > 0 && currentSender === nextSender) { - firstMessageOfSeries = false; - } - messages.push({ ...messageModels[i], firstMessageOfSeries }); - } - - this.setState({ messages }, callback); + count: messagesToFetch, + }); } public getHeaderProps() { @@ -524,8 +416,8 @@ export class SessionConversation extends React.Component { } public getMessagesListProps() { - const { conversation } = this.props; - const { messages, quotedMessageTimestamp, selectedMessages } = this.state; + const { conversation, messages, actions } = this.props; + const { quotedMessageTimestamp, selectedMessages } = this.state; return { selectedMessages, @@ -535,7 +427,7 @@ export class SessionConversation extends React.Component { quotedMessageTimestamp, conversation, selectMessage: this.selectMessage, - getMessages: this.getMessages, + fetchMessagesForConversation: actions.fetchMessagesForConversation, replyToMessage: this.replyToMessage, onClickAttachment: this.onClickAttachment, onDownloadAttachment: this.downloadAttachment, @@ -657,8 +549,7 @@ export class SessionConversation extends React.Component { public async deleteSelectedMessages() { // Get message objects - const { messages } = this.state; - const { conversationKey } = this.props; + const { conversationKey, messages } = this.props; const conversationModel = window.ConversationController.getOrThrow( conversationKey @@ -738,9 +629,7 @@ export class SessionConversation extends React.Component { ); // Update view and trigger update - this.setState({ selectedMessages: [] }, () => { - conversationModel.trigger('change', conversationModel); - }); + this.setState({ selectedMessages: [] }); }; // If removable from server, we "Unsend" - otherwise "Delete" @@ -801,21 +690,24 @@ export class SessionConversation extends React.Component { // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ private async replyToMessage(quotedMessageTimestamp?: number) { if (!_.isEqual(this.state.quotedMessageTimestamp, quotedMessageTimestamp)) { - const { conversation, conversationKey } = this.props; + const { messages, conversationKey } = this.props; const conversationModel = window.ConversationController.getOrThrow( conversationKey ); let quotedMessageProps = null; if (quotedMessageTimestamp) { - const quotedMessageModel = conversationModel.getMessagesWithTimestamp( - conversation.id, - quotedMessageTimestamp - ); - if (quotedMessageModel && quotedMessageModel.length === 1) { - quotedMessageProps = await conversationModel.makeQuote( - quotedMessageModel[0] - ); + const quotedMessage = messages.find(m => m.attributes.sent_at === quotedMessageTimestamp); + + if (quotedMessage) { + const quotedMessageModel = await getMessageById(quotedMessage.id, { + Message: window.Whisper.Message, + }); + if (quotedMessageModel) { + quotedMessageProps = await conversationModel.makeQuote( + quotedMessageModel + ); + } } } this.setState({ quotedMessageTimestamp, quotedMessageProps }, () => { @@ -1197,11 +1089,4 @@ export class SessionConversation extends React.Component { this.setState({ isDraggingFile: false }); } } - - private refreshMessages() { - void this.getMessages( - this.state.messages.length || - Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT - ); - } } diff --git a/ts/components/session/conversation/SessionConversationMessagesList.tsx b/ts/components/session/conversation/SessionConversationMessagesList.tsx index 4cdfab532..359523be4 100644 --- a/ts/components/session/conversation/SessionConversationMessagesList.tsx +++ b/ts/components/session/conversation/SessionConversationMessagesList.tsx @@ -30,7 +30,13 @@ interface Props { conversation: ConversationType; messageContainerRef: React.RefObject; selectMessage: (messageId: string) => void; - getMessages: (numMessages: number) => Promise; + fetchMessagesForConversation: ({ + conversationKey, + count, + }: { + conversationKey: string; + count: number; + }) => void; replyToMessage: (messageId: number) => Promise; onClickAttachment: (attachment: any, message: any) => void; onDownloadAttachment: ({ attachment }: { attachment: any }) => void; @@ -298,6 +304,8 @@ export class SessionConversationMessagesList extends React.Component< // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ public async handleScroll() { const messageContainer = this.messageContainerRef?.current; + + const { fetchMessagesForConversation, conversationKey } = this.props; if (!messageContainer) { return; } @@ -354,7 +362,7 @@ export class SessionConversationMessagesList extends React.Component< const oldLen = messages.length; const previousTopMessage = messages[oldLen - 1]?.id; - await this.props.getMessages(numMessages); + fetchMessagesForConversation({ conversationKey, count: numMessages }); if (previousTopMessage && oldLen !== messages.length) { this.scrollToMessage(previousTopMessage); } diff --git a/ts/state/actions.ts b/ts/state/actions.ts index 48141fb96..468acd133 100644 --- a/ts/state/actions.ts +++ b/ts/state/actions.ts @@ -4,14 +4,16 @@ import { actions as search } from './ducks/search'; import { actions as conversations } from './ducks/conversations'; import { actions as user } from './ducks/user'; import { actions as sections } from './ducks/section'; +import { actions as messages } from './ducks/messages'; const actions = { ...search, ...conversations, ...user, + ...messages, ...sections, }; export function mapDispatchToProps(dispatch: Dispatch): Object { - return bindActionCreators(actions, dispatch); + return { ...bindActionCreators(actions, dispatch) }; } diff --git a/ts/state/createStore.ts b/ts/state/createStore.ts index 13e17fb76..d4df8451a 100644 --- a/ts/state/createStore.ts +++ b/ts/state/createStore.ts @@ -1,9 +1,7 @@ -import { applyMiddleware, createStore as reduxCreateStore } from 'redux'; - import promise from 'redux-promise-middleware'; import { createLogger } from 'redux-logger'; - -import { reducer } from './reducer'; +import { configureStore } from '@reduxjs/toolkit'; +import { reducer as allReducers } from './reducer'; // @ts-ignore const env = window.getEnvironment(); @@ -28,7 +26,10 @@ const logger = createLogger({ const disableLogging = env === 'production' || true; // ALWAYS TURNED OFF const middlewareList = disableLogging ? [promise] : [promise, logger]; -const enhancer = applyMiddleware.apply(null, middlewareList); - export const createStore = (initialState: any) => - reduxCreateStore(reducer, initialState, enhancer); + configureStore({ + reducer: allReducers, + preloadedState: initialState, + middleware: (getDefaultMiddleware: any) => + getDefaultMiddleware().concat(middlewareList), + }); diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 6925f1a9c..7a503f1ac 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -106,13 +106,6 @@ export type SelectedConversationChangedActionType = { messageId?: string; }; }; -export type LoadMoreMessagesActionType = { - type: 'LOAD_MORE_MESSAGES_ACTION_TYPE'; - payload: { - id: string; - currentMessageCount: number; - }; -}; export type ConversationActionType = | ConversationAddedActionType @@ -122,8 +115,7 @@ export type ConversationActionType = | MessageExpiredActionType | SelectedConversationChangedActionType | MessageExpiredActionType - | SelectedConversationChangedActionType - | LoadMoreMessagesActionType; + | SelectedConversationChangedActionType; // Action Creators @@ -135,7 +127,6 @@ export const actions = { messageExpired, openConversationInternal, openConversationExternal, - loadMoreMessages, }; function conversationAdded( @@ -177,19 +168,6 @@ function removeAllConversations(): RemoveAllConversationsActionType { }; } -function loadMoreMessages( - id: string, - currentMessageCount: number -): LoadMoreMessagesActionType { - return { - type: 'LOAD_MORE_MESSAGES_ACTION_TYPE', - payload: { - id, - currentMessageCount, - }, - }; -} - function messageExpired( id: string, conversationId: string @@ -239,13 +217,9 @@ function getEmptyState(): ConversationsStateType { } export function reducer( - state: ConversationsStateType, + state: ConversationsStateType = getEmptyState(), action: ConversationActionType ): ConversationsStateType { - if (!state) { - return getEmptyState(); - } - if (action.type === 'CONVERSATION_ADDED') { const { payload } = action; const { id, data } = payload; diff --git a/ts/state/ducks/messages.ts b/ts/state/ducks/messages.ts index cb5bd2e3f..dbc7afc35 100644 --- a/ts/state/ducks/messages.ts +++ b/ts/state/ducks/messages.ts @@ -1,3 +1,116 @@ -export const reducer = (state: any, _action: any) => { - return state; +import { Constants } from '../../session'; +import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'; +import _ from 'lodash'; +import { MessageType } from './conversations'; + +export type MessagesStateType = Array; + +export async function getMessages( + conversationKey: string, + numMessages: number +) : Promise { + const conversation = window.ConversationController.get(conversationKey); + if (!conversation) { + // no valid conversation, early return + window.log.error('Failed to get convo on reducer.'); + return []; + } + const unreadCount = await conversation.getUnreadCount(); + let msgCount = + numMessages || + Number(Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT) + unreadCount; + msgCount = + msgCount > Constants.CONVERSATION.MAX_MESSAGE_FETCH_COUNT + ? Constants.CONVERSATION.MAX_MESSAGE_FETCH_COUNT + : msgCount; + + if (msgCount < Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT) { + msgCount = Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT; + } + + const messageSet = await window.Signal.Data.getMessagesByConversation( + conversationKey, + { limit: msgCount, MessageCollection: window.Whisper.MessageCollection } + ); + + // Set first member of series here. + const messageModels = messageSet.models; + + const messages = []; + // no need to do that `firstMessageOfSeries` on a private chat + if (conversation.isPrivate()) { + return messageSet.models; + } + + // 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 message is the first of the series if the next message is not from the same authori + for (let i = 0; i < messageModels.length; i++) { + // Handle firstMessageOfSeries for conditional avatar rendering + let firstMessageOfSeries = true; + const currentSender = messageModels[i].propsForMessage?.authorPhoneNumber; + const nextSender = + i < messageModels.length - 1 + ? messageModels[i + 1].propsForMessage?.authorPhoneNumber + : undefined; + if (i > 0 && currentSender === nextSender) { + firstMessageOfSeries = false; + } + messages.push({ ...messageModels[i], firstMessageOfSeries }); + } + return messages; +} + +// ACTIONS +const fetchMessagesForConversation = createAsyncThunk( + 'messages/fetchByConversationKey', + async ({ + conversationKey, + count, + }: { + conversationKey: string; + count: number; + }) => { + return getMessages(conversationKey, count); + } +); + +const toOmitFromMessageModel = [ + 'cid', + 'collection', + // 'changing', + // 'previousAttributes', + '_events', + '_listeningTo', +]; + +const messageSlice = createSlice({ + name: 'messages', + initialState: [] as MessagesStateType, + reducers: { + messageChanged(state, action){ + console.log('message changed ', state, action) + const messageInStoreIndex = state.findIndex(m => m.id === action.payload.id); + if (messageInStoreIndex >= 0) { + state[messageInStoreIndex] = _.omit(action.payload, toOmitFromMessageModel) + ; + } + return state; + }, + }, + extraReducers: { + // Add reducers for additional action types here, and handle loading state as needed + [fetchMessagesForConversation.fulfilled.type]: (state, action) => { + const lightMessages = action.payload.map((m: any) => + _.omit(m, toOmitFromMessageModel) + ); + return lightMessages; + }, + }, +}); + +export const actions = { + ...messageSlice.actions, + fetchMessagesForConversation, }; +export const reducer = messageSlice.reducer; diff --git a/ts/state/reducer.ts b/ts/state/reducer.ts index 16a299e7a..fa18de577 100644 --- a/ts/state/reducer.ts +++ b/ts/state/reducer.ts @@ -8,10 +8,11 @@ import { import { reducer as user, UserStateType } from './ducks/user'; import { reducer as theme, ThemeStateType } from './ducks/theme'; import { reducer as section, SectionStateType } from './ducks/section'; +import { MessagesStateType, reducer as messages } from './ducks/messages'; export type StateType = { search: SearchStateType; - // messages: any; + messages: MessagesStateType; user: UserStateType; conversations: ConversationsStateType; theme: ThemeStateType; @@ -21,8 +22,8 @@ export type StateType = { export const reducers = { search, // Temporary until ./ducks/messages is working - // messages, - messages: search, + messages, + // messages: search, conversations, user, theme, diff --git a/ts/state/smart/SessionConversation.tsx b/ts/state/smart/SessionConversation.tsx index 071cf158d..f2afa7794 100644 --- a/ts/state/smart/SessionConversation.tsx +++ b/ts/state/smart/SessionConversation.tsx @@ -4,39 +4,6 @@ import { SessionConversation } from '../../components/session/conversation/Sessi import { StateType } from '../reducer'; const mapStateToProps = (state: StateType) => { - // Get messages here!!!!! - - // FIXME VINCE: Get messages for all conversations, not just this one - // Store as object of objects with key refs - - // console.log(`[update] State from dispatch:`, state); - - // const message: Array = []; - // if(state.conversations) { - // const conversationKey = state.conversations.selectedConversation; - - // // FIXME VINCE: msgCount should not be a magic number - // const msgCount = 30; - - // const messageSet = await window.Signal.Data.getMessagesByConversation( - // conversationKey, - // { limit: msgCount, MessageCollection: window.Whisper.MessageCollection }, - // ); - - // const messageModels = messageSet.models; - // let previousSender; - // for (let i = 0; i < messageModels.length; i++){ - // // Handle firstMessageOfSeries for conditional avatar rendering - // let firstMessageOfSeries = true; - // if (i > 0 && previousSender === messageModels[i].authorPhoneNumber){ - // firstMessageOfSeries = false; - // } - - // messages.push({...messageModels[i], firstMessageOfSeries}); - // previousSender = messageModels[i].authorPhoneNumber; - // } - // } - const conversationKey = state.conversations.selectedConversation; const conversation = (conversationKey && @@ -47,8 +14,19 @@ const mapStateToProps = (state: StateType) => { conversation, conversationKey, theme: state.theme, + messages: state.messages, }; }; -const smart = connect(mapStateToProps, mapDispatchToProps); +const smart = connect( + mapStateToProps, + mapDispatchToProps, + (stateProps, dispatchProps, ownProps) => { + return { + ...stateProps, + router: ownProps, + actions: dispatchProps, + }; + } +); export const SmartSessionConversation = smart(SessionConversation); diff --git a/yarn.lock b/yarn.lock index 219b0520b..1a64929c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -282,6 +282,16 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= +"@reduxjs/toolkit@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.4.0.tgz#ee2e2384cc3d1d76780d844b9c2da3580d32710d" + integrity sha512-hkxQwVx4BNVRsYdxjNF6cAseRmtrkpSlcgJRr3kLUcHPIAMZAmMJkXmHh/eUEGTMqPzsYpJLM7NN2w9fxQDuGw== + dependencies: + immer "^7.0.3" + redux "^4.0.0" + redux-thunk "^2.3.0" + reselect "^4.0.0" + "@sindresorhus/is@0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.8.0.tgz#073aee40b0aab2d4ace33c0a2a2672a37da6fa12" @@ -5260,6 +5270,11 @@ immediate@~3.0.5: resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= +immer@^7.0.3: + version "7.0.14" + resolved "https://registry.yarnpkg.com/immer/-/immer-7.0.14.tgz#3e605f8584b15a9520d2f2f3fda9441cc9170d25" + integrity sha512-BxCs6pJwhgSEUEOZjywW7OA8DXVzfHjkBelSEl0A+nEu0+zS4cFVdNOONvt55N4WOm8Pu4xqSPYxhm1Lv2iBBA== + import-lazy@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" @@ -9082,6 +9097,11 @@ redux-promise-middleware@6.1.0: resolved "https://registry.yarnpkg.com/redux-promise-middleware/-/redux-promise-middleware-6.1.0.tgz#ecdb22488cdd673c1a3f0d278d82b48d92ca5d06" integrity sha512-C62Ku3TgMwxFh5r3h1/iD+XPdsoizyHLT74dTkqhJ8c0LCbEVl1z9fm8zKitAjI16e6w6+h3mxf6wHdonaYXfQ== +redux-thunk@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" + integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== + redux@4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.1.tgz#436cae6cc40fbe4727689d7c8fae44808f1bfef5" @@ -9355,7 +9375,7 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= -reselect@4.0.0: +reselect@4.0.0, reselect@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==