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') {
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}

View File

@ -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 === ''}

View File

@ -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"

View File

@ -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}