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
This commit is contained in:
113
src/lib/components/matrix/SyncRecoveryBanner.svelte
Normal file
113
src/lib/components/matrix/SyncRecoveryBanner.svelte
Normal file
@@ -0,0 +1,113 @@
|
||||
<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}
|
||||
Reference in New Issue
Block a user