feat: Implement Share Modal for document sharing functionality
- Added ShareModal component to manage user and link sharing for documents. - Created AuthContext to handle user authentication state and token management. - Updated useYjsDocument hook to support sharing via tokens. - Enhanced Yjs document creation to include user information and authentication tokens. - Introduced AuthCallback page to handle authentication redirects and token processing. - Modified EditorPage and KanbanPage to include share functionality. - Created LoginPage with Google and GitHub authentication options. - Added styles for LoginPage. - Defined types for authentication and sharing in respective TypeScript files.
This commit is contained in:
@@ -30,11 +30,14 @@ type Client struct {
|
||||
unregisterOnce sync.Once
|
||||
failureCount int
|
||||
failureMu sync.Mutex
|
||||
observedYjsIDs map[uint64]uint64 // clientID -> maxClock
|
||||
idsMu sync.Mutex
|
||||
}
|
||||
type Room struct {
|
||||
ID string
|
||||
clients map[*Client]bool
|
||||
mu sync.RWMutex
|
||||
ID string
|
||||
clients map[*Client]bool
|
||||
mu sync.RWMutex
|
||||
lastAwareness []byte // 存储最新的 awareness 消息,用于新用户加入时立即同步
|
||||
}
|
||||
|
||||
type Hub struct {
|
||||
@@ -49,7 +52,7 @@ func NewHub() *Hub {
|
||||
rooms: make(map[string]*Room),
|
||||
Register: make(chan *Client),
|
||||
Unregister: make(chan *Client),
|
||||
Broadcast: make(chan *Message),
|
||||
Broadcast: make(chan *Message, 1024),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,26 +84,39 @@ func (h *Hub) registerClient(client *Client) {
|
||||
}
|
||||
room.mu.Lock()
|
||||
room.clients[client] = true
|
||||
// 获取现有的 awareness 数据(如果有的话)
|
||||
awarenessData := room.lastAwareness
|
||||
room.mu.Unlock()
|
||||
log.Printf("Client %s joined room %s (total clients: %d)", client.ID, client.roomID, len(room.clients))
|
||||
|
||||
// 如果房间有之前的 awareness 状态,立即发送给新用户
|
||||
// 这样新用户不需要等待其他用户的下一次广播就能看到在线用户
|
||||
if len(awarenessData) > 0 {
|
||||
select {
|
||||
case client.send <- awarenessData:
|
||||
log.Printf("📤 Sent existing awareness to new client %s", client.ID)
|
||||
default:
|
||||
log.Printf("⚠️ Failed to send awareness to new client %s (channel full)", client.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
func (h *Hub) unregisterClient(client *Client) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
// ---------------------------------------------------
|
||||
// 注意:这里不要用 defer h.mu.Unlock(),因为我们要手动控制锁
|
||||
// ---------------------------------------------------
|
||||
|
||||
room, exists := h.rooms[client.roomID]
|
||||
if !exists {
|
||||
log.Printf("Room %s does not exist for client %s", client.roomID, client.ID)
|
||||
h.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
room.mu.Lock()
|
||||
defer room.mu.Unlock()
|
||||
|
||||
room.mu.Lock() // 锁住房间
|
||||
if _, ok := room.clients[client]; ok {
|
||||
delete(room.clients, client)
|
||||
|
||||
// Safely close send channel exactly once
|
||||
|
||||
// 关闭连接通道
|
||||
client.sendMu.Lock()
|
||||
if !client.sendClosed {
|
||||
close(client.send)
|
||||
@@ -108,14 +124,70 @@ func (h *Hub) unregisterClient(client *Client) {
|
||||
}
|
||||
client.sendMu.Unlock()
|
||||
|
||||
log.Printf("Client %s disconnected from room %s (total clients: %d)",
|
||||
client.ID, client.roomID, len(room.clients))
|
||||
log.Printf("Client disconnected: %s", client.ID)
|
||||
}
|
||||
|
||||
// 检查房间是否还有其他人
|
||||
remainingClientsCount := len(room.clients)
|
||||
room.mu.Unlock() // 解锁房间 (我们已经删完人了)
|
||||
|
||||
// ---------------------------------------------------
|
||||
// [新增] 僵尸用户清理逻辑 (核心修改)
|
||||
// ---------------------------------------------------
|
||||
|
||||
// 只有当房间里还有其他人时,才需要广播通知
|
||||
if remainingClientsCount > 0 {
|
||||
// 1. 从 client 的小本本里取出它用过的 Yjs ID 和对应的 clock
|
||||
client.idsMu.Lock()
|
||||
clientClocks := make(map[uint64]uint64, len(client.observedYjsIDs))
|
||||
for id, clock := range client.observedYjsIDs {
|
||||
clientClocks[id] = clock
|
||||
}
|
||||
client.idsMu.Unlock()
|
||||
|
||||
// 2. 如果有记录到的 ID,就伪造删除消息 (使用 clock+1)
|
||||
if len(clientClocks) > 0 {
|
||||
deleteMsg := MakeYjsDeleteMessage(clientClocks) // 调用工具函数,传入 clientID -> clock map
|
||||
|
||||
log.Printf("🧹 Notifying others to remove Yjs IDs with clocks: %v", clientClocks)
|
||||
|
||||
// 3. 广播给房间里的幸存者
|
||||
// 构造一个消息对象
|
||||
msg := &Message{
|
||||
RoomID: client.roomID,
|
||||
Data: deleteMsg,
|
||||
sender: nil, // sender 设为 nil,表示系统消息
|
||||
}
|
||||
|
||||
// !!特别注意!!
|
||||
// 不要在这里直接调用 h.broadcastMessage(msg),因为那会尝试重新获取 h.mu 锁导致死锁
|
||||
// 我们直接把它扔到 Channel 里,让 Run() 去处理
|
||||
// 必须在一个非阻塞的 goroutine 里发,或者确保 channel 有缓冲
|
||||
|
||||
go func() {
|
||||
// 使用 select 尝试发送,但如果满了,我们要稍微等一下,而不是直接丢弃
|
||||
// 因为这是“清理僵尸”的关键消息,丢了就会出 Bug
|
||||
select {
|
||||
case h.Broadcast <- msg:
|
||||
// 发送成功
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
// 如果 500ms 还没塞进去,那说明系统真的挂了,只能丢弃并打印错误
|
||||
log.Printf("❌ Critical: Failed to broadcast cleanup message (Channel blocked)")
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
if len(room.clients) == 0 {
|
||||
// ---------------------------------------------------
|
||||
// 结束清理逻辑
|
||||
// ---------------------------------------------------
|
||||
|
||||
if remainingClientsCount == 0 {
|
||||
delete(h.rooms, client.roomID)
|
||||
log.Printf("Deleted empty room with ID: %s", client.roomID)
|
||||
log.Printf("Room destroyed: %s", client.roomID)
|
||||
}
|
||||
|
||||
h.mu.Unlock() // 最后解锁 Hub
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -134,6 +206,14 @@ func (h *Hub) broadcastMessage(message *Message) {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是 awareness 消息 (type=1),保存它供新用户使用
|
||||
if len(message.Data) > 0 && message.Data[0] == 1 {
|
||||
room.mu.Lock()
|
||||
room.lastAwareness = make([]byte, len(message.Data))
|
||||
copy(room.lastAwareness, message.Data)
|
||||
room.mu.Unlock()
|
||||
}
|
||||
|
||||
room.mu.RLock()
|
||||
defer room.mu.RUnlock()
|
||||
|
||||
@@ -171,29 +251,59 @@ func (h *Hub) broadcastMessage(message *Message) {
|
||||
|
||||
|
||||
func (c *Client) ReadPump() {
|
||||
c.Conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||
c.Conn.SetPongHandler(func(string) error {
|
||||
c.Conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||
return nil
|
||||
})
|
||||
defer func() {
|
||||
c.unregister()
|
||||
c.Conn.Close()
|
||||
}()
|
||||
for {
|
||||
messageType, message, err := c.Conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("Error reading message from client %s: %v", c.ID, err)
|
||||
break
|
||||
}
|
||||
if messageType == websocket.BinaryMessage {
|
||||
c.hub.Broadcast <- &Message{
|
||||
RoomID: c.roomID,
|
||||
Data: message,
|
||||
sender: c,
|
||||
}
|
||||
}
|
||||
}
|
||||
c.Conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||
c.Conn.SetPongHandler(func(string) error {
|
||||
c.Conn.SetReadDeadline(time.Now().Add(pongWait))
|
||||
return nil
|
||||
})
|
||||
defer func() {
|
||||
c.unregister()
|
||||
c.Conn.Close()
|
||||
}()
|
||||
|
||||
for {
|
||||
messageType, message, err := c.Conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
log.Printf("error: %v", err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// 1. 偷听逻辑 (Sniff) - 必须放在转发之前!
|
||||
// ==========================================================
|
||||
if messageType == websocket.BinaryMessage && len(message) > 0 && message[0] == 1 {
|
||||
clockMap := SniffYjsClientIDs(message)
|
||||
if len(clockMap) > 0 {
|
||||
c.idsMu.Lock()
|
||||
for id, clock := range clockMap {
|
||||
if clock > c.observedYjsIDs[id] {
|
||||
log.Printf("🕵️ [Sniff] Client %s uses YjsID: %d (clock: %d)", c.ID, id, clock)
|
||||
c.observedYjsIDs[id] = clock
|
||||
}
|
||||
}
|
||||
c.idsMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================
|
||||
// 2. 转发逻辑 (Broadcast) - 恢复协作功能
|
||||
// ==========================================================
|
||||
if messageType == websocket.BinaryMessage {
|
||||
// 注意:这里要检查 channel 是否已满,避免阻塞导致 ReadPump 卡死
|
||||
select {
|
||||
case c.hub.Broadcast <- &Message{
|
||||
RoomID: c.roomID,
|
||||
Data: message,
|
||||
sender: c,
|
||||
}:
|
||||
// 发送成功
|
||||
default:
|
||||
log.Printf("⚠️ Hub broadcast channel is full, dropping message from %s", c.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) WritePump() {
|
||||
@@ -238,9 +348,10 @@ func NewClient(id string, userID *uuid.UUID, userName string, userAvatar *string
|
||||
UserName: userName,
|
||||
UserAvatar: userAvatar,
|
||||
Conn: conn,
|
||||
send: make(chan []byte, 256),
|
||||
send: make(chan []byte, 1024),
|
||||
hub: hub,
|
||||
roomID: roomID,
|
||||
observedYjsIDs: make(map[uint64]uint64),
|
||||
}
|
||||
}
|
||||
func (c *Client) unregister() {
|
||||
|
||||
99
backend/internal/hub/yjs_protocol.go
Normal file
99
backend/internal/hub/yjs_protocol.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package hub
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
// 这个函数用来从前端发来的二进制数据里,提取出 Yjs 的 ClientID 和对应的 Clock
|
||||
// data: 前端发来的 []byte
|
||||
// return: map[clientID]clock
|
||||
func SniffYjsClientIDs(data []byte) map[uint64]uint64 {
|
||||
// 简单的防御:如果不是 Type 1 (Awareness) 消息,直接忽略
|
||||
if len(data) < 2 || data[0] != 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make(map[uint64]uint64)
|
||||
offset := 1 // 跳过 [0] MessageType
|
||||
|
||||
// 读取总长度 (跳过)
|
||||
_, n := binary.Uvarint(data[offset:])
|
||||
if n <= 0 { return nil }
|
||||
offset += n
|
||||
|
||||
// 读取 Count (包含几个客户端的信息)
|
||||
count, n := binary.Uvarint(data[offset:])
|
||||
if n <= 0 { return nil }
|
||||
offset += n
|
||||
|
||||
// 循环读取每个客户端的信息
|
||||
for i := 0; i < int(count); i++ {
|
||||
if offset >= len(data) { break }
|
||||
|
||||
// 1. 读取 ClientID
|
||||
id, n := binary.Uvarint(data[offset:])
|
||||
if n <= 0 { break }
|
||||
offset += n
|
||||
|
||||
// 2. 读取 Clock (现在我们需要保存它!)
|
||||
clock, n := binary.Uvarint(data[offset:])
|
||||
if n <= 0 { break }
|
||||
offset += n
|
||||
|
||||
// 保存 clientID -> clock
|
||||
result[id] = clock
|
||||
|
||||
// 3. 跳过 JSON String
|
||||
if offset >= len(data) { break }
|
||||
strLen, n := binary.Uvarint(data[offset:])
|
||||
if n <= 0 { break }
|
||||
offset += n
|
||||
|
||||
// 跳过具体字符串内容
|
||||
offset += int(strLen)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// 这个函数用来伪造一条"删除消息"
|
||||
// 输入:clientClocks map[clientID]clock - 要删除的 ClientID 及其最后已知的 clock 值
|
||||
// 输出:可以广播给前端的 []byte
|
||||
func MakeYjsDeleteMessage(clientClocks map[uint64]uint64) []byte {
|
||||
if len(clientClocks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 构造 Payload (负载)
|
||||
// 格式: [Count] [ID] [Clock] [StringLen] [String] ...
|
||||
payload := make([]byte, 0)
|
||||
|
||||
// 写入 Count (变长整数)
|
||||
buf := make([]byte, 10)
|
||||
n := binary.PutUvarint(buf, uint64(len(clientClocks)))
|
||||
payload = append(payload, buf[:n]...)
|
||||
|
||||
for id, clock := range clientClocks {
|
||||
// ClientID
|
||||
n = binary.PutUvarint(buf, id)
|
||||
payload = append(payload, buf[:n]...)
|
||||
// Clock: 必须使用 clock + 1,这样 Yjs 才会接受这个更新!
|
||||
n = binary.PutUvarint(buf, clock+1)
|
||||
payload = append(payload, buf[:n]...)
|
||||
// String Length (填 4,因为 "null" 长度是 4)
|
||||
n = binary.PutUvarint(buf, 4)
|
||||
payload = append(payload, buf[:n]...)
|
||||
// String Content (这里是关键:null 代表删除用户)
|
||||
payload = append(payload, []byte("null")...)
|
||||
}
|
||||
|
||||
// 构造最终消息: [Type=1] [PayloadLength] [Payload]
|
||||
finalMsg := make([]byte, 0)
|
||||
finalMsg = append(finalMsg, 1) // Type 1
|
||||
|
||||
n = binary.PutUvarint(buf, uint64(len(payload)))
|
||||
finalMsg = append(finalMsg, buf[:n]...) // Length
|
||||
finalMsg = append(finalMsg, payload...) // Body
|
||||
|
||||
return finalMsg
|
||||
}
|
||||
Reference in New Issue
Block a user