Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol diffdown.com

feat: add client representation and presence broadcasting

+105 -3
+78
internal/collaboration/client.go
··· 1 1 package collaboration 2 2 3 3 import ( 4 + "encoding/json" 5 + "log" 6 + 4 7 "github.com/gorilla/websocket" 5 8 ) 6 9 ··· 13 16 Color string 14 17 roomKey string 15 18 } 19 + 20 + type ClientMessage struct { 21 + Type string `json:"type"` 22 + RKey string `json:"rkey,omitempty"` 23 + DID string `json:"did,omitempty"` 24 + Delta json.RawMessage `json:"delta,omitempty"` 25 + Cursor *CursorPos `json:"cursor,omitempty"` 26 + Comment *CommentMsg `json:"comment,omitempty"` 27 + } 28 + 29 + type CursorPos struct { 30 + Position int `json:"position"` 31 + SelectionEnd int `json:"selectionEnd"` 32 + } 33 + 34 + type CommentMsg struct { 35 + ParagraphID string `json:"paragraphId"` 36 + Text string `json:"text"` 37 + } 38 + 39 + type PresenceUser struct { 40 + DID string `json:"did"` 41 + Name string `json:"name"` 42 + Color string `json:"color"` 43 + } 44 + 45 + type PresenceMessage struct { 46 + Type string `json:"type"` 47 + Users []PresenceUser `json:"users"` 48 + } 49 + 50 + func NewClient(hub *Hub, conn *websocket.Conn, did, name, color, roomKey string) *Client { 51 + return &Client{ 52 + hub: hub, 53 + conn: conn, 54 + send: make(chan []byte, 256), 55 + DID: did, 56 + Name: name, 57 + Color: color, 58 + roomKey: roomKey, 59 + } 60 + } 61 + 62 + func (c *Client) ReadPump() { 63 + defer func() { 64 + if room := c.hub.GetRoom(c.roomKey); room != nil { 65 + room.unregister <- c 66 + } 67 + c.conn.Close() 68 + }() 69 + for { 70 + _, message, err := c.conn.ReadMessage() 71 + if err != nil { 72 + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { 73 + log.Printf("WebSocket error: %v", err) 74 + } 75 + break 76 + } 77 + _ = message 78 + } 79 + } 80 + 81 + func (c *Client) WritePump() { 82 + defer c.conn.Close() 83 + for { 84 + message, ok := <-c.send 85 + if !ok { 86 + c.conn.WriteMessage(websocket.CloseMessage, []byte{}) 87 + return 88 + } 89 + if err := c.conn.WriteMessage(websocket.TextMessage, message); err != nil { 90 + return 91 + } 92 + } 93 + }
+27 -3
internal/collaboration/hub.go
··· 1 1 package collaboration 2 2 3 3 import ( 4 + "encoding/json" 4 5 "sync" 5 6 ) 6 7 ··· 42 43 return room 43 44 } 44 45 46 + func (h *Hub) GetRoom(rkey string) *Room { 47 + h.mu.RLock() 48 + defer h.mu.RUnlock() 49 + return h.rooms[rkey] 50 + } 51 + 45 52 func (r *Room) run() { 46 53 for { 47 54 select { ··· 49 56 r.mu.Lock() 50 57 r.clients[client] = true 51 58 r.mu.Unlock() 52 - r.broadcastPresence() 53 59 case client := <-r.unregister: 54 60 r.mu.Lock() 55 61 if _, ok := r.clients[client]; ok { ··· 57 63 close(client.send) 58 64 } 59 65 r.mu.Unlock() 60 - r.broadcastPresence() 61 66 case message := <-r.broadcast: 62 67 r.mu.RLock() 63 68 for client := range r.clients { ··· 77 82 r.broadcast <- message 78 83 } 79 84 85 + func (r *Room) GetPresence() []PresenceUser { 86 + r.mu.RLock() 87 + defer r.mu.RUnlock() 88 + users := make([]PresenceUser, 0, len(r.clients)) 89 + for client := range r.clients { 90 + users = append(users, PresenceUser{ 91 + DID: client.DID, 92 + Name: client.Name, 93 + Color: client.Color, 94 + }) 95 + } 96 + return users 97 + } 98 + 80 99 func (r *Room) broadcastPresence() { 81 - // Implementation in Task 2.2 100 + presence := PresenceMessage{ 101 + Type: "presence", 102 + Users: r.GetPresence(), 103 + } 104 + data, _ := json.Marshal(presence) 105 + r.Broadcast(data) 82 106 }