feat: 1-to-1 message render + web data-lake backend
This commit is contained in:
1 parent
f0afb7ec5b
commit
75425d1bee
110 files changed
+10199
-54
No files matched your search
@@ -0,0 +1,212 @@
|
||||
<script lang="ts">
|
||||
import { Dialog } from "bits-ui";
|
||||
import { getMediaVersions, listMessageVersions } from "$lib/api/endpoints";
|
||||
import type { MediaVersion, MessageVersion } from "$lib/api/types";
|
||||
import MediaVersionThumb from "$lib/components/MediaVersionThumb.svelte";
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
import Spinner from "$lib/components/ui/Spinner.svelte";
|
||||
import { formatFull } from "$lib/format/datetime";
|
||||
import { toasts } from "$lib/stores/toasts.svelte";
|
||||
|
||||
interface Props {
|
||||
chatId: number;
|
||||
messageId: number | null;
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
let { open = $bindable(), chatId, messageId }: Props = $props();
|
||||
|
||||
let versions = $state<MessageVersion[]>([]);
|
||||
let mediaVersions = $state<MediaVersion[]>([]);
|
||||
let loading = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (open && messageId !== null) {
|
||||
loading = true;
|
||||
mediaVersions = [];
|
||||
const id = messageId;
|
||||
Promise.all([
|
||||
listMessageVersions(chatId, id),
|
||||
getMediaVersions(chatId, id),
|
||||
])
|
||||
.then(([text, media]) => {
|
||||
versions = text;
|
||||
mediaVersions = media;
|
||||
})
|
||||
.catch(() => toasts.error("Failed to load edit history"))
|
||||
.finally(() => {
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class="dialog-overlay" />
|
||||
<Dialog.Content class="dialog-content">
|
||||
<header class="dialog-head">
|
||||
<Dialog.Title class="dialog-title">Edit history</Dialog.Title>
|
||||
<Dialog.Close class="dialog-close" aria-label="Close">
|
||||
<Icon name="close" size="1.25rem" />
|
||||
</Dialog.Close>
|
||||
</header>
|
||||
<div class="versions">
|
||||
{#if loading}
|
||||
<div class="centered"><Spinner /></div>
|
||||
{:else if versions.length === 0 && mediaVersions.length === 0}
|
||||
<p class="empty">No versions recorded.</p>
|
||||
{:else}
|
||||
{#if mediaVersions.length > 0}
|
||||
<div class="media-versions">
|
||||
<span class="media-label">Media versions</span>
|
||||
<div class="media-strip">
|
||||
{#each mediaVersions as media (media.id)}
|
||||
<MediaVersionThumb version={media} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#each versions as version, index (version.observed_at)}
|
||||
<div class="version">
|
||||
<div class="version-meta">
|
||||
<span class="version-label">Version {index + 1}</span>
|
||||
<span class="version-date"
|
||||
>{formatFull(version.observed_at)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="version-text">{version.text ?? "(no text)"}</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
|
||||
<style lang="scss">
|
||||
:global(.dialog-overlay) {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: var(--z-modal);
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
:global(.dialog-content) {
|
||||
position: fixed;
|
||||
z-index: var(--z-modal);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: min(32rem, 92vw);
|
||||
max-height: 80vh;
|
||||
border-radius: var(--border-radius-default);
|
||||
|
||||
background-color: var(--color-background);
|
||||
box-shadow: 0 0.5rem 2rem var(--color-default-shadow);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dialog-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid var(--color-borders);
|
||||
}
|
||||
|
||||
:global(.dialog-title) {
|
||||
margin: 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
:global(.dialog-close) {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
padding: 0.375rem;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
color: var(--color-text-secondary);
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-chat-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.versions {
|
||||
overflow-y: auto;
|
||||
padding: 0.75rem 1.25rem 1.25rem;
|
||||
}
|
||||
|
||||
.media-versions {
|
||||
padding-bottom: 0.75rem;
|
||||
margin-bottom: 0.25rem;
|
||||
border-bottom: 1px solid var(--color-borders);
|
||||
}
|
||||
|
||||
.media-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
font-size: 0.8125rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.media-strip {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.version {
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid var(--color-borders);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.version-meta {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.version-label {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.version-date {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.version-text {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.centered {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 1rem 0;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user