diff --git a/app/[locale]/calendar/page.tsx b/app/[locale]/calendar/page.tsx index df64b196..15870554 100644 --- a/app/[locale]/calendar/page.tsx +++ b/app/[locale]/calendar/page.tsx @@ -611,6 +611,7 @@ export default function CalendarPage() { const visibleEvents = useMemo(() => events.filter((e) => { + if (!e.calendarIds) return false; const calIds = Object.keys(e.calendarIds); return calIds.some((id) => selectedCalendarIds.includes(id)); }), diff --git a/app/[locale]/contacts/page.tsx b/app/[locale]/contacts/page.tsx index dc4d314f..2b9c32df 100644 --- a/app/[locale]/contacts/page.tsx +++ b/app/[locale]/contacts/page.tsx @@ -13,6 +13,7 @@ import { ContactForm } from "@/components/contacts/contact-form"; import { ContactGroupForm } from "@/components/contacts/contact-group-form"; import { ContactGroupDetail } from "@/components/contacts/contact-group-detail"; import { ContactsSidebar, type ContactCategory } from "@/components/contacts/contacts-sidebar"; +import { ContactImportDialog } from "@/components/contacts/contact-import-dialog"; import { exportContacts } from "@/components/contacts/contact-export"; import { useContactStore, getContactDisplayName } from "@/stores/contact-store"; import { useAuthStore } from "@/stores/auth-store"; @@ -73,10 +74,12 @@ export default function ContactsPage() { bulkDeleteContacts, bulkAddToGroup, moveContactToAddressBook, + importContacts, } = useContactStore(); const [view, setView] = useState("list"); const [activeCategory, setActiveCategory] = useState("all"); + const [showImportDialog, setShowImportDialog] = useState(false); const [selectedGroupId, setSelectedGroupId] = useState(null); const hasFetched = useRef(false); const { dialogProps: confirmDialogProps, confirm: confirmDialog } = useConfirmDialog(); @@ -84,10 +87,10 @@ export default function ContactsPage() { // Panel resize state - sidebar (categories) const [sidebarWidth, setSidebarWidth] = useState(() => { - try { const v = localStorage.getItem("contacts-sidebar-width"); return v ? Number(v) : 180; } catch { return 180; } + try { const v = localStorage.getItem("contacts-sidebar-width"); return v ? Number(v) : 256; } catch { return 256; } }); const [isSidebarResizing, setIsSidebarResizing] = useState(false); - const sidebarDragStartWidth = useRef(180); + const sidebarDragStartWidth = useRef(256); // Panel resize state - contact list const [listWidth, setListWidth] = useState(() => { @@ -125,21 +128,17 @@ export default function ContactsPage() { // Contacts to display based on active category const displayedContacts = useMemo(() => { - if (activeCategory === "all") return individuals.filter(c => !c.isShared); + if (activeCategory === "all") return individuals; if ("addressBookId" in activeCategory) { const bookId = activeCategory.addressBookId; return individuals.filter(c => { if (!c.addressBookIds) return false; - // Check both namespaced (accountId:bookId) and raw bookId - if (c.addressBookIds[bookId]) return true; - // For shared contacts, match namespaced id - if (c.isShared && c.accountId) { - const namespacedId = `${c.accountId}:${Object.keys(c.addressBookIds).find(k => c.addressBookIds[k])}`; - return namespacedId === bookId; - } - return false; + return c.addressBookIds[bookId] === true; }); } + if ("keyword" in activeCategory) { + return individuals.filter(c => c.keywords?.[activeCategory.keyword]); + } // Show members of the selected group return getGroupMembers(activeCategory.groupId); }, [activeCategory, individuals, getGroupMembers]); @@ -151,6 +150,9 @@ export default function ContactsPage() { const book = addressBooks.find(b => b.id === activeCategory.addressBookId); return book?.name || t("tabs.all"); } + if ("keyword" in activeCategory) { + return activeCategory.keyword; + } const group = contacts.find(c => c.id === activeCategory.groupId); return group ? getContactDisplayName(group) : t("tabs.all"); }, [activeCategory, contacts, addressBooks, t]); @@ -160,6 +162,7 @@ export default function ContactsPage() { clearSelection(); if (typeof category === "object" && "groupId" in category) { setSelectedGroupId(category.groupId); + setView("group-detail"); } else { setSelectedGroupId(null); } @@ -179,6 +182,13 @@ export default function ContactsPage() { } }, [client, moveContactToAddressBook, t]); + const handleImportContacts = useCallback(async (importedContacts: ContactCard[]) => { + return importContacts( + supportsSync && client ? client : null, + importedContacts + ); + }, [supportsSync, client, importContacts]); + const handleSelectContact = (id: string) => { setSelectedContact(id); clearSelection(); @@ -273,6 +283,35 @@ export default function ContactsPage() { setView("group-edit"); }; + const handleEditGroupFromSidebar = useCallback((groupId: string) => { + setSelectedGroupId(groupId); + setActiveCategory({ groupId }); + setView("group-edit"); + }, []); + + const handleDeleteGroupFromSidebar = useCallback(async (groupId: string) => { + const confirmed = await confirmDialog({ + title: t("groups.delete_confirm_title"), + message: t("groups.delete_confirm"), + confirmText: t("form.delete"), + variant: "destructive", + }); + if (!confirmed) return; + + try { + await deleteGroup(supportsSync && client ? client : null, groupId); + toast.success(t("toast.deleted")); + if (selectedGroupId === groupId) { + setSelectedGroupId(null); + setActiveCategory("all"); + setView("list"); + } + } catch (error) { + console.error('Failed to delete group:', error); + toast.error(t("toast.error_delete")); + } + }, [confirmDialog, deleteGroup, supportsSync, client, selectedGroupId, t]); + const handleDeleteGroup = async () => { if (!selectedGroup) return; @@ -416,7 +455,6 @@ export default function ContactsPage() { isMobile={isMobile} onSelectMember={(id) => { setSelectedContact(id); - setActiveCategory("all"); setView("detail"); }} /> @@ -548,17 +586,20 @@ export default function ContactsPage() { onSelectCategory={handleSelectCategory} onCreateGroup={handleCreateGroup} onCreateContact={handleCreateNew} + onImport={() => setShowImportDialog(true)} + onEditGroup={handleEditGroupFromSidebar} + onDeleteGroup={handleDeleteGroupFromSidebar} onDropContacts={handleDropContacts} /> { sidebarDragStartWidth.current = sidebarWidth; setIsSidebarResizing(true); }} - onResize={(delta) => setSidebarWidth(Math.max(140, Math.min(300, sidebarDragStartWidth.current + delta)))} + onResize={(delta) => setSidebarWidth(Math.max(180, Math.min(400, sidebarDragStartWidth.current + delta)))} onResizeEnd={() => { setIsSidebarResizing(false); localStorage.setItem("contacts-sidebar-width", String(sidebarWidth)); }} - onDoubleClick={() => { setSidebarWidth(180); localStorage.setItem("contacts-sidebar-width", "180"); }} + onDoubleClick={() => { setSidebarWidth(256); localStorage.setItem("contacts-sidebar-width", "256"); }} /> )} @@ -642,6 +683,17 @@ export default function ContactsPage() { + {showImportDialog && ( +
+
+ setShowImportDialog(false)} + /> +
+
+ )} ); } diff --git a/components/contacts/contacts-sidebar.tsx b/components/contacts/contacts-sidebar.tsx index 8ce903a8..7973c2b3 100644 --- a/components/contacts/contacts-sidebar.tsx +++ b/components/contacts/contacts-sidebar.tsx @@ -1,14 +1,16 @@ "use client"; -import { useMemo, useState, useCallback, type DragEvent } from "react"; +import { useMemo, useState, useCallback, useEffect, useRef, type DragEvent } from "react"; import { useTranslations } from "next-intl"; -import { BookUser, Users, Plus, UserPlus, Share2, Book } from "lucide-react"; +import { BookUser, Users, Plus, Share2, Book, ChevronRight, ChevronDown, UserPlus, UsersRound, Upload, Tag, Pencil, Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { ContextMenu, ContextMenuItem, ContextMenuSeparator } from "@/components/ui/context-menu"; +import { useContextMenu } from "@/hooks/use-context-menu"; import { cn } from "@/lib/utils"; import type { ContactCard, AddressBook } from "@/lib/jmap/types"; import { getContactDisplayName } from "@/stores/contact-store"; -export type ContactCategory = "all" | { groupId: string } | { addressBookId: string }; +export type ContactCategory = "all" | { groupId: string } | { addressBookId: string } | { keyword: string }; interface ContactsSidebarProps { groups: ContactCard[]; @@ -18,10 +20,30 @@ interface ContactsSidebarProps { onSelectCategory: (category: ContactCategory) => void; onCreateGroup: () => void; onCreateContact: () => void; + onImport?: () => void; + onEditGroup?: (groupId: string) => void; + onDeleteGroup?: (groupId: string) => void; onDropContacts?: (contactIds: string[], addressBook: AddressBook) => void; className?: string; } +const COLLAPSED_KEY = "contacts-sidebar-collapsed"; + +function loadCollapsed(): Record { + try { + const v = localStorage.getItem(COLLAPSED_KEY); + return v ? JSON.parse(v) : {}; + } catch { + return {}; + } +} + +function saveCollapsed(state: Record) { + try { + localStorage.setItem(COLLAPSED_KEY, JSON.stringify(state)); + } catch { /* ignore */ } +} + export function ContactsSidebar({ groups, individuals, @@ -30,10 +52,42 @@ export function ContactsSidebar({ onSelectCategory, onCreateGroup, onCreateContact, + onImport, + onEditGroup, + onDeleteGroup, onDropContacts, className, }: ContactsSidebarProps) { const t = useTranslations("contacts"); + const { contextMenu: groupContextMenu, openContextMenu: openGroupContextMenu, closeContextMenu: closeGroupContextMenu, menuRef: groupMenuRef } = useContextMenu(); + + const [collapsed, setCollapsed] = useState>(loadCollapsed); + const [showMenu, setShowMenu] = useState(false); + const menuRef = useRef(null); + const menuBtnRef = useRef(null); + + const toggleSection = useCallback((key: string) => { + setCollapsed(prev => { + const next = { ...prev, [key]: !prev[key] }; + saveCollapsed(next); + return next; + }); + }, []); + + // Close dropdown on outside click + useEffect(() => { + if (!showMenu) return; + const handler = (e: MouseEvent) => { + if ( + menuRef.current && !menuRef.current.contains(e.target as Node) && + menuBtnRef.current && !menuBtnRef.current.contains(e.target as Node) + ) { + setShowMenu(false); + } + }; + document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); + }, [showMenu]); const sortedGroups = useMemo(() => { return [...groups].sort((a, b) => @@ -73,25 +127,96 @@ export function ContactsSidebar({ if (!contact.addressBookIds) continue; for (const bookId of Object.keys(contact.addressBookIds)) { if (!contact.addressBookIds[bookId]) continue; - // Build the full namespaced key - const key = contact.isShared && contact.accountId ? `${contact.accountId}:${bookId}` : bookId; - counts[key] = (counts[key] || 0) + 1; + counts[bookId] = (counts[bookId] || 0) + 1; } } return counts; }, [individuals]); + // Auto-collect keywords from all contacts + const allKeywords = useMemo(() => { + const counts: Record = {}; + for (const contact of individuals) { + if (!contact.keywords) continue; + for (const [kw, active] of Object.entries(contact.keywords)) { + if (!active) continue; + counts[kw] = (counts[kw] || 0) + 1; + } + } + return Object.entries(counts).sort(([a], [b]) => a.localeCompare(b)); + }, [individuals]); + + // Resolve actual group member counts against living contacts + const memberCountByGroup = useMemo(() => { + const counts: Record = {}; + for (const group of groups) { + if (!group.members) { + counts[group.id] = 0; + continue; + } + const memberKeys = Object.keys(group.members).filter(k => group.members![k]); + const normalizedKeys = memberKeys.map(k => k.startsWith('urn:uuid:') ? k.slice(9) : k); + counts[group.id] = individuals.filter(c => { + if (memberKeys.includes(c.id) || normalizedKeys.includes(c.id)) return true; + if (c.uid) { + const bareUid = c.uid.startsWith('urn:uuid:') ? c.uid.slice(9) : c.uid; + return memberKeys.includes(c.uid) || normalizedKeys.includes(bareUid); + } + return false; + }).length; + } + return counts; + }, [groups, individuals]); + return (
{/* Header */}
{t("title")} - +
+ + {showMenu && ( +
+ + + {onImport && ( + + )} +
+ )} +
- {/* Categories */} + {/* Navigation */}
{/* All contacts */} - {/* Personal address books */} + {/* My Address Books */} {personalBooks.length > 0 && (
-
+
- {personalBooks.map((book) => ( + + {!collapsed.addressBooks && personalBooks.map((book) => ( 0) && ( + {sortedGroups.length > 0 && (
-
+ -
+ - {sortedGroups.map((group) => { + {!collapsed.groups && sortedGroups.map((group) => { const isActive = typeof activeCategory === "object" && "groupId" in activeCategory && activeCategory.groupId === group.id; - const memberCount = group.members - ? Object.values(group.members).filter(Boolean).length - : 0; + const memberCount = memberCountByGroup[group.id] || 0; return (
)} - {sortedGroups.length === 0 && ( -
-
- - {t("tabs.groups")} - -
- + {collapsed.categories ? ( + + ) : ( + + )} + + {t("detail.categories")} + + + + {!collapsed.categories && allKeywords.map(([keyword, count]) => { + const isActive = typeof activeCategory === "object" && "keyword" in activeCategory && activeCategory.keyword === keyword; + return ( + + ); + })}
)} {/* Shared accounts with address books */} {sharedBookGroups.map((group) => (
-
- - - {group.accountName} +
- {group.books.map((book) => ( + + {!collapsed[`shared-${group.accountId}`] && group.books.map((book) => ( ))}
+ + {/* Group context menu */} + {groupContextMenu.data && ( + + { + closeGroupContextMenu(); + onEditGroup?.(groupContextMenu.data!.id); + }} + /> + + { + closeGroupContextMenu(); + onDeleteGroup?.(groupContextMenu.data!.id); + }} + destructive + /> + + )}
); } @@ -266,7 +463,7 @@ function AddressBookItem({ onDragLeave={handleDragLeave} onDrop={handleDrop} className={cn( - "w-full flex items-center gap-2 px-3 text-sm transition-colors", + "w-full flex items-center gap-2 pl-5 pr-3 text-sm transition-colors", isActive ? "bg-accent text-accent-foreground font-medium" : "text-foreground/80 hover:bg-muted", diff --git a/components/settings/calendar-management-settings.tsx b/components/settings/calendar-management-settings.tsx index 991ea495..bd92a0cc 100644 --- a/components/settings/calendar-management-settings.tsx +++ b/components/settings/calendar-management-settings.tsx @@ -296,7 +296,7 @@ export function CalendarManagementSettings() { const buildCalDavUrl = (calendarId: string) => { if (!serverUrl || !username) return null; const base = serverUrl.replace(/\/$/, ''); - return `${base}/dav/calendars/user/${encodeURIComponent(username)}/${encodeURIComponent(calendarId)}/`; + return `${base}/dav/cal/${encodeURIComponent(username)}/${encodeURIComponent(calendarId)}/`; }; const handleCopyUrl = async (url: string) => { diff --git a/lib/jmap/client.ts b/lib/jmap/client.ts index be91e659..7c4feb03 100644 --- a/lib/jmap/client.ts +++ b/lib/jmap/client.ts @@ -2479,6 +2479,9 @@ export class JMAPClient { ...contact, id: isPrimary ? contact.id : `${accountId}:${contact.id}`, originalId: contact.id, + addressBookIds: isPrimary ? contact.addressBookIds : (contact.addressBookIds ? Object.fromEntries( + Object.entries(contact.addressBookIds).map(([bookId, v]) => [`${accountId}:${bookId}`, v]) + ) : contact.addressBookIds), accountId, accountName: account?.name || (isPrimary ? this.username : accountId), isShared: !isPrimary, @@ -2851,8 +2854,8 @@ export class JMAPClient { id: isPrimary ? event.id : `${accountId}:${event.id}`, originalId: event.id, originalCalendarIds: event.calendarIds, - calendarIds: isPrimary ? event.calendarIds : Object.fromEntries( - Object.entries(event.calendarIds).map(([calId, v]) => [`${accountId}:${calId}`, v]) + calendarIds: isPrimary ? (event.calendarIds || {}) : Object.fromEntries( + Object.entries(event.calendarIds || {}).map(([calId, v]) => [`${accountId}:${calId}`, v]) ), accountId, accountName: account?.name || (isPrimary ? this.username : accountId), diff --git a/locales/de/common.json b/locales/de/common.json index 20a01f71..f66f385d 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -1473,11 +1473,12 @@ "title": "Geteilt" }, "address_books": { - "title": "Verzeichnisse", + "title": "Meine Adressbücher", + "shared_prefix": "Geteilt: {name}", "moved": "Kontakt verschoben nach {name}", "moved_plural": "{count} Kontakte verschoben nach {name}", "move_failed": "Kontakt konnte nicht verschoben werden", - "address_book": "Verzeichnis" + "address_book": "Adressbuch" }, "detail": { "emails": "E-Mail-Adressen", diff --git a/locales/en/common.json b/locales/en/common.json index 9bd0b09d..8027b176 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -1473,11 +1473,12 @@ "title": "Shared" }, "address_books": { - "title": "Directories", + "title": "My Address Books", + "shared_prefix": "Shared: {name}", "moved": "Contact moved to {name}", "moved_plural": "{count} contacts moved to {name}", "move_failed": "Failed to move contact", - "address_book": "Directory" + "address_book": "Address Book" }, "detail": { "emails": "Email Addresses", diff --git a/locales/es/common.json b/locales/es/common.json index b61f05bc..8bd161bb 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -1473,11 +1473,12 @@ "title": "Compartidos" }, "address_books": { - "title": "Directorios", + "title": "Mis Libretas de Direcciones", + "shared_prefix": "Compartido: {name}", "moved": "Contacto movido a {name}", "moved_plural": "{count} contactos movidos a {name}", "move_failed": "Error al mover el contacto", - "address_book": "Directorio" + "address_book": "Libreta de direcciones" }, "detail": { "emails": "Direcciones de correo", diff --git a/locales/fr/common.json b/locales/fr/common.json index b59f1e53..f4436cd3 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -1473,11 +1473,12 @@ "title": "Partagés" }, "address_books": { - "title": "Répertoires", + "title": "Mes Carnets d'adresses", + "shared_prefix": "Partagé : {name}", "moved": "Contact déplacé vers {name}", "moved_plural": "{count} contacts déplacés vers {name}", "move_failed": "Échec du déplacement du contact", - "address_book": "Répertoire" + "address_book": "Carnet d'adresses" }, "detail": { "emails": "Adresses e-mail", diff --git a/locales/it/common.json b/locales/it/common.json index c39b9006..af38d806 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -1473,7 +1473,8 @@ "title": "Condivisi" }, "address_books": { - "title": "Rubriche", + "title": "Le mie Rubriche", + "shared_prefix": "Condiviso: {name}", "moved": "Contatto spostato in {name}", "moved_plural": "{count} contatti spostati in {name}", "move_failed": "Impossibile spostare il contatto", diff --git a/locales/ja/common.json b/locales/ja/common.json index cc38ba3b..4106fe78 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -1473,11 +1473,12 @@ "title": "共有" }, "address_books": { - "title": "ディレクトリ", + "title": "マイアドレス帳", + "shared_prefix": "共有: {name}", "moved": "連絡先を {name} に移動しました", "moved_plural": "{count} 件の連絡先を {name} に移動しました", "move_failed": "連絡先の移動に失敗しました", - "address_book": "ディレクトリ" + "address_book": "アドレス帳" }, "detail": { "emails": "メールアドレス", diff --git a/locales/nl/common.json b/locales/nl/common.json index d80069ff..796330a9 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -1473,7 +1473,8 @@ "title": "Gedeeld" }, "address_books": { - "title": "Adresboeken", + "title": "Mijn Adresboeken", + "shared_prefix": "Gedeeld: {name}", "moved": "Contact verplaatst naar {name}", "moved_plural": "{count} contacten verplaatst naar {name}", "move_failed": "Verplaatsen van contact mislukt", diff --git a/locales/pt/common.json b/locales/pt/common.json index 0a420620..af31e184 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -1473,11 +1473,12 @@ "title": "Compartilhados" }, "address_books": { - "title": "Diretórios", + "title": "Meus Catálogos de Endereços", + "shared_prefix": "Compartilhado: {name}", "moved": "Contato movido para {name}", "moved_plural": "{count} contatos movidos para {name}", "move_failed": "Falha ao mover o contato", - "address_book": "Diretório" + "address_book": "Catálogo de endereços" }, "detail": { "emails": "Endereços de e-mail", diff --git a/stores/contact-store.ts b/stores/contact-store.ts index bb1fe825..32c1cc60 100644 --- a/stores/contact-store.ts +++ b/stores/contact-store.ts @@ -87,7 +87,39 @@ interface ContactStore { export const useContactStore = create()( persist( - (set, get) => ({ + (set, get) => { + + // Clean group member references when contacts are removed + function cleanGroupMembers(contacts: ContactCard[], removedIds: Set): ContactCard[] { + // Collect uid/id variants of removed contacts for matching + const removedKeys = new Set(); + for (const c of contacts) { + if (!removedIds.has(c.id)) continue; + removedKeys.add(c.id); + if (c.uid) { + removedKeys.add(c.uid); + const bare = c.uid.startsWith('urn:uuid:') ? c.uid.slice(9) : c.uid; + removedKeys.add(bare); + } + if (c.originalId) removedKeys.add(c.originalId); + } + return contacts.map(c => { + if (c.kind !== 'group' || !c.members) return c; + let changed = false; + const newMembers: Record = {}; + for (const [key, val] of Object.entries(c.members)) { + const bareKey = key.startsWith('urn:uuid:') ? key.slice(9) : key; + if (removedKeys.has(key) || removedKeys.has(bareKey)) { + changed = true; + } else { + newMembers[key] = val; + } + } + return changed ? { ...c, members: newMembers } : c; + }); + } + + return ({ contacts: [], addressBooks: [], selectedContactId: null, @@ -170,10 +202,14 @@ export const useContactStore = create()( const originalId = contact?.originalId || id; const accountId = contact?.isShared ? contact.accountId : undefined; await client.deleteContact(originalId, accountId); - set((state) => ({ - contacts: state.contacts.filter(c => c.id !== id), - selectedContactId: state.selectedContactId === id ? null : state.selectedContactId, - })); + set((state) => { + const removedIds = new Set([id]); + const cleaned = cleanGroupMembers(state.contacts, removedIds); + return { + contacts: cleaned.filter(c => c.id !== id), + selectedContactId: state.selectedContactId === id ? null : state.selectedContactId, + }; + }); } catch (error) { const msg = error instanceof Error ? error.message : 'Failed to delete contact'; set({ error: msg }); @@ -191,10 +227,14 @@ export const useContactStore = create()( ), })), - deleteLocalContact: (id) => set((state) => ({ - contacts: state.contacts.filter(c => c.id !== id), - selectedContactId: state.selectedContactId === id ? null : state.selectedContactId, - })), + deleteLocalContact: (id) => set((state) => { + const removedIds = new Set([id]); + const cleaned = cleanGroupMembers(state.contacts, removedIds); + return { + contacts: cleaned.filter(c => c.id !== id), + selectedContactId: state.selectedContactId === id ? null : state.selectedContactId, + }; + }), setSelectedContact: (id) => set({ selectedContactId: id }), setSearchQuery: (query) => set({ searchQuery: query }), @@ -456,11 +496,14 @@ export const useContactStore = create()( } } - set((state) => ({ - contacts: state.contacts.filter(c => !deletedIds.has(c.id)), - selectedContactId: deletedIds.has(state.selectedContactId || '') ? null : state.selectedContactId, - selectedContactIds: new Set(), - })); + set((state) => { + const cleaned = cleanGroupMembers(state.contacts, deletedIds); + return { + contacts: cleaned.filter(c => !deletedIds.has(c.id)), + selectedContactId: deletedIds.has(state.selectedContactId || '') ? null : state.selectedContactId, + selectedContactIds: new Set(), + }; + }); }, bulkAddToGroup: async (client, groupId, contactIds) => { @@ -485,9 +528,11 @@ export const useContactStore = create()( // Same account: just update the addressBookIds if ((sourceAccountId || primaryAccountId) === (targetAccountId || primaryAccountId)) { await client.updateContact(originalId, { addressBookIds: { [targetBookOriginalId]: true } }, sourceAccountId); + const isTargetPrimary = !targetAccountId || targetAccountId === primaryAccountId; + const localBookId = isTargetPrimary ? targetBookOriginalId : `${targetAccountId}:${targetBookOriginalId}`; set((state) => ({ contacts: state.contacts.map(c => - c.id === id ? { ...c, addressBookIds: { [targetBookOriginalId]: true } } : c + c.id === id ? { ...c, addressBookIds: { [localBookId]: true } } : c ), })); } else { @@ -501,6 +546,7 @@ export const useContactStore = create()( // Update local state const isPrimary = !targetAccountId || targetAccountId === primaryAccountId; + const localBookId = isPrimary ? targetBookOriginalId : `${targetAccountId}:${targetBookOriginalId}`; set((state) => ({ contacts: state.contacts.map(c => { if (c.id !== id) return c; @@ -511,7 +557,7 @@ export const useContactStore = create()( accountId: targetAccountId, accountName: addressBook.accountName || targetAccountId, isShared: !isPrimary, - addressBookIds: { [targetBookOriginalId]: true }, + addressBookIds: { [localBookId]: true }, }; }), })); @@ -544,7 +590,8 @@ export const useContactStore = create()( return imported; }, - }), + }); + }, { name: 'contact-storage', partialize: (state) => ({