Messenger/packages/core/src/events/index.ts
Заид Омар Медхат | Zaid Omar Medhat db5c1610b3 Phase 6: media messages + avatars via MinIO presigned uploads
- 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
2026-07-10 18:11:25 +05:00

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;
}