mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 00:16:38 +00:00
fix(contacts): consistent sender_id format and show contact_type in UI
- Use senderID (id|username) instead of userID in group no-mention path, preventing duplicate contacts for the same Telegram user - Use full name (FirstName + LastName) in both contact insert paths - Show contact_type (User/Group) instead of peer_kind (Direct/Group) in contacts table TYPE column and filter dropdown - Add contact_type filter support in HTTP handler, PG and SQLite stores
This commit is contained in:
1 parent
36b43ed7f1
commit
4e9ce0e0e1
11 files changed
+44
-17
No files matched your search
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 + "%"
|
||||
|
||||
@@ -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 + "%"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -45,6 +45,10 @@
|
||||
"unmergeError": "取消合并失败",
|
||||
"comingSoon": "此功能正在开发中,尚不可用。"
|
||||
},
|
||||
"types": {
|
||||
"user": "用户",
|
||||
"group": "群组"
|
||||
},
|
||||
"filters": {
|
||||
"allChannels": "所有Channel",
|
||||
"allTypes": "所有类型",
|
||||
|
||||
@@ -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() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select value={peerKind || "all"} onValueChange={handlePeerKindChange}>
|
||||
<Select value={contactType || "all"} onValueChange={handleContactTypeChange}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">{t("filters.allTypes")}</SelectItem>
|
||||
<SelectItem value="direct">{t("filters.direct")}</SelectItem>
|
||||
<SelectItem value="group">{t("filters.group")}</SelectItem>
|
||||
<SelectItem value="user">{t("types.user")}</SelectItem>
|
||||
<SelectItem value="group">{t("types.group")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -187,8 +187,8 @@ export function ContactsPage() {
|
||||
) : contacts.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={Contact}
|
||||
title={appliedSearch || channelType || peerKind ? t("noMatchTitle") : t("emptyTitle")}
|
||||
description={appliedSearch || channelType || peerKind ? t("noMatchDescription") : t("emptyDescription")}
|
||||
title={appliedSearch || channelType || contactType ? t("noMatchTitle") : t("emptyTitle")}
|
||||
description={appliedSearch || channelType || contactType ? t("noMatchDescription") : t("emptyDescription")}
|
||||
/>
|
||||
) : (
|
||||
<div className="rounded-md border overflow-x-auto">
|
||||
@@ -249,11 +249,9 @@ export function ContactsPage() {
|
||||
<Badge variant="outline" className="text-[11px]">{c.channel_type}</Badge>
|
||||
</td>
|
||||
<td className="px-3 py-2.5">
|
||||
{c.peer_kind && (
|
||||
<Badge variant={c.peer_kind === "direct" ? "default" : "secondary"} className="text-[11px]">
|
||||
{c.peer_kind === "direct" ? t("filters.direct") : t("filters.group")}
|
||||
</Badge>
|
||||
)}
|
||||
<Badge variant={c.contact_type === "user" ? "default" : "secondary"} className="text-[11px]">
|
||||
{c.contact_type === "user" ? t("types.user") : t("types.group")}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-muted-foreground text-xs">
|
||||
{formatDate(c.last_seen_at)}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user