From 9544fa5f125de1198e4dbab723a4623cbe257bd1 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Thu, 27 Aug 2026 13:11:21 -0700 Subject: [PATCH] Let the owner stop sharing a calendar or an address book Revoking a share meant opening the share dialog, removing each person from it in turn, and saving. That is the right tool for changing who has access and the wrong one for withdrawing it altogether, which is the more urgent of the two and the one someone is likely to want in a hurry. Both now offer "Stop sharing" in the context menu, which clears the lot after a confirmation saying how many people lose access. It appears only when there is something to revoke, so the menu says whether a thing is shared as well as offering to change it. A calendar also says it is shared now. Address books have carried that badge since they gained sharing; calendars never did, so the only way to find out was to open the dialog and look -- which for the owner of a dozen calendars means opening a dozen dialogs. Both go through the existing update paths, so a server that refuses is reported rather than swallowed. Verified against the mock, both kinds: sharing one shows the badge and adds the entry, confirming clears `shareWith`, the badge goes, and the entry disappears with it since there is no longer anything to stop. --- web/src/views/calendar/CalendarSidebar.tsx | 28 +++++++++++++++++++++- web/src/views/contacts/ContactsSidebar.tsx | 26 +++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/web/src/views/calendar/CalendarSidebar.tsx b/web/src/views/calendar/CalendarSidebar.tsx index 2cd8b10..8abbb50 100644 --- a/web/src/views/calendar/CalendarSidebar.tsx +++ b/web/src/views/calendar/CalendarSidebar.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from "react"; import { useLocation } from "wouter"; -import { ChevronLeft, ChevronRight, MoreVertical, Pencil, Plus, Share2, Trash2, Eye, EyeOff, Star, X } from "lucide-react"; +import { ChevronLeft, ChevronRight, MoreVertical, Pencil, Plus, Share2, Trash2, Eye, EyeOff, Star, UserMinus, X } from "lucide-react"; import { useCalendar } from "@/store/calendar"; import { dateTimeKey, useSettings } from "@/store/settings"; import { addMonths, isSameDay, isToday, monthGrid, startOfDay, toLocalDateOnly } from "@/lib/dates"; @@ -66,6 +66,7 @@ export function CalendarSidebar() {
cal.toggleHidden(c.id)} onContextMenu={(e) => { e.preventDefault(); setMenuCal(c); menu.openAt(e.clientX, e.clientY); }}> {c.name} + {Object.keys(c.shareWith ?? {}).length > 0 && } {c.isDefault && }
@@ -123,6 +124,31 @@ export function CalendarSidebar() { : } label={cal.hidden[menuCal.id] ? "Show" : "Hide"} onClick={() => cal.toggleHidden(menuCal.id)} /> } label="Edit" onClick={() => setEditCal(menuCal)} /> } label="Share…" onClick={() => setShare(menuCal)} disabled={!menuCal.myRights.mayShare} /> + {/* Revoking every share at once, without walking the dialog and + removing people one at a time. Only offered when there is + something to revoke. */} + {Object.keys(menuCal.shareWith ?? {}).length > 0 && ( + } + label="Stop sharing" + disabled={!menuCal.myRights.mayShare} + onClick={async () => { + const who = Object.keys(menuCal.shareWith ?? {}).length; + if (!(await confirmDialog({ + title: `Stop sharing “${menuCal.name}”?`, + message: `${who === 1 ? "One person" : `${who} people`} will lose access. Events in it are not affected.`, + confirmLabel: "Stop sharing", + danger: true, + }))) return; + try { + await cal.updateCalendar(menuCal.id, { shareWith: null }); + toast.success("No longer shared"); + } catch (err) { + toast.error((err as Error).message); + } + }} + /> + )} } label="Make default" disabled={menuCal.isDefault} onClick={() => void cal.updateCalendar(menuCal.id, { isDefault: true } as Partial).catch((err) => toast.error((err as Error).message))} /> } label="Delete" disabled={!menuCal.myRights.mayDelete} onClick={async () => { if (await confirmDialog({ title: `Delete “${menuCal.name}”?`, message: "All events in this calendar will be deleted.", confirmLabel: "Delete", danger: true })) void cal.destroyCalendar(menuCal.id).catch((err) => toast.error((err as Error).message)); }} /> diff --git a/web/src/views/contacts/ContactsSidebar.tsx b/web/src/views/contacts/ContactsSidebar.tsx index 065f5b4..17c2604 100644 --- a/web/src/views/contacts/ContactsSidebar.tsx +++ b/web/src/views/contacts/ContactsSidebar.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { Book, BookOpen, Download, Pencil, Plus, RefreshCw, Share2, Trash2, Upload, Users, X } from "lucide-react"; +import { Book, BookOpen, Download, Pencil, Plus, RefreshCw, Share2, Trash2, Upload, UserMinus, Users, X } from "lucide-react"; import { useContacts } from "@/store/contacts"; import { useSession } from "@/store/session"; import { useSettings } from "@/store/settings"; @@ -192,6 +192,30 @@ export function ContactsSidebar() { }} /> } label="Share…" disabled={!menuBook.myRights?.mayShare} onClick={() => setShare(menuBook)} /> + {/* Revoking the lot, rather than removing people one at a time in + the dialog. Only shown when there is something to revoke. */} + {Object.keys(menuBook.shareWith ?? {}).length > 0 && ( + } + label="Stop sharing" + disabled={!menuBook.myRights?.mayShare} + onClick={async () => { + const who = Object.keys(menuBook.shareWith ?? {}).length; + if (!(await confirmDialog({ + title: `Stop sharing “${menuBook.name}”?`, + message: `${who === 1 ? "One person" : `${who} people`} will lose access. The contacts in it are not affected.`, + confirmLabel: "Stop sharing", + danger: true, + }))) return; + try { + await contacts.updateBook(menuBook.id, { shareWith: null }); + toast.success("No longer shared"); + } catch (err) { + toast.error((err as Error).message); + } + }} + /> + )}