diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index f6838c0e65..81e45586cc 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -5826,6 +5826,12 @@ button.module-calling-participants-list__contact { } } +.module-left-pane__dialogs { + & + .module-left-pane__chatFolders { + margin-top: 8px; + } +} + .module-left-pane__archive-helper-text { @include mixins.font-body-2; diff --git a/ts/components/leftPane/LeftPaneChatFolders.dom.tsx b/ts/components/leftPane/LeftPaneChatFolders.dom.tsx index 640109fe7c..b34f47b051 100644 --- a/ts/components/leftPane/LeftPaneChatFolders.dom.tsx +++ b/ts/components/leftPane/LeftPaneChatFolders.dom.tsx @@ -85,6 +85,9 @@ function getChatFolderIconName( return chatFolder.folderType === ChatFolderType.ALL ? 'message' : 'folder'; } +// Needed to conditionally apply margin after network warning/update available +const LEFT_PANE_CHAT_FOLDERS_CLASS_NAME = 'module-left-pane__chatFolders'; + export function LeftPaneChatFolders( props: LeftPaneChatFoldersProps ): JSX.Element | null { @@ -116,7 +119,7 @@ export function LeftPaneChatFolders( if (props.navSidebarWidthBreakpoint === WidthBreakpoint.Narrow) { return ( -
+
; -export type ChatFolderChangeSelectedChatFolderId = ReadonlyDeep<{ - type: typeof CHAT_FOLDER_CHANGE_SELECTED_CHAT_FOLDER_ID; +export type ChatFolderUpdateSelectedChatFolderId = ReadonlyDeep<{ + type: typeof CHAT_FOLDER_UPDATE_SELECTED_CHAT_FOLDER_ID; payload: ChatFolderId | null; }>; +export type ChatFolderUpdateStableConversationIdInChatFolder = ReadonlyDeep<{ + type: typeof CHAT_FOLDER_UPDATE_STABLE_SELECTED_CONVERSATION_ID_IN_CHAT_FOLDER; + payload: string | null; +}>; + export type ChatFolderAction = ReadonlyDeep< - ChatFolderRecordReplaceAll | ChatFolderChangeSelectedChatFolderId + | ChatFolderRecordReplaceAll + | ChatFolderUpdateSelectedChatFolderId + | ChatFolderUpdateStableConversationIdInChatFolder >; export function getEmptyState(): ChatFoldersState { @@ -198,15 +210,24 @@ function updateChatFoldersPositions( }; } -function updateSelectedChangeFolderId( +function updateSelectedChatFolderId( chatFolderId: ChatFolderId | null -): ChatFolderChangeSelectedChatFolderId { +): ChatFolderUpdateSelectedChatFolderId { return { - type: CHAT_FOLDER_CHANGE_SELECTED_CHAT_FOLDER_ID, + type: CHAT_FOLDER_UPDATE_SELECTED_CHAT_FOLDER_ID, payload: chatFolderId, }; } +function updateStableSelectedConversationIdInChatFolder( + conversationId: string | null +): ChatFolderUpdateStableConversationIdInChatFolder { + return { + type: CHAT_FOLDER_UPDATE_STABLE_SELECTED_CONVERSATION_ID_IN_CHAT_FOLDER, + payload: conversationId, + }; +} + function updateChatFolderToggleChat( chatFolderId: ChatFolderId, conversationId: string, @@ -242,6 +263,47 @@ function updateChatFolderToggleChat( }; } +export function updateChatFolderStateOnTargetConversationChanged( + conversationId: string | undefined +): ThunkAction< + void, + RootStateType, + unknown, + | ChatFolderUpdateSelectedChatFolderId + | ChatFolderUpdateStableConversationIdInChatFolder +> { + return async (dispatch, getState) => { + if (conversationId == null) { + // Clear out the stable conversation id since it has been closed. + dispatch(updateStableSelectedConversationIdInChatFolder(null)); + return; + } + + const state = getState(); + const selectedChatFolder = getSelectedChatFolder(state); + if (selectedChatFolder == null) { + return; + } + + const conversationLookup = getConversationLookup(state); + const conversation = getOwn(conversationLookup, conversationId); + strictAssert(conversation != null, 'Target conversation not found'); + + if (isConversationInChatFolder(selectedChatFolder, conversation)) { + // Make sure the targetted conversation doesn't appear from the chat folder + // while its open (in case it gets marked read for example). + dispatch(updateStableSelectedConversationIdInChatFolder(conversationId)); + return; + } + + const currentChatFolders = getCurrentChatFolders(state); + const { currentAllChatFolder } = currentChatFolders; + + dispatch(updateSelectedChatFolderId(currentAllChatFolder?.id ?? null)); + dispatch(updateStableSelectedConversationIdInChatFolder(conversationId)); + }; +} + export const actions = { refetchChatFolders, createChatFolder, @@ -249,7 +311,7 @@ export const actions = { updateChatFolder, deleteChatFolder, updateChatFoldersPositions, - updateSelectedChangeFolderId, + updateSelectedChatFolderId, updateChatFolderToggleChat, }; @@ -291,8 +353,11 @@ function toNextChatFoldersState( ); // Ensure `stableSelectedConversationIdInChatFolder` - // is reset if `selectedChatFolderId` changes - if (nextState.selectedChatFolderId !== prevState.selectedChatFolderId) { + // is reset if `selectedChatFolderId` is null or has been changed + if ( + nextState.selectedChatFolderId == null || + nextState.selectedChatFolderId !== prevState.selectedChatFolderId + ) { nextState.stableSelectedConversationIdInChatFolder = null; } @@ -301,21 +366,20 @@ function toNextChatFoldersState( export function reducer( state: ChatFoldersState = getEmptyState(), - action: ChatFolderAction | TargetedConversationChangedActionType + action: ChatFolderAction ): ChatFoldersState { switch (action.type) { case CHAT_FOLDER_RECORD_REPLACE_ALL: return toNextChatFoldersState(state, { currentChatFolders: action.payload, }); - case CHAT_FOLDER_CHANGE_SELECTED_CHAT_FOLDER_ID: + case CHAT_FOLDER_UPDATE_SELECTED_CHAT_FOLDER_ID: return toNextChatFoldersState(state, { selectedChatFolderId: action.payload, }); - case TARGETED_CONVERSATION_CHANGED: + case CHAT_FOLDER_UPDATE_STABLE_SELECTED_CONVERSATION_ID_IN_CHAT_FOLDER: return toNextChatFoldersState(state, { - stableSelectedConversationIdInChatFolder: - action.payload.conversationId ?? null, + stableSelectedConversationIdInChatFolder: action.payload, }); default: return state; diff --git a/ts/state/ducks/conversations.preload.ts b/ts/state/ducks/conversations.preload.ts index cca6c9dc58..7d7311cd5c 100644 --- a/ts/state/ducks/conversations.preload.ts +++ b/ts/state/ducks/conversations.preload.ts @@ -241,6 +241,7 @@ import { isConversationUnread } from '../../util/isConversationUnread.std.js'; import { CurrentChatFolders } from '../../types/CurrentChatFolders.std.js'; import { itemStorage } from '../../textsecure/Storage.preload.js'; import { enqueuePollVoteForSend as enqueuePollVoteForSendHelper } from '../../polls/enqueuePollVoteForSend.preload.js'; +import { updateChatFolderStateOnTargetConversationChanged } from './chatFolders.preload.js'; const { chunk, @@ -4808,6 +4809,8 @@ function showConversation({ conversation?.setMarkedUnread(false); } + dispatch(updateChatFolderStateOnTargetConversationChanged(conversationId)); + if (conversationId === conversations.selectedConversationId) { if (!conversationId) { return; diff --git a/ts/state/smart/LeftPaneChatFolders.preload.tsx b/ts/state/smart/LeftPaneChatFolders.preload.tsx index a56d8f0787..1e1c0af32f 100644 --- a/ts/state/smart/LeftPaneChatFolders.preload.tsx +++ b/ts/state/smart/LeftPaneChatFolders.preload.tsx @@ -33,7 +33,7 @@ export const SmartLeftPaneChatFolders = memo( ); const location = useSelector(getSelectedLocation); - const { updateSelectedChangeFolderId } = useChatFolderActions(); + const { updateSelectedChatFolderId } = useChatFolderActions(); const { changeLocation } = useNavActions(); const { markChatFolderRead, setChatFolderMuteExpiration } = useConversationsActions(); @@ -66,7 +66,7 @@ export const SmartLeftPaneChatFolders = memo( allChatFoldersUnreadStats={allChatFoldersUnreadStats} allChatFoldersMutedStats={allChatFoldersMutedStats} selectedChatFolder={selectedChatFolder} - onSelectedChatFolderIdChange={updateSelectedChangeFolderId} + onSelectedChatFolderIdChange={updateSelectedChatFolderId} onChatFolderMarkRead={markChatFolderRead} onChatFolderUpdateMute={setChatFolderMuteExpiration} onChatFolderOpenSettings={handleChatFolderOpenSettings} diff --git a/ts/test-electron/state/ducks/conversations_test.preload.ts b/ts/test-electron/state/ducks/conversations_test.preload.ts index 90a4a4522a..2405078b5b 100644 --- a/ts/test-electron/state/ducks/conversations_test.preload.ts +++ b/ts/test-electron/state/ducks/conversations_test.preload.ts @@ -508,7 +508,12 @@ describe('both/state/ducks/conversations', () => { getEmptyRootState, null ); - const action = dispatch.getCall(0).args[0]; + const action = dispatch + .getCalls() + .map(call => call.args[0]) + .find(a => { + return a.type === TARGETED_CONVERSATION_CHANGED; + }); const nextState = reducer(state, action); assert.equal(nextState.selectedConversationId, 'abc123'); @@ -531,7 +536,10 @@ describe('both/state/ducks/conversations', () => { conversationId: 'abc123', messageId: 'xyz987', })(dispatch, getEmptyRootState, null); - const action = dispatch.getCall(0).args[0]; + const action = dispatch + .getCalls() + .map(call => call.args[0]) + .find(a => a.type === TARGETED_CONVERSATION_CHANGED); const nextState = reducer(state, action); assert.equal(nextState.selectedConversationId, 'abc123'); @@ -547,7 +555,12 @@ describe('both/state/ducks/conversations', () => { conversationId: 'fake-conversation-id', switchToAssociatedView: true, })(dispatch, getEmptyRootState, null); - [action] = dispatch.getCall(0).args; + action = dispatch + .getCalls() + .map(call => call.args[0]) + .find(a => { + return a.type === TARGETED_CONVERSATION_CHANGED; + }); }); it('shows the inbox if the conversation is not archived', () => {