From bf98a32e9b3b6dc34da86cbf5c2a1fe692b0b88b Mon Sep 17 00:00:00 2001 From: viettranx Date: Thu, 2 Apr 2026 08:22:57 +0700 Subject: [PATCH] feat(store): add thread support to contact model and collector - Add threadID and threadType to ContactCollector.EnsureContact - Update ContactStore.UpsertContact signature - Update Contact type in web UI --- internal/store/contact_collector.go | 9 +++++---- internal/store/contact_store.go | 9 ++++++--- ui/web/src/types/contact.ts | 4 +++- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/internal/store/contact_collector.go b/internal/store/contact_collector.go index 928f4462..02e760d3 100644 --- a/internal/store/contact_collector.go +++ b/internal/store/contact_collector.go @@ -23,16 +23,17 @@ func NewContactCollector(s ContactStore, c cache.Cache[bool]) *ContactCollector } // EnsureContact creates or refreshes a contact entry, skipping DB if recently seen. -// contactType: "user" (individual sender) or "group" (group chat entity). -func (c *ContactCollector) EnsureContact(ctx context.Context, channelType, channelInstance, senderID, userID, displayName, username, peerKind, contactType string) { - key := channelType + ":" + senderID +// contactType: "user" (individual sender), "group" (group chat entity), or "topic" (forum topic). +// Pass empty threadID/threadType for base contacts (DM, group root). +func (c *ContactCollector) EnsureContact(ctx context.Context, channelType, channelInstance, senderID, userID, displayName, username, peerKind, contactType, threadID, threadType string) { + key := channelType + ":" + senderID + ":" + threadID if _, ok := c.seen.Get(ctx, key); ok { return } if contactType == "" { contactType = "user" } - if err := c.store.UpsertContact(ctx, channelType, channelInstance, senderID, userID, displayName, username, peerKind, contactType); err != nil { + if err := c.store.UpsertContact(ctx, channelType, channelInstance, senderID, userID, displayName, username, peerKind, contactType, threadID, threadType); err != nil { slog.Warn("contact_collector.upsert_failed", "error", err, "channel", channelType, "sender", senderID) return } diff --git a/internal/store/contact_store.go b/internal/store/contact_store.go index a493a2e6..09641f53 100644 --- a/internal/store/contact_store.go +++ b/internal/store/contact_store.go @@ -19,7 +19,9 @@ type ChannelContact struct { Username *string `json:"username,omitempty"` AvatarURL *string `json:"avatar_url,omitempty"` PeerKind *string `json:"peer_kind,omitempty"` - ContactType string `json:"contact_type"` // "user" or "group" + ContactType string `json:"contact_type"` // "user", "group", or "topic" + ThreadID *string `json:"thread_id,omitempty"` + ThreadType *string `json:"thread_type,omitempty"` MergedID *uuid.UUID `json:"merged_id,omitempty"` FirstSeenAt time.Time `json:"first_seen_at"` LastSeenAt time.Time `json:"last_seen_at"` @@ -37,9 +39,10 @@ type ContactListOpts struct { // ContactStore manages channel contacts (auto-collected user info). type ContactStore interface { - // UpsertContact creates or updates a contact. On conflict (channel_type, sender_id), + // UpsertContact creates or updates a contact. On conflict (tenant_id, channel_type, sender_id, thread_id), // updates display_name, username, user_id, channel_instance, and last_seen_at. - UpsertContact(ctx context.Context, channelType, channelInstance, senderID, userID, displayName, username, peerKind, contactType string) error + // Pass empty threadID/threadType for base contacts (DM, group root). + UpsertContact(ctx context.Context, channelType, channelInstance, senderID, userID, displayName, username, peerKind, contactType, threadID, threadType string) error // ListContacts searches contacts with pagination and filters. ListContacts(ctx context.Context, opts ContactListOpts) ([]ChannelContact, error) diff --git a/ui/web/src/types/contact.ts b/ui/web/src/types/contact.ts index 655abf31..ea7f26d8 100644 --- a/ui/web/src/types/contact.ts +++ b/ui/web/src/types/contact.ts @@ -19,7 +19,9 @@ export interface ChannelContact { username?: string; avatar_url?: string; peer_kind?: string; - contact_type: string; // "user" or "group" + contact_type: string; // "user", "group", or "topic" + thread_id?: string; + thread_type?: string; merged_id?: string; first_seen_at: string; last_seen_at: string;