- Media (voice/video/image/file): private bucket, presigned PUT upload, reference-only publish, membership-gated presigned GET for viewing - Avatars: public bucket, presigned PUT + direct public URL, profile.update event - messages.media_key + media_meta (migration 1720000000005_media) - Explicit MinIO region on presigning client (avoids getBucketRegion network call) - Event type values sourced from EventType constants (no raw string literals) - Web: attach button, MediaView renderer, avatar upload in topbar Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFRyKxkKEjfAgpXygzoNiD
102 lines
3 KiB
TypeScript
102 lines
3 KiB
TypeScript
// Realtime event taxonomy (spec section 3). Every live feature is an event on
|
|
// the same receive socket; these names are the contract between backend
|
|
// publishers and client handlers. Payload shapes are added alongside features.
|
|
//
|
|
// Bucket A — durable, must be correct → through the backend (persist, publish)
|
|
// Bucket B — ephemeral, throwaway → published, not persisted
|
|
// Bucket C — connection-derived → Centrifugo built-in presence
|
|
//
|
|
// EventType is the single source of truth for every event `type` value — never
|
|
// write the raw string literal anywhere; reference EventType.* instead.
|
|
|
|
import type { Message } from '../types/message';
|
|
import type { Conversation } from '../types/conversation';
|
|
|
|
export const EventType = {
|
|
// Bucket A
|
|
MessageNew: 'message.new',
|
|
MessageEdit: 'message.edit',
|
|
MessageDelete: 'message.delete',
|
|
ReactionAdd: 'reaction.add',
|
|
ReactionRemove: 'reaction.remove',
|
|
ReadReceipt: 'read.receipt',
|
|
LastSeen: 'last_seen',
|
|
ConversationNew: 'conversation.new',
|
|
ConversationMembership: 'conversation.membership',
|
|
ProfileUpdate: 'profile.update',
|
|
|
|
// Bucket B
|
|
TypingStart: 'typing.start',
|
|
TypingStop: 'typing.stop',
|
|
RecordingAudio: 'recording.audio',
|
|
RecordingVideo: 'recording.video',
|
|
|
|
// Bucket C
|
|
Presence: 'presence',
|
|
} as const;
|
|
|
|
export type EventType = (typeof EventType)[keyof typeof EventType];
|
|
|
|
// --- Realtime event payloads (published by the backend, handled by clients) ---
|
|
// Each `type` field is derived from EventType so a typo is a compile error.
|
|
|
|
export interface MessageNewEvent {
|
|
type: typeof EventType.MessageNew;
|
|
message: Message;
|
|
}
|
|
|
|
export interface MessageEditEvent {
|
|
type: typeof EventType.MessageEdit;
|
|
message: Message;
|
|
}
|
|
|
|
export interface MessageDeleteEvent {
|
|
type: typeof EventType.MessageDelete;
|
|
conversationId: string;
|
|
messageId: string;
|
|
}
|
|
|
|
// Published to a user's personal channel when a new conversation involving them
|
|
// is created (a DM someone started, or a group they were added to) so it appears
|
|
// in their list immediately.
|
|
export interface ConversationNewEvent {
|
|
type: typeof EventType.ConversationNew;
|
|
conversation: Conversation;
|
|
}
|
|
|
|
export type ConversationMembershipAction = 'added' | 'removed';
|
|
|
|
export interface ConversationMembershipEvent {
|
|
type: typeof EventType.ConversationMembership;
|
|
action: ConversationMembershipAction;
|
|
conversationId: string;
|
|
userId: string;
|
|
}
|
|
|
|
export interface ReactionEvent {
|
|
type: typeof EventType.ReactionAdd | typeof EventType.ReactionRemove;
|
|
conversationId: string;
|
|
messageId: string;
|
|
emoji: string;
|
|
userId: string;
|
|
}
|
|
|
|
export interface ReadReceiptEvent {
|
|
type: typeof EventType.ReadReceipt;
|
|
conversationId: string;
|
|
userId: string;
|
|
seq: number;
|
|
}
|
|
|
|
export interface TypingEvent {
|
|
type: typeof EventType.TypingStart | typeof EventType.TypingStop;
|
|
conversationId: string;
|
|
userId: string;
|
|
}
|
|
|
|
export interface ProfileUpdateEvent {
|
|
type: typeof EventType.ProfileUpdate;
|
|
userId: string;
|
|
displayName: string;
|
|
avatarUrl: string | null;
|
|
}
|