Skip to content
Clone
main
142 lines 5.2 KB
/**
* 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<EnvelopeMessageDto['data']>
export type MessagePage = NonNullable<EnvelopeMessagePage['data']>
export type Attachment = NonNullable<EnvelopeAttachmentDto['data']>
export type ReactionAgg = NonNullable<ChatMessage['reactions']>[number]
export type ReactionUser = NonNullable<EnvelopeVecReactionUserDto['data']>[number]
export type MessageEdit = NonNullable<EnvelopeVecMessageEditDto['data']>[number]
/** Room as the sidebar lists it (`GET /chat/spaces/{space}/rooms`). */
export type RoomBrief = NonNullable<EnvelopeRoomBriefDto['data']>
export type RoomKindName = RoomKind
export type DmBrief = NonNullable<EnvelopeDmDto['data']>
export type RoomSetting = NonNullable<EnvelopeRoomSettingDto['data']>
export type RoomMember = NonNullable<EnvelopeVecRoomMemberDto['data']>[number]
export type Presence = NonNullable<EnvelopeVecPresenceDto['data']>[number]
export type PresenceState = PresenceStatus
export type ThreadBrief = NonNullable<EnvelopeThreadDto['data']>
export type ReadState = NonNullable<EnvelopeReadStateDto['data']>
export type Draft = NonNullable<EnvelopeDraftDto['data']>
export type SpaceReadStates = NonNullable<EnvelopeSpaceReadStates['data']>
export type EmojiPage = NonNullable<EnvelopePageEmojiDto['data']>
export type CustomEmoji = EmojiPage['items'][number]
export type UploadIntent = NonNullable<EnvelopeUploadIntentDto['data']>
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)
}