diff --git a/commafeed-client/package-lock.json b/commafeed-client/package-lock.json index 81f2159c..d15a71c3 100644 --- a/commafeed-client/package-lock.json +++ b/commafeed-client/package-lock.json @@ -39,6 +39,7 @@ "react-swipeable": "^7.0.0", "swagger-ui-react": "^4.15.5", "tinycon": "^0.6.8", + "use-local-storage": "^3.0.0", "websocket-heartbeat-js": "^1.1.1" }, "devDependencies": { @@ -10546,6 +10547,14 @@ } } }, + "node_modules/use-local-storage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/use-local-storage/-/use-local-storage-3.0.0.tgz", + "integrity": "sha512-wlPNnBCG3ULIJMr5A+dvWqLiPWCfsN1Kwijq+sAhT5yV4ex0u6XmZuNwP+RerIOfzBuz1pwSZuzhZMiluGQHfQ==", + "peerDependencies": { + "react": ">=16.8.1" + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", @@ -18434,6 +18443,12 @@ "use-isomorphic-layout-effect": "^1.1.1" } }, + "use-local-storage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/use-local-storage/-/use-local-storage-3.0.0.tgz", + "integrity": "sha512-wlPNnBCG3ULIJMr5A+dvWqLiPWCfsN1Kwijq+sAhT5yV4ex0u6XmZuNwP+RerIOfzBuz1pwSZuzhZMiluGQHfQ==", + "requires": {} + }, "use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", diff --git a/commafeed-client/package.json b/commafeed-client/package.json index e6d894a8..2310b047 100644 --- a/commafeed-client/package.json +++ b/commafeed-client/package.json @@ -47,6 +47,7 @@ "react-swipeable": "^7.0.0", "swagger-ui-react": "^4.15.5", "tinycon": "^0.6.8", + "use-local-storage": "^3.0.0", "websocket-heartbeat-js": "^1.1.1" }, "devDependencies": { diff --git a/commafeed-client/src/App.tsx b/commafeed-client/src/App.tsx index f3c4017e..af9e52aa 100644 --- a/commafeed-client/src/App.tsx +++ b/commafeed-client/src/App.tsx @@ -1,7 +1,8 @@ import { i18n } from "@lingui/core" import { I18nProvider } from "@lingui/react" import { ColorScheme, ColorSchemeProvider, MantineProvider } from "@mantine/core" -import { useColorScheme, useLocalStorage } from "@mantine/hooks" +import { useColorScheme } from "@mantine/hooks" +import useLocalStorage from "use-local-storage" import { ModalsProvider } from "@mantine/modals" import { NotificationsProvider } from "@mantine/notifications" import { Constants } from "app/constants" @@ -32,11 +33,7 @@ import Tinycon from "tinycon" function Providers(props: { children: React.ReactNode }) { const preferredColorScheme = useColorScheme() - const [colorScheme, setColorScheme] = useLocalStorage({ - key: "color-scheme", - defaultValue: preferredColorScheme, - getInitialValueInEffect: true, - }) + const [colorScheme, setColorScheme] = useLocalStorage("color-scheme", preferredColorScheme) const toggleColorScheme = (value?: ColorScheme) => setColorScheme(value || (colorScheme === "dark" ? "light" : "dark")) return ( diff --git a/commafeed-client/src/app/slices/user.ts b/commafeed-client/src/app/slices/user.ts index e317004a..8eb27e54 100644 --- a/commafeed-client/src/app/slices/user.ts +++ b/commafeed-client/src/app/slices/user.ts @@ -3,7 +3,7 @@ import { showNotification } from "@mantine/notifications" import { createAsyncThunk, createSlice, isAnyOf } from "@reduxjs/toolkit" import { client } from "app/client" import { RootState } from "app/store" -import { ReadingMode, ReadingOrder, Settings, SharingSettings, UserModel, ViewMode } from "app/types" +import { ReadingMode, ReadingOrder, Settings, SharingSettings, UserModel } from "app/types" // eslint-disable-next-line import/no-cycle import { reloadEntries } from "./entries" @@ -36,46 +36,67 @@ export const changeReadingOrder = createAsyncThunk("settings/viewMode", (viewMode, thunkApi) => { - const { settings } = thunkApi.getState().user - if (!settings) return - client.user.saveSettings({ ...settings, viewMode }) - thunkApi.dispatch(reloadEntries()) -}) -export const changeLanguage = createAsyncThunk("settings/language", (language, thunkApi) => { +export const changeLanguage = createAsyncThunk< + void, + string, + { + state: RootState + } +>("settings/language", (language, thunkApi) => { const { settings } = thunkApi.getState().user if (!settings) return client.user.saveSettings({ ...settings, language }) }) -export const changeScrollSpeed = createAsyncThunk("settings/scrollSpeed", (speed, thunkApi) => { +export const changeScrollSpeed = createAsyncThunk< + void, + boolean, + { + state: RootState + } +>("settings/scrollSpeed", (speed, thunkApi) => { const { settings } = thunkApi.getState().user if (!settings) return client.user.saveSettings({ ...settings, scrollSpeed: speed ? 400 : 0 }) }) -export const changeShowRead = createAsyncThunk("settings/showRead", (showRead, thunkApi) => { +export const changeShowRead = createAsyncThunk< + void, + boolean, + { + state: RootState + } +>("settings/showRead", (showRead, thunkApi) => { const { settings } = thunkApi.getState().user if (!settings) return client.user.saveSettings({ ...settings, showRead }) }) -export const changeScrollMarks = createAsyncThunk("settings/scrollMarks", (scrollMarks, thunkApi) => { +export const changeScrollMarks = createAsyncThunk< + void, + boolean, + { + state: RootState + } +>("settings/scrollMarks", (scrollMarks, thunkApi) => { const { settings } = thunkApi.getState().user if (!settings) return client.user.saveSettings({ ...settings, scrollMarks }) }) -export const changeSharingSetting = createAsyncThunk( - "settings/sharingSetting", - (sharingSetting, thunkApi) => { - const { settings } = thunkApi.getState().user - if (!settings) return - client.user.saveSettings({ - ...settings, - sharingSettings: { - ...settings.sharingSettings, - [sharingSetting.site]: sharingSetting.value, - }, - }) +export const changeSharingSetting = createAsyncThunk< + void, + { site: keyof SharingSettings; value: boolean }, + { + state: RootState } -) +>("settings/sharingSetting", (sharingSetting, thunkApi) => { + const { settings } = thunkApi.getState().user + if (!settings) return + client.user.saveSettings({ + ...settings, + sharingSettings: { + ...settings.sharingSettings, + [sharingSetting.site]: sharingSetting.value, + }, + }) +}) export const userSlice = createSlice({ name: "user", @@ -99,10 +120,6 @@ export const userSlice = createSlice({ if (!state.settings) return state.settings.readingOrder = action.meta.arg }) - builder.addCase(changeViewMode.pending, (state, action) => { - if (!state.settings) return - state.settings.viewMode = action.meta.arg - }) builder.addCase(changeLanguage.pending, (state, action) => { if (!state.settings) return state.settings.language = action.meta.arg diff --git a/commafeed-client/src/app/types.ts b/commafeed-client/src/app/types.ts index 9b8cf32d..37bf7434 100644 --- a/commafeed-client/src/app/types.ts +++ b/commafeed-client/src/app/types.ts @@ -228,7 +228,6 @@ export interface Settings { language: string readingMode: ReadingMode readingOrder: ReadingOrder - viewMode: ViewMode showRead: boolean scrollMarks: boolean theme?: string diff --git a/commafeed-client/src/components/content/FeedEntries.tsx b/commafeed-client/src/components/content/FeedEntries.tsx index 81f64ea3..c1157f8e 100644 --- a/commafeed-client/src/components/content/FeedEntries.tsx +++ b/commafeed-client/src/components/content/FeedEntries.tsx @@ -21,6 +21,7 @@ import throttle from "lodash/throttle" import { useEffect } from "react" import InfiniteScroll from "react-infinite-scroller" import { FeedEntry } from "./FeedEntry" +import { useViewMode } from "../../hooks/useViewMode" export function FeedEntries() { const source = useAppSelector(state => state.entries.source) @@ -28,7 +29,7 @@ export function FeedEntries() { const entriesTimestamp = useAppSelector(state => state.entries.timestamp) const selectedEntryId = useAppSelector(state => state.entries.selectedEntryId) const hasMore = useAppSelector(state => state.entries.hasMore) - const viewMode = useAppSelector(state => state.user.settings?.viewMode) + const { viewMode } = useViewMode() const scrollMarks = useAppSelector(state => state.user.settings?.scrollMarks) const scrollingToEntry = useAppSelector(state => state.entries.scrollingToEntry) const dispatch = useAppDispatch() diff --git a/commafeed-client/src/components/content/FeedEntry.tsx b/commafeed-client/src/components/content/FeedEntry.tsx index ef3781ba..d41d0014 100644 --- a/commafeed-client/src/components/content/FeedEntry.tsx +++ b/commafeed-client/src/components/content/FeedEntry.tsx @@ -1,7 +1,7 @@ import { Anchor, Box, createStyles, Divider, Paper } from "@mantine/core" import { Constants } from "app/constants" import { markEntry } from "app/slices/entries" -import { useAppDispatch, useAppSelector } from "app/store" +import { useAppDispatch } from "app/store" import { Entry, ViewMode } from "app/types" import React from "react" import { useSwipeable } from "react-swipeable" @@ -11,6 +11,7 @@ import { FeedEntryCompactHeader } from "./FeedEntryCompactHeader" import { FeedEntryContextMenu, useFeedEntryContextMenu } from "./FeedEntryContextMenu" import { FeedEntryFooter } from "./FeedEntryFooter" import { FeedEntryHeader } from "./FeedEntryHeader" +import { useViewMode } from "../../hooks/useViewMode" interface FeedEntryProps { entry: Entry @@ -63,7 +64,7 @@ const useStyles = createStyles((theme, props: FeedEntryProps & { viewMode?: View }) export function FeedEntry(props: FeedEntryProps) { - const viewMode = useAppSelector(state => state.user.settings?.viewMode) + const { viewMode } = useViewMode() const { classes } = useStyles({ ...props, viewMode }) const dispatch = useAppDispatch() diff --git a/commafeed-client/src/components/header/ProfileMenu.tsx b/commafeed-client/src/components/header/ProfileMenu.tsx index 8e452aee..6883bcc2 100644 --- a/commafeed-client/src/components/header/ProfileMenu.tsx +++ b/commafeed-client/src/components/header/ProfileMenu.tsx @@ -3,7 +3,6 @@ import { Box, Divider, Group, Menu, SegmentedControl, SegmentedControlItem, useM import { showNotification } from "@mantine/notifications" import { client } from "app/client" import { redirectToAbout, redirectToAdminUsers, redirectToMetrics, redirectToSettings } from "app/slices/redirect" -import { changeViewMode } from "app/slices/user" import { useAppDispatch, useAppSelector } from "app/store" import { ViewMode } from "app/types" import { useState } from "react" @@ -21,6 +20,7 @@ import { TbUsers, TbWorldDownload, } from "react-icons/tb" +import { useViewMode } from "../../hooks/useViewMode" interface ProfileMenuProps { control: React.ReactElement @@ -81,7 +81,7 @@ const viewModeData: ViewModeControlItem[] = [ export function ProfileMenu(props: ProfileMenuProps) { const [opened, setOpened] = useState(false) - const viewMode = useAppSelector(state => state.user.settings?.viewMode) + const { viewMode, setViewMode } = useViewMode() const profile = useAppSelector(state => state.user.profile) const admin = useAppSelector(state => state.user.profile?.admin) const dispatch = useAppDispatch() @@ -139,7 +139,7 @@ export function ProfileMenu(props: ProfileMenuProps) { orientation="vertical" data={viewModeData} value={viewMode} - onChange={e => dispatch(changeViewMode(e as ViewMode))} + onChange={e => setViewMode(e as ViewMode)} mb="xs" /> diff --git a/commafeed-client/src/hooks/useViewMode.ts b/commafeed-client/src/hooks/useViewMode.ts new file mode 100644 index 00000000..620d83df --- /dev/null +++ b/commafeed-client/src/hooks/useViewMode.ts @@ -0,0 +1,7 @@ +import useLocalStorage from "use-local-storage" +import { ViewMode } from "../app/types" + +export function useViewMode() { + const [viewMode, setViewMode] = useLocalStorage("view-mode", "detailed") + return { viewMode, setViewMode } +}