import Link from "next/link"; import { db } from "@turbostarter/db/server"; import { mesh, meshMember, meshTopic, meshTopicMember, meshTopicMessage, } from "@turbostarter/db/schema/mesh"; import { and, asc, count, eq, inArray, isNull, notInArray, or, sql } from "drizzle-orm"; import { pathsConfig } from "~/config/paths"; import { getSession } from "~/lib/auth/server"; import { getMetadata } from "~/lib/metadata"; import { Reveal } from "~/modules/dashboard/universe/reveal"; export const generateMetadata = getMetadata({ title: "Topics", description: "Every topic across every mesh — sorted by activity.", }); const formatRelative = (iso: string | null) => { if (!iso) return "never"; const sec = Math.max(0, Math.floor((Date.now() - new Date(iso).getTime()) / 1000)); if (sec < 60) return `${sec}s ago`; if (sec < 3600) return `${Math.floor(sec / 60)}m ago`; if (sec < 86_400) return `${Math.floor(sec / 3600)}h ago`; if (sec < 86_400 * 30) return `${Math.floor(sec / 86_400)}d ago`; if (sec < 86_400 * 365) return `${Math.floor(sec / (86_400 * 30))}mo ago`; return `${Math.floor(sec / (86_400 * 365))}y ago`; }; export default async function WorkspaceTopicsPage() { const { user } = await getSession(); if (!user) { return null; } // Resolve every active membership for this user → list of (memberId, mesh). const memberships = await db .select({ memberId: meshMember.id, meshId: meshMember.meshId, meshSlug: mesh.slug, meshName: mesh.name, }) .from(meshMember) .innerJoin(mesh, eq(mesh.id, meshMember.meshId)) .where( and( eq(meshMember.userId, user.id), isNull(meshMember.revokedAt), isNull(mesh.archivedAt), ), ); const meshIds = memberships.map((m) => m.meshId); const myMemberIds = memberships.map((m) => m.memberId); const memberByMeshId = new Map(memberships.map((m) => [m.meshId, m])); const topics = meshIds.length ? await db .select({ id: meshTopic.id, meshId: meshTopic.meshId, name: meshTopic.name, description: meshTopic.description, createdAt: meshTopic.createdAt, }) .from(meshTopic) .where( and(inArray(meshTopic.meshId, meshIds), isNull(meshTopic.archivedAt)), ) .orderBy(asc(meshTopic.name)) : []; const topicIds = topics.map((t) => t.id); const lastMessages = topicIds.length ? await db .select({ topicId: meshTopicMessage.topicId, lastAt: sql`max(${meshTopicMessage.createdAt})`, }) .from(meshTopicMessage) .where(inArray(meshTopicMessage.topicId, topicIds)) .groupBy(meshTopicMessage.topicId) : []; const lastByTopic = new Map(lastMessages.map((r) => [r.topicId, r.lastAt])); const unreadCounts = topicIds.length && myMemberIds.length ? await db .select({ topicId: meshTopicMessage.topicId, n: count(meshTopicMessage.id), }) .from(meshTopicMessage) .leftJoin( meshTopicMember, and( eq(meshTopicMember.topicId, meshTopicMessage.topicId), inArray(meshTopicMember.memberId, myMemberIds), ), ) .where( and( inArray(meshTopicMessage.topicId, topicIds), notInArray(meshTopicMessage.senderMemberId, myMemberIds), or( isNull(meshTopicMember.lastReadAt), sql`${meshTopicMessage.createdAt} > ${meshTopicMember.lastReadAt}`, ), ), ) .groupBy(meshTopicMessage.topicId) : []; const unreadByTopic = new Map(unreadCounts.map((r) => [r.topicId, Number(r.n)])); const items = topics .map((t) => { const m = memberByMeshId.get(t.meshId)!; const lastAt = lastByTopic.get(t.id); return { ...t, meshSlug: m.meshSlug, meshName: m.meshName, unread: unreadByTopic.get(t.id) ?? 0, lastMessageAt: lastAt ? new Date(lastAt).toISOString() : null, }; }) .sort((a, b) => { if (a.lastMessageAt && b.lastMessageAt) { return b.lastMessageAt.localeCompare(a.lastMessageAt); } if (a.lastMessageAt) return -1; if (b.lastMessageAt) return 1; return a.name.localeCompare(b.name); }); const totalUnread = items.reduce((acc, t) => acc + t.unread, 0); return (

Every topic,
across every{" "} mesh.

{items.length} topics 0 ? "text-[var(--cm-clay)]" : "text-[var(--cm-fg)]"}`} > {totalUnread} unread {memberships.length} meshes
{items.length === 0 ? (

No topics yet.{" "} Open a mesh {" "} to start one.

) : (
    {items.map((t, i) => (
  • {t.meshSlug} {t.name} {t.description ? ( {t.description} ) : null} {t.unread > 0 ? ( {t.unread} ) : ( · )} {formatRelative(t.lastMessageAt)}
  • ))}
)}
); }