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)}