* feat: add conversations, desktop (Tauri), and offline sync Major new features: - conversations module: Slack-like channels, threads, reactions, pins - Tauri desktop app with local SQLite for offline-first operation - Hybrid logical clock sync engine with conflict resolution - DB provider abstraction (D1/Tauri/memory) with React context Conversations: - Text/voice/announcement channels with categories - Message threads, reactions, attachments, pinning - Real-time presence and typing indicators - Full-text search across messages Desktop (Tauri): - Local SQLite database with sync to cloud D1 - Offline mutation queue with automatic replay - Window management and keyboard shortcuts - Desktop shell with offline banner Sync infrastructure: - Vector clock implementation for causality tracking - Last-write-wins with semantic conflict resolution - Delta sync via checkpoints for bandwidth efficiency - Comprehensive test coverage Also adds e2e test setup with Playwright and CI workflows for desktop releases. * fix(tests): sync engine test schema and checkpoint logic - Add missing process_after column and sync_tombstone table to test schemas - Fix checkpoint update to save cursor even when records array is empty - Revert claude-code-review.yml workflow changes to match main --------- Co-authored-by: Nicholai <nicholaivogelfilms@gmail.com>
109 lines
3.0 KiB
TypeScript
109 lines
3.0 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect, useCallback } from "react"
|
|
import { WifiOff, RefreshCw } from "lucide-react"
|
|
import { cn } from "@/lib/utils"
|
|
import { useDesktop } from "@/hooks/use-desktop"
|
|
import { useSyncStatus, useTriggerSync } from "@/hooks/use-sync-status"
|
|
import { Button } from "@/components/ui/button"
|
|
|
|
interface OfflineBannerProps {
|
|
readonly className?: string
|
|
}
|
|
|
|
// Desktop-specific offline banner that shows pending mutation count.
|
|
// Different from the native offline banner which uses Capacitor Network.
|
|
export function DesktopOfflineBanner({ className }: OfflineBannerProps) {
|
|
const isDesktop = useDesktop()
|
|
const { status, pendingCount } = useSyncStatus()
|
|
const triggerSync = useTriggerSync()
|
|
const [dismissed, setDismissed] = useState(false)
|
|
|
|
const handleRetry = useCallback(() => {
|
|
triggerSync()
|
|
}, [triggerSync])
|
|
|
|
const handleDismiss = useCallback(() => {
|
|
setDismissed(true)
|
|
}, [])
|
|
|
|
// Reset dismissed state when coming back online
|
|
useEffect(() => {
|
|
if (status !== "offline") {
|
|
setDismissed(false)
|
|
}
|
|
}, [status])
|
|
|
|
// Don't render on non-desktop or when online
|
|
if (!isDesktop || status !== "offline") return null
|
|
|
|
// Don't show if dismissed and no new pending items
|
|
if (dismissed && pendingCount === 0) return null
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"flex items-center justify-between gap-3 bg-amber-500/90 px-4 py-2 text-sm font-medium text-white dark:bg-amber-600/90",
|
|
className,
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<WifiOff className="h-4 w-4 shrink-0" />
|
|
<span>
|
|
You're offline.
|
|
{pendingCount > 0 && (
|
|
<span className="ml-1">
|
|
{pendingCount} change{pendingCount !== 1 ? "s" : ""} queued for
|
|
sync.
|
|
</span>
|
|
)}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
className="h-7 bg-white/20 text-white hover:bg-white/30"
|
|
onClick={handleRetry}
|
|
>
|
|
<RefreshCw className="mr-1 h-3 w-3" />
|
|
Retry
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-7 px-2 text-white/80 hover:bg-white/20 hover:text-white"
|
|
onClick={handleDismiss}
|
|
>
|
|
Dismiss
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Minimal version showing just a status bar
|
|
export function OfflineStatusBar({ className }: OfflineBannerProps) {
|
|
const isDesktop = useDesktop()
|
|
const { status, pendingCount } = useSyncStatus()
|
|
|
|
if (!isDesktop || status !== "offline") return null
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"flex items-center justify-center gap-1.5 bg-amber-500/80 px-2 py-0.5 text-xs font-medium text-white",
|
|
className,
|
|
)}
|
|
>
|
|
<WifiOff className="h-3 w-3" />
|
|
<span>Offline</span>
|
|
{pendingCount > 0 && (
|
|
<span className="ml-1 rounded-full bg-white/20 px-1.5">
|
|
{pendingCount}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|