mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
♻️ refactor: Replace fontSize Recoil atom with Jotai (#10171)
* fix: reapply chat font size on load * refactor: streamline font size handling in localStorage * fix: update matchMedia mock to accurately reflect desktop and touchscreen capabilities * refactor: implement Jotai for font size management and initialize on app load - Replaced Recoil with Jotai for font size state management across components. - Added a new `fontSize` atom to handle font size changes and persist them in localStorage. - Implemented `initializeFontSize` function to apply saved font size on app load. - Updated relevant components to utilize the new font size atom. --------- Co-authored-by: ddooochii <ddooochii@gmail.com> Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
114deecc4e
commit
d41b07c0af
11 changed files with 87 additions and 19 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { RouterProvider } from 'react-router-dom';
|
import { RouterProvider } from 'react-router-dom';
|
||||||
|
|
@ -8,6 +9,7 @@ import { Toast, ThemeProvider, ToastProvider } from '@librechat/client';
|
||||||
import { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query';
|
||||||
import { ScreenshotProvider, useApiErrorBoundary } from './hooks';
|
import { ScreenshotProvider, useApiErrorBoundary } from './hooks';
|
||||||
import { getThemeFromEnv } from './utils/getThemeFromEnv';
|
import { getThemeFromEnv } from './utils/getThemeFromEnv';
|
||||||
|
import { initializeFontSize } from '~/store/fontSize';
|
||||||
import { LiveAnnouncer } from '~/a11y';
|
import { LiveAnnouncer } from '~/a11y';
|
||||||
import { router } from './routes';
|
import { router } from './routes';
|
||||||
|
|
||||||
|
|
@ -24,6 +26,10 @@ const App = () => {
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initializeFontSize();
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Load theme from environment variables if available
|
// Load theme from environment variables if available
|
||||||
const envTheme = getThemeFromEnv();
|
const envTheme = getThemeFromEnv();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import type { TMessageContentParts } from 'librechat-data-provider';
|
import type { TMessageContentParts } from 'librechat-data-provider';
|
||||||
import type { TMessageProps, TMessageIcon } from '~/common';
|
import type { TMessageProps, TMessageIcon } from '~/common';
|
||||||
import { useMessageHelpers, useLocalize, useAttachments } from '~/hooks';
|
import { useMessageHelpers, useLocalize, useAttachments } from '~/hooks';
|
||||||
import MessageIcon from '~/components/Chat/Messages/MessageIcon';
|
import MessageIcon from '~/components/Chat/Messages/MessageIcon';
|
||||||
import ContentParts from './Content/ContentParts';
|
import ContentParts from './Content/ContentParts';
|
||||||
|
import { fontSizeAtom } from '~/store/fontSize';
|
||||||
import SiblingSwitch from './SiblingSwitch';
|
import SiblingSwitch from './SiblingSwitch';
|
||||||
import MultiMessage from './MultiMessage';
|
import MultiMessage from './MultiMessage';
|
||||||
import HoverButtons from './HoverButtons';
|
import HoverButtons from './HoverButtons';
|
||||||
|
|
@ -36,7 +38,7 @@ export default function Message(props: TMessageProps) {
|
||||||
regenerateMessage,
|
regenerateMessage,
|
||||||
} = useMessageHelpers(props);
|
} = useMessageHelpers(props);
|
||||||
|
|
||||||
const fontSize = useRecoilValue(store.fontSize);
|
const fontSize = useAtomValue(fontSizeAtom);
|
||||||
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
|
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
|
||||||
const { children, messageId = null, isCreatedByUser } = message ?? {};
|
const { children, messageId = null, isCreatedByUser } = message ?? {};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { CSSTransition } from 'react-transition-group';
|
import { CSSTransition } from 'react-transition-group';
|
||||||
import type { TMessage } from 'librechat-data-provider';
|
import type { TMessage } from 'librechat-data-provider';
|
||||||
import { useScreenshot, useMessageScrolling, useLocalize } from '~/hooks';
|
import { useScreenshot, useMessageScrolling, useLocalize } from '~/hooks';
|
||||||
import ScrollToBottom from '~/components/Messages/ScrollToBottom';
|
import ScrollToBottom from '~/components/Messages/ScrollToBottom';
|
||||||
import { MessagesViewProvider } from '~/Providers';
|
import { MessagesViewProvider } from '~/Providers';
|
||||||
|
import { fontSizeAtom } from '~/store/fontSize';
|
||||||
import MultiMessage from './MultiMessage';
|
import MultiMessage from './MultiMessage';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
@ -15,7 +17,7 @@ function MessagesViewContent({
|
||||||
messagesTree?: TMessage[] | null;
|
messagesTree?: TMessage[] | null;
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const fontSize = useRecoilValue(store.fontSize);
|
const fontSize = useAtomValue(fontSizeAtom);
|
||||||
const { screenshotTargetRef } = useScreenshot();
|
const { screenshotTargetRef } = useScreenshot();
|
||||||
const scrollButtonPreference = useRecoilValue(store.showScrollButton);
|
const scrollButtonPreference = useRecoilValue(store.showScrollButton);
|
||||||
const [currentEditId, setCurrentEditId] = useState<number | string | null>(-1);
|
const [currentEditId, setCurrentEditId] = useState<number | string | null>(-1);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { useAuthContext, useLocalize } from '~/hooks';
|
import { useAuthContext, useLocalize } from '~/hooks';
|
||||||
import type { TMessageProps, TMessageIcon } from '~/common';
|
import type { TMessageProps, TMessageIcon } from '~/common';
|
||||||
import MinimalHoverButtons from '~/components/Chat/Messages/MinimalHoverButtons';
|
import MinimalHoverButtons from '~/components/Chat/Messages/MinimalHoverButtons';
|
||||||
import Icon from '~/components/Chat/Messages/MessageIcon';
|
import Icon from '~/components/Chat/Messages/MessageIcon';
|
||||||
import SearchContent from './Content/SearchContent';
|
import SearchContent from './Content/SearchContent';
|
||||||
|
import { fontSizeAtom } from '~/store/fontSize';
|
||||||
import SearchButtons from './SearchButtons';
|
import SearchButtons from './SearchButtons';
|
||||||
import SubRow from './SubRow';
|
import SubRow from './SubRow';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
|
|
@ -34,8 +36,8 @@ const MessageBody = ({ message, messageLabel, fontSize }) => (
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function SearchMessage({ message }: Pick<TMessageProps, 'message'>) {
|
export default function SearchMessage({ message }: Pick<TMessageProps, 'message'>) {
|
||||||
|
const fontSize = useAtomValue(fontSizeAtom);
|
||||||
const UsernameDisplay = useRecoilValue<boolean>(store.UsernameDisplay);
|
const UsernameDisplay = useRecoilValue<boolean>(store.UsernameDisplay);
|
||||||
const fontSize = useRecoilValue(store.fontSize);
|
|
||||||
const { user } = useAuthContext();
|
const { user } = useAuthContext();
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useCallback, useMemo, memo } from 'react';
|
import React, { useCallback, useMemo, memo } from 'react';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { type TMessage } from 'librechat-data-provider';
|
import { type TMessage } from 'librechat-data-provider';
|
||||||
import type { TMessageProps, TMessageIcon } from '~/common';
|
import type { TMessageProps, TMessageIcon } from '~/common';
|
||||||
|
|
@ -9,6 +10,7 @@ import HoverButtons from '~/components/Chat/Messages/HoverButtons';
|
||||||
import MessageIcon from '~/components/Chat/Messages/MessageIcon';
|
import MessageIcon from '~/components/Chat/Messages/MessageIcon';
|
||||||
import { Plugin } from '~/components/Messages/Content';
|
import { Plugin } from '~/components/Messages/Content';
|
||||||
import SubRow from '~/components/Chat/Messages/SubRow';
|
import SubRow from '~/components/Chat/Messages/SubRow';
|
||||||
|
import { fontSizeAtom } from '~/store/fontSize';
|
||||||
import { MessageContext } from '~/Providers';
|
import { MessageContext } from '~/Providers';
|
||||||
import { useMessageActions } from '~/hooks';
|
import { useMessageActions } from '~/hooks';
|
||||||
import { cn, logger } from '~/utils';
|
import { cn, logger } from '~/utils';
|
||||||
|
|
@ -58,8 +60,8 @@ const MessageRender = memo(
|
||||||
isMultiMessage,
|
isMultiMessage,
|
||||||
setCurrentEditId,
|
setCurrentEditId,
|
||||||
});
|
});
|
||||||
|
const fontSize = useAtomValue(fontSizeAtom);
|
||||||
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
|
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
|
||||||
const fontSize = useRecoilValue(store.fontSize);
|
|
||||||
|
|
||||||
const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]);
|
const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]);
|
||||||
const hasNoChildren = !(msg?.children?.length ?? 0);
|
const hasNoChildren = !(msg?.children?.length ?? 0);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useRecoilValue } from 'recoil';
|
|
||||||
import { useCallback, useMemo, memo } from 'react';
|
import { useCallback, useMemo, memo } from 'react';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
import type { TMessage, TMessageContentParts } from 'librechat-data-provider';
|
import type { TMessage, TMessageContentParts } from 'librechat-data-provider';
|
||||||
import type { TMessageProps, TMessageIcon } from '~/common';
|
import type { TMessageProps, TMessageIcon } from '~/common';
|
||||||
import ContentParts from '~/components/Chat/Messages/Content/ContentParts';
|
import ContentParts from '~/components/Chat/Messages/Content/ContentParts';
|
||||||
|
|
@ -9,6 +10,7 @@ import HoverButtons from '~/components/Chat/Messages/HoverButtons';
|
||||||
import MessageIcon from '~/components/Chat/Messages/MessageIcon';
|
import MessageIcon from '~/components/Chat/Messages/MessageIcon';
|
||||||
import { useAttachments, useMessageActions } from '~/hooks';
|
import { useAttachments, useMessageActions } from '~/hooks';
|
||||||
import SubRow from '~/components/Chat/Messages/SubRow';
|
import SubRow from '~/components/Chat/Messages/SubRow';
|
||||||
|
import { fontSizeAtom } from '~/store/fontSize';
|
||||||
import { cn, logger } from '~/utils';
|
import { cn, logger } from '~/utils';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
|
|
@ -60,8 +62,8 @@ const ContentRender = memo(
|
||||||
isMultiMessage,
|
isMultiMessage,
|
||||||
setCurrentEditId,
|
setCurrentEditId,
|
||||||
});
|
});
|
||||||
|
const fontSize = useAtomValue(fontSizeAtom);
|
||||||
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
|
const maximizeChatSpace = useRecoilValue(store.maximizeChatSpace);
|
||||||
const fontSize = useRecoilValue(store.fontSize);
|
|
||||||
|
|
||||||
const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]);
|
const handleRegenerateMessage = useCallback(() => regenerateMessage(), [regenerateMessage]);
|
||||||
const isLast = useMemo(
|
const isLast = useMemo(
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
import { useRecoilState } from 'recoil';
|
import { useAtom } from 'jotai';
|
||||||
import { Dropdown, applyFontSize } from '@librechat/client';
|
import { Dropdown } from '@librechat/client';
|
||||||
|
import { fontSizeAtom } from '~/store/fontSize';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import store from '~/store';
|
|
||||||
|
|
||||||
export default function FontSizeSelector() {
|
export default function FontSizeSelector() {
|
||||||
const [fontSize, setFontSize] = useRecoilState(store.fontSize);
|
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
|
const [fontSize, setFontSize] = useAtom(fontSizeAtom);
|
||||||
|
|
||||||
const handleChange = (val: string) => {
|
const handleChange = (val: string) => {
|
||||||
setFontSize(val);
|
setFontSize(val);
|
||||||
applyFontSize(val);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const options = [
|
const options = [
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useAtomValue } from 'jotai';
|
||||||
import type { TMessageProps } from '~/common';
|
import type { TMessageProps } from '~/common';
|
||||||
import MinimalHoverButtons from '~/components/Chat/Messages/MinimalHoverButtons';
|
import MinimalHoverButtons from '~/components/Chat/Messages/MinimalHoverButtons';
|
||||||
import MessageContent from '~/components/Chat/Messages/Content/MessageContent';
|
import MessageContent from '~/components/Chat/Messages/Content/MessageContent';
|
||||||
|
|
@ -6,16 +6,16 @@ import SearchContent from '~/components/Chat/Messages/Content/SearchContent';
|
||||||
import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch';
|
import SiblingSwitch from '~/components/Chat/Messages/SiblingSwitch';
|
||||||
import { Plugin } from '~/components/Messages/Content';
|
import { Plugin } from '~/components/Messages/Content';
|
||||||
import SubRow from '~/components/Chat/Messages/SubRow';
|
import SubRow from '~/components/Chat/Messages/SubRow';
|
||||||
|
import { fontSizeAtom } from '~/store/fontSize';
|
||||||
import { MessageContext } from '~/Providers';
|
import { MessageContext } from '~/Providers';
|
||||||
import { useAttachments } from '~/hooks';
|
import { useAttachments } from '~/hooks';
|
||||||
|
|
||||||
import MultiMessage from './MultiMessage';
|
import MultiMessage from './MultiMessage';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
import store from '~/store';
|
|
||||||
|
|
||||||
import Icon from './MessageIcon';
|
import Icon from './MessageIcon';
|
||||||
export default function Message(props: TMessageProps) {
|
export default function Message(props: TMessageProps) {
|
||||||
const fontSize = useRecoilValue(store.fontSize);
|
const fontSize = useAtomValue(fontSizeAtom);
|
||||||
const {
|
const {
|
||||||
message,
|
message,
|
||||||
siblingIdx,
|
siblingIdx,
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ describe('useFocusChatEffect', () => {
|
||||||
(useNavigate as jest.Mock).mockReturnValue(mockNavigate);
|
(useNavigate as jest.Mock).mockReturnValue(mockNavigate);
|
||||||
|
|
||||||
// Mock window.matchMedia
|
// Mock window.matchMedia
|
||||||
window.matchMedia = jest.fn().mockImplementation(() => ({
|
window.matchMedia = jest.fn().mockImplementation((query) => ({
|
||||||
matches: false,
|
matches: query === '(hover: hover)', // Desktop has hover capability
|
||||||
media: '',
|
media: '',
|
||||||
onchange: null,
|
onchange: null,
|
||||||
addListener: jest.fn(),
|
addListener: jest.fn(),
|
||||||
|
|
@ -83,8 +83,8 @@ describe('useFocusChatEffect', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not focus textarea on touchscreen devices', () => {
|
test('should not focus textarea on touchscreen devices', () => {
|
||||||
window.matchMedia = jest.fn().mockImplementation(() => ({
|
window.matchMedia = jest.fn().mockImplementation((query) => ({
|
||||||
matches: true, // This indicates a touchscreen
|
matches: query === '(pointer: coarse)', // Touchscreen has coarse pointer
|
||||||
media: '',
|
media: '',
|
||||||
onchange: null,
|
onchange: null,
|
||||||
addListener: jest.fn(),
|
addListener: jest.fn(),
|
||||||
|
|
|
||||||
54
client/src/store/fontSize.ts
Normal file
54
client/src/store/fontSize.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { atom } from 'jotai';
|
||||||
|
import { atomWithStorage } from 'jotai/utils';
|
||||||
|
import { applyFontSize } from '@librechat/client';
|
||||||
|
|
||||||
|
const DEFAULT_FONT_SIZE = 'text-base';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base storage atom for font size
|
||||||
|
*/
|
||||||
|
const fontSizeStorageAtom = atomWithStorage<string>('fontSize', DEFAULT_FONT_SIZE, undefined, {
|
||||||
|
getOnInit: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derived atom that applies font size changes to the DOM
|
||||||
|
* Read: returns the current font size
|
||||||
|
* Write: updates storage and applies the font size to the DOM
|
||||||
|
*/
|
||||||
|
export const fontSizeAtom = atom(
|
||||||
|
(get) => get(fontSizeStorageAtom),
|
||||||
|
(get, set, newValue: string) => {
|
||||||
|
set(fontSizeStorageAtom, newValue);
|
||||||
|
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
||||||
|
applyFontSize(newValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize font size on app load
|
||||||
|
*/
|
||||||
|
export const initializeFontSize = () => {
|
||||||
|
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedValue = localStorage.getItem('fontSize');
|
||||||
|
|
||||||
|
if (savedValue !== null) {
|
||||||
|
try {
|
||||||
|
const parsedValue = JSON.parse(savedValue);
|
||||||
|
applyFontSize(parsedValue);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
'Error parsing localStorage key "fontSize", resetting to default. Error:',
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
localStorage.setItem('fontSize', JSON.stringify(DEFAULT_FONT_SIZE));
|
||||||
|
applyFontSize(DEFAULT_FONT_SIZE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
applyFontSize(DEFAULT_FONT_SIZE);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -21,7 +21,6 @@ const localStorageAtoms = {
|
||||||
// General settings
|
// General settings
|
||||||
autoScroll: atomWithLocalStorage('autoScroll', false),
|
autoScroll: atomWithLocalStorage('autoScroll', false),
|
||||||
hideSidePanel: atomWithLocalStorage('hideSidePanel', false),
|
hideSidePanel: atomWithLocalStorage('hideSidePanel', false),
|
||||||
fontSize: atomWithLocalStorage('fontSize', 'text-base'),
|
|
||||||
enableUserMsgMarkdown: atomWithLocalStorage<boolean>(
|
enableUserMsgMarkdown: atomWithLocalStorage<boolean>(
|
||||||
LocalStorageKeys.ENABLE_USER_MSG_MARKDOWN,
|
LocalStorageKeys.ENABLE_USER_MSG_MARKDOWN,
|
||||||
true,
|
true,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue