mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
Persist thread sidebar visibility (#3390)
This commit is contained in:
@@ -57,6 +57,7 @@ import {
|
||||
persistReaction,
|
||||
setActiveThread,
|
||||
setSelectedThread,
|
||||
setThreadSidebarVisible,
|
||||
THREAD_NOT_FOUND_MESSAGE,
|
||||
updateThreadTitle,
|
||||
} from '../store/threadSlice';
|
||||
@@ -187,10 +188,16 @@ const Conversations = ({
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { threads, selectedThreadId, messages, isLoadingMessages, messagesError, activeThreadId } =
|
||||
useAppSelector(state => state.thread);
|
||||
const {
|
||||
threads,
|
||||
selectedThreadId,
|
||||
threadSidebarVisible = false,
|
||||
messages,
|
||||
isLoadingMessages,
|
||||
messagesError,
|
||||
activeThreadId,
|
||||
} = useAppSelector(state => state.thread);
|
||||
|
||||
const [showSidebar, setShowSidebar] = useState(false);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -1224,7 +1231,7 @@ const Conversations = ({
|
||||
labelTabs.find(tab => tab.value === selectedLabel)?.label ?? selectedLabel;
|
||||
|
||||
const isSidebar = variant === 'sidebar';
|
||||
const effectiveShowSidebar = showSidebar;
|
||||
const effectiveShowSidebar = threadSidebarVisible;
|
||||
|
||||
// Stable title resolver used by both the sidebar thread list and the header.
|
||||
const resolveThreadDisplayTitle = (threadId: string | null): string => {
|
||||
@@ -1405,7 +1412,7 @@ const Conversations = ({
|
||||
<button
|
||||
type="button"
|
||||
data-analytics-id="chat-header-toggle-sidebar"
|
||||
onClick={() => setShowSidebar(prev => !prev)}
|
||||
onClick={() => dispatch(setThreadSidebarVisible(!effectiveShowSidebar))}
|
||||
className="w-7 h-7 flex items-center justify-center rounded-lg hover:bg-stone-100 dark:hover:bg-neutral-800 dark:bg-neutral-800 dark:hover:bg-neutral-800/60 text-stone-500 dark:text-neutral-400 hover:text-stone-700 dark:hover:text-neutral-200 dark:text-neutral-200 dark:hover:text-neutral-200 transition-colors"
|
||||
title={effectiveShowSidebar ? t('chat.hideSidebar') : t('chat.showSidebar')}>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
||||
@@ -206,8 +206,7 @@ async function renderConversations(preload: Record<string, unknown> = {}) {
|
||||
return store;
|
||||
}
|
||||
|
||||
/** Click the sidebar toggle so the thread list becomes visible.
|
||||
* The sidebar starts hidden (showSidebar=false) in this PR. */
|
||||
/** Click the sidebar toggle so the thread list becomes visible. */
|
||||
async function openSidebar() {
|
||||
const toggleBtn = screen.getByTitle('Show sidebar');
|
||||
await act(async () => {
|
||||
@@ -219,6 +218,7 @@ async function openSidebar() {
|
||||
const emptyThreadState = {
|
||||
threads: [],
|
||||
selectedThreadId: null,
|
||||
threadSidebarVisible: false,
|
||||
activeThreadId: null,
|
||||
welcomeThreadId: null,
|
||||
messagesByThreadId: {},
|
||||
@@ -312,7 +312,7 @@ describe('Conversations — smoke render (#1123 welcome-lock removal)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Covers line 906: const effectiveShowSidebar = showSidebar;
|
||||
// Covers line 906: const effectiveShowSidebar = threadSidebarVisible;
|
||||
// Covers line 941: <div className="flex-1 overflow-y-auto"> (always rendered in page mode)
|
||||
it('renders the Threads sidebar header in page mode', async () => {
|
||||
await act(async () => {
|
||||
@@ -326,6 +326,26 @@ describe('Conversations — smoke render (#1123 welcome-lock removal)', () => {
|
||||
expect(screen.getByText('Threads')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('restores and updates the persisted thread sidebar visibility', async () => {
|
||||
let renderedStore: ReturnType<typeof buildStore> | undefined;
|
||||
await act(async () => {
|
||||
renderedStore = await renderConversations({
|
||||
thread: { ...emptyThreadState, threadSidebarVisible: true },
|
||||
});
|
||||
});
|
||||
|
||||
expect(screen.getByText('Threads')).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTitle('Hide sidebar'));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Threads')).not.toBeInTheDocument();
|
||||
});
|
||||
expect(renderedStore?.getState().thread.threadSidebarVisible).toBe(false);
|
||||
});
|
||||
|
||||
// Covers line 941 empty branch
|
||||
it('shows the General empty message when the default bucket has no threads', async () => {
|
||||
await act(async () => {
|
||||
|
||||
@@ -138,7 +138,11 @@ const persistedNotificationReducer = persistReducer(notificationPersistConfig, n
|
||||
// they were instead of falling through to "create a new thread". The
|
||||
// thread list and per-thread message caches are re-fetched from the core
|
||||
// on boot, so we deliberately don't persist them.
|
||||
const threadPersistConfig = { key: 'thread', storage, whitelist: ['selectedThreadId'] };
|
||||
const threadPersistConfig = {
|
||||
key: 'thread',
|
||||
storage,
|
||||
whitelist: ['selectedThreadId', 'threadSidebarVisible'],
|
||||
};
|
||||
const persistedThreadReducer = persistReducer(threadPersistConfig, threadReducer);
|
||||
|
||||
// Persist only previously persisted mascot appearance fields plus the custom
|
||||
|
||||
@@ -11,6 +11,7 @@ export const THREAD_NOT_FOUND_MESSAGE = 'This thread is no longer available.';
|
||||
interface ThreadState {
|
||||
threads: Thread[];
|
||||
selectedThreadId: string | null;
|
||||
threadSidebarVisible: boolean;
|
||||
activeThreadId: string | null;
|
||||
/**
|
||||
* @deprecated — welcome-agent replaced by Joyride walkthrough. Always null
|
||||
@@ -27,6 +28,7 @@ interface ThreadState {
|
||||
const initialState: ThreadState = {
|
||||
threads: [],
|
||||
selectedThreadId: null,
|
||||
threadSidebarVisible: false,
|
||||
activeThreadId: null,
|
||||
welcomeThreadId: null,
|
||||
messagesByThreadId: {},
|
||||
@@ -337,6 +339,9 @@ const threadSlice = createSlice({
|
||||
setActiveThread: (state, action: { payload: string | null }) => {
|
||||
state.activeThreadId = action.payload;
|
||||
},
|
||||
setThreadSidebarVisible: (state, action: PayloadAction<boolean>) => {
|
||||
state.threadSidebarVisible = action.payload;
|
||||
},
|
||||
clearStaleThread: (state, action: PayloadAction<string>) => {
|
||||
const threadId = action.payload;
|
||||
state.threads = state.threads.filter(thread => thread.id !== threadId);
|
||||
@@ -459,6 +464,7 @@ export const {
|
||||
setSelectedThread,
|
||||
clearSelectedThread,
|
||||
setActiveThread,
|
||||
setThreadSidebarVisible,
|
||||
clearStaleThread,
|
||||
clearAllThreads,
|
||||
resetThreadCachesPreservingSelection,
|
||||
|
||||
Reference in New Issue
Block a user