refactor: grouped list (#210)
* refactor: grouped list Signed-off-by: Innei <i@innei.in> * fix: padding Signed-off-by: Innei <i@innei.in> --------- Signed-off-by: Innei <i@innei.in>
This commit is contained in:
parent
744422fc12
commit
f43e7e57f4
|
|
@ -39,5 +39,6 @@
|
|||
"rsshub"
|
||||
],
|
||||
"editor.foldingImportsByDefault": true,
|
||||
"commentTranslate.hover.enabled": false
|
||||
"commentTranslate.hover.enabled": false,
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const createDefaultSettings = (): GeneralSettings => ({
|
|||
renderMarkUnread: false,
|
||||
// UX
|
||||
// autoHideFeedColumn: true,
|
||||
groupByDate: true,
|
||||
})
|
||||
|
||||
export const {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useRefValue } from "@renderer/hooks/common"
|
||||
import { createAtomHooks } from "@renderer/lib/jotai"
|
||||
import { getStorageNS } from "@renderer/lib/ns"
|
||||
import { createSettingBuilder } from "@renderer/modules/settings/setting-builder"
|
||||
import { useAtomValue } from "jotai"
|
||||
import { atomWithStorage, selectAtom } from "jotai/utils"
|
||||
import { useMemo } from "react"
|
||||
|
|
@ -74,3 +75,40 @@ export const createSettingAtom = <T extends object>(
|
|||
settingAtom: atom,
|
||||
}
|
||||
}
|
||||
|
||||
export const createDefineSettingItem =
|
||||
<T>(
|
||||
_getSetting: () => T,
|
||||
setSetting: (key: any, value: Partial<T>) => void,
|
||||
) =>
|
||||
<K extends keyof T>(
|
||||
key: K,
|
||||
options: {
|
||||
label: string
|
||||
description?: string
|
||||
onChange?: (value: T[K]) => void
|
||||
},
|
||||
) => {
|
||||
const { label, description, onChange } = options
|
||||
return {
|
||||
key,
|
||||
label,
|
||||
description,
|
||||
onChange: (value: any) => {
|
||||
if (onChange) return onChange(value as any)
|
||||
setSetting(key, value as any)
|
||||
},
|
||||
} as any
|
||||
}
|
||||
|
||||
export const createSetting = <T extends object>(
|
||||
useSetting: () => T,
|
||||
setSetting: (key: any, value: Partial<T>) => void,
|
||||
) => {
|
||||
const SettingBuilder = createSettingBuilder(useSetting)
|
||||
const defineSettingItem = createDefineSettingItem(useSetting, setSetting)
|
||||
return {
|
||||
SettingBuilder,
|
||||
defineSettingItem,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,10 @@ import type { User } from "@auth/core/types"
|
|||
import { createAtomHooks } from "@renderer/lib/jotai"
|
||||
import { atom } from "jotai"
|
||||
|
||||
export const [, , useMe, useSetMe, getMe, setMe] = createAtomHooks(
|
||||
export const [, , useWhoami, useSetWhoami, whoami, setWhoami] = createAtomHooks(
|
||||
atom<Nullable<User>>(null),
|
||||
)
|
||||
|
||||
export { useMe as useWhoAmI }
|
||||
|
||||
export const [
|
||||
,
|
||||
,
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export const ListItemHoverOverlay = ({
|
|||
)}
|
||||
<div
|
||||
ref={ref}
|
||||
className="relative z-[1]"
|
||||
className="relative"
|
||||
onMouseEnter={() => {
|
||||
setMouseEnter(true)
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { getMe } from "@renderer/atoms/user"
|
||||
import { whoami } from "@renderer/atoms/user"
|
||||
import { useModalStack } from "@renderer/components/ui/modal"
|
||||
import type { NativeMenuItem } from "@renderer/lib/native-menu"
|
||||
import { useFeedClaimModal } from "@renderer/modules/claim"
|
||||
|
|
@ -88,7 +88,7 @@ export const useFeedActions = ({
|
|||
},
|
||||
] :
|
||||
[]),
|
||||
...(feed.ownerUserId === getMe()?.id ?
|
||||
...(feed.ownerUserId === whoami()?.id ?
|
||||
[
|
||||
{
|
||||
type: "text" as const,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { signOut } from "@hono/auth-js/react"
|
||||
import { setMe } from "@renderer/atoms/user"
|
||||
import { setWhoami } from "@renderer/atoms/user"
|
||||
import { QUERY_PERSIST_KEY } from "@renderer/constants"
|
||||
import { tipcClient } from "@renderer/lib/client"
|
||||
import { clearStorage } from "@renderer/lib/ns"
|
||||
|
|
@ -12,7 +12,7 @@ export const useSignOut = () =>
|
|||
localStorage.removeItem(QUERY_PERSIST_KEY)
|
||||
|
||||
// setLoginModalShow(true)
|
||||
setMe(null)
|
||||
setWhoami(null)
|
||||
|
||||
// Clear local storage
|
||||
clearStorage()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { env } from "@env"
|
||||
import { getMe } from "@renderer/atoms/user"
|
||||
import { whoami } from "@renderer/atoms/user"
|
||||
import type { CaptureOptions, Properties } from "posthog-js"
|
||||
|
||||
declare global {
|
||||
|
|
@ -43,7 +43,7 @@ export const initPostHog = async () => {
|
|||
},
|
||||
}
|
||||
|
||||
const user = getMe()
|
||||
const user = whoami()
|
||||
if (user) {
|
||||
posthog.identify(user.id, { name: user.name, handle: user.handle })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export function ArticleItem({
|
|||
|
||||
export const ArticleItemSkeleton = (
|
||||
<div className="relative h-[120px] rounded-md bg-theme-background text-zinc-700 transition-colors dark:text-neutral-400">
|
||||
<div className="relative z-[1]">
|
||||
<div className="relative">
|
||||
<div className="group relative flex py-4 pl-3 pr-2">
|
||||
<Skeleton className="mr-2 size-5 rounded-sm" />
|
||||
<div className="-mt-0.5 flex-1 text-sm leading-tight">
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export function AudioItem({
|
|||
|
||||
export const AudioItemSkeleton = (
|
||||
<div className="relative mx-auto w-full max-w-lg rounded-md bg-theme-background text-zinc-700 transition-colors dark:text-neutral-400">
|
||||
<div className="relative z-[1]">
|
||||
<div className="relative">
|
||||
<div className="group relative flex py-4 pl-3 pr-2">
|
||||
<div className="-mt-0.5 line-clamp-4 flex-1 text-sm leading-tight">
|
||||
<div className="flex gap-1 text-[10px] font-bold text-zinc-400 dark:text-neutral-500">
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export const DateItem = memo(
|
|||
|
||||
const className = cn(
|
||||
isFirst ? "pt-2" : "pt-8",
|
||||
"flex items-center gap-1 pl-2 text-sm font-bold text-zinc-800 dark:text-neutral-400",
|
||||
`relative z-10 -mx-2 flex items-center gap-1 bg-background px-4 text-sm font-bold text-zinc-800 dark:text-neutral-400`,
|
||||
)
|
||||
|
||||
if (view === FeedViewType.SocialMedia) {
|
||||
|
|
|
|||
|
|
@ -159,26 +159,34 @@ export const useEntriesByView = ({ onReset }: { onReset?: () => void }) => {
|
|||
sortEntriesIdByStarAt(mergedEntries) :
|
||||
sortEntriesIdByEntryPublishedAt(mergedEntries)
|
||||
|
||||
const entriesWithDate = useMemo(() => {
|
||||
const groupByDate = useGeneralSettingKey("groupByDate")
|
||||
const groupedCounts: number[] | undefined = useMemo(() => {
|
||||
if (views[view].gridMode) {
|
||||
return sortEntries
|
||||
return
|
||||
}
|
||||
if (!groupByDate) {
|
||||
return
|
||||
}
|
||||
const entriesId2Map = entryActions.getFlattenMapEntries()
|
||||
const counts = [] as number[]
|
||||
let lastDate = ""
|
||||
const entriesWithDate = [] as string[]
|
||||
for (const id of sortEntries) {
|
||||
const entry = entriesId2Map[id]
|
||||
if (entry) {
|
||||
const date = new Date(entry.entries.publishedAt).toDateString()
|
||||
if (date !== lastDate) {
|
||||
entriesWithDate.push(date)
|
||||
lastDate = date
|
||||
}
|
||||
if (!entry) {
|
||||
continue
|
||||
}
|
||||
const date = new Date(entry.entries.publishedAt).toDateString()
|
||||
if (date !== lastDate) {
|
||||
counts.push(1)
|
||||
lastDate = date
|
||||
} else {
|
||||
const last = counts.pop()
|
||||
if (last) counts.push(last + 1)
|
||||
}
|
||||
entriesWithDate.push(id)
|
||||
}
|
||||
return entriesWithDate
|
||||
}, [sortEntries, view])
|
||||
|
||||
return counts
|
||||
}, [groupByDate, sortEntries, view])
|
||||
|
||||
return {
|
||||
...query,
|
||||
|
|
@ -187,7 +195,8 @@ export const useEntriesByView = ({ onReset }: { onReset?: () => void }) => {
|
|||
refetch: useCallback(() => {
|
||||
query.refetch()
|
||||
}, [query]),
|
||||
entriesIds: entriesWithDate,
|
||||
entriesIds: sortEntries,
|
||||
groupedCounts,
|
||||
totalCount: query.data?.pages?.[0]?.total ?? mergedEntries.length,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,8 @@ import {
|
|||
setGeneralSetting,
|
||||
useGeneralSettingKey,
|
||||
} from "@renderer/atoms/settings/general"
|
||||
import { useMe } from "@renderer/atoms/user"
|
||||
import { useWhoami } from "@renderer/atoms/user"
|
||||
import { m } from "@renderer/components/common/Motion"
|
||||
import { EmptyIcon } from "@renderer/components/icons/empty"
|
||||
import { AutoResizeHeight } from "@renderer/components/ui/auto-resize-height"
|
||||
import { ActionButton } from "@renderer/components/ui/button"
|
||||
import { DividerVertical } from "@renderer/components/ui/divider"
|
||||
|
|
@ -25,24 +24,22 @@ import { EntryHeader } from "@renderer/modules/entry-content/header"
|
|||
import { useRefreshFeedMutation } from "@renderer/queries/feed"
|
||||
import { entryActions, useEntry } from "@renderer/store/entry"
|
||||
import { useFeedById, useFeedHeaderTitle } from "@renderer/store/feed"
|
||||
import type { HTMLMotionProps } from "framer-motion"
|
||||
import type { FC } from "react"
|
||||
import { forwardRef, useCallback, useEffect, useRef } from "react"
|
||||
import { useCallback, useEffect, useRef } from "react"
|
||||
import type {
|
||||
ScrollSeekConfiguration,
|
||||
VirtuosoHandle,
|
||||
VirtuosoProps,
|
||||
} from "react-virtuoso"
|
||||
import { Virtuoso, VirtuosoGrid } from "react-virtuoso"
|
||||
import { VirtuosoGrid } from "react-virtuoso"
|
||||
|
||||
import { DateItem } from "./date-item"
|
||||
import { EntryColumnShortcutHandler } from "./EntryColumnShortcutHandler"
|
||||
import { useEntriesByView, useEntryMarkReadHandler } from "./hooks"
|
||||
import {
|
||||
EntryItem,
|
||||
EntryItemSkeleton,
|
||||
EntryItemSkeletonWithDelayShow,
|
||||
} from "./item"
|
||||
import { EntryEmptyList, EntryList, EntryListContent } from "./lists"
|
||||
import { MarkAllButton } from "./mark-all-button"
|
||||
import { girdClassNames } from "./styles"
|
||||
|
||||
|
|
@ -59,7 +56,7 @@ export function EntryColumn() {
|
|||
})
|
||||
}, []),
|
||||
})
|
||||
const { entriesIds, isFetchingNextPage } = entries
|
||||
const { entriesIds, isFetchingNextPage, groupedCounts } = entries
|
||||
|
||||
const {
|
||||
entryId: activeEntryId,
|
||||
|
|
@ -88,7 +85,7 @@ export function EntryColumn() {
|
|||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const virtuosoOptions = {
|
||||
components: {
|
||||
List: ListContent,
|
||||
List: EntryListContent,
|
||||
Footer: useCallback(() => {
|
||||
if (!isFetchingNextPage) return null
|
||||
return <EntryItemSkeletonWithDelayShow view={view} />
|
||||
|
|
@ -119,14 +116,10 @@ export function EntryColumn() {
|
|||
}
|
||||
},
|
||||
itemContent: useCallback(
|
||||
(index, entryId) => {
|
||||
(_, entryId) => {
|
||||
if (!entryId) return null
|
||||
|
||||
if (entryId.includes(" ")) {
|
||||
return <DateItem date={entryId} view={view} isFirst={index === 0} />
|
||||
} else {
|
||||
return <EntryItem key={entryId} entryId={entryId} view={view} />
|
||||
}
|
||||
return <EntryItem key={entryId} entryId={entryId} view={view} />
|
||||
},
|
||||
[view],
|
||||
),
|
||||
|
|
@ -178,7 +171,7 @@ export function EntryColumn() {
|
|||
entries.isLoading ?
|
||||
null :
|
||||
(
|
||||
<EmptyList />
|
||||
<EntryEmptyList />
|
||||
)
|
||||
) : view && views[view].gridMode ?
|
||||
(
|
||||
|
|
@ -193,6 +186,7 @@ export function EntryColumn() {
|
|||
{...virtuosoOptions}
|
||||
virtuosoRef={virtuosoRef}
|
||||
refetch={entries.refetch}
|
||||
groupCounts={groupedCounts}
|
||||
/>
|
||||
)}
|
||||
</ScrollArea.ScrollArea>
|
||||
|
|
@ -239,7 +233,7 @@ const ListHeader: FC<{
|
|||
routerParams.feedId,
|
||||
)
|
||||
|
||||
const user = useMe()
|
||||
const user = useWhoami()
|
||||
const isOnline = useIsOnline()
|
||||
|
||||
const feed = useFeedById(routerParams.feedId)
|
||||
|
|
@ -338,64 +332,3 @@ const ListHeader: FC<{
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ListContent = forwardRef<HTMLDivElement>((props, ref) => (
|
||||
<div className="px-2" {...props} ref={ref} />
|
||||
))
|
||||
|
||||
const EmptyList = forwardRef<HTMLDivElement, HTMLMotionProps<"div">>(
|
||||
(props, ref) => {
|
||||
const unreadOnly = useGeneralSettingKey("unreadOnly")
|
||||
return (
|
||||
<m.div
|
||||
className="absolute -mt-6 flex size-full grow flex-col items-center justify-center gap-2 text-zinc-400"
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
{unreadOnly ? (
|
||||
<>
|
||||
<i className="i-mgc-celebrate-cute-re -mt-11 text-3xl" />
|
||||
<span className="text-base">Zero Unread</span>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex -translate-y-6 flex-col items-center justify-center gap-2">
|
||||
<EmptyIcon className="size-[30px]" />
|
||||
<span className="text-base">Zero Items</span>
|
||||
</div>
|
||||
)}
|
||||
</m.div>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const EntryList: FC<
|
||||
VirtuosoProps<string, unknown> & {
|
||||
virtuosoRef: React.RefObject<VirtuosoHandle>
|
||||
|
||||
refetch: () => void
|
||||
}
|
||||
> = ({ virtuosoRef, refetch, ...virtuosoOptions }) => {
|
||||
// Prevent scroll list move when press up/down key, the up/down key should be taken over by the shortcut key we defined.
|
||||
const handleKeyDown: React.KeyboardEventHandler<HTMLDivElement> = useCallback(
|
||||
(e) => {
|
||||
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
||||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
[],
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<Virtuoso
|
||||
onKeyDown={handleKeyDown}
|
||||
{...virtuosoOptions}
|
||||
ref={virtuosoRef}
|
||||
/>
|
||||
<EntryColumnShortcutHandler
|
||||
refetch={refetch}
|
||||
data={virtuosoOptions.data!}
|
||||
virtuosoRef={virtuosoRef}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,165 @@
|
|||
import { useGeneralSettingKey } from "@renderer/atoms/settings/general"
|
||||
import { m } from "@renderer/components/common/Motion"
|
||||
import { EmptyIcon } from "@renderer/components/icons/empty"
|
||||
import { ReactVirtuosoItemPlaceholder } from "@renderer/components/ui/placeholder"
|
||||
import { useRouteParamsSelector } from "@renderer/hooks/biz/useRouteParams"
|
||||
import { useEntry } from "@renderer/store/entry"
|
||||
import type { HTMLMotionProps } from "framer-motion"
|
||||
import type { DOMAttributes, FC } from "react"
|
||||
import { forwardRef, memo, useCallback } from "react"
|
||||
import type {
|
||||
VirtuosoHandle,
|
||||
VirtuosoProps,
|
||||
} from "react-virtuoso"
|
||||
import { GroupedVirtuoso, Virtuoso } from "react-virtuoso"
|
||||
|
||||
import { DateItem } from "./date-item"
|
||||
import { EntryColumnShortcutHandler } from "./EntryColumnShortcutHandler"
|
||||
|
||||
export const EntryListContent = forwardRef<HTMLDivElement>((props, ref) => (
|
||||
<div className="px-2" {...props} ref={ref} />
|
||||
))
|
||||
|
||||
export const EntryEmptyList = forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLMotionProps<"div">
|
||||
>((props, ref) => {
|
||||
const unreadOnly = useGeneralSettingKey("unreadOnly")
|
||||
return (
|
||||
<m.div
|
||||
className="absolute -mt-6 flex size-full grow flex-col items-center justify-center gap-2 text-zinc-400"
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
{unreadOnly ? (
|
||||
<>
|
||||
<i className="i-mgc-celebrate-cute-re -mt-11 text-3xl" />
|
||||
<span className="text-base">Zero Unread</span>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex -translate-y-6 flex-col items-center justify-center gap-2">
|
||||
<EmptyIcon className="size-[30px]" />
|
||||
<span className="text-base">Zero Items</span>
|
||||
</div>
|
||||
)}
|
||||
</m.div>
|
||||
)
|
||||
})
|
||||
|
||||
type BaseEntryProps = {
|
||||
virtuosoRef: React.RefObject<VirtuosoHandle>
|
||||
refetch: () => void
|
||||
}
|
||||
type EntryListProps = VirtuosoProps<string, unknown> & {
|
||||
groupCounts?: number[]
|
||||
} & BaseEntryProps
|
||||
export const EntryList: FC<EntryListProps> = memo(
|
||||
({
|
||||
virtuosoRef,
|
||||
refetch,
|
||||
groupCounts,
|
||||
|
||||
...virtuosoOptions
|
||||
}) => {
|
||||
// Prevent scroll list move when press up/down key, the up/down key should be taken over by the shortcut key we defined.
|
||||
const handleKeyDown: React.KeyboardEventHandler<HTMLDivElement> =
|
||||
useCallback((e) => {
|
||||
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
||||
e.preventDefault()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{groupCounts ? (
|
||||
<EntryGroupedList
|
||||
groupCounts={groupCounts}
|
||||
onKeyDown={handleKeyDown}
|
||||
{...virtuosoOptions}
|
||||
ref={virtuosoRef}
|
||||
/>
|
||||
) : (
|
||||
<Virtuoso
|
||||
onKeyDown={handleKeyDown}
|
||||
{...virtuosoOptions}
|
||||
ref={virtuosoRef}
|
||||
/>
|
||||
)}
|
||||
<EntryColumnShortcutHandler
|
||||
refetch={refetch}
|
||||
data={virtuosoOptions.data!}
|
||||
virtuosoRef={virtuosoRef}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const EntryGroupedList = forwardRef<
|
||||
VirtuosoHandle,
|
||||
VirtuosoProps<string, unknown> &
|
||||
DOMAttributes<HTMLDivElement> & {
|
||||
groupCounts: number[]
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
groupCounts,
|
||||
itemContent,
|
||||
onKeyDown,
|
||||
|
||||
...virtuosoOptions
|
||||
},
|
||||
ref,
|
||||
) => (
|
||||
<GroupedVirtuoso
|
||||
ref={ref}
|
||||
groupContent={useCallback(
|
||||
(index: number) => {
|
||||
const entryId = getGetGroupDataIndex(
|
||||
groupCounts!,
|
||||
index,
|
||||
virtuosoOptions.data!,
|
||||
)
|
||||
|
||||
return <EntryHeadDateItem entryId={entryId} />
|
||||
},
|
||||
[groupCounts, virtuosoOptions.data],
|
||||
)}
|
||||
groupCounts={groupCounts}
|
||||
onKeyDown={onKeyDown}
|
||||
{...virtuosoOptions}
|
||||
itemContent={useCallback(
|
||||
(index: number, _: number, entryId: string, c: any) =>
|
||||
itemContent?.(index, entryId, c),
|
||||
[itemContent],
|
||||
)}
|
||||
/>
|
||||
),
|
||||
)
|
||||
|
||||
function getGetGroupDataIndex<T>(
|
||||
groupCounts: number[],
|
||||
groupIndex: number,
|
||||
data: readonly T[],
|
||||
) {
|
||||
// Get first grouped of data index
|
||||
//
|
||||
let sum = 0
|
||||
for (let i = 0; i < groupIndex; i++) {
|
||||
sum += groupCounts[i]
|
||||
}
|
||||
return data[sum]
|
||||
}
|
||||
|
||||
const EntryHeadDateItem: FC<{
|
||||
entryId: string
|
||||
}> = ({ entryId }) => {
|
||||
const entry = useEntry(entryId)
|
||||
|
||||
const view = useRouteParamsSelector((s) => s.view)
|
||||
|
||||
if (!entry) return <ReactVirtuosoItemPlaceholder />
|
||||
const date = new Date(entry.entries.publishedAt).toDateString()
|
||||
return <DateItem date={date} view={view} isFirst={true} />
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ export function NotificationItem({
|
|||
}
|
||||
|
||||
export const NotificationItemSkeleton = (
|
||||
<div className="relative z-[1] mx-auto w-full max-w-lg">
|
||||
<div className="relative mx-auto w-full max-w-lg">
|
||||
<div className="group relative flex py-4 pl-3 pr-2">
|
||||
<Skeleton className="mr-2 size-5 shrink-0 overflow-hidden rounded-sm " />
|
||||
<div className="-mt-0.5 line-clamp-4 flex-1 text-sm leading-tight">
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export function PictureItem({
|
|||
|
||||
export const PictureItemSkeleton = (
|
||||
<div className="relative max-w-md rounded-md bg-theme-background text-zinc-700 transition-colors dark:text-neutral-400">
|
||||
<div className="relative z-[1]">
|
||||
<div className="relative">
|
||||
<div className="p-1.5">
|
||||
<div className="relative flex gap-2 overflow-x-auto">
|
||||
<div className="relative flex aspect-square w-full shrink-0 items-center overflow-hidden rounded-md">
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ const ActionBar = ({ entryId }: { entryId: string }) => {
|
|||
|
||||
export const SocialMediaItemSkeleton = (
|
||||
<div className="relative m-auto w-[75ch] rounded-md bg-theme-background text-zinc-700 transition-colors dark:text-neutral-400">
|
||||
<div className="relative z-[1]">
|
||||
<div className="relative">
|
||||
<div className="group relative flex py-4 pl-3 pr-2">
|
||||
<Skeleton className="mr-2 size-9" />
|
||||
<div className="ml-2 min-w-0 flex-1">
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ const PreviewVideoModalContent: ModalContentComponent<{
|
|||
|
||||
export const VideoItemSkeleton = (
|
||||
<div className="relative mx-auto w-full max-w-lg rounded-md bg-theme-background text-zinc-700 transition-colors dark:text-neutral-400">
|
||||
<div className="relative z-[1]">
|
||||
<div className="relative">
|
||||
<div className="p-1.5">
|
||||
<div className="w-full">
|
||||
<div className="overflow-x-auto">
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
useEntryReadabilityContent,
|
||||
} from "@renderer/atoms/readability"
|
||||
import { useUISettingKey } from "@renderer/atoms/settings/ui"
|
||||
import { useMe } from "@renderer/atoms/user"
|
||||
import { useWhoami } from "@renderer/atoms/user"
|
||||
import { m } from "@renderer/components/common/Motion"
|
||||
import { Logo } from "@renderer/components/icons/logo"
|
||||
import { AutoResizeHeight } from "@renderer/components/ui/auto-resize-height"
|
||||
|
|
@ -55,7 +55,7 @@ export const EntryContent = ({ entryId }: { entryId: ActiveEntryId }) => {
|
|||
}
|
||||
|
||||
function EntryContentRender({ entryId }: { entryId: string }) {
|
||||
const user = useMe()
|
||||
const user = useWhoami()
|
||||
|
||||
const { error, data, isPending } = useAuthQuery(
|
||||
Queries.entries.byId(entryId),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useMe } from "@renderer/atoms/user"
|
||||
import { useWhoami } from "@renderer/atoms/user"
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
|
|
@ -20,7 +20,7 @@ import { usePresentUserProfileModal } from "../profile/hooks"
|
|||
export const EntryReadHistory: Component<{ entryId: string }> = ({
|
||||
entryId,
|
||||
}) => {
|
||||
const me = useMe()
|
||||
const me = useWhoami()
|
||||
const entryHistory = useEntryReadHistory(entryId)
|
||||
|
||||
useAuthQuery(Queries.entries.entryReadingHistory(entryId), {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { getMainContainerElement } from "@renderer/atoms/dom"
|
||||
import { useMe } from "@renderer/atoms/user"
|
||||
import { useWhoami } from "@renderer/atoms/user"
|
||||
import { FeedIcon } from "@renderer/components/feed-icon"
|
||||
import {
|
||||
Tooltip,
|
||||
|
|
@ -63,7 +63,7 @@ const FeedItemImpl = ({
|
|||
const feed = useFeedById(feedId)
|
||||
|
||||
const { items } = useFeedActions({ feedId, view })
|
||||
const me = useMe()
|
||||
const me = useWhoami()
|
||||
const isOwned = feed && feed.ownerUserId === me?.id
|
||||
|
||||
if (!feed) return null
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ type SharedSettingItem = {
|
|||
disabled?: boolean
|
||||
}
|
||||
|
||||
type SettingItem<T, K extends keyof T = keyof T> = {
|
||||
export type SettingItem<T, K extends keyof T = keyof T> = {
|
||||
key: K
|
||||
label: string
|
||||
description?: string
|
||||
|
|
@ -113,15 +113,3 @@ export const createSettingBuilder =
|
|||
)
|
||||
})
|
||||
}
|
||||
|
||||
export function createSettingItemTypeHelper<Setting extends () => object>(
|
||||
_t: Setting,
|
||||
) {
|
||||
return function <const Key extends keyof ReturnType<Setting>>(schema: {
|
||||
key: Key
|
||||
onChange: (v: ReturnType<Setting>[Key]) => void
|
||||
label: string
|
||||
}): any {
|
||||
return schema
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,20 @@ import {
|
|||
setGeneralSetting,
|
||||
useGeneralSettingValue,
|
||||
} from "@renderer/atoms/settings/general"
|
||||
import {
|
||||
createSetting,
|
||||
} from "@renderer/atoms/settings/helper"
|
||||
import { initPostHog } from "@renderer/initialize/posthog"
|
||||
import { tipcClient } from "@renderer/lib/client"
|
||||
import { clearLocalPersistStoreData } from "@renderer/store/utils/clear"
|
||||
import { useCallback, useEffect } from "react"
|
||||
|
||||
import { createSettingBuilder } from "../setting-builder"
|
||||
import { SettingsTitle } from "../title"
|
||||
|
||||
const SettingBuilder = createSettingBuilder(useGeneralSettingValue)
|
||||
const { defineSettingItem, SettingBuilder } = createSetting(
|
||||
useGeneralSettingValue,
|
||||
setGeneralSetting,
|
||||
)
|
||||
export const SettingGeneral = () => {
|
||||
useEffect(() => {
|
||||
tipcClient?.getLoginItemSettings().then((settings) => {
|
||||
|
|
@ -45,53 +50,51 @@ export const SettingGeneral = () => {
|
|||
},
|
||||
{
|
||||
type: "title",
|
||||
value: "view",
|
||||
value: "timeline",
|
||||
},
|
||||
{
|
||||
key: "unreadOnly",
|
||||
defineSettingItem("unreadOnly", {
|
||||
label: "Show unread content on launch",
|
||||
description:
|
||||
"Display only unread content when the app is launched.",
|
||||
onChange: (value) => setGeneralSetting("unreadOnly", value),
|
||||
},
|
||||
{
|
||||
key: "scrollMarkUnread",
|
||||
}),
|
||||
defineSettingItem("groupByDate", {
|
||||
label: "Group by date",
|
||||
description: "Group entries by date.",
|
||||
}),
|
||||
|
||||
{ type: "title", value: "unread" },
|
||||
|
||||
defineSettingItem("scrollMarkUnread", {
|
||||
label: "Mark as read when scrolling",
|
||||
description:
|
||||
"Automatically mark entries as read when scrolled out of the view.",
|
||||
onChange: (value) => setGeneralSetting("scrollMarkUnread", value),
|
||||
},
|
||||
{
|
||||
key: "hoverMarkUnread",
|
||||
}),
|
||||
defineSettingItem("hoverMarkUnread", {
|
||||
label: "Mark as read when hovering",
|
||||
description: "Automatically mark entries as read when hovered.",
|
||||
onChange: (value) => setGeneralSetting("hoverMarkUnread", value),
|
||||
},
|
||||
{
|
||||
key: "renderMarkUnread",
|
||||
}),
|
||||
defineSettingItem("renderMarkUnread", {
|
||||
label: "Mark as read when in the view",
|
||||
description:
|
||||
"Automatically mark single-level entries (e.g., social media posts, pictures, video views) as read when they enter the view.",
|
||||
onChange: (value) => setGeneralSetting("renderMarkUnread", value),
|
||||
},
|
||||
}),
|
||||
|
||||
{
|
||||
type: "title",
|
||||
value: "Privacy & Data",
|
||||
},
|
||||
{
|
||||
key: "dataPersist",
|
||||
|
||||
defineSettingItem("dataPersist", {
|
||||
label: "Persist data for offline usage",
|
||||
description:
|
||||
"Persist data locally to enable offline access and local search.",
|
||||
onChange: (value) => setGeneralSetting("dataPersist", value),
|
||||
},
|
||||
{
|
||||
key: "sendAnonymousData",
|
||||
}),
|
||||
|
||||
defineSettingItem("sendAnonymousData", {
|
||||
label: "Send anonymous data",
|
||||
description:
|
||||
"By opting to send anonymized telemetry data, you contribute to improving the overall user experience of Follow.",
|
||||
onChange: (value) => {
|
||||
onChange(value) {
|
||||
setGeneralSetting("sendAnonymousData", value)
|
||||
if (value) {
|
||||
initPostHog()
|
||||
|
|
@ -100,7 +103,8 @@ export const SettingGeneral = () => {
|
|||
delete window.posthog
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
{
|
||||
label: "Rebuild Database",
|
||||
action: async () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useMe } from "@renderer/atoms/user"
|
||||
import { useWhoami } from "@renderer/atoms/user"
|
||||
import { Divider } from "@renderer/components/ui/divider"
|
||||
import { LoadingCircle } from "@renderer/components/ui/loading"
|
||||
import {
|
||||
|
|
@ -15,7 +15,7 @@ import { ClaimDailyReward } from "./claim-daily-reward"
|
|||
import { CreateWallet } from "./create-wallet"
|
||||
|
||||
export const MyWalletSection = () => {
|
||||
const user = useMe()
|
||||
const user = useWhoami()
|
||||
const wallet = useWallet({ userId: user?.id })
|
||||
const myWallet = wallet.data?.[0]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useMe } from "@renderer/atoms/user"
|
||||
import { useWhoami } from "@renderer/atoms/user"
|
||||
import { Logo } from "@renderer/components/icons/logo"
|
||||
import {
|
||||
Avatar,
|
||||
|
|
@ -21,7 +21,7 @@ import { SettingSectionTitle } from "@renderer/modules/settings/section"
|
|||
import { useWallet, useWalletTransactions } from "@renderer/queries/wallet"
|
||||
|
||||
export const TransactionsSection = () => {
|
||||
const user = useMe()
|
||||
const user = useWhoami()
|
||||
const wallet = useWallet({ userId: user?.id })
|
||||
const myWallet = wallet.data?.[0]
|
||||
|
||||
|
|
@ -146,7 +146,7 @@ const UserRenderer = ({
|
|||
| "fromUser"
|
||||
| "toUser"]
|
||||
}) => {
|
||||
const me = useMe()
|
||||
const me = useWhoami()
|
||||
const isMe = user?.id === me?.id
|
||||
|
||||
const name = isMe ? "You" : user?.name || APP_NAME
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useMe } from "@renderer/atoms/user"
|
||||
import { useWhoami } from "@renderer/atoms/user"
|
||||
import { StyledButton } from "@renderer/components/ui/button"
|
||||
import { Divider } from "@renderer/components/ui/divider"
|
||||
import { LoadingCircle } from "@renderer/components/ui/loading"
|
||||
|
|
@ -22,7 +22,7 @@ export const TipModalContent: FC<{
|
|||
userId?: string
|
||||
feedId?: string
|
||||
}> = ({ userId, feedId }) => {
|
||||
const user = useMe()
|
||||
const user = useWhoami()
|
||||
const myWallet = useWallet({ userId: user?.id })
|
||||
const myWalletData = myWallet.data?.[0]
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { repository } from "@pkg"
|
|||
import { useFeedColumnShow } from "@renderer/atoms/app"
|
||||
import { setMainContainerElement } from "@renderer/atoms/dom"
|
||||
import { getUISettings, setUISetting } from "@renderer/atoms/settings/ui"
|
||||
import { useLoginModalShow, useMe } from "@renderer/atoms/user"
|
||||
import { useLoginModalShow, useWhoami } from "@renderer/atoms/user"
|
||||
import { AppErrorBoundary } from "@renderer/components/common/AppErrorBoundary"
|
||||
import { ErrorComponentType } from "@renderer/components/errors"
|
||||
import { PanelSplitter } from "@renderer/components/ui/divider"
|
||||
|
|
@ -54,7 +54,7 @@ const FooterInfo = () => (
|
|||
|
||||
export function Component() {
|
||||
const isAuthFail = useLoginModalShow()
|
||||
const user = useMe()
|
||||
const user = useWhoami()
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useMe } from "@renderer/atoms/user"
|
||||
import { useWhoami } from "@renderer/atoms/user"
|
||||
import { StyledButton } from "@renderer/components/ui/button"
|
||||
import {
|
||||
Form,
|
||||
|
|
@ -35,7 +35,7 @@ export const loader = defineSettingPage({
|
|||
})
|
||||
|
||||
export function Component() {
|
||||
const user = useMe()
|
||||
const user = useWhoami()
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useSetMe } from "@renderer/atoms/user"
|
||||
import { useSetWhoami } from "@renderer/atoms/user"
|
||||
import { tipcClient } from "@renderer/lib/client"
|
||||
import { useSession } from "@renderer/queries/auth"
|
||||
import { CleanerService } from "@renderer/services/cleaner"
|
||||
|
|
@ -6,7 +6,7 @@ import { useEffect } from "react"
|
|||
|
||||
export const UserProvider = () => {
|
||||
const { session } = useSession()
|
||||
const setUser = useSetMe()
|
||||
const setUser = useSetWhoami()
|
||||
useEffect(() => {
|
||||
if (!session?.user) return
|
||||
setUser(session.user)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { getMe } from "@renderer/atoms/user"
|
||||
import { whoami } from "@renderer/atoms/user"
|
||||
import { runTransactionInScope } from "@renderer/database"
|
||||
import { apiClient } from "@renderer/lib/api-fetch"
|
||||
import type { FeedModel } from "@renderer/models"
|
||||
|
|
@ -61,7 +61,7 @@ class FeedActions {
|
|||
}),
|
||||
|
||||
async () => {
|
||||
const currentUser = getMe()
|
||||
const currentUser = whoami()
|
||||
if (!currentUser) return
|
||||
this.updateFeedOwnership(feedId, currentUser.id)
|
||||
const feed = get().feeds[feedId]
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export interface GeneralSettings {
|
|||
scrollMarkUnread: boolean
|
||||
hoverMarkUnread: boolean
|
||||
renderMarkUnread: boolean
|
||||
groupByDate: boolean
|
||||
}
|
||||
|
||||
export interface UISettings {
|
||||
|
|
|
|||
Loading…
Reference in New Issue