/** * Chat domain types, all derived from the generated response envelopes so they * follow the API instead of a hand-written copy. Anything the transcript needs * beyond the wire shape (optimistic rows, upload progress) is defined here too, * because those are domain states, not component details. */ import type { ChatMessageType, EnvelopeAttachmentDto, EnvelopeDmDto, EnvelopeDraftDto, EnvelopeMessageDto, EnvelopeMessagePage, EnvelopePageEmojiDto, EnvelopeReadStateDto, EnvelopeRoomBriefDto, EnvelopeRoomSettingDto, EnvelopeSpaceReadStates, EnvelopeThreadDto, EnvelopeUploadIntentDto, EnvelopeVecMessageEditDto, EnvelopeVecPresenceDto, EnvelopeVecReactionUserDto, EnvelopeVecRoomMemberDto, PresenceStatus, RoomKind, UserDto, } from '@app/services/api' /** One row of `GET /chat/rooms/{id}/messages` — also the WS payload. */ export type ChatMessage = NonNullable export type MessagePage = NonNullable export type Attachment = NonNullable export type ReactionAgg = NonNullable[number] export type ReactionUser = NonNullable[number] export type MessageEdit = NonNullable[number] /** Room as the sidebar lists it (`GET /chat/spaces/{space}/rooms`). */ export type RoomBrief = NonNullable export type RoomKindName = RoomKind export type DmBrief = NonNullable export type RoomSetting = NonNullable export type RoomMember = NonNullable[number] export type Presence = NonNullable[number] export type PresenceState = PresenceStatus export type ThreadBrief = NonNullable export type ReadState = NonNullable export type Draft = NonNullable export type SpaceReadStates = NonNullable export type EmojiPage = NonNullable export type CustomEmoji = EmojiPage['items'][number] export type UploadIntent = NonNullable export type UserInfo = UserDto /** What `POST /chat/rooms/{id}/messages` accepts; re-declared so the composer * can build a command without importing the whole generated namespace. */ export interface SendMessageInput { content: string nonce?: string reply_to_id?: string | null thread_id?: string | null thread_broadcast?: boolean attachments?: string[] urgent?: boolean } /** Optimistic state of a message the local user pressed send on. */ export type PendingState = 'sending' | 'failed' /** A local-only row: the server has not confirmed it (or confirmed and the echo * has not arrived yet). It renders in the transcript with a state chip. */ export interface PendingMessage { /** Client-side key; never a uuid the server could confuse with a real id. */ localId: string /** Idempotency key sent as `nonce`: a retry can never duplicate the post. */ nonce: string room_id: string author_id: number content: string created_at: string reply_to_id?: string | null thread_id?: string | null thread_broadcast?: boolean attachments: Attachment[] state: PendingState /** Server reason (`ApiFailure.message`) when state is failed. */ error?: string /** True while the request is in flight; used to hide the retry chip. */ sending: boolean } /** One upload in the composer's staging strip. */ export interface UploadItem { localId: string file: File /** Object URL preview for images; revoked when the item is dropped. */ previewUrl?: string /** Set once `POST /chat/attachments` answered. */ attachmentId?: string /** Dimensions probed from an image, sent to `finalize`. */ width?: number height?: number /** Finalized metadata, present once `state` is ready. */ attachment?: Attachment progress: number state: 'checking' | 'uploading' | 'finalizing' | 'ready' | 'failed' | 'too_big' error?: string } /** Message row the transcript renders: either a server row or a pending one. */ export type TranscriptRow = { kind: 'message'; message: ChatMessage } | { kind: 'pending'; message: PendingMessage } /** * System-post kinds get their own compact rendering. A quoted reply is *not* one: * it carries the author's own text plus a `reply_to_id` quote and renders as a * full message row. */ export const SYSTEM_MESSAGE_TYPES: readonly ChatMessageType[] = [ 'ThreadCreated', 'ChannelJoin', 'ChannelLeave', 'ChannelTopic', 'ChannelPinnedItem', 'Reminder', 'System', ] export function isSystemMessage(message: { message_type: ChatMessageType }): boolean { return message.message_type !== 'Default' && SYSTEM_MESSAGE_TYPES.includes(message.message_type) } /** Room kinds that carry no text transcript. */ const VOICE_KINDS: readonly RoomKindName[] = ['Voice', 'Stage'] export function isVoiceRoom(kind: RoomKindName | undefined): boolean { return kind !== undefined && VOICE_KINDS.includes(kind) } const DM_KINDS: readonly RoomKindName[] = ['Dm', 'DirectMessage', 'GroupDm'] export function isDmKind(kind: RoomKindName | undefined): boolean { return kind !== undefined && DM_KINDS.includes(kind) }