Skip to content

Commit

Permalink
feat: inbox switch button
Browse files Browse the repository at this point in the history
  • Loading branch information
DIYgod committed Feb 10, 2025
1 parent 5867d54 commit 4bbfb16
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 201 deletions.
2 changes: 1 addition & 1 deletion apps/renderer/src/hooks/biz/useTimelineList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const useTimelineList = () => {

return [
...[0, 1, 2, 3].map((view) => `view-${view}`),
...lists.map((list) => `list-${list.listId}`),
...inboxes.map((inbox) => `inbox-${inbox.inboxId}`),
...lists.map((list) => `list-${list.listId}`),
]
}
190 changes: 20 additions & 170 deletions apps/renderer/src/modules/feed-column/index.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,33 @@
import { useDroppable } from "@dnd-kit/core"
import { ActionButton } from "@follow/components/ui/button/index.js"
import { RootPortal } from "@follow/components/ui/portal/index.js"
import { ScrollArea } from "@follow/components/ui/scroll-area/ScrollArea.js"
import type { FeedViewType } from "@follow/constants"
import { Routes, views } from "@follow/constants"
import { Routes } from "@follow/constants"
import { useTypeScriptHappyCallback } from "@follow/hooks"
import { useRegisterGlobalContext } from "@follow/shared/bridge"
import { nextFrame } from "@follow/utils/dom"
import { clamp, cn } from "@follow/utils/utils"
import { useWheel } from "@use-gesture/react"
import { AnimatePresence, m } from "framer-motion"
import { Lethargy } from "lethargy"
import type { FC, PropsWithChildren } from "react"
import { startTransition, useCallback, useLayoutEffect, useRef, useState } from "react"
import { useCallback, useLayoutEffect, useRef, useState } from "react"
import { isHotkeyPressed, useHotkeys } from "react-hotkeys-hook"
import { useTranslation } from "react-i18next"

import { useShowContextMenu } from "~/atoms/context-menu"
import { getMainContainerElement, useRootContainerElement } from "~/atoms/dom"
import { useRootContainerElement } from "~/atoms/dom"
import { useUISettingKey } from "~/atoms/settings/ui"
import { setFeedColumnShow, useFeedColumnShow } from "~/atoms/sidebar"
import {
HotKeyScopeMap,
isElectronBuild,
ROUTE_TIMELINE_OF_INBOX,
ROUTE_TIMELINE_OF_LIST,
ROUTE_TIMELINE_OF_VIEW,
} from "~/constants"
import { HotKeyScopeMap, isElectronBuild } from "~/constants"
import { shortcuts } from "~/constants/shortcuts"
import { useListActions } from "~/hooks/biz/useFeedActions"
import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry"
import { useReduceMotion } from "~/hooks/biz/useReduceMotion"
import { useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
import { useTimelineList } from "~/hooks/biz/useTimelineList"
import { useContextMenu } from "~/hooks/common"
import { useListById } from "~/store/list"
import { subscriptionActions } from "~/store/subscription"
import { useFeedUnreadStore } from "~/store/unread"
import { useUnreadByView } from "~/store/unread/hooks"

import { WindowUnderBlur } from "../../components/ui/background"
import { FeedIcon } from "../feed/feed-icon"
import { getSelectedFeedIds, resetSelectedFeedIds, setSelectedFeedIds } from "./atom"
import { FeedColumnHeader } from "./header"
import { useShouldFreeUpSpace } from "./hook"
import { FeedList } from "./list"
import TimelineList from "./timeline-list"
import TimelineSwitchButton from "./timeline-switch-button"

const lethargy = new Lethargy()

Expand Down Expand Up @@ -72,10 +55,7 @@ export function FeedColumn({ children, className }: PropsWithChildren<{ classNam
view: s.view,
listId: s.listId,
}))
const { timelineId, listId } = routeParams
let { view } = routeParams
const list = useListById(listId)
view = list?.view ?? view ?? 0
const { timelineId } = routeParams
const navigateBackHome = useBackHome(timelineId)
const setActive = useCallback(
(args: string | ((prev: string | undefined, index: number) => string)) => {
Expand Down Expand Up @@ -191,10 +171,10 @@ export function FeedColumn({ children, className }: PropsWithChildren<{ classNam
}
}, [])}
>
<SwipeWrapper active={view}>
{views.map((item, index) => (
<section key={item.name} className="h-full w-feed-col shrink-0 snap-center">
<FeedList className="flex size-full flex-col text-sm" view={index} />
<SwipeWrapper active={timelineId!}>
{timelineList.map((timelineId) => (
<section key={timelineId} className="h-full w-feed-col shrink-0 snap-center">
<TimelineList key={timelineId} timelineId={timelineId} />
</section>
))}
</SwipeWrapper>
Expand All @@ -205,143 +185,13 @@ export function FeedColumn({ children, className }: PropsWithChildren<{ classNam
)
}

const TimelineSwitchButton = ({ timelineId }: { timelineId: string }) => {
const activeTimelineId = useRouteParamsSelector((s) => s.timelineId)
const isActive = activeTimelineId === timelineId
const navigate = useNavigateEntry()
const setActive = useCallback(() => {
navigate({
timelineId,
feedId: null,
entryId: null,
})
resetSelectedFeedIds()
}, [navigate, timelineId])

if (timelineId.startsWith(ROUTE_TIMELINE_OF_VIEW)) {
const id = Number.parseInt(timelineId.slice(ROUTE_TIMELINE_OF_VIEW.length), 10) as FeedViewType
return <ViewSwitchButton view={id} isActive={isActive} setActive={setActive} />
} else if (timelineId.startsWith(ROUTE_TIMELINE_OF_LIST)) {
const id = timelineId.slice(ROUTE_TIMELINE_OF_LIST.length)
return <ListSwitchButton listId={id} isActive={isActive} setActive={setActive} />
} else if (timelineId.startsWith(ROUTE_TIMELINE_OF_INBOX)) {
return null // TODO
}
}

const ViewSwitchButton: FC<{
view: FeedViewType
isActive: boolean
setActive: () => void
}> = ({ view, isActive, setActive }) => {
const unreadByView = useUnreadByView()
const { t } = useTranslation()
const showSidebarUnreadCount = useUISettingKey("sidebarShowUnreadCount")
const item = views.find((item) => item.view === view)!

const { isOver, setNodeRef } = useDroppable({
id: `view-${item.name}`,
data: {
category: "",
view: item.view,
},
})

return (
<ActionButton
ref={setNodeRef}
key={item.name}
tooltip={t(item.name as any)}
shortcut={`${view + 1}`}
className={cn(
isActive && item.className,
"flex h-11 shrink-0 flex-col items-center gap-1 text-[1.375rem]",
ELECTRON ? "hover:!bg-theme-item-hover" : "",
isOver && "border-theme-accent-400 bg-theme-accent-400/60",
)}
onClick={(e) => {
startTransition(() => {
setActive()
})
e.stopPropagation()
}}
>
{item.icon}
{showSidebarUnreadCount ? (
<div className="text-[0.625rem] font-medium leading-none">
{unreadByView[view]! > 99 ? <span className="-mr-0.5">99+</span> : unreadByView[view]}
</div>
) : (
<i
className={cn(
"i-mgc-round-cute-fi text-[0.25rem]",
unreadByView[view] ? (isActive ? "opacity-100" : "opacity-60") : "opacity-0",
)}
/>
)}
</ActionButton>
)
}

const ListSwitchButton: FC<{
listId: string
isActive: boolean
setActive: () => void
}> = ({ listId, isActive, setActive }) => {
const list = useListById(listId)
const listUnread = useFeedUnreadStore((state) => state.data[listId] || 0)

const handleNavigate = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()

setActive()
subscriptionActions.markReadByFeedIds({
listId,
})
// focus to main container in order to let keyboard can navigate entry items by arrow keys
nextFrame(() => {
getMainContainerElement()?.focus()
})
},
[listId, setActive],
)

const items = useListActions({ listId })
const showContextMenu = useShowContextMenu()
const contextMenuProps = useContextMenu({
onContextMenu: async (e) => {
await showContextMenu(items, e)
},
})

if (!list) return null

return (
<ActionButton
key={list.id}
tooltip={list.title}
className={cn(
"flex h-11 shrink-0 flex-col items-center gap-1 text-xl grayscale",
ELECTRON ? "hover:!bg-theme-item-hover" : "",
isActive && "grayscale-0",
)}
onClick={handleNavigate}
{...contextMenuProps}
>
<FeedIcon fallback feed={list} size={22} noMargin />
<div className="center h-2.5 text-[0.25rem]">
<i className={cn("i-mgc-round-cute-fi", !listUnread && "opacity-0")} />
</div>
</ActionButton>
)
}

const SwipeWrapper: FC<{
active: number
active: string
children: React.JSX.Element[]
}> = ({ children, active }) => {
const reduceMotion = useReduceMotion()
const timelineList = useTimelineList()
const index = timelineList.indexOf(active)

const feedColumnWidth = useUISettingKey("feedColWidth")
const containerRef = useRef<HTMLDivElement>(null)
Expand All @@ -350,26 +200,26 @@ const SwipeWrapper: FC<{
const [isReady, setIsReady] = useState(false)

const [direction, setDirection] = useState<"left" | "right">("right")
const [currentAnimtedActive, setCurrentAnimatedActive] = useState(active)
const [currentAnimtedActive, setCurrentAnimatedActive] = useState(index)

useLayoutEffect(() => {
const prevActiveIndex = prevActiveIndexRef.current
if (prevActiveIndex !== active) {
if (prevActiveIndex < active) {
if (prevActiveIndex !== index) {
if (prevActiveIndex < index) {
setDirection("right")
} else {
setDirection("left")
}
}
// eslint-disable-next-line @eslint-react/web-api/no-leaked-timeout
setTimeout(() => {
setCurrentAnimatedActive(active)
setCurrentAnimatedActive(index)
}, 0)
if (prevActiveIndexRef.current !== -1) {
setIsReady(true)
}
prevActiveIndexRef.current = active
}, [active])
prevActiveIndexRef.current = index
}, [index])

if (reduceMotion) {
return <div ref={containerRef}>{children[currentAnimtedActive]}</div>
Expand Down
32 changes: 2 additions & 30 deletions apps/renderer/src/modules/feed-column/list.desktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useDraggable } from "@dnd-kit/core"
import { ScrollArea } from "@follow/components/ui/scroll-area/index.js"
import { cn, isKeyForMultiSelectPressed } from "@follow/utils/utils"
import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import Selecto from "react-selecto"
import { useEventListener } from "usehooks-ts"

Expand All @@ -17,36 +16,19 @@ import {
} from "./atom"
import { DraggableContext } from "./context"
import { useShouldFreeUpSpace } from "./hook"
import {
EmptyFeedList,
ListHeader,
StarredItem,
useFeedsGroupedData,
useInboxesGroupedData,
useListsGroupedData,
} from "./list.shared"
import { EmptyFeedList, ListHeader, StarredItem, useFeedsGroupedData } from "./list.shared"
import { SortableFeedList } from "./sort-by"

const FeedListImpl = forwardRef<HTMLDivElement, { className?: string; view: number }>(
({ className, view }, ref) => {
const feedsData = useFeedsGroupedData(view)
const listsData = useListsGroupedData(view)
const inboxesData = useInboxesGroupedData(view)
const categoryOpenStateData = useCategoryOpenStateByView(view)

const hasData =
Object.keys(feedsData).length > 0 ||
Object.keys(listsData).length > 0 ||
Object.keys(inboxesData).length > 0

const { t } = useTranslation()
const hasData = Object.keys(feedsData).length > 0

// Data prefetch
useAuthQuery(Queries.lists.list())

const hasListData = Object.keys(listsData).length > 0
const hasInboxData = Object.keys(inboxesData).length > 0

const scrollerRef = useRef<HTMLDivElement>(null)
const selectoRef = useRef<Selecto>(null)
const [selectedFeedIds, setSelectedFeedIds] = useSelectedFeedIdsState()
Expand Down Expand Up @@ -216,16 +198,6 @@ const FeedListImpl = forwardRef<HTMLDivElement, { className?: string; view: numb
<StarredItem view={view} />
<DraggableContext.Provider value={draggableContextValue}>
<div className="space-y-px" id="feeds-area" ref={setNodeRef}>
{(hasListData || hasInboxData) && (
<div
className={cn(
"mb-1 flex h-6 w-full shrink-0 items-center rounded-md px-2.5 text-xs font-semibold text-theme-vibrancyFg transition-colors",
Object.keys(feedsData).length === 0 ? "mt-0" : "mt-1",
)}
>
{t("words.feeds")}
</div>
)}
{hasData ? (
<SortableFeedList
view={view}
Expand Down
22 changes: 22 additions & 0 deletions apps/renderer/src/modules/feed-column/timeline-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { FeedViewType } from "@follow/constants"

import {
ROUTE_TIMELINE_OF_INBOX,
ROUTE_TIMELINE_OF_LIST,
ROUTE_TIMELINE_OF_VIEW,
} from "~/constants"

import { FeedList } from "./list"

export default function TimelineList({ timelineId }: { timelineId: string }) {
if (timelineId.startsWith(ROUTE_TIMELINE_OF_VIEW)) {
const id = Number.parseInt(timelineId.slice(ROUTE_TIMELINE_OF_VIEW.length), 10) as FeedViewType
return <FeedList className="flex size-full flex-col text-sm" view={id} />
} else if (timelineId.startsWith(ROUTE_TIMELINE_OF_LIST)) {
// const id = timelineId.slice(ROUTE_TIMELINE_OF_LIST.length)
return null
} else if (timelineId.startsWith(ROUTE_TIMELINE_OF_INBOX)) {
// const id = timelineId.slice(ROUTE_TIMELINE_OF_INBOX.length)
return null
}
}
Loading

0 comments on commit 4bbfb16

Please sign in to comment.