All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 50s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
298 lines
7.0 KiB
TypeScript
298 lines
7.0 KiB
TypeScript
import type { ChatMessage } from "./chat.ts";
|
|
import type { Comment } from "./comment.ts";
|
|
import type { Dump } from "./dump.ts";
|
|
import type { Notification } from "./notification.ts";
|
|
import type { Playlist } from "./playlist.ts";
|
|
import type { PublicUser } from "./user.ts";
|
|
|
|
/**
|
|
* The socket protocol. Every message has a `type` discriminant, so each union
|
|
* narrows with a switch. The server sends these as JSON, so the client reads
|
|
* `Wire<ServerToClientMessage>`: its Dates arrive as strings.
|
|
*/
|
|
|
|
// ── Client → Server ──────────────────────────────────────────────────────────
|
|
|
|
export interface PingMessage {
|
|
type: "ping";
|
|
}
|
|
|
|
export interface PongMessage {
|
|
type: "pong";
|
|
}
|
|
|
|
export interface VoteCastMessage {
|
|
type: "vote_cast";
|
|
dumpId: string;
|
|
}
|
|
|
|
export interface VoteRemoveMessage {
|
|
type: "vote_remove";
|
|
dumpId: string;
|
|
}
|
|
|
|
export interface CommentLikeCastMessage {
|
|
type: "comment_like_cast";
|
|
commentId: string;
|
|
}
|
|
|
|
export interface CommentLikeRemoveMessage {
|
|
type: "comment_like_remove";
|
|
commentId: string;
|
|
}
|
|
|
|
export interface ChatSendMessage {
|
|
type: "chat_send";
|
|
body: string;
|
|
/** Id of the message being replied to, if this is a reply. */
|
|
replyToId?: string;
|
|
/** Client-chosen id echoed on the broadcast (or on the error), so the
|
|
* sender can match the outcome to its pending message. */
|
|
nonce?: string;
|
|
}
|
|
|
|
// Sent while the user is composing (true) and when they stop (false). The
|
|
// server rebroadcasts the live set of typers to everyone as ChatTypingUpdate.
|
|
export interface ChatTypingMessage {
|
|
type: "chat_typing";
|
|
typing: boolean;
|
|
}
|
|
|
|
// Tells the server whether this client currently has the chatbox open, so that
|
|
// mention notifications can be skipped for users who are already reading chat.
|
|
export interface ChatFocusMessage {
|
|
type: "chat_focus";
|
|
open: boolean;
|
|
}
|
|
|
|
export interface ChatEditMessage {
|
|
type: "chat_edit";
|
|
id: string;
|
|
body: string;
|
|
}
|
|
|
|
export interface ChatDeleteMessage {
|
|
type: "chat_delete";
|
|
id: string;
|
|
}
|
|
|
|
// The user has seen the chat up to the message created at `at`.
|
|
export interface ChatReadMessage {
|
|
type: "chat_read";
|
|
at: string;
|
|
}
|
|
|
|
export type ClientToServerMessage =
|
|
| PingMessage
|
|
| PongMessage
|
|
| VoteCastMessage
|
|
| VoteRemoveMessage
|
|
| CommentLikeCastMessage
|
|
| CommentLikeRemoveMessage
|
|
| ChatSendMessage
|
|
| ChatTypingMessage
|
|
| ChatFocusMessage
|
|
| ChatEditMessage
|
|
| ChatDeleteMessage
|
|
| ChatReadMessage;
|
|
|
|
// ── Server → Client ──────────────────────────────────────────────────────────
|
|
|
|
export interface OnlineUser {
|
|
userId: string;
|
|
username: string;
|
|
hasAvatar: boolean;
|
|
avatarVersion?: number;
|
|
}
|
|
|
|
export interface WelcomeMessage {
|
|
type: "welcome";
|
|
users: OnlineUser[];
|
|
myVotes: string[];
|
|
myCommentLikes: string[];
|
|
unreadNotificationCount: number;
|
|
/** The user's chat read marker and the unread count behind it (signed-in
|
|
* users only). */
|
|
chatReadAt?: string;
|
|
unreadChatCount: number;
|
|
}
|
|
|
|
export interface PresenceUpdateMessage {
|
|
type: "presence_update";
|
|
users: OnlineUser[];
|
|
}
|
|
|
|
export interface VotesUpdateMessage {
|
|
type: "votes_update";
|
|
dumpId: string;
|
|
voteCount: number;
|
|
voterId: string;
|
|
action: "cast" | "remove";
|
|
}
|
|
|
|
export interface VoteAckMessage {
|
|
type: "vote_ack";
|
|
dumpId: string;
|
|
action: "cast" | "remove";
|
|
voteCount: number;
|
|
}
|
|
|
|
export interface CommentLikesUpdateMessage {
|
|
type: "comment_likes_update";
|
|
commentId: string;
|
|
likeCount: number;
|
|
likerId: string;
|
|
action: "cast" | "remove";
|
|
}
|
|
|
|
export interface CommentLikeAckMessage {
|
|
type: "comment_like_ack";
|
|
commentId: string;
|
|
action: "cast" | "remove";
|
|
likeCount: number;
|
|
}
|
|
|
|
export interface DumpCreatedMessage {
|
|
type: "dump_created";
|
|
dump: Dump;
|
|
}
|
|
|
|
export interface DumpUpdatedMessage {
|
|
type: "dump_updated";
|
|
dump: Dump;
|
|
}
|
|
|
|
export interface DumpDeletedMessage {
|
|
type: "dump_deleted";
|
|
dumpId: string;
|
|
}
|
|
|
|
export interface PlaylistCreatedMessage {
|
|
type: "playlist_created";
|
|
playlist: Playlist;
|
|
}
|
|
|
|
export interface PlaylistUpdatedMessage {
|
|
type: "playlist_updated";
|
|
playlist: Playlist;
|
|
}
|
|
|
|
export interface PlaylistDeletedMessage {
|
|
type: "playlist_deleted";
|
|
playlistId: string;
|
|
userId: string;
|
|
}
|
|
|
|
export interface PlaylistDumpsUpdatedMessage {
|
|
type: "playlist_dumps_updated";
|
|
playlistId: string;
|
|
/** The members this recipient can see, in order. */
|
|
dumpIds: string[];
|
|
/** Every member, as Playlist.dumpCount counts them. */
|
|
dumpCount: number;
|
|
}
|
|
|
|
export interface UserUpdatedMessage {
|
|
type: "user_updated";
|
|
user: PublicUser;
|
|
}
|
|
|
|
export interface CommentCreatedMessage {
|
|
type: "comment_created";
|
|
comment: Comment;
|
|
}
|
|
|
|
export interface CommentUpdatedMessage {
|
|
type: "comment_updated";
|
|
comment: Comment;
|
|
}
|
|
|
|
export interface CommentDeletedMessage {
|
|
type: "comment_deleted";
|
|
commentId: string;
|
|
dumpId: string;
|
|
}
|
|
|
|
export interface NotificationCreatedMessage {
|
|
type: "notification_created";
|
|
notification: Notification;
|
|
}
|
|
|
|
export interface ErrorMessage {
|
|
type: "error";
|
|
message?: string;
|
|
// Context for optimistic-action failures so the client can revert the exact
|
|
// pending vote/like immediately instead of waiting for its ACK timeout.
|
|
dumpId?: string;
|
|
commentId?: string;
|
|
action?: "cast" | "remove";
|
|
/** Set when a chat action failed, so the client can surface it in the chat. */
|
|
scope?: "chat";
|
|
/** Echo of the failed chat_send's nonce. */
|
|
nonce?: string;
|
|
}
|
|
|
|
export interface ForceLogoutMessage {
|
|
type: "force_logout";
|
|
}
|
|
|
|
export interface ChatMessageMessage {
|
|
type: "chat_message";
|
|
message: ChatMessage;
|
|
/** The sender's nonce, so its pending copy can be retired. */
|
|
nonce?: string;
|
|
}
|
|
|
|
export interface ChatMessageUpdatedMessage {
|
|
type: "chat_message_updated";
|
|
message: ChatMessage;
|
|
}
|
|
|
|
export interface ChatMessageDeletedMessage {
|
|
type: "chat_message_deleted";
|
|
id: string;
|
|
}
|
|
|
|
// The current set of users composing a chat message. Reuses OnlineUser so the
|
|
// client can render the same avatars as the presence row.
|
|
export interface ChatTypingUpdateMessage {
|
|
type: "chat_typing_update";
|
|
users: OnlineUser[];
|
|
}
|
|
|
|
// The user's read marker moved (from any of their tabs or devices).
|
|
export interface ChatReadUpdateMessage {
|
|
type: "chat_read_update";
|
|
readAt: string;
|
|
unread: number;
|
|
}
|
|
|
|
export type ServerToClientMessage =
|
|
| PingMessage
|
|
| PongMessage
|
|
| WelcomeMessage
|
|
| PresenceUpdateMessage
|
|
| VotesUpdateMessage
|
|
| VoteAckMessage
|
|
| CommentLikesUpdateMessage
|
|
| CommentLikeAckMessage
|
|
| DumpCreatedMessage
|
|
| DumpUpdatedMessage
|
|
| DumpDeletedMessage
|
|
| PlaylistCreatedMessage
|
|
| PlaylistUpdatedMessage
|
|
| PlaylistDeletedMessage
|
|
| PlaylistDumpsUpdatedMessage
|
|
| UserUpdatedMessage
|
|
| CommentCreatedMessage
|
|
| CommentUpdatedMessage
|
|
| CommentDeletedMessage
|
|
| NotificationCreatedMessage
|
|
| ErrorMessage
|
|
| ForceLogoutMessage
|
|
| ChatMessageMessage
|
|
| ChatMessageUpdatedMessage
|
|
| ChatMessageDeletedMessage
|
|
| ChatTypingUpdateMessage
|
|
| ChatReadUpdateMessage;
|