Files
root-org/src/lib/components/message/MessageContainer.svelte

195 lines
5.2 KiB
Svelte

<script lang="ts">
import { MatrixAvatar } from "$lib/components/ui";
import { getReadReceiptsForEvent } from "$lib/matrix";
import type { Message } from "$lib/matrix/types";
import { formatTime } from "./utils";
import {
MessageContent,
MessageMedia,
MessageReactions,
MessageActions,
MessageReadReceipts,
} from "./parts";
interface ReplyPreview {
senderName: string;
content: string;
senderAvatar: string | null;
hasAttachment: boolean;
}
interface Props {
message: Message;
isGrouped?: boolean;
isOwnMessage?: boolean;
isPinned?: boolean;
currentUserId?: string;
replyPreview?: ReplyPreview | null;
onReact?: (emoji: string) => void;
onToggleReaction?: (emoji: string, reactionEventId: string | null) => void;
onEdit?: () => void;
onDelete?: () => void;
onReply?: () => void;
onPin?: () => void;
onScrollToMessage?: (eventId: string) => void;
}
let {
message,
isGrouped = false,
isOwnMessage = false,
isPinned = false,
currentUserId = "",
replyPreview = null,
onReact,
onToggleReaction,
onEdit,
onDelete,
onReply,
onPin,
onScrollToMessage,
}: Props = $props();
let showActions = $state(false);
// Get read receipts for own messages
const readReceipts = $derived(
isOwnMessage
? getReadReceiptsForEvent(message.roomId, message.eventId)
: [],
);
// Check if message has media
const hasMedia = $derived(
["image", "video", "audio", "file"].includes(message.type) && message.media,
);
</script>
<div
class="group relative px-5 py-0.5 hover:bg-light/[0.02] transition-colors {message.isPending
? 'opacity-40'
: ''}"
onmouseenter={() => (showActions = true)}
onmouseleave={() => (showActions = false)}
role="article"
id="message-{message.eventId}"
>
<!-- Reply preview -->
{#if replyPreview && message.replyTo}
<button
class="flex items-center gap-1.5 ml-12 mt-1 mb-0.5 text-[11px] hover:opacity-80 transition-opacity cursor-pointer"
onclick={() => onScrollToMessage?.(message.replyTo!)}
>
<div class="w-0.5 h-3 bg-primary/40 rounded-full shrink-0"></div>
<MatrixAvatar
mxcUrl={replyPreview.senderAvatar}
name={replyPreview.senderName}
size="xs"
/>
<span class="text-light/50 font-body">{replyPreview.senderName}</span>
<span class="text-light/30 truncate max-w-xs">
{#if replyPreview.hasAttachment}
<span
class="material-symbols-rounded align-middle mr-0.5"
style="font-size: 12px;">image</span
>
{/if}
{replyPreview.content}
</span>
</button>
{/if}
{#if isGrouped}
<!-- Grouped message -->
<div class="flex gap-3">
<div class="w-9 shrink-0 flex items-center justify-center">
<span
class="text-[10px] text-light/20 opacity-0 group-hover:opacity-100 transition-opacity select-none"
>
{formatTime(message.timestamp)}
</span>
</div>
<div class="flex-1 min-w-0">
{#if hasMedia && message.media}
<MessageMedia
type={message.type as "image" | "video" | "audio" | "file"}
media={message.media}
altText={message.content}
/>
{:else}
<MessageContent
content={message.content}
isEdited={message.isEdited}
isRedacted={message.isRedacted}
/>
{/if}
</div>
</div>
{:else}
<!-- Full message with avatar -->
<div class="flex gap-3 mt-3 first:mt-0">
<div class="w-9 shrink-0 pt-0.5">
<MatrixAvatar
mxcUrl={message.senderAvatar}
name={message.senderName}
size="sm"
/>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-baseline gap-2 mb-px">
<span
class="text-[13px] font-heading text-white hover:underline cursor-pointer"
>
{message.senderName}
</span>
<span class="text-[10px] text-light/25 select-none">
{formatTime(message.timestamp)}
</span>
</div>
{#if hasMedia && message.media}
<MessageMedia
type={message.type as "image" | "video" | "audio" | "file"}
media={message.media}
altText={message.content}
/>
{:else}
<MessageContent
content={message.content}
isEdited={message.isEdited}
isRedacted={message.isRedacted}
/>
{/if}
</div>
</div>
{/if}
<!-- Reactions -->
<MessageReactions
reactions={message.reactions}
{currentUserId}
isRedacted={message.isRedacted}
{onReact}
{onToggleReaction}
/>
<!-- Read receipts (own messages only) -->
{#if isOwnMessage}
<MessageReadReceipts receipts={readReceipts} />
{/if}
<!-- Action buttons (show on hover) -->
{#if showActions && !message.isRedacted}
<MessageActions
{isOwnMessage}
{isPinned}
messageContent={message.content}
messageEventId={message.eventId}
{onReact}
{onReply}
{onEdit}
{onDelete}
{onPin}
/>
{/if}
</div>