Merge pull request #108 from LINUXexpert-org/stop-sharing

Let the owner stop sharing a calendar or an address book
This commit is contained in:
LINUXexpert.org
2026-08-27 13:13:21 -07:00
committed by GitHub
2 changed files with 52 additions and 2 deletions
+27 -1
View File
@@ -1,6 +1,6 @@
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { useLocation } from "wouter"; 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 { useCalendar } from "@/store/calendar";
import { dateTimeKey, useSettings } from "@/store/settings"; import { dateTimeKey, useSettings } from "@/store/settings";
import { addMonths, isSameDay, isToday, monthGrid, startOfDay, toLocalDateOnly } from "@/lib/dates"; import { addMonths, isSameDay, isToday, monthGrid, startOfDay, toLocalDateOnly } from "@/lib/dates";
@@ -66,6 +66,7 @@ export function CalendarSidebar() {
<div key={c.id} className={`cal-list-item ${cal.hidden[c.id] ? "hidden-cal" : ""}`} onClick={() => cal.toggleHidden(c.id)} onContextMenu={(e) => { e.preventDefault(); setMenuCal(c); menu.openAt(e.clientX, e.clientY); }}> <div key={c.id} className={`cal-list-item ${cal.hidden[c.id] ? "hidden-cal" : ""}`} onClick={() => cal.toggleHidden(c.id)} onContextMenu={(e) => { e.preventDefault(); setMenuCal(c); menu.openAt(e.clientX, e.clientY); }}>
<span className="cal-color" style={{ background: c.color ?? "var(--accent)", borderColor: c.color ?? "var(--accent)" }} /> <span className="cal-color" style={{ background: c.color ?? "var(--accent)", borderColor: c.color ?? "var(--accent)" }} />
<span className="cal-name">{c.name}</span> <span className="cal-name">{c.name}</span>
{Object.keys(c.shareWith ?? {}).length > 0 && <Share2 size={12} className="faint" aria-label="Shared" />}
{c.isDefault && <Star size={12} className="faint" />} {c.isDefault && <Star size={12} className="faint" />}
<button className="icon-btn xs nav-more" onClick={(e) => { e.stopPropagation(); setMenuCal(c); menu.open(e); }} aria-label="Calendar options"><MoreVertical size={14} /></button> <button className="icon-btn xs nav-more" onClick={(e) => { e.stopPropagation(); setMenuCal(c); menu.open(e); }} aria-label="Calendar options"><MoreVertical size={14} /></button>
</div> </div>
@@ -123,6 +124,31 @@ export function CalendarSidebar() {
<MenuItem icon={cal.hidden[menuCal.id] ? <Eye size={16} /> : <EyeOff size={16} />} label={cal.hidden[menuCal.id] ? "Show" : "Hide"} onClick={() => cal.toggleHidden(menuCal.id)} /> <MenuItem icon={cal.hidden[menuCal.id] ? <Eye size={16} /> : <EyeOff size={16} />} label={cal.hidden[menuCal.id] ? "Show" : "Hide"} onClick={() => cal.toggleHidden(menuCal.id)} />
<MenuItem icon={<Pencil size={16} />} label="Edit" onClick={() => setEditCal(menuCal)} /> <MenuItem icon={<Pencil size={16} />} label="Edit" onClick={() => setEditCal(menuCal)} />
<MenuItem icon={<Share2 size={16} />} label="Share…" onClick={() => setShare(menuCal)} disabled={!menuCal.myRights.mayShare} /> <MenuItem icon={<Share2 size={16} />} 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 && (
<MenuItem
icon={<UserMinus size={16} />}
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);
}
}}
/>
)}
<MenuItem icon={<Star size={16} />} label="Make default" disabled={menuCal.isDefault} onClick={() => void cal.updateCalendar(menuCal.id, { isDefault: true } as Partial<Calendar>).catch((err) => toast.error((err as Error).message))} /> <MenuItem icon={<Star size={16} />} label="Make default" disabled={menuCal.isDefault} onClick={() => void cal.updateCalendar(menuCal.id, { isDefault: true } as Partial<Calendar>).catch((err) => toast.error((err as Error).message))} />
<MenuSep /> <MenuSep />
<MenuItem danger icon={<Trash2 size={16} />} 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)); }} /> <MenuItem danger icon={<Trash2 size={16} />} 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)); }} />
+25 -1
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react"; 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 { useContacts } from "@/store/contacts";
import { useSession } from "@/store/session"; import { useSession } from "@/store/session";
import { useSettings } from "@/store/settings"; import { useSettings } from "@/store/settings";
@@ -192,6 +192,30 @@ export function ContactsSidebar() {
}} }}
/> />
<MenuItem icon={<Share2 size={16} />} label="Share…" disabled={!menuBook.myRights?.mayShare} onClick={() => setShare(menuBook)} /> <MenuItem icon={<Share2 size={16} />} 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 && (
<MenuItem
icon={<UserMinus size={16} />}
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);
}
}}
/>
)}
<MenuSep /> <MenuSep />
<MenuItem <MenuItem
danger danger