diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 81fbdd683..5a4e7e21c 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -243,7 +243,7 @@ const ConversationListItem = (props: Props) => {
{ - dispatch(openConversationExternal(conversationId)); + dispatch(openConversationExternal({ id: conversationId })); }} onContextMenu={(e: any) => { contextMenu.show({ diff --git a/ts/components/MessageSearchResult.tsx b/ts/components/MessageSearchResult.tsx index 1787c1801..3faf761bc 100644 --- a/ts/components/MessageSearchResult.tsx +++ b/ts/components/MessageSearchResult.tsx @@ -82,7 +82,7 @@ const AvatarItem = (props: { from: FindAndFormatContactType }) => { ); }; export const MessageSearchResult = (props: Props) => { - const { from, id, isSelected, conversationId, receivedAt, snippet, to } = props; + const { from, id: messageId, isSelected, conversationId, receivedAt, snippet, to } = props; const dispatch = useDispatch(); @@ -94,7 +94,7 @@ export const MessageSearchResult = (props: Props) => {
{ - dispatch(openConversationExternal(conversationId, id)); + dispatch(openConversationExternal({ id: conversationId, messageId })); }} className={classNames( 'module-message-search-result', diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 333a6fb35..52c073430 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -14,20 +14,19 @@ import { getConversationHeaderProps, getConversationHeaderTitleProps, getSelectedConversation, + getSelectedMessageIds, + isMessageDetailView, + isMessageSelectionMode, } from '../../state/selectors/conversations'; import { useDispatch, useSelector } from 'react-redux'; import { useMembersAvatars } from '../../hooks/useMembersAvatar'; + +import { deleteMessagesById } from '../../interactions/conversationInteractions'; import { closeMessageDetailsView, openRightPanel, resetSelectedMessageIds, -} from '../../state/ducks/conversationScreen'; -import { - getSelectedMessageIds, - isMessageDetailView, - isMessageSelectionMode, -} from '../../state/selectors/conversationScreen'; -import { deleteMessagesById } from '../../interactions/conversationInteractions'; +} from '../../state/ducks/conversations'; export interface TimerOption { name: string; diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 1f9c5973c..133bff565 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -42,12 +42,12 @@ import autoBind from 'auto-bind'; import { AudioPlayerWithEncryptedFile } from './H5AudioPlayer'; import { ClickToTrustSender } from './message/ClickToTrustSender'; import { getMessageById } from '../../data/data'; -import { showMessageDetailsView } from '../../state/ducks/conversationScreen'; import { deleteMessagesById } from '../../interactions/conversationInteractions'; import { getSelectedMessage } from '../../state/selectors/search'; import { connect } from 'react-redux'; import { StateType } from '../../state/reducer'; -import { getSelectedMessageIds, isMessageSelected } from '../../state/selectors/conversationScreen'; +import { getSelectedMessageIds } from '../../state/selectors/conversations'; +import { showMessageDetailsView } from '../../state/ducks/conversations'; // Same as MIN_WIDTH in ImageGrid.tsx const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200; diff --git a/ts/components/conversation/MessageDetail.tsx b/ts/components/conversation/MessageDetail.tsx index 74682aaae..d8bebfa56 100644 --- a/ts/components/conversation/MessageDetail.tsx +++ b/ts/components/conversation/MessageDetail.tsx @@ -8,8 +8,8 @@ import { Message } from './Message'; import { MessageRegularProps } from '../../models/messageType'; import { deleteMessagesById } from '../../interactions/conversationInteractions'; import { useSelector } from 'react-redux'; -import { getMessageDetailsViewProps } from '../../state/selectors/conversationScreen'; import { ContactPropsMessageDetail } from '../../state/ducks/conversations'; +import { getMessageDetailsViewProps } from '../../state/selectors/conversations'; const AvatarItem = (props: { contact: ContactPropsMessageDetail }) => { const { avatarPath, phoneNumber, name, profileName } = props.contact; diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index bae63749c..c94a8d1bc 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -37,7 +37,11 @@ import { getOpenGroupManager } from '../../opengroup/opengroupV2/OpenGroupManage import { forceRefreshRandomSnodePool } from '../../session/snode_api/snodePool'; import { getSwarmPollingInstance } from '../../session/snode_api'; import { DURATION } from '../../session/constants'; -import { actions as conversationActions } from '../../state/ducks/conversations'; +import { + actions as conversationActions, + conversationChanged, + conversationRemoved, +} from '../../state/ducks/conversations'; import { editProfileModal, onionPathModal } from '../../state/ducks/modalDialog'; import { uploadOurAvatar } from '../../interactions/conversationInteractions'; import { ModalContainer } from './ModalContainer'; @@ -173,9 +177,9 @@ const removeAllV1OpenGroups = async () => { window.log.info(`deleting v1convo : ${v1Convo.id}`); getConversationController().unsafeDelete(v1Convo); if (window.inboxStore) { - window.inboxStore?.dispatch(conversationActions.conversationRemoved(v1Convo.id)); + window.inboxStore?.dispatch(conversationRemoved(v1Convo.id)); window.inboxStore?.dispatch( - conversationActions.conversationChanged(v1Convo.id, v1Convo.getProps()) + conversationChanged({ id: v1Convo.id, data: v1Convo.getProps() }) ); } } catch (e) { diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index acd15c412..f2ea3701c 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -319,7 +319,7 @@ export class LeftPaneMessageSection extends React.Component { pubkeyorOns, ConversationTypeEnum.PRIVATE ); - window.inboxStore?.dispatch(openConversationExternal(pubkeyorOns)); + window.inboxStore?.dispatch(openConversationExternal({ id: pubkeyorOns })); this.handleToggleOverlay(undefined); } else { // this might be an ONS, validate the regex first @@ -339,7 +339,7 @@ export class LeftPaneMessageSection extends React.Component { resolvedSessionID, ConversationTypeEnum.PRIVATE ); - window.inboxStore?.dispatch(openConversationExternal(resolvedSessionID)); + window.inboxStore?.dispatch(openConversationExternal({ id: resolvedSessionID })); this.handleToggleOverlay(undefined); } catch (e) { window?.log?.warn('failed to resolve ons name', pubkeyorOns, e); diff --git a/ts/components/session/SessionInboxView.tsx b/ts/components/session/SessionInboxView.tsx index 2c0a42add..ff9a2bb87 100644 --- a/ts/components/session/SessionInboxView.tsx +++ b/ts/components/session/SessionInboxView.tsx @@ -23,7 +23,6 @@ import { SessionMainPanel } from '../SessionMainPanel'; import { PersistGate } from 'redux-persist/integration/react'; import { persistStore } from 'redux-persist'; import { TimerOptionsArray, TimerOptionsState } from '../../state/ducks/timerOptions'; -import { initialConversationScreen } from '../../state/ducks/conversationScreen'; // Workaround: A react component's required properties are filtering up through connect() // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363 @@ -103,6 +102,10 @@ export class SessionInboxView extends React.Component { conversations: { conversationLookup: makeLookup(fullFilledConversations, 'id'), messages: [], + showRightPanel: false, + messageDetailProps: undefined, + selectedMessageIds: [], + selectedConversation: undefined, }, user: { ourNumber: UserUtils.getOurPubKeyStrFromCache(), @@ -118,7 +121,6 @@ export class SessionInboxView extends React.Component { timerOptions: { timerOptions, }, - conversationScreen: initialConversationScreen, }; this.store = createStore(initialState); diff --git a/ts/components/session/conversation/SessionConversation.tsx b/ts/components/session/conversation/SessionConversation.tsx index 20eaca802..5644e0ffd 100644 --- a/ts/components/session/conversation/SessionConversation.tsx +++ b/ts/components/session/conversation/SessionConversation.tsx @@ -22,6 +22,7 @@ import { fetchMessagesForConversation, PropsForMessage, ReduxConversationType, + resetSelectedMessageIds, SortedMessageModelProps, } from '../../../state/ducks/conversations'; import { MessageView } from '../../MainViewController'; @@ -34,7 +35,6 @@ import { getDecryptedMediaUrl } from '../../../session/crypto/DecryptedAttachmen import { updateMentionsMembers } from '../../../state/ducks/mentionsInput'; import { sendDataExtractionNotification } from '../../../session/messages/outgoing/controlMessage/DataExtractionNotificationMessage'; -import { resetSelectedMessageIds } from '../../../state/ducks/conversationScreen'; interface State { unreadCount: number; diff --git a/ts/components/session/conversation/SessionMessagesList.tsx b/ts/components/session/conversation/SessionMessagesList.tsx index f1e6e43ea..6043972e1 100644 --- a/ts/components/session/conversation/SessionMessagesList.tsx +++ b/ts/components/session/conversation/SessionMessagesList.tsx @@ -26,11 +26,11 @@ import autoBind from 'auto-bind'; import { ConversationTypeEnum } from '../../../models/conversation'; import { DataExtractionNotification } from '../../conversation/DataExtractionNotification'; import { StateType } from '../../../state/reducer'; -import { getSelectedMessageIds } from '../../../state/selectors/conversationScreen'; import { connect } from 'react-redux'; import { getSelectedConversation, getSelectedConversationKey, + getSelectedMessageIds, } from '../../../state/selectors/conversations'; interface State { diff --git a/ts/components/session/conversation/SessionRightPanel.tsx b/ts/components/session/conversation/SessionRightPanel.tsx index 3a670dbb6..38f0e6a1b 100644 --- a/ts/components/session/conversation/SessionRightPanel.tsx +++ b/ts/components/session/conversation/SessionRightPanel.tsx @@ -38,10 +38,9 @@ import { MediaItemType } from '../../LightboxGallery'; import useInterval from 'react-use/lib/useInterval'; import { useDispatch, useSelector } from 'react-redux'; import { getTimerOptions } from '../../../state/selectors/timerOptions'; -import { closeRightPanel } from '../../../state/ducks/conversationScreen'; -import { isRightPanelShowing } from '../../../state/selectors/conversationScreen'; -import { getSelectedConversation } from '../../../state/selectors/conversations'; +import { getSelectedConversation, isRightPanelShowing } from '../../../state/selectors/conversations'; import { useMembersAvatars } from '../../../hooks/useMembersAvatar'; +import { closeRightPanel } from '../../../state/ducks/conversations'; type Props = { memberAvatars?: Array; // this is added by usingClosedConversationDetails diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index 17a297732..110a916ba 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -32,14 +32,17 @@ import { lastAvatarUploadTimestamp, removeAllMessagesInConversation, } from '../data/data'; -import { conversationReset, SortedMessageModelProps } from '../state/ducks/conversations'; +import { + conversationReset, + resetSelectedMessageIds, + SortedMessageModelProps, +} from '../state/ducks/conversations'; import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsManager'; import { IMAGE_JPEG } from '../types/MIME'; import { FSv2 } from '../fileserver'; import { fromBase64ToArray, toHex } from '../session/utils/String'; import { SessionButtonColor } from '../components/session/SessionButton'; import { perfEnd, perfStart } from '../session/utils/Performance'; -import { resetSelectedMessageIds } from '../state/ducks/conversationScreen'; export const getCompleteUrlForV2ConvoId = async (convoId: string) => { if (convoId.match(openGroupV2ConversationIdRegex)) { @@ -286,11 +289,7 @@ export function showChangeNickNameByConvoId(conversationId: string) { export async function deleteMessagesByConvoIdNoConfirmation(conversationId: string) { const conversation = getConversationController().get(conversationId); await removeAllMessagesInConversation(conversationId); - window.inboxStore?.dispatch( - conversationReset({ - conversationKey: conversationId, - }) - ); + window.inboxStore?.dispatch(conversationReset(conversationId)); // destroy message keeps the active timestamp set so the // conversation still appears on the conversation list but is empty diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 4505dd990..1b9d83d42 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -23,6 +23,7 @@ import { import { fromArrayBufferToBase64, fromBase64ToArrayBuffer } from '../session/utils/String'; import { actions as conversationActions, + conversationChanged, LastMessageStatusType, MessageModelProps, ReduxConversationType, @@ -202,7 +203,7 @@ export class ConversationModel extends Backbone.Model { this.typingRefreshTimer = null; this.typingPauseTimer = null; - window.inboxStore?.dispatch(conversationActions.conversationChanged(this.id, this.getProps())); + window.inboxStore?.dispatch(conversationChanged({ id: this.id, data: this.getProps() })); } public idForLogging() { @@ -885,9 +886,12 @@ export class ConversationModel extends Backbone.Model { public triggerUIRefresh() { window.inboxStore?.dispatch( - conversationActions.conversationChanged(this.id, { - ...this.getProps(), - isSelected: false, + conversationChanged({ + id: this.id, + data: { + ...this.getProps(), + isSelected: false, + }, }) ); } diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index b9c5d6073..100e9d755 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -31,7 +31,10 @@ import { forceSyncConfigurationNowIfNeeded } from '../session/utils/syncUtils'; import { getMessageController } from '../session/messages'; import { ClosedGroupEncryptionPairReplyMessage } from '../session/messages/outgoing/controlMessage/group/ClosedGroupEncryptionPairReplyMessage'; import { queueAllCachedFromSource } from './receiver'; -import { actions as conversationActions } from '../state/ducks/conversations'; +import { + actions as conversationActions, + openConversationExternal, +} from '../state/ducks/conversations'; import { getSwarmPollingInstance } from '../session/snode_api'; import { MessageModel } from '../models/message'; @@ -952,7 +955,7 @@ export async function createClosedGroup(groupName: string, members: Array { if (window?.inboxStore) { window.inboxStore?.dispatch( - conversationActions.conversationAdded(conversation.id, conversation.getProps()) + conversationActions.conversationAdded({ + id: conversation.id, + data: conversation.getProps(), + }) ); } if (!conversation.isPublic()) { @@ -245,7 +248,10 @@ export class ConversationController { if (window?.inboxStore) { window.inboxStore?.dispatch(conversationActions.conversationRemoved(conversation.id)); window.inboxStore?.dispatch( - conversationActions.conversationChanged(conversation.id, conversation.getProps()) + conversationActions.conversationChanged({ + id: conversation.id, + data: conversation.getProps(), + }) ); } window.log.info(`deleteContact !isPrivate, convo removed from store: ${id}`); diff --git a/ts/state/createStore.ts b/ts/state/createStore.ts index 196ed439f..93240e04c 100644 --- a/ts/state/createStore.ts +++ b/ts/state/createStore.ts @@ -1,7 +1,7 @@ import promise from 'redux-promise-middleware'; import { createLogger } from 'redux-logger'; import { configureStore } from '@reduxjs/toolkit'; -import { reducer as allReducers } from './reducer'; +import { rootReducer } from './reducer'; import { persistReducer } from 'redux-persist'; // tslint:disable-next-line: no-submodule-imports match-default-export-name @@ -32,7 +32,7 @@ export const persistConfig = { whitelist: ['userConfig'], }; -const persistedReducer = persistReducer(persistConfig, allReducers); +const persistedReducer = persistReducer(persistConfig, rootReducer); // Exclude logger if we're in production mode const disableLogging = env === 'production' || true; // ALWAYS TURNED OFF diff --git a/ts/state/ducks/conversationScreen.tsx b/ts/state/ducks/conversationScreen.tsx deleted file mode 100644 index c7e015a97..000000000 --- a/ts/state/ducks/conversationScreen.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { MessagePropsDetails } from './conversations'; - -export type ConversationScreenState = { - messageDetailProps: MessagePropsDetails | undefined; - showRightPanel: boolean; - selectedMessageIds: Array; -}; - -export const initialConversationScreen: ConversationScreenState = { - messageDetailProps: undefined, - showRightPanel: false, - selectedMessageIds: [], -}; - -/** - * This slice is the one holding the layout of the Conversation Screen of the app - */ -const conversationScreenSlice = createSlice({ - name: 'conversationScreen', - initialState: initialConversationScreen, - reducers: { - showMessageDetailsView( - state: ConversationScreenState, - action: PayloadAction - ) { - // force the right panel to be hidden when showing message detail view - return { ...state, messageDetailProps: action.payload, showRightPanel: false }; - }, - - closeMessageDetailsView(state: ConversationScreenState) { - return { ...state, messageDetailProps: undefined }; - }, - - openRightPanel(state: ConversationScreenState) { - return { ...state, showRightPanel: true }; - }, - closeRightPanel(state: ConversationScreenState) { - return { ...state, showRightPanel: false }; - }, - addMessageIdToSelection(state: ConversationScreenState, action: PayloadAction) { - if (state.selectedMessageIds.some(id => id === action.payload)) { - return state; - } - return { ...state, selectedMessageIds: [...state.selectedMessageIds, action.payload] }; - }, - removeMessageIdFromSelection(state: ConversationScreenState, action: PayloadAction) { - const index = state.selectedMessageIds.findIndex(id => id === action.payload); - - if (index === -1) { - return state; - } - return { ...state, selectedMessageIds: state.selectedMessageIds.splice(index, 1) }; - }, - toggleSelectedMessageId(state: ConversationScreenState, action: PayloadAction) { - const index = state.selectedMessageIds.findIndex(id => id === action.payload); - - if (index === -1) { - return { ...state, selectedMessageIds: [...state.selectedMessageIds, action.payload] }; - } - return { ...state, selectedMessageIds: state.selectedMessageIds.splice(index, 1) }; - }, - resetSelectedMessageIds(state: ConversationScreenState) { - return { ...state, selectedMessageIds: [] }; - }, - }, -}); - -// destructures -const { actions, reducer } = conversationScreenSlice; -export const { - showMessageDetailsView, - closeMessageDetailsView, - openRightPanel, - closeRightPanel, - addMessageIdToSelection, - resetSelectedMessageIds, -} = actions; -export const defaultConversationScreenReducer = reducer; diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 535a4c9e0..368029324 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -1,9 +1,8 @@ import _, { omit } from 'lodash'; import { Constants } from '../../session'; -import { createAsyncThunk } from '@reduxjs/toolkit'; +import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { getConversationController } from '../../session/conversations'; -import { MessageModel } from '../../models/message'; import { getMessagesByConversation } from '../../data/data'; import { ConversationNotificationSettingType, @@ -231,6 +230,9 @@ export type ConversationsStateType = { conversationLookup: ConversationLookupType; selectedConversation?: string; messages: Array; + messageDetailProps: MessagePropsDetails | undefined; + showRightPanel: boolean; + selectedMessageIds: Array; }; async function getMessages( @@ -338,246 +340,15 @@ export const fetchMessagesForConversation = createAsyncThunk( } ); -// Actions - -type ConversationAddedActionType = { - type: 'CONVERSATION_ADDED'; - payload: { - id: string; - data: ReduxConversationType; - }; -}; -type ConversationChangedActionType = { - type: 'CONVERSATION_CHANGED'; - payload: { - id: string; - data: ReduxConversationType; - }; -}; -type ConversationRemovedActionType = { - type: 'CONVERSATION_REMOVED'; - payload: { - id: string; - }; -}; -export type RemoveAllConversationsActionType = { - type: 'CONVERSATIONS_REMOVE_ALL'; - payload: null; -}; -export type MessageExpiredActionType = { - type: 'MESSAGE_EXPIRED'; - payload: { - messageId: string; - conversationKey: string; - }; -}; -export type MessageChangedActionType = { - type: 'MESSAGE_CHANGED'; - payload: MessageModelProps; -}; -export type MessagesChangedActionType = { - type: 'MESSAGES_CHANGED'; - payload: Array; -}; -export type MessageAddedActionType = { - type: 'MESSAGE_ADDED'; - payload: { - conversationKey: string; - messageModelProps: MessageModelProps; - }; -}; -export type MessageDeletedActionType = { - type: 'MESSAGE_DELETED'; - payload: { - conversationKey: string; - messageId: string; - }; -}; -export type ConversationResetActionType = { - type: 'CONVERSATION_RESET'; - payload: { - conversationKey: string; - }; -}; -export type SelectedConversationChangedActionType = { - type: 'SELECTED_CONVERSATION_CHANGED'; - payload: { - id: string; - messageId?: string; - }; -}; - -export type FetchMessagesForConversationType = { - type: 'messages/fetchByConversationKey/fulfilled'; - payload: { - conversationKey: string; - messages: Array; - }; -}; - -export type ConversationActionType = - | ConversationAddedActionType - | ConversationChangedActionType - | ConversationRemovedActionType - | ConversationResetActionType - | RemoveAllConversationsActionType - | MessageExpiredActionType - | MessageAddedActionType - | MessageDeletedActionType - | MessageChangedActionType - | MessagesChangedActionType - | SelectedConversationChangedActionType - | SelectedConversationChangedActionType - | FetchMessagesForConversationType; - -// Action Creators - -export const actions = { - conversationAdded, - conversationChanged, - conversationRemoved, - removeAllConversations, - messageExpired, - messageAdded, - messageDeleted, - conversationReset, - messageChanged, - messagesChanged, - fetchMessagesForConversation, - openConversationExternal, -}; - -function conversationAdded(id: string, data: ReduxConversationType): ConversationAddedActionType { - return { - type: 'CONVERSATION_ADDED', - payload: { - id, - data, - }, - }; -} -function conversationChanged( - id: string, - data: ReduxConversationType -): ConversationChangedActionType { - return { - type: 'CONVERSATION_CHANGED', - payload: { - id, - data, - }, - }; -} -function conversationRemoved(id: string): ConversationRemovedActionType { - return { - type: 'CONVERSATION_REMOVED', - payload: { - id, - }, - }; -} -function removeAllConversations(): RemoveAllConversationsActionType { - return { - type: 'CONVERSATIONS_REMOVE_ALL', - payload: null, - }; -} - -function messageExpired({ - conversationKey, - messageId, -}: { - conversationKey: string; - messageId: string; -}): MessageExpiredActionType { - return { - type: 'MESSAGE_EXPIRED', - payload: { - conversationKey, - messageId, - }, - }; -} - -function messageChanged(messageModelProps: MessageModelProps): MessageChangedActionType { - return { - type: 'MESSAGE_CHANGED', - payload: messageModelProps, - }; -} - -function messagesChanged(messageModelsProps: Array): MessagesChangedActionType { - return { - type: 'MESSAGES_CHANGED', - payload: messageModelsProps, - }; -} - -function messageAdded({ - conversationKey, - messageModelProps, -}: { - conversationKey: string; - messageModelProps: MessageModelProps; -}): MessageAddedActionType { - return { - type: 'MESSAGE_ADDED', - payload: { - conversationKey, - messageModelProps, - }, - }; -} - -function messageDeleted({ - conversationKey, - messageId, -}: { - conversationKey: string; - messageId: string; -}): MessageDeletedActionType { - return { - type: 'MESSAGE_DELETED', - payload: { - conversationKey, - messageId, - }, - }; -} - -export function conversationReset({ - conversationKey, -}: { - conversationKey: string; -}): ConversationResetActionType { - return { - type: 'CONVERSATION_RESET', - payload: { - conversationKey, - }, - }; -} - -export function openConversationExternal( - id: string, - messageId?: string -): SelectedConversationChangedActionType { - window?.log?.info(`openConversationExternal with convoId: ${id}; messageId: ${messageId}`); - return { - type: 'SELECTED_CONVERSATION_CHANGED', - payload: { - id, - messageId, - }, - }; -} - // Reducer function getEmptyState(): ConversationsStateType { return { conversationLookup: {}, messages: [], + messageDetailProps: undefined, + showRightPanel: false, + selectedMessageIds: [], }; } @@ -607,7 +378,13 @@ function sortMessages( return messagesSorted; } -function handleMessageAdded(state: ConversationsStateType, action: MessageAddedActionType) { +function handleMessageAdded( + state: ConversationsStateType, + action: PayloadAction<{ + conversationKey: string; + messageModelProps: MessageModelProps; + }> +) { const { messages } = state; const { conversationKey, messageModelProps: addedMessageProps } = action.payload; if (conversationKey === state.selectedConversation) { @@ -631,9 +408,7 @@ function handleMessageAdded(state: ConversationsStateType, action: MessageAddedA return state; } -function handleMessageChanged(state: ConversationsStateType, action: MessageChangedActionType) { - const { payload } = action; - +function handleMessageChanged(state: ConversationsStateType, payload: MessageModelProps) { const messageInStoreIndex = state?.messages?.findIndex( m => m.propsForMessage.id === payload.propsForMessage.id ); @@ -661,15 +436,10 @@ function handleMessageChanged(state: ConversationsStateType, action: MessageChan return state; } -function handleMessagesChanged(state: ConversationsStateType, action: MessagesChangedActionType) { - const { payload } = action; - +function handleMessagesChanged(state: ConversationsStateType, payload: Array) { payload.forEach(element => { // tslint:disable-next-line: no-parameter-reassignment - state = handleMessageChanged(state, { - payload: element, - type: 'MESSAGE_CHANGED', - }); + state = handleMessageChanged(state, element); }); return state; @@ -677,7 +447,10 @@ function handleMessagesChanged(state: ConversationsStateType, action: MessagesCh function handleMessageExpiredOrDeleted( state: ConversationsStateType, - action: MessageDeletedActionType | MessageExpiredActionType + action: PayloadAction<{ + messageId: string; + conversationKey: string; + }> ) { const { conversationKey, messageId } = action.payload; if (conversationKey === state.selectedConversation) { @@ -709,11 +482,8 @@ function handleMessageExpiredOrDeleted( return state; } -function handleConversationReset( - state: ConversationsStateType, - action: ConversationResetActionType -) { - const { conversationKey } = action.payload; +function handleConversationReset(state: ConversationsStateType, action: PayloadAction) { + const conversationKey = action.payload; if (conversationKey === state.selectedConversation) { // just empty the list of messages return { @@ -724,110 +494,220 @@ function handleConversationReset( return state; } -// tslint:disable: cyclomatic-complexity -// tslint:disable: max-func-body-length -export function reducer( - state: ConversationsStateType = getEmptyState(), - action: ConversationActionType -): ConversationsStateType { - if (action.type === 'CONVERSATION_ADDED') { - const { payload } = action; - const { id, data } = payload; - const { conversationLookup } = state; +const conversationsSlice = createSlice({ + name: 'conversations', + initialState: getEmptyState(), + reducers: { + showMessageDetailsView( + state: ConversationsStateType, + action: PayloadAction + ) { + // force the right panel to be hidden when showing message detail view + return { ...state, messageDetailProps: action.payload, showRightPanel: false }; + }, - return { - ...state, - conversationLookup: { - ...conversationLookup, - [id]: data, - }, - }; - } - if (action.type === 'CONVERSATION_CHANGED') { - const { payload } = action; - const { id, data } = payload; - const { conversationLookup, selectedConversation } = state; - - const existing = conversationLookup[id]; - // In the change case we only modify the lookup if we already had that conversation - if (!existing) { - return state; - } + closeMessageDetailsView(state: ConversationsStateType) { + return { ...state, messageDetailProps: undefined }; + }, - return { - ...state, - selectedConversation, - conversationLookup: { - ...conversationLookup, - [id]: data, - }, - }; - } - if (action.type === 'CONVERSATION_REMOVED') { - const { payload } = action; - const { id } = payload; - const { conversationLookup, selectedConversation } = state; - return { - ...state, - conversationLookup: omit(conversationLookup, [id]), - selectedConversation: selectedConversation === id ? undefined : selectedConversation, - }; - } - if (action.type === 'CONVERSATIONS_REMOVE_ALL') { - return getEmptyState(); - } + openRightPanel(state: ConversationsStateType) { + return { ...state, showRightPanel: true }; + }, + closeRightPanel(state: ConversationsStateType) { + return { ...state, showRightPanel: false }; + }, + addMessageIdToSelection(state: ConversationsStateType, action: PayloadAction) { + if (state.selectedMessageIds.some(id => id === action.payload)) { + return state; + } + return { ...state, selectedMessageIds: [...state.selectedMessageIds, action.payload] }; + }, + removeMessageIdFromSelection(state: ConversationsStateType, action: PayloadAction) { + const index = state.selectedMessageIds.findIndex(id => id === action.payload); + + if (index === -1) { + return state; + } + return { ...state, selectedMessageIds: state.selectedMessageIds.splice(index, 1) }; + }, + toggleSelectedMessageId(state: ConversationsStateType, action: PayloadAction) { + const index = state.selectedMessageIds.findIndex(id => id === action.payload); + + if (index === -1) { + return { ...state, selectedMessageIds: [...state.selectedMessageIds, action.payload] }; + } + return { ...state, selectedMessageIds: state.selectedMessageIds.splice(index, 1) }; + }, + resetSelectedMessageIds(state: ConversationsStateType) { + return { ...state, selectedMessageIds: [] }; + }, - if (action.type === 'SELECTED_CONVERSATION_CHANGED') { - const { payload } = action; - const { id } = payload; - const oldSelectedConversation = state.selectedConversation; - const newSelectedConversation = id; + conversationAdded( + state: ConversationsStateType, + action: PayloadAction<{ + id: string; + data: ReduxConversationType; + }> + ) { + const { conversationLookup } = state; - if (newSelectedConversation !== oldSelectedConversation) { - // empty the message list return { ...state, - messages: [], - selectedConversation: id, + conversationLookup: { + ...conversationLookup, + [action.payload.id]: action.payload.data, + }, }; - } - return { - ...state, - selectedConversation: id, - }; - } + }, + conversationChanged( + state: ConversationsStateType, + action: PayloadAction<{ + id: string; + data: ReduxConversationType; + }> + ) { + const { payload } = action; + const { id, data } = payload; + const { conversationLookup, selectedConversation } = state; + + const existing = conversationLookup[id]; + // In the change case we only modify the lookup if we already had that conversation + if (!existing) { + return state; + } - // this is called once the messages are loaded from the db for the currently selected conversation - if (action.type === fetchMessagesForConversation.fulfilled.type) { - const { messagesProps, conversationKey } = action.payload as FetchedMessageResults; - // double check that this update is for the shown convo - if (conversationKey === state.selectedConversation) { return { ...state, - messages: messagesProps, + selectedConversation, + conversationLookup: { + ...conversationLookup, + [id]: data, + }, }; - } - return state; - } + }, - if (action.type === 'MESSAGE_CHANGED') { - return handleMessageChanged(state, action); - } + conversationRemoved( + state: ConversationsStateType, + action: PayloadAction<{ + id: string; + }> + ) { + const { payload } = action; + const { id } = payload; + const { conversationLookup, selectedConversation } = state; + return { + ...state, + conversationLookup: omit(conversationLookup, [id]), + selectedConversation: selectedConversation === id ? undefined : selectedConversation, + }; + }, - if (action.type === 'MESSAGES_CHANGED') { - return handleMessagesChanged(state, action); - } + removeAllConversations() { + return getEmptyState(); + }, - if (action.type === 'MESSAGE_ADDED') { - return handleMessageAdded(state, action); - } - if (action.type === 'MESSAGE_EXPIRED' || action.type === 'MESSAGE_DELETED') { - return handleMessageExpiredOrDeleted(state, action); - } + messageAdded( + state: ConversationsStateType, + action: PayloadAction<{ + conversationKey: string; + messageModelProps: MessageModelProps; + }> + ) { + return handleMessageAdded(state, action); + }, - if (action.type === 'CONVERSATION_RESET') { - return handleConversationReset(state, action); - } + messageChanged(state: ConversationsStateType, action: PayloadAction) { + return handleMessageChanged(state, action.payload); + }, + messagesChanged( + state: ConversationsStateType, + action: PayloadAction> + ) { + return handleMessagesChanged(state, action.payload); + }, - return state; -} + messageExpired( + state: ConversationsStateType, + action: PayloadAction<{ + messageId: string; + conversationKey: string; + }> + ) { + return handleMessageExpiredOrDeleted(state, action); + }, + + messageDeleted( + state: ConversationsStateType, + action: PayloadAction<{ + messageId: string; + conversationKey: string; + }> + ) { + return handleMessageExpiredOrDeleted(state, action); + }, + + conversationReset(state: ConversationsStateType, action: PayloadAction) { + return handleConversationReset(state, action); + }, + + openConversationExternal( + state: ConversationsStateType, + action: PayloadAction<{ + id: string; + messageId?: string; + }> + ) { + if (state.selectedConversation === action.payload.id) { + return state; + } + state.showRightPanel = false; + state.messageDetailProps = undefined; + state.selectedMessageIds = []; + state.selectedConversation = action.payload.id; + state.messages = []; + return state; + }, + }, + extraReducers: (builder: any) => { + // Add reducers for additional action types here, and handle loading state as needed + builder.addCase( + fetchMessagesForConversation.fulfilled, + (state: ConversationsStateType, action: any) => { + // this is called once the messages are loaded from the db for the currently selected conversation + const { messagesProps, conversationKey } = action.payload as FetchedMessageResults; + // double check that this update is for the shown convo + if (conversationKey === state.selectedConversation) { + return { + ...state, + messages: messagesProps, + }; + } + return state; + } + ); + }, +}); + +// destructures +export const { actions, reducer } = conversationsSlice; +export const { + // conversation and messages list + conversationAdded, + conversationChanged, + conversationRemoved, + removeAllConversations, + messageExpired, + messageAdded, + messageDeleted, + conversationReset, + messageChanged, + messagesChanged, + openConversationExternal, + // layout stuff + showMessageDetailsView, + closeMessageDetailsView, + openRightPanel, + closeRightPanel, + addMessageIdToSelection, + resetSelectedMessageIds, +} = actions; diff --git a/ts/state/ducks/search.ts b/ts/state/ducks/search.ts index 463142266..3719a32d5 100644 --- a/ts/state/ducks/search.ts +++ b/ts/state/ducks/search.ts @@ -6,11 +6,9 @@ import { searchConversations, searchMessages } from '../../../ts/data/data'; import { makeLookup } from '../../util/makeLookup'; import { - MessageExpiredActionType, + openConversationExternal, PropsForSearchResults, ReduxConversationType, - RemoveAllConversationsActionType, - SelectedConversationChangedActionType, } from './conversations'; import { PubKey } from '../../session/types'; import { MessageModel } from '../../models/message'; @@ -64,10 +62,7 @@ type ClearSearchActionType = { export type SEARCH_TYPES = | SearchResultsFulfilledActionType | UpdateSearchTermActionType - | ClearSearchActionType - | MessageExpiredActionType - | RemoveAllConversationsActionType - | SelectedConversationChangedActionType; + | ClearSearchActionType; // Action Creators @@ -332,39 +327,39 @@ export function reducer(state: SearchStateType | undefined, action: SEARCH_TYPES }; } - if (action.type === 'CONVERSATIONS_REMOVE_ALL') { - return getEmptyState(); - } - - if (action.type === 'SELECTED_CONVERSATION_CHANGED') { - const { payload } = action; - const { messageId } = payload; - - if (!messageId) { - return state; - } - - return { - ...state, - selectedMessage: messageId, - }; - } - - if (action.type === 'MESSAGE_EXPIRED') { - const { messages, messageLookup } = state; - if (!messages.length) { - return state; - } - - const { payload } = action; - const { messageId } = payload; - - return { - ...state, - messages: reject(messages, message => messageId === message.id), - messageLookup: omit(messageLookup, ['id']), - }; - } + // if (action.type === 'CONVERSATIONS_REMOVE_ALL') { + // return getEmptyState(); + // } + + // if (action.type === openConversationExternal.name) { + // const { payload } = action; + // const { messageId } = payload; + + // if (!messageId) { + // return state; + // } + + // return { + // ...state, + // selectedMessage: messageId, + // }; + // } + + // if (action.type === 'MESSAGE_EXPIRED') { + // const { messages, messageLookup } = state; + // if (!messages.length) { + // return state; + // } + + // const { payload } = action; + // const { messageId } = payload; + + // return { + // ...state, + // messages: reject(messages, message => messageId === message.id), + // messageLookup: omit(messageLookup, ['id']), + // }; + // } return state; } diff --git a/ts/state/reducer.ts b/ts/state/reducer.ts index 8760c5cc8..b75f2e52d 100644 --- a/ts/state/reducer.ts +++ b/ts/state/reducer.ts @@ -10,10 +10,7 @@ import { defaultMentionsInputReducer as mentionsInput, MentionsInputState, } from './ducks/mentionsInput'; -import { - ConversationScreenState, - defaultConversationScreenReducer as conversationScreen, -} from './ducks/conversationScreen'; + import { defaultOnionReducer as onionPaths, OnionState } from './ducks/onion'; import { modalReducer as modals, ModalState } from './ducks/modalDialog'; import { userConfigReducer as userConfig, UserConfigState } from './ducks/userConfig'; @@ -31,7 +28,6 @@ export type StateType = { modals: ModalState; userConfig: UserConfigState; timerOptions: TimerOptionsState; - conversationScreen: ConversationScreenState; }; export const reducers = { @@ -46,10 +42,9 @@ export const reducers = { modals, userConfig, timerOptions, - conversationScreen, }; // Making this work would require that our reducer signature supported AnyAction, not // our restricted actions // @ts-ignore -export const reducer = combineReducers(reducers); +export const rootReducer = combineReducers(reducers); diff --git a/ts/state/selectors/conversationScreen.ts b/ts/state/selectors/conversationScreen.ts deleted file mode 100644 index 8e633f247..000000000 --- a/ts/state/selectors/conversationScreen.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { createSelector } from 'reselect'; - -import { StateType } from '../reducer'; -import { ConversationScreenState } from '../ducks/conversationScreen'; -import { MessagePropsDetails } from '../ducks/conversations'; - -export const getConversationScreenState = (state: StateType): ConversationScreenState => - state.conversationScreen; - -export const isMessageDetailView = createSelector( - getConversationScreenState, - (state: ConversationScreenState): boolean => state.messageDetailProps !== undefined -); - -export const getMessageDetailsViewProps = createSelector( - getConversationScreenState, - (state: ConversationScreenState): MessagePropsDetails | undefined => state.messageDetailProps -); - -export const isRightPanelShowing = createSelector( - getConversationScreenState, - (state: ConversationScreenState): boolean => state.showRightPanel -); - -export const isMessageSelectionMode = createSelector( - getConversationScreenState, - (state: ConversationScreenState): boolean => state.selectedMessageIds.length > 0 -); - -export const getSelectedMessageIds = createSelector( - getConversationScreenState, - (state: ConversationScreenState): Array => state.selectedMessageIds -); - -export const isMessageSelected = (messageId: string) => - createSelector(getConversationScreenState, (state: ConversationScreenState): boolean => - state.selectedMessageIds.includes(messageId) - ); diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index d755da206..17717f69a 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -4,6 +4,7 @@ import { StateType } from '../reducer'; import { ConversationLookupType, ConversationsStateType, + MessagePropsDetails, ReduxConversationType, SortedMessageModelProps, } from '../ducks/conversations'; @@ -257,3 +258,28 @@ export const getNumberOfPinnedConversations = createSelector(getConversations, ( const values = Object.values(state.conversationLookup); return values.filter(conversation => conversation.isPinned).length; }); + +export const isMessageDetailView = createSelector( + getConversations, + (state: ConversationsStateType): boolean => state.messageDetailProps !== undefined +); + +export const getMessageDetailsViewProps = createSelector( + getConversations, + (state: ConversationsStateType): MessagePropsDetails | undefined => state.messageDetailProps +); + +export const isRightPanelShowing = createSelector( + getConversations, + (state: ConversationsStateType): boolean => state.showRightPanel +); + +export const isMessageSelectionMode = createSelector( + getConversations, + (state: ConversationsStateType): boolean => state.selectedMessageIds.length > 0 +); + +export const getSelectedMessageIds = createSelector( + getConversations, + (state: ConversationsStateType): Array => state.selectedMessageIds +); diff --git a/ts/state/smart/SessionConversation.tsx b/ts/state/smart/SessionConversation.tsx index f8a31cef3..50eebefdd 100644 --- a/ts/state/smart/SessionConversation.tsx +++ b/ts/state/smart/SessionConversation.tsx @@ -7,13 +7,11 @@ import { getMessagesOfSelectedConversation, getSelectedConversation, getSelectedConversationKey, -} from '../selectors/conversations'; -import { getOurNumber } from '../selectors/user'; -import { getSelectedMessageIds, isMessageDetailView, isRightPanelShowing, -} from '../selectors/conversationScreen'; +} from '../selectors/conversations'; +import { getOurNumber } from '../selectors/user'; const mapStateToProps = (state: StateType) => { return {