diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 2533dae..f9c523a 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -205,24 +205,24 @@ func main() { logger.Infoln("Shutting down server...") - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - if err := server.Shutdown(ctx); err != nil { - logger.Error("Server forced to shutdown: %v", err) - } - workerCancel() drained := make(chan struct{}) go func() { workersWG.Wait() close(drained) }() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := server.Shutdown(ctx); err != nil { + logger.Error("Server forced to shutdown: %v", err) + } + select { case <-drained: logger.Infoln("Background workers drained cleanly") - case <-time.After(30 * time.Second): - logger.Error("Background workers did not drain within 30s — exiting anyway") + case <-time.After(10 * time.Second): + logger.Error("Background workers did not drain in time — exiting anyway") } logger.Infoln("Server exited") diff --git a/backend/internal/firehose/ingester.go b/backend/internal/firehose/ingester.go index 0b784a4..12d27ef 100644 --- a/backend/internal/firehose/ingester.go +++ b/backend/internal/firehose/ingester.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "net" "strings" "sync" "time" @@ -203,8 +204,15 @@ func (i *Ingester) subscribe(ctx context.Context) error { default: } + _ = conn.SetReadDeadline(time.Now().Add(5 * time.Second)) _, message, err := conn.ReadMessage() if err != nil { + if ctx.Err() != nil { + return nil + } + if ne, ok := err.(net.Error); ok && ne.Timeout() { + continue + } return fmt.Errorf("websocket read failed: %w", err) } diff --git a/web/src/api/client.ts b/web/src/api/client.ts index b363e67..de70ca0 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -14,12 +14,11 @@ export type { Collection } from "../types"; export const sessionAtom = atom(null); -export async function checkSession(): Promise { +export async function checkSession(): Promise { try { const res = await fetch("/auth/session"); if (!res.ok) { - sessionAtom.set(null); - return null; + return undefined; } const data = await res.json(); @@ -84,8 +83,7 @@ export async function checkSession(): Promise { return null; } catch (e) { console.error("Session check failed:", e); - sessionAtom.set(null); - return null; + return undefined; } } @@ -108,14 +106,12 @@ async function apiRequest( }); if (response.status === 401 && !skipAuthRedirect) { - sessionAtom.set(null); - try { - await fetch("/auth/logout", { method: "POST" }); - } catch { - // Ignore - } - if (window.location.pathname !== "/login") { - window.location.href = "/login"; + const verified = await checkSession(); + if (verified === null) { + sessionAtom.set(null); + if (window.location.pathname !== "/login") { + window.location.href = "/login"; + } } } diff --git a/web/src/components/common/Card.tsx b/web/src/components/common/Card.tsx index cbf6c68..1a6193f 100644 --- a/web/src/components/common/Card.tsx +++ b/web/src/components/common/Card.tsx @@ -39,6 +39,7 @@ import { } from "../../api/client"; import { $user } from "../../store/auth"; import { $preferences } from "../../store/preferences"; +import { displayHandle } from "../../lib/handle"; import { useStore } from "@nanostores/react"; import type { AnnotationItem, @@ -377,7 +378,8 @@ export default function Card({ size="xs" /> - {item.addedBy.displayName || `@${item.addedBy.handle}`} + {item.addedBy.displayName || + `@${displayHandle(item.addedBy.handle, item.addedBy.did)}`} @@ -451,11 +453,12 @@ export default function Card({ href={`/profile/${item.author?.did}`} className="font-semibold text-surface-900 dark:text-white text-[15px] hover:underline block truncate sm:whitespace-normal sm:overflow-visible" > - {item.author?.displayName || item.author?.handle} + {item.author?.displayName || + displayHandle(item.author?.handle, item.author?.did)} - @{item.author?.handle} + @{displayHandle(item.author?.handle, item.author?.did)} · diff --git a/web/src/components/common/ProfileHoverCard.tsx b/web/src/components/common/ProfileHoverCard.tsx index fbc1984..c2d8761 100644 --- a/web/src/components/common/ProfileHoverCard.tsx +++ b/web/src/components/common/ProfileHoverCard.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from "react"; import Avatar from "../ui/Avatar"; import RichText from "./RichText"; import { getProfile } from "../../api/client"; +import { displayHandle } from "../../lib/handle"; import type { UserProfile } from "../../types"; import { Loader2 } from "lucide-react"; import { useTranslation } from "react-i18next"; @@ -127,10 +128,11 @@ export default function ProfileHoverCard({ />

- {profile.displayName || profile.handle} + {profile.displayName || + displayHandle(profile.handle, profile.did)}

- @{profile.handle} + @{displayHandle(profile.handle, profile.did)}

diff --git a/web/src/components/feed/ReplyList.tsx b/web/src/components/feed/ReplyList.tsx index 3afb4b7..d3a7095 100644 --- a/web/src/components/feed/ReplyList.tsx +++ b/web/src/components/feed/ReplyList.tsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"; import { MessageSquare, Trash2, Reply } from "lucide-react"; import type { AnnotationItem, UserProfile } from "../../types"; import { getAvatarUrl } from "../../api/client"; +import { displayHandle } from "../../lib/handle"; import { clsx } from "clsx"; interface ReplyListProps { @@ -49,7 +50,10 @@ const ReplyItem: React.FC = ({ > {isInline ? ( <> - + {getAvatarUrl(author.did, author.avatar) ? ( = ({ depth > 0 ? "text-xs" : "text-sm", )} > - {author.displayName || author.handle} + {author.displayName || + displayHandle(author.handle, author.did)}
{reply.createdAt @@ -120,7 +125,10 @@ const ReplyItem: React.FC = ({ ) : (
- + {getAvatarUrl(author.did, author.avatar) ? ( = ({
- {author.displayName || author.handle} + {author.displayName || + displayHandle(author.handle, author.did)}
diff --git a/web/src/components/navigation/MobileNav.tsx b/web/src/components/navigation/MobileNav.tsx index 5dce832..f3df9e1 100644 --- a/web/src/components/navigation/MobileNav.tsx +++ b/web/src/components/navigation/MobileNav.tsx @@ -16,6 +16,7 @@ import { import { useEffect, useState } from "react"; import { getUnreadNotificationCount } from "../../api/client"; import { $user, logout } from "../../store/auth"; +import { displayHandle } from "../../lib/handle"; import { AppleIcon } from "../common/Icons"; import { useTranslation } from "react-i18next"; @@ -97,10 +98,11 @@ export default function MobileNav({ )}
- {user.displayName || user.handle} + {user.displayName || + displayHandle(user.handle, user.did)} - @{user.handle} + @{displayHandle(user.handle, user.did)}
diff --git a/web/src/components/navigation/RightSidebar.tsx b/web/src/components/navigation/RightSidebar.tsx index f409200..b280b83 100644 --- a/web/src/components/navigation/RightSidebar.tsx +++ b/web/src/components/navigation/RightSidebar.tsx @@ -7,6 +7,7 @@ import { type Tag, } from "../../api/client"; import { Avatar } from "../ui"; +import { displayHandle } from "../../lib/handle"; import { useTranslation } from "react-i18next"; function looksLikeUrl(query: string): boolean { @@ -195,10 +196,11 @@ export default function RightSidebar({ onNavigate }: RightSidebarProps) {
- {actor.displayName || actor.handle} + {actor.displayName || + displayHandle(actor.handle, actor.did)}
- @{actor.handle} + @{displayHandle(actor.handle, actor.did)}
diff --git a/web/src/components/navigation/Sidebar.tsx b/web/src/components/navigation/Sidebar.tsx index 504770c..ecc75d6 100644 --- a/web/src/components/navigation/Sidebar.tsx +++ b/web/src/components/navigation/Sidebar.tsx @@ -19,6 +19,7 @@ import { useStore } from "@nanostores/react"; import { $user, logout } from "../../store/auth"; import { $theme, cycleTheme } from "../../store/theme"; import { getUnreadNotificationCount } from "../../api/client"; +import { displayHandle } from "../../lib/handle"; import { Avatar, CountBadge } from "../ui"; import { useTranslation } from "react-i18next"; @@ -221,7 +222,7 @@ export default function Sidebar({ { @@ -235,10 +236,10 @@ export default function Sidebar({

- {user.displayName || user.handle} + {user.displayName || displayHandle(user.handle, user.did)}

- @{user.handle} + @{displayHandle(user.handle, user.did)}

diff --git a/web/src/lib/handle.ts b/web/src/lib/handle.ts new file mode 100644 index 0000000..4b7f180 --- /dev/null +++ b/web/src/lib/handle.ts @@ -0,0 +1,27 @@ +const INVALID_HANDLE = "handle.invalid"; + +export function isInvalidHandle(handle?: string | null): boolean { + return !handle || handle === INVALID_HANDLE; +} + +export function shortenDid(did?: string | null): string { + if (!did) return ""; + if (did.startsWith("did:plc:")) { + const id = did.slice("did:plc:".length); + return id.length > 10 ? `did:plc:${id.slice(0, 4)}…${id.slice(-4)}` : did; + } + if (did.startsWith("did:web:")) { + return did.slice("did:web:".length); + } + return did; +} + +export function displayHandle( + handle?: string | null, + did?: string | null, +): string { + if (isInvalidHandle(handle)) { + return shortenDid(did) || handle || ""; + } + return handle as string; +} diff --git a/web/src/store/auth.ts b/web/src/store/auth.ts index 608c8b4..ca52fd3 100644 --- a/web/src/store/auth.ts +++ b/web/src/store/auth.ts @@ -1,5 +1,6 @@ import { atom } from "nanostores"; import { loadPreferences } from "./preferences"; +import { sessionAtom } from "../api/client"; import type { UserProfile } from "../types"; import { analytics } from "../lib/analytics"; @@ -15,6 +16,18 @@ $user.subscribe((user) => { } }); +let syncing = false; +function keepInSync(from: typeof $user, to: typeof $user) { + from.subscribe((value) => { + if (syncing || to.get() === value) return; + syncing = true; + to.set(value); + syncing = false; + }); +} +keepInSync(sessionAtom, $user); +keepInSync($user, sessionAtom); + export function logout() { analytics.capture("user_logged_out"); analytics.reset(); diff --git a/web/src/types.ts b/web/src/types.ts index b16567c..0a59b8f 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -88,6 +88,7 @@ export interface AnnotationItem { }; }; parentUri?: string; + rootUri?: string; labels?: ContentLabel[]; } diff --git a/web/src/views/AppShell.tsx b/web/src/views/AppShell.tsx index d324bab..d3173e7 100644 --- a/web/src/views/AppShell.tsx +++ b/web/src/views/AppShell.tsx @@ -346,18 +346,11 @@ export default function AppShell() { }); useEffect(() => { - const ssrUser = window.__MARGIN_USER__; - if ($user.get() === null && ssrUser === null) return; - - if (ssrUser) { - checkSession().then((user) => { - if (user) $user.set(user); - }); - } else if (ssrUser === undefined) { - checkSession().then((user) => { + checkSession().then((user) => { + if (user !== undefined) { $user.set(user); - }); - } + } + }); }, []); return ( diff --git a/web/src/views/auth/Login.tsx b/web/src/views/auth/Login.tsx index ee86c65..1aae4e2 100644 --- a/web/src/views/auth/Login.tsx +++ b/web/src/views/auth/Login.tsx @@ -12,6 +12,7 @@ import { Avatar } from "../../components/ui"; import { useStore } from "@nanostores/react"; import { $theme } from "../../store/theme"; import { analytics } from "../../lib/analytics"; +import { displayHandle } from "../../lib/handle"; interface LoginProps { initialError?: string; @@ -280,7 +281,7 @@ export default function Login({ initialError }: LoginProps) { {actor.displayName || actor.handle}
- @{actor.handle} + @{displayHandle(actor.handle, actor.did)}
diff --git a/web/src/views/core/Notifications.tsx b/web/src/views/core/Notifications.tsx index dd85ac1..fbf68d5 100644 --- a/web/src/views/core/Notifications.tsx +++ b/web/src/views/core/Notifications.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import type { TFunction } from "i18next"; import { getNotifications, markNotificationsRead } from "../../api/client"; +import { displayHandle } from "../../lib/handle"; import type { NotificationItem, AnnotationItem } from "../../types"; import { Heart, @@ -33,6 +34,19 @@ function getContentType( return "unknown"; } +function annotationHref(uri: string, subject?: AnnotationItem): string { + if (!uri) return "#"; + if (getContentType(uri) !== "reply") { + return `/annotation/${encodeURIComponent(uri)}`; + } + const target = subject?.rootUri || subject?.inReplyTo || subject?.parentUri; + if (!target || getContentType(target) === "reply") { + return `/annotation/${encodeURIComponent(subject?.rootUri || uri)}`; + } + const rkey = uri.split("/").pop(); + return `/annotation/${encodeURIComponent(target)}${rkey ? `#reply-${rkey}` : ""}`; +} + function getNotificationVerb( notifType: string, contentType: string, @@ -134,7 +148,7 @@ function SubjectPreview({ if (!item?.uri && !subjectUri) return null; const contentType = getContentType(subjectUri); - const href = `/annotation/${encodeURIComponent(subjectUri)}`; + const href = annotationHref(subjectUri, item); let preview: React.ReactNode = null; @@ -203,7 +217,7 @@ function SubjectPreview({

{t("notifications.inReplyTo")}{" "} e.stopPropagation()} > @@ -357,11 +371,15 @@ export default function Notifications({ href={`/profile/${n.actor.did}`} className="font-semibold text-surface-900 dark:text-white hover:underline" > - {n.actor.displayName || `@${n.actor.handle}`} + {n.actor.displayName || + `@${displayHandle(n.actor.handle, n.actor.did)}`} {" "} {n.type !== "follow" && n.subjectUri ? ( {verb} diff --git a/web/src/views/core/Settings.tsx b/web/src/views/core/Settings.tsx index 00ec92c..9eccb3e 100644 --- a/web/src/views/core/Settings.tsx +++ b/web/src/views/core/Settings.tsx @@ -65,6 +65,7 @@ import { AppleIcon } from "../../components/common/Icons"; import { HighlightImporter } from "./HighlightImporter"; import IOSShortcutModal from "../../components/modals/IOSShortcutModal"; import { analytics } from "../../lib/analytics"; +import { displayHandle } from "../../lib/handle"; export default function Settings() { const { t } = useTranslation(); @@ -178,7 +179,7 @@ export default function Settings() { {user.displayName || user.handle}

- @{user.handle} + @{displayHandle(user.handle, user.did)}

{b.author?.handle && (

- @{b.author.handle} + @{displayHandle(b.author.handle, b.author.did)}

)} @@ -514,7 +515,7 @@ export default function Settings() {

{m.author?.handle && (

- @{m.author.handle} + @{displayHandle(m.author.handle, m.author.did)}

)} diff --git a/web/src/views/profile/Profile.tsx b/web/src/views/profile/Profile.tsx index 06d09f2..eb32af2 100644 --- a/web/src/views/profile/Profile.tsx +++ b/web/src/views/profile/Profile.tsx @@ -43,6 +43,7 @@ import { Tabs, } from "../../components/ui"; import { $user } from "../../store/auth"; +import { displayHandle } from "../../lib/handle"; import { $preferences, loadPreferences } from "../../store/preferences"; import type { Collection, @@ -377,10 +378,11 @@ export default function Profile({ did, initialProfile }: ProfileProps) {

- {profile.displayName || profile.handle} + {profile.displayName || + displayHandle(profile.handle, profile.did)}

- @{profile.handle} + @{displayHandle(profile.handle, profile.did)}