feat: channel threads

This commit is contained in:
Timothy Jaeryang Baek 2024-12-31 02:16:07 -08:00
parent 16a48ef4eb
commit a754a4388a
4 changed files with 120 additions and 92 deletions

View File

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

View File

@ -23,6 +23,8 @@
export let placeholder = $i18n.t('Send a Message'); export let placeholder = $i18n.t('Send a Message');
export let transparentBackground = false; export let transparentBackground = false;
export let id = null;
let draggedOver = false; let draggedOver = false;
let recording = false; let recording = false;
@ -257,7 +259,7 @@
await tick(); await tick();
const chatInputElement = document.getElementById('chat-input'); const chatInputElement = document.getElementById(`chat-input-${id}`);
chatInputElement?.focus(); chatInputElement?.focus();
}; };
@ -267,7 +269,7 @@
onMount(async () => { onMount(async () => {
window.setTimeout(() => { window.setTimeout(() => {
const chatInput = document.getElementById('chat-input'); const chatInput = document.getElementById(`chat-input-${id}`);
chatInput?.focus(); chatInput?.focus();
}, 0); }, 0);
@ -373,7 +375,7 @@
recording = false; recording = false;
await tick(); await tick();
document.getElementById('chat-input')?.focus(); document.getElementById(`chat-input-${id}`)?.focus();
}} }}
on:confirm={async (e) => { on:confirm={async (e) => {
const { text, filename } = e.detail; const { text, filename } = e.detail;
@ -381,7 +383,7 @@
recording = false; recording = false;
await tick(); await tick();
document.getElementById('chat-input')?.focus(); document.getElementById(`chat-input-${id}`)?.focus();
}} }}
/> />
{:else} {:else}
@ -478,61 +480,21 @@
</InputMenu> </InputMenu>
</div> </div>
{#if $settings?.richTextInput ?? true} <div
<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"
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
<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}
bind:value={content} 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) => { on:keydown={async (e) => {
e = e.detail.event; e = e.detail.event;
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
@ -560,17 +522,12 @@
console.log('Escape'); console.log('Escape');
} }
}} }}
rows="1" on:paste={async (e) => {
on:input={async (e) => { e = e.detail.event;
e.target.style.height = ''; console.log(e);
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';
}} }}
/> />
{/if} </div>
<div class="self-end mb-1.5 flex space-x-1 mr-1"> <div class="self-end mb-1.5 flex space-x-1 mr-1">
{#if content === ''} {#if content === ''}

View File

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

View File

@ -1,14 +1,14 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { socket } from '$lib/stores'; import { socket, user } from '$lib/stores';
import { getChannelThreadMessages, sendMessage } from '$lib/apis/channels'; import { getChannelThreadMessages, sendMessage } from '$lib/apis/channels';
import XMark from '$lib/components/icons/XMark.svelte'; import XMark from '$lib/components/icons/XMark.svelte';
import MessageInput from './MessageInput.svelte'; import MessageInput from './MessageInput.svelte';
import Messages from './Messages.svelte'; import Messages from './Messages.svelte';
import { onMount } from 'svelte'; import { onMount, tick } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
export let threadId = null; 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 }) => { const submitHandler = async ({ content, data }) => {
if (!content) { if (!content) {
return; return;
@ -71,6 +129,10 @@
} }
}); });
}; };
onMount(() => {
$socket?.on('channel-events', channelEventHandler);
});
</script> </script>
{#if channel} {#if channel}
@ -113,6 +175,6 @@
}} }}
/> />
<MessageInput {typingUsers} {onChange} onSubmit={submitHandler} /> <MessageInput id={threadId} {typingUsers} {onChange} onSubmit={submitHandler} />
</div> </div>
{/if} {/if}