Skip to content

Commit f71354f

Browse files
authored
Merge pull request ChatGPTNextWeb#509 from xiaotianxt/feat/dnd-xiaotianxt
Drag & Drop support for ChatList
2 parents 796eafb + f920b20 commit f71354f

File tree

6 files changed

+195
-49
lines changed

6 files changed

+195
-49
lines changed

app/components/chat-list.tsx

+81-44
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
import { useState, useRef, useEffect, useLayoutEffect } from "react";
21
import DeleteIcon from "../icons/delete.svg";
32
import styles from "./home.module.scss";
4-
53
import {
6-
Message,
7-
SubmitKey,
8-
useChatStore,
9-
ChatSession,
10-
BOT_HELLO,
11-
} from "../store";
4+
DragDropContext,
5+
Droppable,
6+
Draggable,
7+
OnDragEndResponder,
8+
} from "@hello-pangea/dnd";
9+
10+
import { useChatStore } from "../store";
1211

1312
import Locale from "../locales";
1413
import { isMobileScreen } from "../utils";
@@ -20,54 +19,92 @@ export function ChatItem(props: {
2019
count: number;
2120
time: string;
2221
selected: boolean;
22+
id: number;
23+
index: number;
2324
}) {
2425
return (
25-
<div
26-
className={`${styles["chat-item"]} ${
27-
props.selected && styles["chat-item-selected"]
28-
}`}
29-
onClick={props.onClick}
30-
>
31-
<div className={styles["chat-item-title"]}>{props.title}</div>
32-
<div className={styles["chat-item-info"]}>
33-
<div className={styles["chat-item-count"]}>
34-
{Locale.ChatItem.ChatItemCount(props.count)}
26+
<Draggable draggableId={`${props.id}`} index={props.index}>
27+
{(provided) => (
28+
<div
29+
className={`${styles["chat-item"]} ${
30+
props.selected && styles["chat-item-selected"]
31+
}`}
32+
onClick={props.onClick}
33+
ref={provided.innerRef}
34+
{...provided.draggableProps}
35+
{...provided.dragHandleProps}
36+
>
37+
<div className={styles["chat-item-title"]}>{props.title}</div>
38+
<div className={styles["chat-item-info"]}>
39+
<div className={styles["chat-item-count"]}>
40+
{Locale.ChatItem.ChatItemCount(props.count)}
41+
</div>
42+
<div className={styles["chat-item-date"]}>{props.time}</div>
43+
</div>
44+
<div className={styles["chat-item-delete"]} onClick={props.onDelete}>
45+
<DeleteIcon />
46+
</div>
3547
</div>
36-
<div className={styles["chat-item-date"]}>{props.time}</div>
37-
</div>
38-
<div className={styles["chat-item-delete"]} onClick={props.onDelete}>
39-
<DeleteIcon />
40-
</div>
41-
</div>
48+
)}
49+
</Draggable>
4250
);
4351
}
4452

4553
export function ChatList() {
46-
const [sessions, selectedIndex, selectSession, removeSession] = useChatStore(
47-
(state) => [
54+
const [sessions, selectedIndex, selectSession, removeSession, moveSession] =
55+
useChatStore((state) => [
4856
state.sessions,
4957
state.currentSessionIndex,
5058
state.selectSession,
5159
state.removeSession,
52-
],
53-
);
60+
state.moveSession,
61+
]);
62+
63+
const onDragEnd: OnDragEndResponder = (result) => {
64+
const { destination, source } = result;
65+
if (!destination) {
66+
return;
67+
}
68+
69+
if (
70+
destination.droppableId === source.droppableId &&
71+
destination.index === source.index
72+
) {
73+
return;
74+
}
75+
76+
moveSession(source.index, destination.index);
77+
};
5478

5579
return (
56-
<div className={styles["chat-list"]}>
57-
{sessions.map((item, i) => (
58-
<ChatItem
59-
title={item.topic}
60-
time={item.lastUpdate}
61-
count={item.messages.length}
62-
key={i}
63-
selected={i === selectedIndex}
64-
onClick={() => selectSession(i)}
65-
onDelete={() =>
66-
(!isMobileScreen() || confirm(Locale.Home.DeleteChat)) &&
67-
removeSession(i)
68-
}
69-
/>
70-
))}
71-
</div>
80+
<DragDropContext onDragEnd={onDragEnd}>
81+
<Droppable droppableId="chat-list">
82+
{(provided) => (
83+
<div
84+
className={styles["chat-list"]}
85+
ref={provided.innerRef}
86+
{...provided.droppableProps}
87+
>
88+
{sessions.map((item, i) => (
89+
<ChatItem
90+
title={item.topic}
91+
time={item.lastUpdate}
92+
count={item.messages.length}
93+
key={item.id}
94+
id={item.id}
95+
index={i}
96+
selected={i === selectedIndex}
97+
onClick={() => selectSession(i)}
98+
onDelete={() =>
99+
(!isMobileScreen() || confirm(Locale.Home.DeleteChat)) &&
100+
removeSession(i)
101+
}
102+
/>
103+
))}
104+
{provided.placeholder}
105+
</div>
106+
)}
107+
</Droppable>
108+
</DragDropContext>
72109
);
73110
}

app/components/home.module.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@
125125
border-radius: 10px;
126126
margin-bottom: 10px;
127127
box-shadow: var(--card-shadow);
128-
transition: all 0.3s ease;
128+
transition: background-color 0.3s ease;
129129
cursor: pointer;
130130
user-select: none;
131131
border: 2px solid transparent;

app/components/home.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import CloseIcon from "../icons/close.svg";
1919
import { useChatStore } from "../store";
2020
import { isMobileScreen } from "../utils";
2121
import Locale from "../locales";
22-
import { ChatList } from "./chat-list";
2322
import { Chat } from "./chat";
2423

2524
import dynamic from "next/dynamic";
@@ -39,6 +38,10 @@ const Settings = dynamic(async () => (await import("./settings")).Settings, {
3938
loading: () => <Loading noLogo />,
4039
});
4140

41+
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
42+
loading: () => <Loading noLogo />,
43+
});
44+
4245
function useSwitchTheme() {
4346
const config = useChatStore((state) => state.config);
4447

app/store/app.ts

+26
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ interface ChatStore {
201201
currentSessionIndex: number;
202202
clearSessions: () => void;
203203
removeSession: (index: number) => void;
204+
moveSession: (from: number, to: number) => void;
204205
selectSession: (index: number) => void;
205206
newSession: () => void;
206207
currentSession: () => ChatSession;
@@ -291,6 +292,31 @@ export const useChatStore = create<ChatStore>()(
291292
});
292293
},
293294

295+
moveSession(from: number, to: number) {
296+
set((state) => {
297+
const { sessions, currentSessionIndex: oldIndex } = state;
298+
299+
// move the session
300+
const newSessions = [...sessions];
301+
const session = newSessions[from];
302+
newSessions.splice(from, 1);
303+
newSessions.splice(to, 0, session);
304+
305+
// modify current session id
306+
let newIndex = oldIndex === from ? to : oldIndex;
307+
if (oldIndex > from && oldIndex <= to) {
308+
newIndex -= 1;
309+
} else if (oldIndex < from && oldIndex >= to) {
310+
newIndex += 1;
311+
}
312+
313+
return {
314+
currentSessionIndex: newIndex,
315+
sessions: newSessions,
316+
};
317+
});
318+
},
319+
294320
newSession() {
295321
set((state) => ({
296322
currentSessionIndex: 0,

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"prepare": "husky install"
1313
},
1414
"dependencies": {
15+
"@hello-pangea/dnd": "^16.2.0",
1516
"@svgr/webpack": "^6.5.1",
1617
"@vercel/analytics": "^0.1.11",
1718
"emoji-picker-react": "^4.4.7",

0 commit comments

Comments
 (0)