Pārlūkot izejas kodu

feat: channel threads

Timothy Jaeryang Baek 4 mēneši atpakaļ
vecāks
revīzija
a754a4388a

+ 8 - 1
src/lib/components/channel/Channel.svelte

@@ -94,12 +94,18 @@
 				}
 			} else if (type === 'message:delete') {
 				messages = messages.filter((message) => message.id !== data.id);
+			} else if (type === 'message:reply') {
+				const idx = messages.findIndex((message) => message.id === data.id);
+
+				if (idx !== -1) {
+					messages[idx] = data;
+				}
 			} else if (type.includes('message:reaction')) {
 				const idx = messages.findIndex((message) => message.id === data.id);
 				if (idx !== -1) {
 					messages[idx] = data;
 				}
-			} else if (type === 'typing') {
+			} else if (type === 'typing' && event.message_id === null) {
 				if (event.user.id === $user.id) {
 					return;
 				}
@@ -242,6 +248,7 @@
 
 			<div class=" pb-[1rem]">
 				<MessageInput
+					id="root"
 					{typingUsers}
 					{onChange}
 					onSubmit={submitHandler}

+ 24 - 67
src/lib/components/channel/MessageInput.svelte

@@ -23,6 +23,8 @@
 	export let placeholder = $i18n.t('Send a Message');
 	export let transparentBackground = false;
 
+	export let id = null;
+
 	let draggedOver = false;
 
 	let recording = false;
@@ -257,7 +259,7 @@
 
 		await tick();
 
-		const chatInputElement = document.getElementById('chat-input');
+		const chatInputElement = document.getElementById(`chat-input-${id}`);
 		chatInputElement?.focus();
 	};
 
@@ -267,7 +269,7 @@
 
 	onMount(async () => {
 		window.setTimeout(() => {
-			const chatInput = document.getElementById('chat-input');
+			const chatInput = document.getElementById(`chat-input-${id}`);
 			chatInput?.focus();
 		}, 0);
 
@@ -373,7 +375,7 @@
 						recording = false;
 
 						await tick();
-						document.getElementById('chat-input')?.focus();
+						document.getElementById(`chat-input-${id}`)?.focus();
 					}}
 					on:confirm={async (e) => {
 						const { text, filename } = e.detail;
@@ -381,7 +383,7 @@
 						recording = false;
 
 						await tick();
-						document.getElementById('chat-input')?.focus();
+						document.getElementById(`chat-input-${id}`)?.focus();
 					}}
 				/>
 			{:else}
@@ -478,61 +480,21 @@
 								</InputMenu>
 							</div>
 
-							{#if $settings?.richTextInput ?? true}
-								<div
-									class="scrollbar-hidden text-left bg-transparent dark:text-gray-100 outline-none w-full py-2.5 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
-								>
-									<RichTextInput
-										bind:value={content}
-										id="chat-input"
-										messageInput={true}
-										shiftEnter={!$mobile ||
-											!(
-												'ontouchstart' in window ||
-												navigator.maxTouchPoints > 0 ||
-												navigator.msMaxTouchPoints > 0
-											)}
-										{placeholder}
-										largeTextAsFile={$settings?.largeTextAsFile ?? false}
-										on:keydown={async (e) => {
-											e = e.detail.event;
-											const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
-											if (
-												!$mobile ||
-												!(
-													'ontouchstart' in window ||
-													navigator.maxTouchPoints > 0 ||
-													navigator.msMaxTouchPoints > 0
-												)
-											) {
-												// Prevent Enter key from creating a new line
-												// Uses keyCode '13' for Enter key for chinese/japanese keyboards
-												if (e.keyCode === 13 && !e.shiftKey) {
-													e.preventDefault();
-												}
-
-												// Submit the content when Enter key is pressed
-												if (content !== '' && e.keyCode === 13 && !e.shiftKey) {
-													submitHandler();
-												}
-											}
-
-											if (e.key === 'Escape') {
-												console.log('Escape');
-											}
-										}}
-										on:paste={async (e) => {
-											e = e.detail.event;
-											console.log(e);
-										}}
-									/>
-								</div>
-							{:else}
-								<textarea
-									id="chat-input"
-									class="scrollbar-hidden bg-transparent dark:text-gray-100 outline-none w-full py-3 px-1 rounded-xl resize-none h-[48px]"
-									{placeholder}
+							<div
+								class="scrollbar-hidden text-left bg-transparent dark:text-gray-100 outline-none w-full py-2.5 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
+							>
+								<RichTextInput
 									bind:value={content}
+									id={`chat-input-${id}`}
+									messageInput={true}
+									shiftEnter={!$mobile ||
+										!(
+											'ontouchstart' in window ||
+											navigator.maxTouchPoints > 0 ||
+											navigator.msMaxTouchPoints > 0
+										)}
+									{placeholder}
+									largeTextAsFile={$settings?.largeTextAsFile ?? false}
 									on:keydown={async (e) => {
 										e = e.detail.event;
 										const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
@@ -560,17 +522,12 @@
 											console.log('Escape');
 										}
 									}}
-									rows="1"
-									on:input={async (e) => {
-										e.target.style.height = '';
-										e.target.style.height = Math.min(e.target.scrollHeight, 320) + 'px';
-									}}
-									on:focus={async (e) => {
-										e.target.style.height = '';
-										e.target.style.height = Math.min(e.target.scrollHeight, 320) + 'px';
+									on:paste={async (e) => {
+										e = e.detail.event;
+										console.log(e);
 									}}
 								/>
-							{/if}
+							</div>
 
 							<div class="self-end mb-1.5 flex space-x-1 mr-1">
 								{#if content === ''}

+ 23 - 21
src/lib/components/channel/Messages/Message.svelte

@@ -77,7 +77,7 @@
 			? 'max-w-full'
 			: 'max-w-5xl'} mx-auto group hover:bg-gray-300/5 dark:hover:bg-gray-700/5 transition relative"
 	>
-		{#if (message.user_id === $user.id || $user.role === 'admin') && !edit}
+		{#if !edit}
 			<div
 				class=" absolute {showButtons ? '' : 'invisible group-hover:visible'} right-1 -top-2 z-30"
 			>
@@ -116,26 +116,28 @@
 						</Tooltip>
 					{/if}
 
-					<Tooltip content={$i18n.t('Edit')}>
-						<button
-							class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
-							on:click={() => {
-								edit = true;
-								editedContent = message.content;
-							}}
-						>
-							<Pencil />
-						</button>
-					</Tooltip>
+					{#if message.user_id === $user.id || $user.role === 'admin'}
+						<Tooltip content={$i18n.t('Edit')}>
+							<button
+								class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
+								on:click={() => {
+									edit = true;
+									editedContent = message.content;
+								}}
+							>
+								<Pencil />
+							</button>
+						</Tooltip>
 
-					<Tooltip content={$i18n.t('Delete')}>
-						<button
-							class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
-							on:click={() => (showDeleteConfirmDialog = true)}
-						>
-							<GarbageBin />
-						</button>
-					</Tooltip>
+						<Tooltip content={$i18n.t('Delete')}>
+							<button
+								class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
+								on:click={() => (showDeleteConfirmDialog = true)}
+							>
+								<GarbageBin />
+							</button>
+						</Tooltip>
+					{/if}
 				</div>
 			</div>
 		{/if}
@@ -326,7 +328,7 @@
 						</div>
 					{/if}
 
-					{#if message.reply_count > 0}
+					{#if !thread && message.reply_count > 0}
 						<div class="flex items-center gap-1.5 -mt-0.5 mb-1.5">
 							<button
 								class="flex items-center text-xs py-1 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition"

+ 65 - 3
src/lib/components/channel/Thread.svelte

@@ -1,14 +1,14 @@
 <script lang="ts">
 	import { goto } from '$app/navigation';
 
-	import { socket } from '$lib/stores';
+	import { socket, user } from '$lib/stores';
 
 	import { getChannelThreadMessages, sendMessage } from '$lib/apis/channels';
 
 	import XMark from '$lib/components/icons/XMark.svelte';
 	import MessageInput from './MessageInput.svelte';
 	import Messages from './Messages.svelte';
-	import { onMount } from 'svelte';
+	import { onMount, tick } from 'svelte';
 	import { toast } from 'svelte-sonner';
 
 	export let threadId = null;
@@ -44,6 +44,64 @@
 		}
 	};
 
+	const channelEventHandler = async (event) => {
+		console.log(event);
+
+		if (event.channel_id === channel.id) {
+			const type = event?.data?.type ?? null;
+			const data = event?.data?.data ?? null;
+
+			if (type === 'message') {
+				if ((data?.parent_id ?? null) === threadId) {
+					messages = [data, ...messages];
+
+					if (typingUsers.find((user) => user.id === event.user.id)) {
+						typingUsers = typingUsers.filter((user) => user.id !== event.user.id);
+					}
+				}
+			} else if (type === 'message:update') {
+				const idx = messages.findIndex((message) => message.id === data.id);
+
+				if (idx !== -1) {
+					messages[idx] = data;
+				}
+			} else if (type === 'message:delete') {
+				messages = messages.filter((message) => message.id !== data.id);
+			} else if (type.includes('message:reaction')) {
+				const idx = messages.findIndex((message) => message.id === data.id);
+				if (idx !== -1) {
+					messages[idx] = data;
+				}
+			} else if (type === 'typing' && event.message_id === threadId) {
+				if (event.user.id === $user.id) {
+					return;
+				}
+
+				typingUsers = data.typing
+					? [
+							...typingUsers,
+							...(typingUsers.find((user) => user.id === event.user.id)
+								? []
+								: [
+										{
+											id: event.user.id,
+											name: event.user.name
+										}
+									])
+						]
+					: typingUsers.filter((user) => user.id !== event.user.id);
+
+				if (typingUsersTimeout[event.user.id]) {
+					clearTimeout(typingUsersTimeout[event.user.id]);
+				}
+
+				typingUsersTimeout[event.user.id] = setTimeout(() => {
+					typingUsers = typingUsers.filter((user) => user.id !== event.user.id);
+				}, 5000);
+			}
+		}
+	};
+
 	const submitHandler = async ({ content, data }) => {
 		if (!content) {
 			return;
@@ -71,6 +129,10 @@
 			}
 		});
 	};
+
+	onMount(() => {
+		$socket?.on('channel-events', channelEventHandler);
+	});
 </script>
 
 {#if channel}
@@ -113,6 +175,6 @@
 			}}
 		/>
 
-		<MessageInput {typingUsers} {onChange} onSubmit={submitHandler} />
+		<MessageInput id={threadId} {typingUsers} {onChange} onSubmit={submitHandler} />
 	</div>
 {/if}