import { create } from "zustand"; import type { IJMAPClient } from "@/lib/jmap/client-interface"; import type { Principal, CalendarRights, AddressBookRights, FileNodeRights, MailboxRights, } from "@/lib/jmap/types"; import { type SharedResourceKind, MAILBOX_ROLE_LABELS, CALENDAR_ROLE_LABELS, ADDRESSBOOK_ROLE_LABELS, FILE_ROLE_LABELS, resolveRights, detectMailboxPreset, detectCalendarPreset, detectAddressBookPreset, } from "@/lib/sharing-rights"; export type { SharedResourceKind } from "@/lib/sharing-rights"; export interface SharedFolder { id: string; resourceId: string; resourceName: string; resourceKind: SharedResourceKind; principalId: string; principalName: string; principalEmail: string | null; role: string; direction: "byMe" | "withMe"; pending: boolean; accountId?: string; } interface SharingState { sharedByMe: SharedFolder[]; sharedWithMe: SharedFolder[]; loading: boolean; principalsCache: Principal[]; lastMessage: { type: 'success' | 'error'; text: string } | null; loadPrincipals: (client: IJMAPClient) => Promise; fetchShares: (client: IJMAPClient) => Promise; shareFolder: ( client: IJMAPClient, resourceId: string, resourceName: string, resourceKind: SharedResourceKind, principalId: string, role: string, message?: string, accountId?: string, ) => Promise; revokeShare: ( client: IJMAPClient, resourceId: string, resourceKind: SharedResourceKind, principalId: string, accountId?: string, ) => Promise; changeRole: ( client: IJMAPClient, resourceId: string, resourceKind: SharedResourceKind, principalId: string, role: string, accountId?: string, ) => Promise; acceptShare: (client: IJMAPClient, share: SharedFolder) => Promise; declineShare: (client: IJMAPClient, share: SharedFolder) => Promise; } function roleLabel(kind: SharedResourceKind, role: string): string { switch (kind) { case "mailbox": return MAILBOX_ROLE_LABELS[role] ?? role; case "calendar": return CALENDAR_ROLE_LABELS[role] ?? role; case "addressBook": return ADDRESSBOOK_ROLE_LABELS[role] ?? role; case "file": return FILE_ROLE_LABELS[role] ?? role; default: return role; } } export const useSharingStore = create((set, get) => ({ sharedByMe: [], sharedWithMe: [], loading: false, principalsCache: [], lastMessage: null, async loadPrincipals(client) { const cached = get().principalsCache; if (cached.length > 0) return cached; const principals = await client.getPrincipals(); set({ principalsCache: principals }); return principals; }, async fetchShares(client) { set({ loading: true }); try { const principals = await client.getPrincipals(); const principalMap = new Map(); for (const p of principals) principalMap.set(p.id, p); const byMe: SharedFolder[] = []; const withMe: SharedFolder[] = []; try { const mailboxes = await client.getAllMailboxes(); for (const mb of mailboxes) { const shares = mb.shareWith; if (shares && Object.keys(shares).length > 0) { for (const [principalId, rights] of Object.entries(shares)) { const p = principalMap.get(principalId); byMe.push({ id: `mb-${mb.id}-${principalId}`, resourceId: mb.id, resourceName: mb.name, resourceKind: "mailbox", principalId, principalName: p?.name ?? principalId, principalEmail: p?.email ?? null, role: roleLabel("mailbox", detectMailboxPreset(rights)), direction: "byMe", pending: false, accountId: mb.accountId, }); } } if (mb.isShared && mb.myRights) { withMe.push({ id: `mb-withme-${mb.id}`, resourceId: mb.id, resourceName: mb.name, resourceKind: "mailbox", principalId: mb.accountId || "unknown", principalName: mb.accountName || "Unknown", principalEmail: null, role: roleLabel("mailbox", detectMailboxPreset(mb.myRights)), direction: "withMe", pending: false, accountId: mb.accountId, }); } } } catch { /* mailboxes may not be available */ } try { if (client.supportsCalendars()) { const calendars = await client.getAllCalendars(); for (const cal of calendars) { const shares = cal.shareWith; if (shares && Object.keys(shares).length > 0) { for (const [principalId, rights] of Object.entries(shares)) { const p = principalMap.get(principalId); byMe.push({ id: `cal-${cal.id}-${principalId}`, resourceId: cal.id, resourceName: cal.name, resourceKind: "calendar", principalId, principalName: p?.name ?? principalId, principalEmail: p?.email ?? null, role: roleLabel("calendar", detectCalendarPreset(rights)), direction: "byMe", pending: false, accountId: cal.accountId, }); } } if (cal.isShared && cal.myRights) { withMe.push({ id: `cal-withme-${cal.id}`, resourceId: cal.id, resourceName: cal.name, resourceKind: "calendar", principalId: cal.accountId || "unknown", principalName: cal.accountName || "Unknown", principalEmail: null, role: roleLabel("calendar", detectCalendarPreset(cal.myRights)), direction: "withMe", pending: false, accountId: cal.accountId, }); } } } } catch { /* calendars may not be available */ } try { if (client.supportsContacts()) { const books = await client.getAllAddressBooks(); for (const book of books) { const shares = book.shareWith; if (shares && Object.keys(shares).length > 0) { for (const [principalId, rights] of Object.entries(shares)) { const p = principalMap.get(principalId); byMe.push({ id: `ab-${book.id}-${principalId}`, resourceId: book.id, resourceName: book.name, resourceKind: "addressBook", principalId, principalName: p?.name ?? principalId, principalEmail: p?.email ?? null, role: roleLabel( "addressBook", detectAddressBookPreset(rights), ), direction: "byMe", pending: false, accountId: book.accountId, }); } } if (book.isShared && book.myRights) { withMe.push({ id: `ab-withme-${book.id}`, resourceId: book.id, resourceName: book.name, resourceKind: "addressBook", principalId: book.accountId || "unknown", principalName: book.accountName || "Unknown", principalEmail: null, role: roleLabel("addressBook", detectAddressBookPreset(book.myRights)), direction: "withMe", pending: false, accountId: book.accountId, }); } } } } catch { /* address books may not be available */ } set({ sharedByMe: byMe, sharedWithMe: withMe, loading: false, principalsCache: principals }); } catch { set({ loading: false }); } }, async shareFolder( client, resourceId, resourceName, resourceKind, principalId, role, _message, accountId, ) { const rights = resolveRights(resourceKind, role); await applyShare(client, resourceKind, resourceId, principalId, rights, accountId); const princ = get().principalsCache.find((p) => p.id === principalId); const entry: SharedFolder = { id: `${resourceKind}-${resourceId}-${principalId}`, resourceId, resourceName, resourceKind, principalId, principalName: princ?.name ?? principalId, principalEmail: princ?.email ?? null, role, direction: "byMe", pending: false, accountId, }; set((s) => ({ sharedByMe: [ ...s.sharedByMe.filter( (f) => !( f.resourceId === resourceId && f.principalId === principalId && f.resourceKind === resourceKind ), ), entry, ], })); set({ lastMessage: { type: 'success', text: `Shared "${resourceName}"` } }); }, async revokeShare(client, resourceId, resourceKind, principalId, accountId) { await applyShare(client, resourceKind, resourceId, principalId, null, accountId); set((s) => ({ sharedByMe: s.sharedByMe.filter( (f) => !( f.resourceId === resourceId && f.principalId === principalId && f.resourceKind === resourceKind ), ), sharedWithMe: s.sharedWithMe.filter( (f) => !( f.resourceId === resourceId && f.principalId === principalId && f.resourceKind === resourceKind ), ), })); set({ lastMessage: { type: 'success', text: "Access revoked" } }); }, async changeRole( client, resourceId, resourceKind, principalId, role, accountId, ) { const rights = resolveRights(resourceKind, role); await applyShare(client, resourceKind, resourceId, principalId, rights, accountId); set((s) => ({ sharedByMe: s.sharedByMe.map((f) => f.resourceId === resourceId && f.principalId === principalId && f.resourceKind === resourceKind ? { ...f, role } : f, ), })); set({ lastMessage: { type: 'success', text: "Role updated" } }); }, async acceptShare(_client, share) { set((s) => ({ sharedWithMe: s.sharedWithMe.map((f) => f.id === share.id ? { ...f, pending: false } : f, ), })); set({ lastMessage: { type: 'success', text: `Accepted share: ${share.resourceName}` } }); }, async declineShare(_client, share) { set((s) => ({ sharedWithMe: s.sharedWithMe.filter((f) => f.id !== share.id), })); set({ lastMessage: { type: 'success', text: `Declined share: ${share.resourceName}` } }); }, })); async function applyShare( client: IJMAPClient, kind: SharedResourceKind, resourceId: string, principalId: string, rights: | MailboxRights | CalendarRights | AddressBookRights | FileNodeRights | null, accountId?: string, ): Promise { switch (kind) { case "mailbox": await client.setMailboxShare( resourceId, principalId, rights as MailboxRights | null, accountId, ); break; case "calendar": await client.setCalendarShare( resourceId, principalId, rights as CalendarRights | null, accountId, ); break; case "addressBook": await client.setAddressBookShare( resourceId, principalId, rights as AddressBookRights | null, accountId, ); break; case "file": await client.setFileNodeShare( resourceId, principalId, rights as FileNodeRights | null, accountId, ); break; } }