diff --git a/server/router/api/v1/memo_service.go b/server/router/api/v1/memo_service.go index 47f3a9c36..70a6a2790 100644 --- a/server/router/api/v1/memo_service.go +++ b/server/router/api/v1/memo_service.go @@ -142,8 +142,8 @@ func (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoR } // Broadcast live refresh event. - s.SSEHub.Broadcast(&MemoEvent{ - Type: MemoEventCreated, + s.SSEHub.Broadcast(&SSEEvent{ + Type: SSEEventMemoCreated, Name: memoMessage.Name, }) @@ -478,8 +478,8 @@ func (s *APIV1Service) UpdateMemo(ctx context.Context, request *v1pb.UpdateMemoR } // Broadcast live refresh event. - s.SSEHub.Broadcast(&MemoEvent{ - Type: MemoEventUpdated, + s.SSEHub.Broadcast(&SSEEvent{ + Type: SSEEventMemoUpdated, Name: memoMessage.Name, }) @@ -552,8 +552,8 @@ func (s *APIV1Service) DeleteMemo(ctx context.Context, request *v1pb.DeleteMemoR } // Broadcast live refresh event. - s.SSEHub.Broadcast(&MemoEvent{ - Type: MemoEventDeleted, + s.SSEHub.Broadcast(&SSEEvent{ + Type: SSEEventMemoDeleted, Name: request.Name, }) diff --git a/server/router/api/v1/reaction_service.go b/server/router/api/v1/reaction_service.go index a7c7cc3bd..a4c521fe8 100644 --- a/server/router/api/v1/reaction_service.go +++ b/server/router/api/v1/reaction_service.go @@ -97,6 +97,12 @@ func (s *APIV1Service) UpsertMemoReaction(ctx context.Context, request *v1pb.Ups reactionMessage := convertReactionFromStore(reaction) + // Broadcast live refresh event (reaction belongs to a memo). + s.SSEHub.Broadcast(&SSEEvent{ + Type: SSEEventReactionUpserted, + Name: request.Reaction.ContentId, + }) + return reactionMessage, nil } @@ -136,6 +142,12 @@ func (s *APIV1Service) DeleteMemoReaction(ctx context.Context, request *v1pb.Del return nil, status.Errorf(codes.Internal, "failed to delete reaction") } + // Broadcast live refresh event (reaction belongs to a memo). + s.SSEHub.Broadcast(&SSEEvent{ + Type: SSEEventReactionDeleted, + Name: reaction.ContentID, + }) + return &emptypb.Empty{}, nil } diff --git a/server/router/api/v1/sse_hub.go b/server/router/api/v1/sse_hub.go index accebe97a..c75c3cc2c 100644 --- a/server/router/api/v1/sse_hub.go +++ b/server/router/api/v1/sse_hub.go @@ -5,24 +5,27 @@ import ( "sync" ) -// MemoEventType represents the type of memo change event. -type MemoEventType string +// SSEEventType represents the type of change event. +type SSEEventType string const ( - MemoEventCreated MemoEventType = "memo.created" - MemoEventUpdated MemoEventType = "memo.updated" - MemoEventDeleted MemoEventType = "memo.deleted" + SSEEventMemoCreated SSEEventType = "memo.created" + SSEEventMemoUpdated SSEEventType = "memo.updated" + SSEEventMemoDeleted SSEEventType = "memo.deleted" + SSEEventReactionUpserted SSEEventType = "reaction.upserted" + SSEEventReactionDeleted SSEEventType = "reaction.deleted" ) -// MemoEvent represents a memo change event sent to SSE clients. -type MemoEvent struct { - Type MemoEventType `json:"type"` - // Name is the memo resource name (e.g., "memos/xxxx"). +// SSEEvent represents a change event sent to SSE clients. +type SSEEvent struct { + Type SSEEventType `json:"type"` + // Name is the affected resource name (e.g., "memos/xxxx"). + // For reaction events, this is the memo resource name that the reaction belongs to. Name string `json:"name"` } // JSON returns the JSON representation of the event. -func (e *MemoEvent) JSON() []byte { +func (e *SSEEvent) JSON() []byte { data, _ := json.Marshal(e) return data } @@ -72,7 +75,7 @@ func (h *SSEHub) Unsubscribe(c *sseClient) { // Broadcast sends an event to all connected clients. // Slow clients that have a full buffer will have the event dropped // to avoid blocking the broadcaster. -func (h *SSEHub) Broadcast(event *MemoEvent) { +func (h *SSEHub) Broadcast(event *SSEEvent) { data := event.JSON() h.mu.RLock() defer h.mu.RUnlock() diff --git a/web/src/hooks/useLiveMemoRefresh.ts b/web/src/hooks/useLiveMemoRefresh.ts index e1a220d40..8dc9c40c1 100644 --- a/web/src/hooks/useLiveMemoRefresh.ts +++ b/web/src/hooks/useLiveMemoRefresh.ts @@ -81,7 +81,7 @@ export function useLiveMemoRefresh() { const jsonStr = line.slice(6); try { const event = JSON.parse(jsonStr) as { type: string; name: string }; - handleMemoEvent(event, queryClient); + handleSSEEvent(event, queryClient); } catch { // Ignore malformed JSON. } @@ -119,12 +119,12 @@ export function useLiveMemoRefresh() { }, [queryClient]); } -interface MemoChangeEvent { +interface SSEChangeEvent { type: string; name: string; } -function handleMemoEvent(event: MemoChangeEvent, queryClient: ReturnType) { +function handleSSEEvent(event: SSEChangeEvent, queryClient: ReturnType) { switch (event.type) { case "memo.created": // Invalidate memo lists so new memos appear. @@ -148,5 +148,13 @@ function handleMemoEvent(event: MemoChangeEvent, queryClient: ReturnType