Files
root-org/src/lib/components/matrix/SyncRecoveryBanner.svelte
AlacrisDevs d1ce5d0951 feat: integrate Matrix chat (Option 2 - credentials stored in Supabase)
- Add matrix-js-sdk, marked, highlight.js, twemoji, @tanstack/svelte-virtual deps
- Copy Matrix core layer: /matrix/, /stores/matrix.ts, /cache/, /services/
- Copy Matrix components: matrix/, message/, chat-layout/, chat-settings/
- Copy UI components: EmojiPicker, Twemoji, ImagePreviewModal, VirtualList
- Copy utils: emojiData, twemoji, twemojiGlobal
- Replace lucide-svelte with Material Symbols in SyncRecoveryBanner
- Extend Avatar with xs size and status indicator prop
- Fix ui.ts store conflict: re-export toasts from toast.svelte.ts
- Add migration 020_matrix_credentials for storing Matrix tokens per user/org
- Add /api/matrix-credentials endpoint (GET/POST/DELETE)
- Create [orgSlug]/chat page with Matrix login form + full chat UI
- Add Chat to sidebar navigation
2026-02-07 01:44:06 +02:00

114 lines
3.1 KiB
Svelte

<script lang="ts">
import { syncState, syncError, clearState } from "$lib/stores/matrix";
import { clearAllCache } from "$lib/cache";
interface Props {
onHardRefresh?: () => void;
}
let { onHardRefresh }: Props = $props();
let isRefreshing = $state(false);
let dismissed = $state(false);
let consecutiveErrors = $state(0);
// Track consecutive sync errors
$effect(() => {
if ($syncState === "ERROR") {
consecutiveErrors++;
} else if ($syncState === "SYNCING" || $syncState === "PREPARED") {
consecutiveErrors = 0;
dismissed = false;
}
});
// Show banner after 3+ consecutive errors
const shouldShow = $derived(
!dismissed && consecutiveErrors >= 3 && $syncState === "ERROR",
);
async function handleHardRefresh() {
isRefreshing = true;
try {
// Clear local cache
await clearAllCache();
// Clear in-memory state
clearState();
// Trigger callback for full re-sync
onHardRefresh?.();
// Reload the page for clean state
window.location.reload();
} catch (error) {
console.error("[SyncRecovery] Hard refresh failed:", error);
isRefreshing = false;
}
}
function handleDismiss() {
dismissed = true;
}
</script>
{#if shouldShow}
<div
class="fixed top-4 left-1/2 -translate-x-1/2 z-50 max-w-md w-full mx-4
bg-red-900/90 backdrop-blur-sm border border-red-500/50
rounded-lg shadow-xl p-4 animate-in slide-in-from-top duration-300"
role="alert"
>
<div class="flex items-start gap-3">
<span
class="material-symbols-rounded text-red-400 flex-shrink-0 mt-0.5"
style="font-size: 20px;">warning</span
>
<div class="flex-1 min-w-0">
<h3 class="font-semibold text-red-100">Sync Connection Lost</h3>
<p class="text-sm text-red-200/80 mt-1">
{$syncError ||
"Unable to sync with the server. Your messages may be outdated."}
</p>
<div class="flex items-center gap-2 mt-3">
<button
class="flex items-center gap-2 px-3 py-1.5 bg-red-600 hover:bg-red-500
text-white text-sm font-medium rounded-md transition-colors
disabled:opacity-50 disabled:cursor-not-allowed"
onclick={handleHardRefresh}
disabled={isRefreshing}
>
<span
class="material-symbols-rounded {isRefreshing
? 'animate-spin'
: ''}"
style="font-size: 16px;">refresh</span
>
{isRefreshing ? "Refreshing..." : "Hard Refresh"}
</button>
<button
class="px-3 py-1.5 text-red-200 hover:text-white text-sm transition-colors"
onclick={handleDismiss}
>
Dismiss
</button>
</div>
</div>
<button
class="text-red-400 hover:text-red-200 transition-colors"
onclick={handleDismiss}
aria-label="Close"
>
<span class="material-symbols-rounded" style="font-size: 20px;"
>close</span
>
</button>
</div>
</div>
{/if}