evOo-local/apps/mobile/lib/inbox-spaces.ts

111 lines
3.9 KiB
TypeScript

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<InboxSpace, "isDefault" | "canDelete"> & Partial<Pick<InboxSpace, "hasContent">>,
) {
return !space.isDefault && space.canDelete === true;
}
export function spaceInboxItems(spaces: InboxSpace[]): InboxSpaceItem[] {
const seenProjectNames = new Set<string>();
const canonicalProjectIds = new Set<string>();
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<void>) {
if (!(await selectSpace(spaceId).catch(() => false))) return false;
await refresh();
return true;
}
export async function retryInboxSpaceFallback(spaceId: string, refresh: () => Promise<void>) {
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<void>,
): Promise<string | null> {
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;
}