ChatItem.svelte 7.6 KB

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