- 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
114 lines
3.1 KiB
Svelte
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}
|