ChatItem.svelte 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. <script lang="ts">
  2. import { toast } from 'svelte-sonner';
  3. import { goto, invalidate, invalidateAll } from '$app/navigation';
  4. import { onMount, getContext, createEventDispatcher, tick } from 'svelte';
  5. const i18n = getContext('i18n');
  6. const dispatch = createEventDispatcher();
  7. import {
  8. archiveChatById,
  9. cloneChatById,
  10. deleteChatById,
  11. getChatList,
  12. getChatListByTagName,
  13. getPinnedChatList,
  14. updateChatById
  15. } from '$lib/apis/chats';
  16. import {
  17. chatId,
  18. chatTitle as _chatTitle,
  19. chats,
  20. mobile,
  21. pinnedChats,
  22. showSidebar,
  23. currentChatPage
  24. } from '$lib/stores';
  25. import ChatMenu from './ChatMenu.svelte';
  26. import ShareChatModal from '$lib/components/chat/ShareChatModal.svelte';
  27. import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
  28. import Tooltip from '$lib/components/common/Tooltip.svelte';
  29. import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
  30. export let chat;
  31. export let selected = false;
  32. export let shiftKey = false;
  33. let mouseOver = false;
  34. let showShareChatModal = false;
  35. let confirmEdit = false;
  36. let chatTitle = chat.title;
  37. const editChatTitle = async (id, title) => {
  38. if (title === '') {
  39. toast.error($i18n.t('Title cannot be an empty string.'));
  40. } else {
  41. await updateChatById(localStorage.token, id, {
  42. title: title
  43. });
  44. if (id === $chatId) {
  45. _chatTitle.set(title);
  46. }
  47. currentChatPage.set(1);
  48. await chats.set(await getChatList(localStorage.token, $currentChatPage));
  49. await pinnedChats.set(await getPinnedChatList(localStorage.token));
  50. }
  51. };
  52. const cloneChatHandler = async (id) => {
  53. const res = await cloneChatById(localStorage.token, id).catch((error) => {
  54. toast.error(error);
  55. return null;
  56. });
  57. if (res) {
  58. goto(`/c/${res.id}`);
  59. currentChatPage.set(1);
  60. await chats.set(await getChatList(localStorage.token, $currentChatPage));
  61. await pinnedChats.set(await getPinnedChatList(localStorage.token));
  62. }
  63. };
  64. const archiveChatHandler = async (id) => {
  65. await archiveChatById(localStorage.token, id);
  66. currentChatPage.set(1);
  67. await chats.set(await getChatList(localStorage.token, $currentChatPage));
  68. await pinnedChats.set(await getPinnedChatList(localStorage.token));
  69. };
  70. const focusEdit = async (node: HTMLInputElement) => {
  71. node.focus();
  72. };
  73. </script>
  74. <ShareChatModal bind:show={showShareChatModal} chatId={chat.id} />
  75. <div class=" w-full pr-2 relative group">
  76. {#if confirmEdit}
  77. <div
  78. class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId || confirmEdit
  79. ? 'bg-gray-200 dark:bg-gray-900'
  80. : selected
  81. ? 'bg-gray-100 dark:bg-gray-950'
  82. : 'group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
  83. >
  84. <input
  85. use:focusEdit
  86. bind:value={chatTitle}
  87. class=" bg-transparent w-full outline-none mr-10"
  88. />
  89. </div>
  90. {:else}
  91. <a
  92. class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId || confirmEdit
  93. ? 'bg-gray-200 dark:bg-gray-900'
  94. : selected
  95. ? 'bg-gray-100 dark:bg-gray-950'
  96. : ' group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
  97. href="/c/{chat.id}"
  98. on:click={() => {
  99. dispatch('select');
  100. if ($mobile) {
  101. showSidebar.set(false);
  102. }
  103. }}
  104. on:dblclick={() => {
  105. chatTitle = chat.title;
  106. confirmEdit = true;
  107. }}
  108. on:mouseenter={(e) => {
  109. mouseOver = true;
  110. }}
  111. on:mouseleave={(e) => {
  112. mouseOver = false;
  113. }}
  114. on:focus={(e) => {}}
  115. draggable="false"
  116. >
  117. <div class=" flex self-center flex-1 w-full">
  118. <div class=" text-left self-center overflow-hidden w-full h-[20px]">
  119. {chat.title}
  120. </div>
  121. </div>
  122. </a>
  123. {/if}
  124. <!-- svelte-ignore a11y-no-static-element-interactions -->
  125. <div
  126. class="
  127. {chat.id === $chatId || confirmEdit
  128. ? 'from-gray-200 dark:from-gray-900'
  129. : selected
  130. ? 'from-gray-100 dark:from-gray-950'
  131. : 'invisible group-hover:visible from-gray-100 dark:from-gray-950'}
  132. absolute right-[10px] top-[6px] py-1 pr-2 pl-5 bg-gradient-to-l from-80%
  133. to-transparent"
  134. on:mouseenter={(e) => {
  135. mouseOver = true;
  136. }}
  137. on:mouseleave={(e) => {
  138. mouseOver = false;
  139. }}
  140. >
  141. {#if confirmEdit}
  142. <div class="flex self-center space-x-1.5 z-10">
  143. <Tooltip content={$i18n.t('Confirm')}>
  144. <button
  145. class=" self-center dark:hover:text-white transition"
  146. on:click={() => {
  147. editChatTitle(chat.id, chatTitle);
  148. confirmEdit = false;
  149. chatTitle = '';
  150. }}
  151. >
  152. <svg
  153. xmlns="http://www.w3.org/2000/svg"
  154. viewBox="0 0 20 20"
  155. fill="currentColor"
  156. class="w-4 h-4"
  157. >
  158. <path
  159. fill-rule="evenodd"
  160. d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
  161. clip-rule="evenodd"
  162. />
  163. </svg>
  164. </button>
  165. </Tooltip>
  166. <Tooltip content={$i18n.t('Cancel')}>
  167. <button
  168. class=" self-center dark:hover:text-white transition"
  169. on:click={() => {
  170. confirmEdit = false;
  171. chatTitle = '';
  172. }}
  173. >
  174. <svg
  175. xmlns="http://www.w3.org/2000/svg"
  176. viewBox="0 0 20 20"
  177. fill="currentColor"
  178. class="w-4 h-4"
  179. >
  180. <path
  181. d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
  182. />
  183. </svg>
  184. </button>
  185. </Tooltip>
  186. </div>
  187. {:else if shiftKey && mouseOver}
  188. <div class=" flex items-center self-center space-x-1.5">
  189. <Tooltip content={$i18n.t('Archive')} className="flex items-center">
  190. <button
  191. class=" self-center dark:hover:text-white transition"
  192. on:click={() => {
  193. archiveChatHandler(chat.id);
  194. }}
  195. type="button"
  196. >
  197. <ArchiveBox className="size-4 translate-y-[0.5px]" strokeWidth="2" />
  198. </button>
  199. </Tooltip>
  200. <Tooltip content={$i18n.t('Delete')}>
  201. <button
  202. class=" self-center dark:hover:text-white transition"
  203. on:click={() => {
  204. dispatch('delete', 'shift');
  205. }}
  206. type="button"
  207. >
  208. <GarbageBin strokeWidth="2" />
  209. </button>
  210. </Tooltip>
  211. </div>
  212. {:else}
  213. <div class="flex self-center space-x-1 z-10">
  214. <ChatMenu
  215. chatId={chat.id}
  216. cloneChatHandler={() => {
  217. cloneChatHandler(chat.id);
  218. }}
  219. shareHandler={() => {
  220. showShareChatModal = true;
  221. }}
  222. archiveChatHandler={() => {
  223. archiveChatHandler(chat.id);
  224. }}
  225. renameHandler={() => {
  226. chatTitle = chat.title;
  227. confirmEdit = true;
  228. }}
  229. deleteHandler={() => {
  230. dispatch('delete');
  231. }}
  232. onClose={() => {
  233. dispatch('unselect');
  234. }}
  235. on:change={async () => {
  236. await pinnedChats.set(await getPinnedChatList(localStorage.token));
  237. }}
  238. on:tag={(e) => {
  239. dispatch('tag', e.detail);
  240. }}
  241. >
  242. <button
  243. aria-label="Chat Menu"
  244. class=" self-center dark:hover:text-white transition"
  245. on:click={() => {
  246. dispatch('select');
  247. }}
  248. >
  249. <svg
  250. xmlns="http://www.w3.org/2000/svg"
  251. viewBox="0 0 16 16"
  252. fill="currentColor"
  253. class="w-4 h-4"
  254. >
  255. <path
  256. d="M2 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM6.5 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM12.5 6.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"
  257. />
  258. </svg>
  259. </button>
  260. </ChatMenu>
  261. {#if chat.id === $chatId}
  262. <!-- Shortcut support using "delete-chat-button" id -->
  263. <button
  264. id="delete-chat-button"
  265. class="hidden"
  266. on:click={() => {
  267. dispatch('delete');
  268. }}
  269. >
  270. <svg
  271. xmlns="http://www.w3.org/2000/svg"
  272. viewBox="0 0 16 16"
  273. fill="currentColor"
  274. class="w-4 h-4"
  275. >
  276. <path
  277. d="M2 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM6.5 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM12.5 6.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"
  278. />
  279. </svg>
  280. </button>
  281. {/if}
  282. </div>
  283. {/if}
  284. </div>
  285. </div>