Timothy Jaeryang Baek 4 months ago
parent
commit
2840ff405b

+ 112 - 31
src/lib/components/channel/Channel.svelte

@@ -1,5 +1,7 @@
 <script lang="ts">
 	import { toast } from 'svelte-sonner';
+	import { Pane, PaneGroup, PaneResizer } from 'paneforge';
+
 	import { onDestroy, onMount, tick } from 'svelte';
 	import { goto } from '$app/navigation';
 
@@ -9,6 +11,9 @@
 	import Messages from './Messages.svelte';
 	import MessageInput from './MessageInput.svelte';
 	import Navbar from './Navbar.svelte';
+	import Drawer from '../common/Drawer.svelte';
+	import EllipsisVertical from '../icons/EllipsisVertical.svelte';
+	import Thread from './Messages/Thread.svelte';
 
 	export let id = '';
 
@@ -20,6 +25,8 @@
 	let channel = null;
 	let messages = null;
 
+	let threadId = null;
+
 	let typingUsers = [];
 	let typingUsersTimeout = {};
 
@@ -150,12 +157,28 @@
 		});
 	};
 
+	let mediaQuery;
+	let largeScreen = false;
+
 	onMount(() => {
 		if ($chatId) {
 			chatId.set('');
 		}
 
 		$socket?.on('channel-events', channelEventHandler);
+
+		mediaQuery = window.matchMedia('(min-width: 1024px)');
+
+		const handleMediaQuery = async (e) => {
+			if (e.matches) {
+				largeScreen = true;
+			} else {
+				largeScreen = false;
+			}
+		};
+
+		mediaQuery.addEventListener('change', handleMediaQuery);
+		handleMediaQuery(mediaQuery);
 	});
 
 	onDestroy(() => {
@@ -173,40 +196,98 @@
 		: ''} w-full max-w-full flex flex-col"
 	id="channel-container"
 >
-	<Navbar {channel} />
-
-	<div class="flex-1 overflow-y-auto">
-		{#if channel}
-			<div
-				class=" pb-2.5 max-w-full z-10 scrollbar-hidden w-full h-full pt-6 flex-1 flex flex-col-reverse overflow-auto"
-				id="messages-container"
-				bind:this={messagesContainerElement}
-				on:scroll={(e) => {
-					scrollEnd = Math.abs(messagesContainerElement.scrollTop) <= 50;
-				}}
+	<PaneGroup direction="horizontal" class="w-full h-full">
+		<Pane defaultSize={50} minSize={50} class="h-full flex flex-col w-full relative">
+			<Navbar {channel} />
+
+			<div class="flex-1 overflow-y-auto">
+				{#if channel}
+					<div
+						class=" pb-2.5 max-w-full z-10 scrollbar-hidden w-full h-full pt-6 flex-1 flex flex-col-reverse overflow-auto"
+						id="messages-container"
+						bind:this={messagesContainerElement}
+						on:scroll={(e) => {
+							scrollEnd = Math.abs(messagesContainerElement.scrollTop) <= 50;
+						}}
+					>
+						{#key id}
+							<Messages
+								{channel}
+								{messages}
+								{top}
+								onThread={(id) => {
+									threadId = id;
+								}}
+								onLoad={async () => {
+									const newMessages = await getChannelMessages(
+										localStorage.token,
+										id,
+										messages.length
+									);
+
+									messages = [...messages, ...newMessages];
+
+									if (newMessages.length < 50) {
+										top = true;
+										return;
+									}
+								}}
+							/>
+						{/key}
+					</div>
+				{/if}
+			</div>
+
+			<div class=" pb-[1rem]">
+				<MessageInput
+					{typingUsers}
+					{onChange}
+					onSubmit={submitHandler}
+					{scrollToBottom}
+					{scrollEnd}
+				/>
+			</div>
+		</Pane>
+
+		{#if !largeScreen}
+			{#if threadId !== null}
+				<Drawer
+					show={threadId !== null}
+					on:close={() => {
+						threadId = null;
+					}}
+				>
+					<div class=" {threadId !== null ? ' h-screen  w-screen' : 'px-6 py-4'} h-full">
+						<Thread
+							{threadId}
+							{channel}
+							onClose={() => {
+								threadId = null;
+							}}
+						/>
+					</div>
+				</Drawer>
+			{/if}
+		{:else if threadId !== null}
+			<PaneResizer
+				class="relative flex w-[3px] items-center justify-center bg-background group bg-gray-50 dark:bg-gray-850"
 			>
-				{#key id}
-					<Messages
+				<div class="z-10 flex h-7 w-5 items-center justify-center rounded-sm">
+					<EllipsisVertical className="size-4 invisible group-hover:visible" />
+				</div>
+			</PaneResizer>
+
+			<Pane defaultSize={50} minSize={20} class="h-full w-full">
+				<div class="h-full w-full shadow-xl">
+					<Thread
+						{threadId}
 						{channel}
-						{messages}
-						{top}
-						onLoad={async () => {
-							const newMessages = await getChannelMessages(localStorage.token, id, messages.length);
-
-							messages = [...messages, ...newMessages];
-
-							if (newMessages.length < 50) {
-								top = true;
-								return;
-							}
+						onClose={() => {
+							threadId = null;
 						}}
 					/>
-				{/key}
-			</div>
+				</div>
+			</Pane>
 		{/if}
-	</div>
-
-	<div class=" pb-[1rem]">
-		<MessageInput {typingUsers} {onChange} onSubmit={submitHandler} {scrollToBottom} {scrollEnd} />
-	</div>
+	</PaneGroup>
 </div>

+ 14 - 1
src/lib/components/channel/Messages.svelte

@@ -25,6 +25,7 @@
 	export let top = false;
 
 	export let onLoad: Function = () => {};
+	export let onThread: Function = () => {};
 
 	let messagesLoading = false;
 
@@ -118,6 +119,9 @@
 						return null;
 					});
 				}}
+				onThread={(id) => {
+					onThread(id);
+				}}
 				onReaction={(name) => {
 					if (
 						(message?.reactions ?? [])
@@ -127,7 +131,16 @@
 					) {
 						messages = messages.map((m) => {
 							if (m.id === message.id) {
-								m.reactions = m.reactions.filter((reaction) => reaction.name !== name);
+								const reaction = m.reactions.find((reaction) => reaction.name === name);
+
+								if (reaction) {
+									reaction.user_ids = reaction.user_ids.filter((id) => id !== $user.id);
+									reaction.count = reaction.user_ids.length;
+
+									if (reaction.count === 0) {
+										m.reactions = m.reactions.filter((r) => r.name !== name);
+									}
+								}
 							}
 							return m;
 						});

+ 14 - 11
src/lib/components/channel/Messages/Message.svelte

@@ -35,6 +35,7 @@
 
 	export let onDelete: Function = () => {};
 	export let onEdit: Function = () => {};
+	export let onThread: Function = () => {};
 	export let onReaction: Function = () => {};
 
 	let showButtons = false;
@@ -100,17 +101,18 @@
 						</Tooltip>
 					</ReactionPicker>
 
-					<Tooltip content={$i18n.t('Reply in Thread')}>
-						<button
-							class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
-							on:click={() => {
-								edit = true;
-								editedContent = message.content;
-							}}
-						>
-							<ChatBubbleOvalEllipsis />
-						</button>
-					</Tooltip>
+					{#if message?.parent_id === null}
+						<Tooltip content={$i18n.t('Reply in Thread')}>
+							<button
+								class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
+								on:click={() => {
+									onThread(message.id);
+								}}
+							>
+								<ChatBubbleOvalEllipsis />
+							</button>
+						</Tooltip>
+					{/if}
 
 					<Tooltip content={$i18n.t('Edit')}>
 						<button
@@ -288,6 +290,7 @@
 													].toLowerCase()}.svg"
 													alt={reaction.name}
 													class=" size-4"
+													loading="lazy"
 												/>
 											{:else}
 												<div>

+ 28 - 0
src/lib/components/channel/Messages/Thread.svelte

@@ -0,0 +1,28 @@
+<script lang="ts">
+	import XMark from '$lib/components/icons/XMark.svelte';
+
+	export let threadId = null;
+	export let channel = null;
+
+	export let onClose = () => {};
+</script>
+
+<div class="flex flex-col w-full h-full bg-gray-50 dark:bg-gray-900 px-3.5 py-3">
+	<div class="flex items-center justify-between">
+		<div class=" font-medium text-lg">Thread</div>
+
+		<div>
+			<button
+				class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300 p-2"
+				on:click={() => {
+					onClose();
+				}}
+			>
+				<XMark />
+			</button>
+		</div>
+	</div>
+	{threadId}
+
+	{channel}
+</div>