diff --git a/internal/channels/telegram/handlers.go b/internal/channels/telegram/handlers.go index 62373b69..3dda6131 100644 --- a/internal/channels/telegram/handlers.go +++ b/internal/channels/telegram/handlers.go @@ -336,7 +336,7 @@ func (c *Channel) handleMessage(ctx context.Context, update telego.Update) { // Collect contact even when bot is not mentioned (cache prevents DB spam). if cc := c.ContactCollector(); cc != nil { contactName := strings.TrimSpace(user.FirstName + " " + user.LastName) - cc.EnsureContact(ctx, c.Type(), c.Name(), userID, userID, contactName, user.Username, "group", "user") + cc.EnsureContact(ctx, c.Type(), c.Name(), senderID, userID, contactName, user.Username, "group", "user") // Also collect group chat itself as a contact (for group permission / merge). cc.EnsureContact(ctx, c.Type(), c.Name(), chatIDStr, "", message.Chat.Title, "", "group", "group") } @@ -590,7 +590,8 @@ func (c *Channel) handleMessage(ctx context.Context, update telego.Update) { // Collect contact for processed messages (DM + group-mentioned). if cc := c.ContactCollector(); cc != nil { - cc.EnsureContact(ctx, c.Type(), c.Name(), senderID, userID, user.FirstName, user.Username, peerKind, "user") + contactName := strings.TrimSpace(user.FirstName + " " + user.LastName) + cc.EnsureContact(ctx, c.Type(), c.Name(), senderID, userID, contactName, user.Username, peerKind, "user") // Also collect group chat itself as a contact (for group permission / merge). if isGroup { cc.EnsureContact(ctx, c.Type(), c.Name(), chatIDStr, "", message.Chat.Title, "", "group", "group") diff --git a/internal/http/channel_instances.go b/internal/http/channel_instances.go index c5e868f3..6a92ae46 100644 --- a/internal/http/channel_instances.go +++ b/internal/http/channel_instances.go @@ -476,6 +476,9 @@ func (h *ChannelInstancesHandler) handleListContacts(w http.ResponseWriter, r *h if v := r.URL.Query().Get("peer_kind"); v != "" { opts.PeerKind = v } + if v := r.URL.Query().Get("contact_type"); v != "" { + opts.ContactType = v + } if v := r.URL.Query().Get("limit"); v != "" { if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 200 { opts.Limit = n diff --git a/internal/store/contact_store.go b/internal/store/contact_store.go index d3f55c80..a493a2e6 100644 --- a/internal/store/contact_store.go +++ b/internal/store/contact_store.go @@ -30,6 +30,7 @@ type ContactListOpts struct { Search string // ILIKE on display_name, username, sender_id ChannelType string // filter by platform (telegram, discord, etc.) PeerKind string // "direct" or "group" + ContactType string // "user" or "group" Limit int Offset int } diff --git a/internal/store/pg/channel_contacts.go b/internal/store/pg/channel_contacts.go index f052d9c8..073301d1 100644 --- a/internal/store/pg/channel_contacts.go +++ b/internal/store/pg/channel_contacts.go @@ -68,6 +68,11 @@ func contactWhereClause(ctx context.Context, opts store.ContactListOpts) (string args = append(args, opts.PeerKind) argIdx++ } + if opts.ContactType != "" { + conditions = append(conditions, fmt.Sprintf("contact_type = $%d", argIdx)) + args = append(args, opts.ContactType) + argIdx++ + } if opts.Search != "" { escaped := strings.NewReplacer("%", "\\%", "_", "\\_").Replace(opts.Search) pattern := escaped + "%" diff --git a/internal/store/sqlitestore/contacts.go b/internal/store/sqlitestore/contacts.go index 66f3d1fb..93ed8077 100644 --- a/internal/store/sqlitestore/contacts.go +++ b/internal/store/sqlitestore/contacts.go @@ -72,6 +72,10 @@ func contactWhereSQLite(ctx context.Context, opts store.ContactListOpts) (string conditions = append(conditions, "peer_kind = ?") args = append(args, opts.PeerKind) } + if opts.ContactType != "" { + conditions = append(conditions, "contact_type = ?") + args = append(args, opts.ContactType) + } if opts.Search != "" { escaped := strings.NewReplacer("%", "\\%", "_", "\\_").Replace(opts.Search) pattern := escaped + "%" diff --git a/ui/web/src/i18n/locales/en/contacts.json b/ui/web/src/i18n/locales/en/contacts.json index 163a5675..1da4a965 100644 --- a/ui/web/src/i18n/locales/en/contacts.json +++ b/ui/web/src/i18n/locales/en/contacts.json @@ -45,6 +45,10 @@ "unmergeError": "Failed to unmerge contacts", "comingSoon": "This feature is under development and not yet available." }, + "types": { + "user": "User", + "group": "Group" + }, "filters": { "allChannels": "All Channels", "allTypes": "All Types", diff --git a/ui/web/src/i18n/locales/vi/contacts.json b/ui/web/src/i18n/locales/vi/contacts.json index 795432ce..8e0fcbe8 100644 --- a/ui/web/src/i18n/locales/vi/contacts.json +++ b/ui/web/src/i18n/locales/vi/contacts.json @@ -45,6 +45,10 @@ "unmergeError": "Không thể tách liên hệ", "comingSoon": "Tính năng này đang được phát triển và chưa khả dụng." }, + "types": { + "user": "Người dùng", + "group": "Nhóm" + }, "filters": { "allChannels": "Tất cả Channel", "allTypes": "Tất cả loại", diff --git a/ui/web/src/i18n/locales/zh/contacts.json b/ui/web/src/i18n/locales/zh/contacts.json index 26766783..8fb47921 100644 --- a/ui/web/src/i18n/locales/zh/contacts.json +++ b/ui/web/src/i18n/locales/zh/contacts.json @@ -45,6 +45,10 @@ "unmergeError": "取消合并失败", "comingSoon": "此功能正在开发中,尚不可用。" }, + "types": { + "user": "用户", + "group": "群组" + }, "filters": { "allChannels": "所有Channel", "allTypes": "所有类型", diff --git a/ui/web/src/pages/contacts/contacts-page.tsx b/ui/web/src/pages/contacts/contacts-page.tsx index f7f866d0..acb141a2 100644 --- a/ui/web/src/pages/contacts/contacts-page.tsx +++ b/ui/web/src/pages/contacts/contacts-page.tsx @@ -33,7 +33,7 @@ export function ContactsPage() { const [search, setSearch] = useState(""); const [appliedSearch, setAppliedSearch] = useState(""); const [channelType, setChannelType] = useState(""); - const [peerKind, setPeerKind] = useState(""); + const [contactType, setContactType] = useState(""); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(20); @@ -44,7 +44,7 @@ export function ContactsPage() { const { contacts, total, loading, fetching, refresh } = useContacts({ search: appliedSearch || undefined, channelType: channelType || undefined, - peerKind: peerKind || undefined, + contactType: contactType || undefined, limit: pageSize, offset: (page - 1) * pageSize, }); @@ -57,7 +57,7 @@ export function ContactsPage() { // Clear selection on page/filter change useEffect(() => { setSelectedIds(new Set()); - }, [page, pageSize, appliedSearch, channelType, peerKind]); + }, [page, pageSize, appliedSearch, channelType, contactType]); const handleSearchSubmit = (e: React.FormEvent) => { e.preventDefault(); @@ -70,8 +70,8 @@ export function ContactsPage() { setPage(1); }; - const handlePeerKindChange = (val: string) => { - setPeerKind(val === "all" ? "" : val); + const handleContactTypeChange = (val: string) => { + setContactType(val === "all" ? "" : val); setPage(1); }; @@ -149,14 +149,14 @@ export function ContactsPage() { - {t("filters.allTypes")} - {t("filters.direct")} - {t("filters.group")} + {t("types.user")} + {t("types.group")} @@ -187,8 +187,8 @@ export function ContactsPage() { ) : contacts.length === 0 ? ( ) : (
@@ -249,11 +249,9 @@ export function ContactsPage() { {c.channel_type} - {c.peer_kind && ( - - {c.peer_kind === "direct" ? t("filters.direct") : t("filters.group")} - - )} + + {c.contact_type === "user" ? t("types.user") : t("types.group")} + {formatDate(c.last_seen_at)} diff --git a/ui/web/src/pages/contacts/hooks/use-contacts.ts b/ui/web/src/pages/contacts/hooks/use-contacts.ts index a1a66c10..f4ee3129 100644 --- a/ui/web/src/pages/contacts/hooks/use-contacts.ts +++ b/ui/web/src/pages/contacts/hooks/use-contacts.ts @@ -10,6 +10,7 @@ export interface ContactFilters { search?: string; channelType?: string; peerKind?: string; + contactType?: string; limit?: number; offset?: number; } @@ -27,6 +28,7 @@ export function useContacts(filters: ContactFilters = {}) { if (filters.search) params.search = filters.search; if (filters.channelType) params.channel_type = filters.channelType; if (filters.peerKind) params.peer_kind = filters.peerKind; + if (filters.contactType) params.contact_type = filters.contactType; if (filters.limit) params.limit = String(filters.limit); if (filters.offset !== undefined) params.offset = String(filters.offset); diff --git a/ui/web/src/types/contact.ts b/ui/web/src/types/contact.ts index a6db8dec..655abf31 100644 --- a/ui/web/src/types/contact.ts +++ b/ui/web/src/types/contact.ts @@ -19,6 +19,7 @@ export interface ChannelContact { username?: string; avatar_url?: string; peer_kind?: string; + contact_type: string; // "user" or "group" merged_id?: string; first_seen_at: string; last_seen_at: string;