/** * `FabriChatPlacement`: one room placed in a container, a space that shows * chats among other things (`docs/specs/fabrichat/FabriChatPlacement.md`). * * It holds a link to its room and nothing read from it, and it has no * rendering. Everything it offers is read through the link when it is read, * under the viewer's own access, and offered as the `chat` group of its * `[VIEWS]`. `FabriChatAdapter` renders it. */ import { type Cell, computed, NAME, pattern, spaceAccess, VIEWS, wish, } from "commonfabric"; import { type ChatReaction, type ChatReactionTally, type ChatRoomAbout, type ProfileCell, reactionTalliesOf, } from "./schemas.tsx"; /** One message's reaction tallies. */ export interface PlacedTallies { /** The message. */ message: Cell; /** Its reactions, by emoji, in the order each was first used. */ tallies: ChatReactionTally[]; } /** A message as a placement reads it: only its reactions. */ interface PlacedMessage { /** Everyone's reactions to the message. */ reactions?: ChatReaction[]; } /** A room's message list as a placement reads it. */ export interface PlacedMessageList { /** How many messages the room holds. */ count?: number; /** The newest messages of the main conversation. */ latest?: { messages?: Cell[]; hasOlder?: boolean }; } /** * A room as a placement reads it through its link: only the part of * `ChatRoomOutput` it offers. */ export interface PlacedRoom { /** What the room says about itself. */ about?: ChatRoomAbout; /** The room's participants. */ participants?: ProfileCell[]; /** The room's messages. */ messages?: PlacedMessageList; /** Whether the reader can send. */ canSend?: boolean; } /** Whether the viewer can read a placed room. */ export type PlacementState = "member" | "not-member" | "unavailable"; /** * A placement's data face: what a placed chat holds, and what the viewer may * see. */ export interface FabriChatPlacementView { /** Whether the viewer can read the room. */ state: PlacementState; /** What the room says about itself; absent unless `state` is `"member"`. */ about?: ChatRoomAbout; /** The room's messages; absent unless `state` is `"member"`. */ messages?: PlacedMessageList; /** Whether the viewer can send. */ canSend: boolean; /** The room's participants. */ participants: ProfileCell[]; /** Reactions on the newest messages. */ reactionTallies: PlacedTallies[]; } /** What a placement holds. */ export interface FabriChatPlacementInput { /** The room, set when the placement is created. */ room: Cell; } /** What a placement offers. */ export interface FabriChatPlacementOutput { /** * The placed room's title, for lists of pieces: `"Chat"` for a room with no * title, and `"Chat (unavailable)"` for a room the viewer can't read. */ [NAME]: string; /** The room, so a client can reach its own streams. */ room: Cell; /** The placement's data face. */ [VIEWS]: { chat: FabriChatPlacementView }; } /** * A room placed in a container. The viewer's own access decides what it * offers: a viewer who can't read the room sees that there is one, and * nothing of it. */ const FabriChatPlacement = pattern< FabriChatPlacementInput, FabriChatPlacementOutput >(({ room }) => { // Wished for as a cell, which the tallies compare reactors against; the // placement never tests it for absence, which a cell's handle would defeat. const profileWish = wish({ query: "#profile" }); const viewer = profileWish.result; const about = computed(() => room.get()?.about); // The viewer's access to the room's space decides: `"none"` is not a // member, and `undefined`, not known yet, is unavailable, as is a room a // member can't read yet. const state = computed((): PlacementState => { const level = spaceAccess(room); return level === "none" ? "not-member" : level === undefined || about?.kind === undefined ? "unavailable" : "member"; }); const isMember = computed(() => state === "member"); const messages = computed(() => isMember ? room.get()?.messages : undefined); const reactionTallies = computed((): PlacedTallies[] => { const list = isMember ? room.get()?.messages : undefined; return (list?.latest?.messages ?? []).map((message) => ({ message, tallies: reactionTalliesOf(message.get()?.reactions ?? [], viewer), })); }); const chat = { state, about: computed(() => (isMember ? about : undefined)), messages, canSend: computed(() => isMember && room.get()?.canSend === true), participants: computed(() => isMember ? [...(room.get()?.participants ?? [])] : [] ), reactionTallies, }; return { [NAME]: computed(() => isMember ? about?.title ?? "Chat" : "Chat (unavailable)" ), room, [VIEWS]: { chat }, }; }); export default FabriChatPlacement;