Skip to content

Commit

Permalink
feat: dnd (RSSNext#1471)
Browse files Browse the repository at this point in the history
  • Loading branch information
hyoban authored Nov 8, 2024
1 parent 7b074f7 commit c9333d5
Show file tree
Hide file tree
Showing 12 changed files with 422 additions and 134 deletions.
1 change: 1 addition & 0 deletions apps/renderer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@dnd-kit/core": "^6.1.0",
"@egoist/tipc": "0.3.2",
"@electron-toolkit/preload": "^3.0.1",
"@follow/electron-main": "workspace:*",
Expand Down
20 changes: 20 additions & 0 deletions apps/renderer/src/hooks/biz/useSubscriptionActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,23 @@ const UnfollowInfo = ({ title, undo }: { title: string; undo: () => any }) => {
</>
)
}

export const useBatchUpdateSubscription = () => {
return useMutation({
mutationFn: async ({
feedIdList,
category,
view,
}: {
feedIdList: string[]
category: string
view: number
}) => {
await subscriptionActions.batchUpdateSubscription({
category,
feedIdList,
view,
})
},
})
}
18 changes: 16 additions & 2 deletions apps/renderer/src/modules/feed-column/category.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useDroppable } from "@dnd-kit/core"
import { MotionButtonBase } from "@follow/components/ui/button/index.js"
import { LoadingCircle } from "@follow/components/ui/loading/index.jsx"
import { useScrollViewElement } from "@follow/components/ui/scroll-area/hooks.js"
Expand Down Expand Up @@ -153,13 +154,26 @@ function FeedCategoryImpl({ data: ids, view, categoryOpenStateData }: FeedCatego
const listList = useOwnedListByView(view!)
const showContextMenu = useShowContextMenu()

const isAutoGroupedCategory = !!folderName && !subscriptionCategoryExist(folderName)

const { isOver, setNodeRef } = useDroppable({
id: `category-${folderName}`,
disabled: isAutoGroupedCategory,
data: {
category: folderName,
view,
},
})

return (
<div tabIndex={-1} onClick={stopPropagation}>
{!!showCollapse && (
<div
ref={setNodeRef}
data-active={isActive || isContextMenuOpen}
className={cn(
"my-px flex w-full cursor-menu items-center justify-between rounded-md px-2.5",
isOver && "border-theme-accent-400 bg-theme-accent-400/60",
feedColumnStyles.item,
)}
onClick={(e) => {
Expand Down Expand Up @@ -242,7 +256,7 @@ function FeedCategoryImpl({ data: ids, view, categoryOpenStateData }: FeedCatego
{
type: "text",
label: t("sidebar.feed_column.context_menu.delete_category"),
hide: !folderName || !subscriptionCategoryExist(folderName),
hide: !folderName || isAutoGroupedCategory,
click: () => {
present({
title: t("sidebar.feed_column.context_menu.delete_category_confirmation", {
Expand Down Expand Up @@ -304,7 +318,7 @@ function FeedCategoryImpl({ data: ids, view, categoryOpenStateData }: FeedCatego
{open && (
<m.div
ref={itemsRef}
className="space-y-px overflow-hidden"
className="space-y-px"
initial={
!!showCollapse && {
height: 0,
Expand Down
9 changes: 9 additions & 0 deletions apps/renderer/src/modules/feed-column/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { DraggableAttributes, DraggableSyntheticListeners } from "@dnd-kit/core"
import type { CSSProperties } from "react"
import { createContext } from "react"

export const DraggableContext = createContext<{
attributes: DraggableAttributes
listeners: DraggableSyntheticListeners
style?: CSSProperties | undefined
} | null>(null)
232 changes: 138 additions & 94 deletions apps/renderer/src/modules/feed-column/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { DndContext, pointerWithin, useDroppable } from "@dnd-kit/core"
import { ActionButton } from "@follow/components/ui/button/index.js"
import type { FeedViewType } from "@follow/constants"
import { Routes, views } from "@follow/constants"
import { useTypeScriptHappyCallback } from "@follow/hooks"
import { useRegisterGlobalContext } from "@follow/shared/bridge"
Expand All @@ -19,13 +21,14 @@ import { shortcuts } from "~/constants/shortcuts"
import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry"
import { useReduceMotion } from "~/hooks/biz/useReduceMotion"
import { getRouteParams } from "~/hooks/biz/useRouteParams"
import { useBatchUpdateSubscription } from "~/hooks/biz/useSubscriptionActions"
import { useAuthQuery } from "~/hooks/common"
import { Queries } from "~/queries"
import { useSubscriptionStore } from "~/store/subscription"
import { useFeedUnreadStore } from "~/store/unread"

import { WindowUnderBlur } from "../../components/ui/background"
import { getSelectedFeedIds, setSelectedFeedIds } from "./atom"
import { getSelectedFeedIds, setSelectedFeedIds, useSelectedFeedIds } from "./atom"
import { FeedColumnHeader } from "./header"
import { FeedList } from "./list"

Expand Down Expand Up @@ -87,27 +90,6 @@ export function FeedColumn({ children, className }: PropsWithChildren<{ classNam
}
}, [setActive_])

const [useHotkeysSwitch, setUseHotkeysSwitch] = useState<boolean>(false)
useHotkeys(
shortcuts.feeds.switchBetweenViews.key,
(e) => {
e.preventDefault()
setUseHotkeysSwitch(true)
if (isHotkeyPressed("Left")) {
setActive((i) => {
if (i === 0) {
return views.length - 1
} else {
return i - 1
}
})
} else {
setActive((i) => (i + 1) % views.length)
}
},
{ scopes: HotKeyScopeMap.Home },
)

useWheel(
({ event, last, memo: wait = false, direction: [dx], delta: [dex] }) => {
if (!last) {
Expand All @@ -131,88 +113,150 @@ export function FeedColumn({ children, className }: PropsWithChildren<{ classNam
},
)

const unreadByView = useUnreadByView()
const { t } = useTranslation()

const showSidebarUnreadCount = useUISettingKey("sidebarShowUnreadCount")

useRegisterGlobalContext("goToDiscover", () => {
window.router.navigate(Routes.Discover)
})

const [selectedIds, setSelectedIds] = useSelectedFeedIds()

const { mutate } = useBatchUpdateSubscription()

return (
<WindowUnderBlur
className={cn("relative flex h-full flex-col space-y-3 pt-2.5", className)}
onClick={useCallback(() => navigateBackHome(), [navigateBackHome])}
>
<FeedColumnHeader />
<DndContext
collisionDetection={pointerWithin}
onDragEnd={(event) => {
if (!event.over) {
return
}

<div
className="flex w-full justify-between px-3 text-xl text-theme-vibrancyFg"
onClick={stopPropagation}
>
{views.map((item, index) => (
<ActionButton
key={item.name}
tooltip={t(item.name)}
shortcut={`${index + 1}`}
className={cn(
active === index && item.className,
"flex h-11 flex-col items-center gap-1 text-xl",
ELECTRON ? "hover:!bg-theme-item-hover" : "",
active === index && useHotkeysSwitch ? "bg-theme-item-active" : "",
)}
onClick={(e) => {
setActive(index)
setUseHotkeysSwitch(false)
e.stopPropagation()
}}
>
{item.icon}
{showSidebarUnreadCount ? (
<div className="text-[0.625rem] font-medium leading-none">
{unreadByView[index] > 99 ? (
<span className="-mr-0.5">99+</span>
) : (
unreadByView[index]
)}
</div>
) : (
<i
className={cn(
"i-mgc-round-cute-fi text-[0.25rem]",
unreadByView[index]
? active === index
? "opacity-100"
: "opacity-60"
: "opacity-0",
)}
/>
)}
</ActionButton>
))}
</div>
<div
className="relative flex size-full overflow-hidden"
ref={carouselRef}
onPointerDown={useTypeScriptHappyCallback((e) => {
if (!(e.target instanceof HTMLElement) || !e.target.closest("[data-feed-id]")) {
const nextSelectedFeedIds = getSelectedFeedIds()
setSelectedFeedIds(nextSelectedFeedIds.length === 0 ? nextSelectedFeedIds : [])
}
}, [])}
const { category, view } = event.over.data.current as {
category: string
view: FeedViewType
}

mutate({ category, view, feedIdList: selectedIds })

setSelectedIds([])
}}
>
<WindowUnderBlur
className={cn("relative flex h-full flex-col space-y-3 pt-2.5", className)}
onClick={useCallback(() => navigateBackHome(), [navigateBackHome])}
>
<SwipeWrapper active={active}>
<FeedColumnHeader />

<div
className="flex w-full justify-between px-3 text-xl text-theme-vibrancyFg"
onClick={stopPropagation}
>
{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} />
</section>
<ViewSwitchButton
key={item.name}
item={item}
index={index}
active={active}
setActive={setActive}
/>
))}
</SwipeWrapper>
</div>
</div>
<div
className="relative flex size-full"
ref={carouselRef}
onPointerDown={useTypeScriptHappyCallback((e) => {
if (!(e.target instanceof HTMLElement) || !e.target.closest("[data-feed-id]")) {
const nextSelectedFeedIds = getSelectedFeedIds()
setSelectedFeedIds(nextSelectedFeedIds.length === 0 ? nextSelectedFeedIds : [])
}
}, [])}
>
<SwipeWrapper active={active}>
{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} />
</section>
))}
</SwipeWrapper>
</div>

{children}
</WindowUnderBlur>
</DndContext>
)
}

const ViewSwitchButton: FC<{
item: (typeof views)[number]
index: number

active: number
setActive: (next: number | ((prev: number) => number)) => void
}> = ({ item, index, active, setActive }) => {
const [useHotkeysSwitch, setUseHotkeysSwitch] = useState<boolean>(false)
useHotkeys(
shortcuts.feeds.switchBetweenViews.key,
(e) => {
e.preventDefault()
setUseHotkeysSwitch(true)
if (isHotkeyPressed("Left")) {
setActive((i) => {
if (i === 0) {
return views.length - 1
} else {
return i - 1
}
})
} else {
setActive((i) => (i + 1) % views.length)
}
},
{ scopes: HotKeyScopeMap.Home },
)

{children}
</WindowUnderBlur>
const unreadByView = useUnreadByView()
const { t } = useTranslation()
const showSidebarUnreadCount = useUISettingKey("sidebarShowUnreadCount")

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

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

Expand Down
Loading

0 comments on commit c9333d5

Please sign in to comment.