Skip to content

Commit c9333d5

Browse files
authored
feat: dnd (#1471)
1 parent 7b074f7 commit c9333d5

File tree

12 files changed

+422
-134
lines changed

12 files changed

+422
-134
lines changed

apps/renderer/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"typecheck": "tsc --noEmit"
1111
},
1212
"dependencies": {
13+
"@dnd-kit/core": "^6.1.0",
1314
"@egoist/tipc": "0.3.2",
1415
"@electron-toolkit/preload": "^3.0.1",
1516
"@follow/electron-main": "workspace:*",

apps/renderer/src/hooks/biz/useSubscriptionActions.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,23 @@ const UnfollowInfo = ({ title, undo }: { title: string; undo: () => any }) => {
106106
</>
107107
)
108108
}
109+
110+
export const useBatchUpdateSubscription = () => {
111+
return useMutation({
112+
mutationFn: async ({
113+
feedIdList,
114+
category,
115+
view,
116+
}: {
117+
feedIdList: string[]
118+
category: string
119+
view: number
120+
}) => {
121+
await subscriptionActions.batchUpdateSubscription({
122+
category,
123+
feedIdList,
124+
view,
125+
})
126+
},
127+
})
128+
}

apps/renderer/src/modules/feed-column/category.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useDroppable } from "@dnd-kit/core"
12
import { MotionButtonBase } from "@follow/components/ui/button/index.js"
23
import { LoadingCircle } from "@follow/components/ui/loading/index.jsx"
34
import { useScrollViewElement } from "@follow/components/ui/scroll-area/hooks.js"
@@ -153,13 +154,26 @@ function FeedCategoryImpl({ data: ids, view, categoryOpenStateData }: FeedCatego
153154
const listList = useOwnedListByView(view!)
154155
const showContextMenu = useShowContextMenu()
155156

157+
const isAutoGroupedCategory = !!folderName && !subscriptionCategoryExist(folderName)
158+
159+
const { isOver, setNodeRef } = useDroppable({
160+
id: `category-${folderName}`,
161+
disabled: isAutoGroupedCategory,
162+
data: {
163+
category: folderName,
164+
view,
165+
},
166+
})
167+
156168
return (
157169
<div tabIndex={-1} onClick={stopPropagation}>
158170
{!!showCollapse && (
159171
<div
172+
ref={setNodeRef}
160173
data-active={isActive || isContextMenuOpen}
161174
className={cn(
162175
"my-px flex w-full cursor-menu items-center justify-between rounded-md px-2.5",
176+
isOver && "border-theme-accent-400 bg-theme-accent-400/60",
163177
feedColumnStyles.item,
164178
)}
165179
onClick={(e) => {
@@ -242,7 +256,7 @@ function FeedCategoryImpl({ data: ids, view, categoryOpenStateData }: FeedCatego
242256
{
243257
type: "text",
244258
label: t("sidebar.feed_column.context_menu.delete_category"),
245-
hide: !folderName || !subscriptionCategoryExist(folderName),
259+
hide: !folderName || isAutoGroupedCategory,
246260
click: () => {
247261
present({
248262
title: t("sidebar.feed_column.context_menu.delete_category_confirmation", {
@@ -304,7 +318,7 @@ function FeedCategoryImpl({ data: ids, view, categoryOpenStateData }: FeedCatego
304318
{open && (
305319
<m.div
306320
ref={itemsRef}
307-
className="space-y-px overflow-hidden"
321+
className="space-y-px"
308322
initial={
309323
!!showCollapse && {
310324
height: 0,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { DraggableAttributes, DraggableSyntheticListeners } from "@dnd-kit/core"
2+
import type { CSSProperties } from "react"
3+
import { createContext } from "react"
4+
5+
export const DraggableContext = createContext<{
6+
attributes: DraggableAttributes
7+
listeners: DraggableSyntheticListeners
8+
style?: CSSProperties | undefined
9+
} | null>(null)

apps/renderer/src/modules/feed-column/index.tsx

Lines changed: 138 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { DndContext, pointerWithin, useDroppable } from "@dnd-kit/core"
12
import { ActionButton } from "@follow/components/ui/button/index.js"
3+
import type { FeedViewType } from "@follow/constants"
24
import { Routes, views } from "@follow/constants"
35
import { useTypeScriptHappyCallback } from "@follow/hooks"
46
import { useRegisterGlobalContext } from "@follow/shared/bridge"
@@ -19,13 +21,14 @@ import { shortcuts } from "~/constants/shortcuts"
1921
import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry"
2022
import { useReduceMotion } from "~/hooks/biz/useReduceMotion"
2123
import { getRouteParams } from "~/hooks/biz/useRouteParams"
24+
import { useBatchUpdateSubscription } from "~/hooks/biz/useSubscriptionActions"
2225
import { useAuthQuery } from "~/hooks/common"
2326
import { Queries } from "~/queries"
2427
import { useSubscriptionStore } from "~/store/subscription"
2528
import { useFeedUnreadStore } from "~/store/unread"
2629

2730
import { WindowUnderBlur } from "../../components/ui/background"
28-
import { getSelectedFeedIds, setSelectedFeedIds } from "./atom"
31+
import { getSelectedFeedIds, setSelectedFeedIds, useSelectedFeedIds } from "./atom"
2932
import { FeedColumnHeader } from "./header"
3033
import { FeedList } from "./list"
3134

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

90-
const [useHotkeysSwitch, setUseHotkeysSwitch] = useState<boolean>(false)
91-
useHotkeys(
92-
shortcuts.feeds.switchBetweenViews.key,
93-
(e) => {
94-
e.preventDefault()
95-
setUseHotkeysSwitch(true)
96-
if (isHotkeyPressed("Left")) {
97-
setActive((i) => {
98-
if (i === 0) {
99-
return views.length - 1
100-
} else {
101-
return i - 1
102-
}
103-
})
104-
} else {
105-
setActive((i) => (i + 1) % views.length)
106-
}
107-
},
108-
{ scopes: HotKeyScopeMap.Home },
109-
)
110-
11193
useWheel(
11294
({ event, last, memo: wait = false, direction: [dx], delta: [dex] }) => {
11395
if (!last) {
@@ -131,88 +113,150 @@ export function FeedColumn({ children, className }: PropsWithChildren<{ classNam
131113
},
132114
)
133115

134-
const unreadByView = useUnreadByView()
135-
const { t } = useTranslation()
136-
137-
const showSidebarUnreadCount = useUISettingKey("sidebarShowUnreadCount")
138-
139116
useRegisterGlobalContext("goToDiscover", () => {
140117
window.router.navigate(Routes.Discover)
141118
})
142119

120+
const [selectedIds, setSelectedIds] = useSelectedFeedIds()
121+
122+
const { mutate } = useBatchUpdateSubscription()
123+
143124
return (
144-
<WindowUnderBlur
145-
className={cn("relative flex h-full flex-col space-y-3 pt-2.5", className)}
146-
onClick={useCallback(() => navigateBackHome(), [navigateBackHome])}
147-
>
148-
<FeedColumnHeader />
125+
<DndContext
126+
collisionDetection={pointerWithin}
127+
onDragEnd={(event) => {
128+
if (!event.over) {
129+
return
130+
}
149131

150-
<div
151-
className="flex w-full justify-between px-3 text-xl text-theme-vibrancyFg"
152-
onClick={stopPropagation}
153-
>
154-
{views.map((item, index) => (
155-
<ActionButton
156-
key={item.name}
157-
tooltip={t(item.name)}
158-
shortcut={`${index + 1}`}
159-
className={cn(
160-
active === index && item.className,
161-
"flex h-11 flex-col items-center gap-1 text-xl",
162-
ELECTRON ? "hover:!bg-theme-item-hover" : "",
163-
active === index && useHotkeysSwitch ? "bg-theme-item-active" : "",
164-
)}
165-
onClick={(e) => {
166-
setActive(index)
167-
setUseHotkeysSwitch(false)
168-
e.stopPropagation()
169-
}}
170-
>
171-
{item.icon}
172-
{showSidebarUnreadCount ? (
173-
<div className="text-[0.625rem] font-medium leading-none">
174-
{unreadByView[index] > 99 ? (
175-
<span className="-mr-0.5">99+</span>
176-
) : (
177-
unreadByView[index]
178-
)}
179-
</div>
180-
) : (
181-
<i
182-
className={cn(
183-
"i-mgc-round-cute-fi text-[0.25rem]",
184-
unreadByView[index]
185-
? active === index
186-
? "opacity-100"
187-
: "opacity-60"
188-
: "opacity-0",
189-
)}
190-
/>
191-
)}
192-
</ActionButton>
193-
))}
194-
</div>
195-
<div
196-
className="relative flex size-full overflow-hidden"
197-
ref={carouselRef}
198-
onPointerDown={useTypeScriptHappyCallback((e) => {
199-
if (!(e.target instanceof HTMLElement) || !e.target.closest("[data-feed-id]")) {
200-
const nextSelectedFeedIds = getSelectedFeedIds()
201-
setSelectedFeedIds(nextSelectedFeedIds.length === 0 ? nextSelectedFeedIds : [])
202-
}
203-
}, [])}
132+
const { category, view } = event.over.data.current as {
133+
category: string
134+
view: FeedViewType
135+
}
136+
137+
mutate({ category, view, feedIdList: selectedIds })
138+
139+
setSelectedIds([])
140+
}}
141+
>
142+
<WindowUnderBlur
143+
className={cn("relative flex h-full flex-col space-y-3 pt-2.5", className)}
144+
onClick={useCallback(() => navigateBackHome(), [navigateBackHome])}
204145
>
205-
<SwipeWrapper active={active}>
146+
<FeedColumnHeader />
147+
148+
<div
149+
className="flex w-full justify-between px-3 text-xl text-theme-vibrancyFg"
150+
onClick={stopPropagation}
151+
>
206152
{views.map((item, index) => (
207-
<section key={item.name} className="h-full w-feed-col shrink-0 snap-center">
208-
<FeedList className="flex size-full flex-col text-sm" view={index} />
209-
</section>
153+
<ViewSwitchButton
154+
key={item.name}
155+
item={item}
156+
index={index}
157+
active={active}
158+
setActive={setActive}
159+
/>
210160
))}
211-
</SwipeWrapper>
212-
</div>
161+
</div>
162+
<div
163+
className="relative flex size-full"
164+
ref={carouselRef}
165+
onPointerDown={useTypeScriptHappyCallback((e) => {
166+
if (!(e.target instanceof HTMLElement) || !e.target.closest("[data-feed-id]")) {
167+
const nextSelectedFeedIds = getSelectedFeedIds()
168+
setSelectedFeedIds(nextSelectedFeedIds.length === 0 ? nextSelectedFeedIds : [])
169+
}
170+
}, [])}
171+
>
172+
<SwipeWrapper active={active}>
173+
{views.map((item, index) => (
174+
<section key={item.name} className="h-full w-feed-col shrink-0 snap-center">
175+
<FeedList className="flex size-full flex-col text-sm" view={index} />
176+
</section>
177+
))}
178+
</SwipeWrapper>
179+
</div>
180+
181+
{children}
182+
</WindowUnderBlur>
183+
</DndContext>
184+
)
185+
}
186+
187+
const ViewSwitchButton: FC<{
188+
item: (typeof views)[number]
189+
index: number
190+
191+
active: number
192+
setActive: (next: number | ((prev: number) => number)) => void
193+
}> = ({ item, index, active, setActive }) => {
194+
const [useHotkeysSwitch, setUseHotkeysSwitch] = useState<boolean>(false)
195+
useHotkeys(
196+
shortcuts.feeds.switchBetweenViews.key,
197+
(e) => {
198+
e.preventDefault()
199+
setUseHotkeysSwitch(true)
200+
if (isHotkeyPressed("Left")) {
201+
setActive((i) => {
202+
if (i === 0) {
203+
return views.length - 1
204+
} else {
205+
return i - 1
206+
}
207+
})
208+
} else {
209+
setActive((i) => (i + 1) % views.length)
210+
}
211+
},
212+
{ scopes: HotKeyScopeMap.Home },
213+
)
213214

214-
{children}
215-
</WindowUnderBlur>
215+
const unreadByView = useUnreadByView()
216+
const { t } = useTranslation()
217+
const showSidebarUnreadCount = useUISettingKey("sidebarShowUnreadCount")
218+
219+
const { isOver, setNodeRef } = useDroppable({
220+
id: `view-${item.name}`,
221+
data: {
222+
category: "",
223+
view: item.view,
224+
},
225+
})
226+
227+
return (
228+
<ActionButton
229+
ref={setNodeRef}
230+
key={item.name}
231+
tooltip={t(item.name)}
232+
shortcut={`${index + 1}`}
233+
className={cn(
234+
active === index && item.className,
235+
"flex h-11 flex-col items-center gap-1 text-xl",
236+
ELECTRON ? "hover:!bg-theme-item-hover" : "",
237+
active === index && useHotkeysSwitch ? "bg-theme-item-active" : "",
238+
isOver && "border-theme-accent-400 bg-theme-accent-400/60",
239+
)}
240+
onClick={(e) => {
241+
setActive(index)
242+
setUseHotkeysSwitch(false)
243+
e.stopPropagation()
244+
}}
245+
>
246+
{item.icon}
247+
{showSidebarUnreadCount ? (
248+
<div className="text-[0.625rem] font-medium leading-none">
249+
{unreadByView[index] > 99 ? <span className="-mr-0.5">99+</span> : unreadByView[index]}
250+
</div>
251+
) : (
252+
<i
253+
className={cn(
254+
"i-mgc-round-cute-fi text-[0.25rem]",
255+
unreadByView[index] ? (active === index ? "opacity-100" : "opacity-60") : "opacity-0",
256+
)}
257+
/>
258+
)}
259+
</ActionButton>
216260
)
217261
}
218262

0 commit comments

Comments
 (0)