big update: added reactions, groups, media share
CI / Go (build + vet + test) (push) Canceled after 0s
CI / Web (build + lint) (push) Canceled after 0s

This commit is contained in:
vantablackTea
2026-09-27 21:59:48 +03:00
parent ae24bdd0e0
commit 9d03ea4c51
41 changed files with 2548 additions and 172 deletions
+47 -10
View File
@@ -9,10 +9,16 @@ import (
// Event type constants sent over the wire.
const (
EventMessageCreated = "message.created"
EventTyping = "typing"
EventPresence = "presence"
EventRead = "read"
EventMessageCreated = "message.created"
EventTyping = "typing"
EventPresence = "presence"
EventRead = "read"
EventConversationAdded = "conversation.added"
EventConversationRemoved = "conversation.removed"
EventMemberAdded = "member.added"
EventMemberRemoved = "member.removed"
EventReactionAdded = "reaction.added"
EventReactionRemoved = "reaction.removed"
)
// Presence status values.
@@ -23,12 +29,24 @@ const (
// MessageCreated is published when a message is stored.
type MessageCreated struct {
TempID string `json:"temp_id,omitempty"`
ConversationID int64 `json:"conversation_id"`
MessageID int64 `json:"message_id"`
SenderID int64 `json:"sender_id"`
Body string `json:"body"`
CreatedAt time.Time `json:"created_at"`
TempID string `json:"temp_id,omitempty"`
ConversationID int64 `json:"conversation_id"`
MessageID int64 `json:"message_id"`
SenderID int64 `json:"sender_id"`
Body string `json:"body"`
MediaID *int64 `json:"media_id,omitempty"`
Media *MediaInfo `json:"media,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
// MediaInfo mirrors attachment metadata delivered with a message event.
type MediaInfo struct {
ID int64 `json:"id"`
Filename string `json:"filename"`
ContentType string `json:"content_type"`
Size int64 `json:"size"`
URL string `json:"url"`
ThumbURL string `json:"thumb_url,omitempty"`
}
// Typing is published when a user starts or stops typing in a conversation.
@@ -51,6 +69,25 @@ type Read struct {
MessageID int64 `json:"message_id"`
}
// ConversationChanged is sent to a user when they gain or lose a conversation.
type ConversationChanged struct {
ConversationID int64 `json:"conversation_id"`
}
// MemberChanged is published when a user joins or leaves a conversation.
type MemberChanged struct {
ConversationID int64 `json:"conversation_id"`
UserID int64 `json:"user_id"`
}
// ReactionChanged is published when a reaction is added to or removed from a message.
type ReactionChanged struct {
ConversationID int64 `json:"conversation_id"`
MessageID int64 `json:"message_id"`
UserID int64 `json:"user_id"`
Emoji string `json:"emoji"`
}
// envelope is the wire format for every message, both inbound and outbound.
type envelope struct {
Type string `json:"type"`
+61
View File
@@ -84,6 +84,67 @@ func (h *Hub) PublishRead(conversationID, userID, messageID int64) {
h.sendToConversation(conversationID, envelope{Type: EventRead, Data: mustMarshal(evt)}, userID)
}
// Subscribe adds a user to a conversation's subscriber set so they receive its
// events without reconnecting (used when a member is added to a group).
func (h *Hub) Subscribe(userID, conversationID int64) {
h.mu.Lock()
defer h.mu.Unlock()
if h.subs[conversationID] == nil {
h.subs[conversationID] = make(map[int64]struct{})
}
h.subs[conversationID][userID] = struct{}{}
}
// Unsubscribe removes a user from a conversation's subscriber set.
func (h *Hub) Unsubscribe(userID, conversationID int64) {
h.mu.Lock()
defer h.mu.Unlock()
users := h.subs[conversationID]
delete(users, userID)
if len(users) == 0 {
delete(h.subs, conversationID)
}
}
// SendToUser delivers an event to every connection of a single user.
func (h *Hub) SendToUser(userID int64, evtType string, data any) {
payload, err := json.Marshal(envelope{Type: evtType, Data: mustMarshal(data)})
if err != nil {
slog.Error("ws: marshal direct event", "error", err)
return
}
h.mu.Lock()
defer h.mu.Unlock()
for c := range h.conns[userID] {
c.enqueueRaw(payload)
}
}
// PublishMemberChanged tells a conversation's members that someone joined or left.
func (h *Hub) PublishMemberChanged(conversationID, userID int64, added bool) {
evtType := EventMemberAdded
if !added {
evtType = EventMemberRemoved
}
evt := MemberChanged{ConversationID: conversationID, UserID: userID}
h.sendToConversation(conversationID, envelope{Type: evtType, Data: mustMarshal(evt)}, 0)
}
// PublishReactionChanged tells a conversation's members that a reaction changed.
func (h *Hub) PublishReactionChanged(conversationID, messageID, userID int64, emoji string, added bool) {
evtType := EventReactionAdded
if !added {
evtType = EventReactionRemoved
}
evt := ReactionChanged{
ConversationID: conversationID,
MessageID: messageID,
UserID: userID,
Emoji: emoji,
}
h.sendToConversation(conversationID, envelope{Type: evtType, Data: mustMarshal(evt)}, 0)
}
func (h *Hub) register(c *client, conversationIDs []int64) {
h.mu.Lock()
defer h.mu.Unlock()