import type { SpaceBot, SpaceGroup } from "@rakazo/contracts"; import { groupBotsForSidebar } from "@rakazo/core"; import { adoptDeletedSpaceFallback, type MobileBot, type MobileGroup, type MobileSpace, rpc, selectedSpaceId, selectSpace, } from "./api"; export type InboxSpace = Pick< MobileSpace, "id" | "name" | "isDefault" | "hasContent" | "canDelete" | "botSections" | "icon" | "color" > & { bots: (MobileBot | SpaceBot)[]; groups: (MobileGroup | SpaceGroup)[]; }; export type InboxSpaceItem = | { type: "bot"; bot: MobileBot | SpaceBot } | { type: "group"; group: MobileGroup | SpaceGroup } | { type: "heading"; key: string; title: string; space?: InboxSpace }; export function canDeleteInboxSpace( space: Pick & Partial>, ) { return !space.isDefault && space.canDelete === true; } export function spaceInboxItems(spaces: InboxSpace[]): InboxSpaceItem[] { const seenProjectNames = new Set(); const canonicalProjectIds = new Set(); const orderedProjects = [...spaces] .filter((space) => !space.isDefault) .sort((left, right) => Number(Boolean(right.hasContent)) - Number(Boolean(left.hasContent))); for (const space of orderedProjects) { const normalized = space.name.trim().toLowerCase(); if (normalized && seenProjectNames.has(normalized)) continue; if (normalized) seenProjectNames.add(normalized); canonicalProjectIds.add(space.id); } const visibleSpaces = spaces.filter( (space) => space.isDefault || canonicalProjectIds.has(space.id), ); return visibleSpaces.flatMap((space): InboxSpaceItem[] => { const normalizedSpaceName = space.name.trim().toLowerCase(); const chats = [ // A main bot is reached through its space's main conversation, not listed again. ...space.bots .filter((chat) => chat.kind !== "main") .map((chat) => ({ type: "bot" as const, bot: chat, ...chat })), ...space.groups .filter((chat) => { const normalizedGroupName = chat.name.trim().toLowerCase(); if (!space.isDefault && normalizedGroupName === normalizedSpaceName) return false; if (space.isDefault && chat.kind !== "main" && seenProjectNames.has(normalizedGroupName)) return false; return true; }) .map((chat) => ({ type: "group" as const, group: chat, ...chat })), ]; const items: InboxSpaceItem[] = []; if (visibleSpaces.length > 1 || !space.isDefault || chats.length === 0) { items.push({ type: "heading", key: space.id, title: space.name, space }); } for (const group of groupBotsForSidebar(chats, space.botSections)) { if (group.title) { items.push({ type: "heading", key: `${space.id}:${group.key}`, title: group.title }); } items.push(...group.bots); } return items; }); } export async function selectInboxSpace(spaceId: string, refresh: () => Promise) { if (!(await selectSpace(spaceId).catch(() => false))) return false; await refresh(); return true; } export async function retryInboxSpaceFallback(spaceId: string, refresh: () => Promise) { if (!(await adoptDeletedSpaceFallback(spaceId))) return false; await refresh(); return true; } // Return the selection to retry if native storage fails after a successful deletion. // The inbox must not refresh against the deleted selection in that case. export async function removeInboxSpace( spaceId: string, refresh: () => Promise, ): Promise { const response = await rpc<{ ok: true; activeSpaceId: string }>("spaces/remove", { spaceId }); if (selectedSpaceId() === spaceId) { if (await selectSpace(response.activeSpaceId).catch(() => false)) { await refresh(); return null; } const durable = await adoptDeletedSpaceFallback(response.activeSpaceId); await refresh(); return durable ? null : response.activeSpaceId; } await refresh(); return null; }