diff --git a/app/components/chat-list.tsx b/app/components/chat-list.tsx index 8ad2b7dc..8d02805f 100644 --- a/app/components/chat-list.tsx +++ b/app/components/chat-list.tsx @@ -1,14 +1,13 @@ -import { useState, useRef, useEffect, useLayoutEffect } from "react"; import DeleteIcon from "../icons/delete.svg"; import styles from "./home.module.scss"; - import { - Message, - SubmitKey, - useChatStore, - ChatSession, - BOT_HELLO, -} from "../store"; + DragDropContext, + Droppable, + Draggable, + OnDragEndResponder, +} from "@hello-pangea/dnd"; + +import { useChatStore } from "../store"; import Locale from "../locales"; import { isMobileScreen } from "../utils"; @@ -20,54 +19,92 @@ export function ChatItem(props: { count: number; time: string; selected: boolean; + id: number; + index: number; }) { return ( -
-
{props.title}
-
-
- {Locale.ChatItem.ChatItemCount(props.count)} + + {(provided) => ( +
+
{props.title}
+
+
+ {Locale.ChatItem.ChatItemCount(props.count)} +
+
{props.time}
+
+
+ +
-
{props.time}
-
-
- -
-
+ )} + ); } export function ChatList() { - const [sessions, selectedIndex, selectSession, removeSession] = useChatStore( - (state) => [ + const [sessions, selectedIndex, selectSession, removeSession, moveSession] = + useChatStore((state) => [ state.sessions, state.currentSessionIndex, state.selectSession, state.removeSession, - ], - ); + state.moveSession, + ]); + + const onDragEnd: OnDragEndResponder = (result) => { + const { destination, source } = result; + if (!destination) { + return; + } + + if ( + destination.droppableId === source.droppableId && + destination.index === source.index + ) { + return; + } + + moveSession(source.index, destination.index); + }; return ( -
- {sessions.map((item, i) => ( - selectSession(i)} - onDelete={() => - (!isMobileScreen() || confirm(Locale.Home.DeleteChat)) && - removeSession(i) - } - /> - ))} -
+ + + {(provided) => ( +
+ {sessions.map((item, i) => ( + selectSession(i)} + onDelete={() => + (!isMobileScreen() || confirm(Locale.Home.DeleteChat)) && + removeSession(i) + } + /> + ))} + {provided.placeholder} +
+ )} +
+
); } diff --git a/app/components/home.module.scss b/app/components/home.module.scss index 64ac2363..da954dc1 100644 --- a/app/components/home.module.scss +++ b/app/components/home.module.scss @@ -125,7 +125,7 @@ border-radius: 10px; margin-bottom: 10px; box-shadow: var(--card-shadow); - transition: all 0.3s ease; + transition: background-color 0.3s ease; cursor: pointer; user-select: none; border: 2px solid transparent; diff --git a/app/components/home.tsx b/app/components/home.tsx index 66f86348..9e57cb87 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -19,7 +19,6 @@ import CloseIcon from "../icons/close.svg"; import { useChatStore } from "../store"; import { isMobileScreen } from "../utils"; import Locale from "../locales"; -import { ChatList } from "./chat-list"; import { Chat } from "./chat"; import dynamic from "next/dynamic"; @@ -39,6 +38,10 @@ const Settings = dynamic(async () => (await import("./settings")).Settings, { loading: () => , }); +const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, { + loading: () => , +}); + function useSwitchTheme() { const config = useChatStore((state) => state.config); diff --git a/app/store/app.ts b/app/store/app.ts index 64faa31d..c63fa9d4 100644 --- a/app/store/app.ts +++ b/app/store/app.ts @@ -201,6 +201,7 @@ interface ChatStore { currentSessionIndex: number; clearSessions: () => void; removeSession: (index: number) => void; + moveSession: (from: number, to: number) => void; selectSession: (index: number) => void; newSession: () => void; currentSession: () => ChatSession; @@ -291,6 +292,31 @@ export const useChatStore = create()( }); }, + moveSession(from: number, to: number) { + set((state) => { + const { sessions, currentSessionIndex: oldIndex } = state; + + // move the session + const newSessions = [...sessions]; + const session = newSessions[from]; + newSessions.splice(from, 1); + newSessions.splice(to, 0, session); + + // modify current session id + let newIndex = oldIndex === from ? to : oldIndex; + if (oldIndex > from && oldIndex <= to) { + newIndex -= 1; + } else if (oldIndex < from && oldIndex >= to) { + newIndex += 1; + } + + return { + currentSessionIndex: newIndex, + sessions: newSessions, + }; + }); + }, + newSession() { set((state) => ({ currentSessionIndex: 0, diff --git a/package.json b/package.json index 69dec164..9fcb74e7 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "prepare": "husky install" }, "dependencies": { + "@hello-pangea/dnd": "^16.2.0", "@svgr/webpack": "^6.5.1", "@vercel/analytics": "^0.1.11", "emoji-picker-react": "^4.4.7", diff --git a/yarn.lock b/yarn.lock index 9a937276..88aa5982 100644 --- a/yarn.lock +++ b/yarn.lock @@ -954,7 +954,7 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.20.7", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.12.1", "@babel/runtime@^7.19.4", "@babel/runtime@^7.20.7", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== @@ -1027,6 +1027,19 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.37.0.tgz#cf1b5fa24217fe007f6487a26d765274925efa7d" integrity sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A== +"@hello-pangea/dnd@^16.2.0": + version "16.2.0" + resolved "https://registry.npmmirror.com/@hello-pangea/dnd/-/dnd-16.2.0.tgz#58cbadeb56f8c7a381da696bb7aa3bfbb87876ec" + integrity sha512-inACvMcvvLr34CG0P6+G/3bprVKhwswxjcsFUSJ+fpOGjhvDj9caiA9X3clby0lgJ6/ILIJjyedHZYECB7GAgA== + dependencies: + "@babel/runtime" "^7.19.4" + css-box-model "^1.2.1" + memoize-one "^6.0.0" + raf-schd "^4.0.3" + react-redux "^8.0.4" + redux "^4.2.0" + use-memo-one "^1.1.3" + "@humanwhocodes/config-array@^0.11.8": version "0.11.8" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" @@ -1333,6 +1346,14 @@ dependencies: "@types/unist" "*" +"@types/hoist-non-react-statics@^3.3.1": + version "3.3.1" + resolved "https://registry.npmmirror.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -1408,6 +1429,11 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/use-sync-external-store@^0.0.3": + version "0.0.3" + resolved "https://registry.npmmirror.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" + integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== + "@typescript-eslint/parser@^5.42.0": version "5.57.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.57.0.tgz#f675bf2cd1a838949fd0de5683834417b757e4fa" @@ -1912,6 +1938,13 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +css-box-model@^1.2.1: + version "1.2.1" + resolved "https://registry.npmmirror.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" + integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== + dependencies: + tiny-invariant "^1.0.6" + css-select@^4.1.3: version "4.3.0" resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" @@ -2885,6 +2918,13 @@ highlight.js@~11.7.0: resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.7.0.tgz#3ff0165bc843f8c9bce1fd89e2fda9143d24b11e" integrity sha512-1rRqesRFhMO/PRF+G86evnyJkCgaZFOI+Z6kdj15TA18funfoqJXvgPCLSf0SWq3SRfg1j3HlDs8o4s3EGq1oQ== +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.npmmirror.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + human-signals@^4.3.0: version "4.3.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2" @@ -3527,6 +3567,11 @@ mdn-data@2.0.14: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== +memoize-one@^6.0.0: + version "6.0.0" + resolved "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" + integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -4211,6 +4256,11 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +raf-schd@^4.0.3: + version "4.0.3" + resolved "https://registry.npmmirror.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" + integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== + react-dom@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" @@ -4219,7 +4269,7 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.0" -react-is@^16.13.1: +react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -4250,6 +4300,18 @@ react-markdown@^8.0.5: unist-util-visit "^4.0.0" vfile "^5.0.0" +react-redux@^8.0.4: + version "8.0.5" + resolved "https://registry.npmmirror.com/react-redux/-/react-redux-8.0.5.tgz#e5fb8331993a019b8aaf2e167a93d10af469c7bd" + integrity sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw== + dependencies: + "@babel/runtime" "^7.12.1" + "@types/hoist-non-react-statics" "^3.3.1" + "@types/use-sync-external-store" "^0.0.3" + hoist-non-react-statics "^3.3.2" + react-is "^18.0.0" + use-sync-external-store "^1.0.0" + react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" @@ -4264,6 +4326,13 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +redux@^4.2.0: + version "4.2.1" + resolved "https://registry.npmmirror.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" + integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== + dependencies: + "@babel/runtime" "^7.9.2" + regenerate-unicode-properties@^10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" @@ -4774,6 +4843,11 @@ tiny-glob@^0.2.9: globalyzer "0.1.0" globrex "^0.1.2" +tiny-invariant@^1.0.6: + version "1.3.1" + resolved "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" + integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -4979,7 +5053,12 @@ use-debounce@^9.0.3: resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-9.0.3.tgz#bac660c19ab7b38662e08608fee23c7ad303f532" integrity sha512-FhtlbDtDXILJV7Lix5OZj5yX/fW1tzq+VrvK1fnT2bUrPOGruU9Rw8NCEn+UI9wopfERBEZAOQ8lfeCJPllgnw== -use-sync-external-store@1.2.0: +use-memo-one@^1.1.3: + version "1.1.3" + resolved "https://registry.npmmirror.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" + integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== + +use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==