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);
+ }
+ }}
+ />
+ )}