ChatItem.svelte 7.2 KB

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