Chat folder fixes

Co-authored-by: Jamie <113370520+jamiebuilds-signal@users.noreply.github.com>
This commit is contained in:
automated-signal
2025-10-29 18:10:08 -05:00
committed by GitHub
parent 5c2904337e
commit d3c9a7aa22
6 changed files with 116 additions and 27 deletions

View File

@@ -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;

View File

@@ -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 (
<div className={tw('px-2')}>
<div className={tw(LEFT_PANE_CHAT_FOLDERS_CLASS_NAME, 'px-2')}>
<AxoSelect.Root
value={props.selectedChatFolder?.id ?? null}
onValueChange={handleValueChange}

View File

@@ -9,25 +9,28 @@ import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions.s
import { useBoundActions } from '../../hooks/useBoundActions.std.js';
import {
ChatFolderParamsSchema,
isConversationInChatFolder,
type ChatFolder,
type ChatFolderId,
type ChatFolderParams,
} from '../../types/ChatFolder.std.js';
import { getCurrentChatFolders } from '../selectors/chatFolders.std.js';
import {
getCurrentChatFolders,
getSelectedChatFolder,
} from '../selectors/chatFolders.std.js';
import { DataReader, DataWriter } from '../../sql/Client.preload.js';
import { storageServiceUploadJob } from '../../services/storage.preload.js';
import { parseStrict } from '../../util/schemas.std.js';
import { chatFolderCleanupService } from '../../services/expiring/chatFolderCleanupService.preload.js';
import { drop } from '../../util/drop.std.js';
import {
TARGETED_CONVERSATION_CHANGED,
type TargetedConversationChangedActionType,
} from './conversations.preload.js';
import type { ShowToastActionType } from './toast.preload.js';
import { showToast } from './toast.preload.js';
import { ToastType } from '../../types/Toast.dom.js';
import type { CurrentChatFolder } from '../../types/CurrentChatFolders.std.js';
import { CurrentChatFolders } from '../../types/CurrentChatFolders.std.js';
import { getConversationLookup } from '../selectors/conversations.dom.js';
import { getOwn } from '../../util/getOwn.std.js';
import { strictAssert } from '../../util/assert.std.js';
export type ChatFoldersState = ReadonlyDeep<{
currentChatFolders: CurrentChatFolders;
@@ -37,21 +40,30 @@ export type ChatFoldersState = ReadonlyDeep<{
const CHAT_FOLDER_RECORD_REPLACE_ALL =
'chatFolders/CHAT_FOLDER_RECORD_REPLACE_ALL';
const CHAT_FOLDER_CHANGE_SELECTED_CHAT_FOLDER_ID =
'chatFolders/CHANGE_SELECTED_CHAT_FOLDER_ID';
const CHAT_FOLDER_UPDATE_SELECTED_CHAT_FOLDER_ID =
'chatFolders/UPDATE_SELECTED_CHAT_FOLDER_ID';
const CHAT_FOLDER_UPDATE_STABLE_SELECTED_CONVERSATION_ID_IN_CHAT_FOLDER =
'chatFolders/CHAT_FOLDER_UPDATE_STABLE_SELECTED_CONVERSATION_ID_IN_CHAT_FOLDER';
export type ChatFolderRecordReplaceAll = ReadonlyDeep<{
type: typeof CHAT_FOLDER_RECORD_REPLACE_ALL;
payload: CurrentChatFolders;
}>;
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;

View File

@@ -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;

View File

@@ -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}

View File

@@ -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', () => {