Skip to content
Clone
951a522
145 lines 5.6 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 {
AppResponseData_AttachmentDto,
AppResponseData_DmDto,
AppResponseData_DraftDto,
AppResponseData_MessageDto,
AppResponseData_MessagePage,
AppResponseData_Page_EmojiDto,
AppResponseData_Page_MessageDto,
AppResponseData_ReadStateDto,
AppResponseData_RoomBriefDto,
AppResponseData_RoomSettingDto,
AppResponseData_SpaceReadStatePage,
AppResponseData_ThreadDto,
AppResponseData_UploadIntentDto,
AppResponseData_Vec_MessageEditDto,
AppResponseData_Vec_PresenceDto,
AppResponseData_Vec_ReactionUserDto,
AppResponseData_Vec_RoomMemberDto,
ChatMessageType,
PresenceStatus,
RoomKind,
UserDto,
} from '@/shared/api'
/** One row of `GET /chat/rooms/{id}/messages` — also the WS payload. */
export type ChatMessage = NonNullable<AppResponseData_MessageDto['data']>
export type MessagePage = NonNullable<AppResponseData_MessagePage['data']>
export type Attachment = NonNullable<AppResponseData_AttachmentDto['data']>
export type ReactionAgg = NonNullable<ChatMessage['reactions']>[number]
export type ReactionUser = NonNullable<AppResponseData_Vec_ReactionUserDto['data']>[number]
export type MessageEdit = NonNullable<AppResponseData_Vec_MessageEditDto['data']>[number]
/** Room as the sidebar lists it (`GET /chat/spaces/{space}/rooms`). */
export type RoomBrief = NonNullable<AppResponseData_RoomBriefDto['data']>
export type RoomKindName = RoomKind
export type DmBrief = NonNullable<AppResponseData_DmDto['data']>
export type RoomSetting = NonNullable<AppResponseData_RoomSettingDto['data']>
export type RoomMember = NonNullable<AppResponseData_Vec_RoomMemberDto['data']>[number]
export type Presence = NonNullable<AppResponseData_Vec_PresenceDto['data']>[number]
export type PresenceState = PresenceStatus
export type ThreadBrief = NonNullable<AppResponseData_ThreadDto['data']>
export type ReadState = NonNullable<AppResponseData_ReadStateDto['data']>
export type Draft = NonNullable<AppResponseData_DraftDto['data']>
export type SpaceReadStates = NonNullable<AppResponseData_SpaceReadStatePage['data']>
export type CustomEmoji = NonNullable<AppResponseData_Page_EmojiDto['data']>['items'][number]
export type UploadIntent = NonNullable<AppResponseData_UploadIntentDto['data']>
export type PinnedPage = NonNullable<AppResponseData_Page_MessageDto['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)
}