Files
zep/src/chat/components/ThreadView.svelte
T
2026-04-04 17:41:43 -04:00

139 lines
4.6 KiB
Svelte

<script lang="ts">
import { Identity } from "spacetimedb";
import type * as Types from "../../module_bindings/types";
import ThreadMessageList from "./ThreadMessageList.svelte";
import ThreadMessageInput from "./ThreadMessageInput.svelte";
import RichText from "./RichText.svelte";
import { getContext } from "svelte";
import type { ChatService } from "../services/chat.svelte";
let {
activeThreadId,
setActiveThreadId,
pendingThreadParentMessageId,
setPendingThreadParentMessageId,
activeChannelId,
activeServer: _activeServer,
isFullyAuthenticated,
users,
identity,
allThreads,
allMessages,
allImages
}: {
activeThreadId: bigint | null,
setActiveThreadId: (id: bigint | null) => void,
pendingThreadParentMessageId: bigint | null,
setPendingThreadParentMessageId: (id: bigint | null) => void,
activeChannelId: bigint | null,
activeServer: Types.Server | undefined,
isFullyAuthenticated: boolean,
users: readonly Types.User[],
identity: Identity | null,
allThreads: readonly Types.Thread[],
allMessages: readonly Types.Message[],
allImages: readonly Types.Image[]
} = $props();
const chat = getContext<ChatService>("chat");
const activeThread = $derived(allThreads.find((t) => t.id === activeThreadId));
const pendingParentMessage = $derived(allMessages.find(m => m.id === pendingThreadParentMessageId));
const threadName = $derived(activeThread ? activeThread.name : (pendingParentMessage ? `New thread: ${pendingParentMessage.text.substring(0, 20)}...` : "Thread"));
function handleClose() {
setActiveThreadId(null);
setPendingThreadParentMessageId(null);
}
const threadMessages = $derived.by(() => {
if (activeThreadId) {
return allMessages
.filter((m) => m.threadId === activeThreadId)
.sort((a, b) =>
a.sent.microsSinceUnixEpoch < b.sent.microsSinceUnixEpoch ? -1 : 1
);
}
return [];
});
</script>
{#if (activeThreadId && activeThread) || pendingThreadParentMessageId}
<div class="thread-view">
<div
class="thread-header"
style="border-bottom: 1px solid var(--background-accent); padding: 8px; display: flex; justify-content: space-between; align-items: center;"
>
<div style="display: flex; align-items: center; gap: 8px">
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<span
style="color: var(--brand); cursor: pointer; font-size: 1rem;"
onclick={handleClose}
>
<i class="fas fa-arrow-left"></i>
</span>
<span style="font-weight: bold; font-size: 0.9rem">
{threadName}
</span>
</div>
<button
class="close-btn"
onclick={handleClose}
aria-label="Close thread view"
>
<i class="fas fa-times"></i>
</button>
</div>
{#if pendingParentMessage && !activeThreadId}
<div class="thread-messages-list">
<div class="message-item" style="border-bottom: 1px solid var(--background-accent); opacity: 0.8">
<div class="message-content">
<div style="font-size: 0.75rem; color: var(--text-muted); margin-bottom: 4px">Starting thread from:</div>
<div class="message-text">
<RichText text={pendingParentMessage.text} messageId={pendingParentMessage.id} />
</div>
</div>
</div>
</div>
{/if}
<ThreadMessageList
{threadMessages}
{users}
{identity}
images={allImages}
/>
<div class="typing-indicator" style="margin-left: 12px; margin-top: 4px;">
{#if chat.typingUsers.length > 0}
<div class="dots">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
<span style="font-weight: bold;">
{#if chat.typingUsers.length === 1}
{chat.typingUsers[0].name || "Someone"}
{:else if chat.typingUsers.length === 2}
{chat.typingUsers[0].name || "Someone"} and {chat.typingUsers[1].name || "Someone"}
{:else if chat.typingUsers.length === 3}
{chat.typingUsers[0].name || "Someone"}, {chat.typingUsers[1].name || "Someone"} and {chat.typingUsers[2].name || "Someone"}
{:else}
Several people
{/if}
</span>
<span>{chat.typingUsers.length === 1 ? "is" : "are"} typing...</span>
{/if}
</div>
<ThreadMessageInput
{activeChannelId}
{activeThreadId}
{isFullyAuthenticated}
/>
</div>
{/if}