Feature: per-viewer colors for shared calendars (#345)
This commit is contained in:
@@ -60,6 +60,7 @@ import { useConfirmDialog } from "@/hooks/use-confirm-dialog";
|
||||
import { CreateCalendarModal } from "@/components/calendar/create-calendar-modal";
|
||||
import { getUserParticipantId } from "@/lib/calendar-participants";
|
||||
import { generateBirthdayEvents, createBirthdayCalendar, BIRTHDAY_CALENDAR_ID } from "@/lib/birthday-calendar";
|
||||
import { sharedCalendarColorKey, pickUnusedCalendarColor } from "@/lib/shared-calendar-colors";
|
||||
import { debug } from "@/lib/debug";
|
||||
import { consumePendingWebcal, hasPendingWebcal, subscribeToPendingWebcal } from "@/lib/protocol-handlers/session";
|
||||
import type { ParsedWebcal } from "@/lib/protocol-handlers/webcal";
|
||||
@@ -97,6 +98,9 @@ export default function CalendarPage() {
|
||||
refreshAllSubscriptions, icalSubscriptions,
|
||||
} = useCalendarStore();
|
||||
const { firstDayOfWeek, timeFormat, showWeekNumbers, enableCalendarTasks, showTasksOnCalendar, calendarHoverPreview, showBirthdayCalendar, birthdayCalendarColor, updateSetting } = useSettingsStore();
|
||||
const sharedCalendarColors = useSettingsStore((s) => s.sharedCalendarColors);
|
||||
const setSharedCalendarColor = useSettingsStore((s) => s.setSharedCalendarColor);
|
||||
const removeSharedCalendarColor = useSettingsStore((s) => s.removeSharedCalendarColor);
|
||||
const taskStore = useTaskStore();
|
||||
const fetchTasksFn = useTaskStore(state => state.fetchTasks);
|
||||
const { identities } = useIdentityStore();
|
||||
@@ -1030,10 +1034,47 @@ export default function CalendarPage() {
|
||||
try { return t('birthday_calendar'); } catch { return 'Birthdays'; }
|
||||
})();
|
||||
|
||||
// Apply each shared calendar's local color override (per-viewer recolor,
|
||||
// #345). The override replaces the calendar's color and wins over per-event
|
||||
// colors via the `colorIsLocalOverride` flag (see getEventColor). Personal
|
||||
// calendars are passed through untouched.
|
||||
const displayCalendars = useMemo(() => {
|
||||
return calendars.map((cal) => {
|
||||
if (!cal.isShared) return cal;
|
||||
const override = sharedCalendarColors[sharedCalendarColorKey(cal)];
|
||||
if (!override) return cal;
|
||||
return { ...cal, color: override, colorIsLocalOverride: true };
|
||||
});
|
||||
}, [calendars, sharedCalendarColors]);
|
||||
|
||||
// Auto-assign a random, not-yet-used palette color to any freshly shared
|
||||
// calendar so multiple shared calendars don't collide on one color. Runs
|
||||
// once per calendar (guarded by the presence of an existing key), and the
|
||||
// user can still overwrite it from the sidebar.
|
||||
useEffect(() => {
|
||||
const shared = calendars.filter((c) => c.isShared);
|
||||
const missing = shared.filter((c) => !sharedCalendarColors[sharedCalendarColorKey(c)]);
|
||||
if (missing.length === 0) return;
|
||||
// Seed "used" with personal calendar colors plus already-assigned shared
|
||||
// overrides so the picks stay distinct from what's already on screen.
|
||||
const used = new Set<string>();
|
||||
for (const c of calendars) {
|
||||
if (!c.isShared && c.color) used.add(c.color.toLowerCase());
|
||||
}
|
||||
for (const color of Object.values(sharedCalendarColors)) {
|
||||
if (color) used.add(color.toLowerCase());
|
||||
}
|
||||
for (const cal of missing) {
|
||||
const color = pickUnusedCalendarColor(used);
|
||||
used.add(color.toLowerCase());
|
||||
setSharedCalendarColor(sharedCalendarColorKey(cal), color);
|
||||
}
|
||||
}, [calendars, sharedCalendarColors, setSharedCalendarColor]);
|
||||
|
||||
const allCalendars = useMemo(() => {
|
||||
if (!showBirthdayCalendar) return calendars;
|
||||
return [...calendars, createBirthdayCalendar(birthdayCalendarName, birthdayCalendarColor)];
|
||||
}, [calendars, showBirthdayCalendar, birthdayCalendarName, birthdayCalendarColor]);
|
||||
if (!showBirthdayCalendar) return displayCalendars;
|
||||
return [...displayCalendars, createBirthdayCalendar(birthdayCalendarName, birthdayCalendarColor)];
|
||||
}, [displayCalendars, showBirthdayCalendar, birthdayCalendarName, birthdayCalendarColor]);
|
||||
|
||||
const visibleEvents = useMemo(() => {
|
||||
const filtered = events.filter((e) => {
|
||||
@@ -1219,7 +1260,7 @@ export default function CalendarPage() {
|
||||
/>
|
||||
<TaskListView
|
||||
tasks={taskStore.tasks}
|
||||
calendars={calendars}
|
||||
calendars={displayCalendars}
|
||||
selectedCalendarIds={selectedCalendarIds}
|
||||
filter={taskStore.filter}
|
||||
showCompleted={taskStore.showCompleted}
|
||||
@@ -1317,8 +1358,21 @@ export default function CalendarPage() {
|
||||
updateSetting('birthdayCalendarColor', color);
|
||||
return;
|
||||
}
|
||||
// Shared calendars: recolor locally only (the viewer usually
|
||||
// can't write the owner's calendar, and it'd recolor it for
|
||||
// everyone). Personal calendars write through to the server.
|
||||
const cal = allCalendars.find((c) => c.id === calendarId);
|
||||
if (cal?.isShared) {
|
||||
setSharedCalendarColor(sharedCalendarColorKey(cal), color);
|
||||
return;
|
||||
}
|
||||
updateCalendar(client, calendarId, { color });
|
||||
} : undefined}
|
||||
onResetColor={(cal) => {
|
||||
// Drop the local override; the auto-assign effect picks a
|
||||
// fresh unused color (so it never reverts to a collision).
|
||||
removeSharedCalendarColor(sharedCalendarColorKey(cal));
|
||||
}}
|
||||
onShareCalendar={client ? (cal) => setSharingCalendarId(cal.id) : undefined}
|
||||
onCreateEvent={(cal: Calendar) => {
|
||||
setDefaultCalendarIdForCreate(cal.id);
|
||||
@@ -1389,7 +1443,7 @@ export default function CalendarPage() {
|
||||
onSubscribe={() => setShowSubscriptionModal(true)}
|
||||
isMobile={isMobile}
|
||||
onNavigateBack={isMobile && mobileReturnToMonth && normalizedViewMode === "day" ? navigateBackToMonth : undefined}
|
||||
calendars={calendars}
|
||||
calendars={displayCalendars}
|
||||
selectedCalendarIds={selectedCalendarIds}
|
||||
onToggleVisibility={toggleCalendarVisibility}
|
||||
enableCalendarTasks={enableCalendarTasks}
|
||||
@@ -1419,7 +1473,7 @@ export default function CalendarPage() {
|
||||
<EventModal
|
||||
key={editEvent?.id ?? 'new'}
|
||||
event={editEvent}
|
||||
calendars={calendars}
|
||||
calendars={displayCalendars}
|
||||
defaultDate={defaultModalDate}
|
||||
defaultEndDate={defaultModalEndDate}
|
||||
defaultAllDay={defaultModalAllDay}
|
||||
@@ -1442,7 +1496,7 @@ export default function CalendarPage() {
|
||||
<TaskModal
|
||||
key={editTask?.id ?? 'new-task'}
|
||||
task={editTask}
|
||||
calendars={calendars}
|
||||
calendars={displayCalendars}
|
||||
onSave={handleSaveTask}
|
||||
onDelete={handleDeleteTask}
|
||||
onClose={() => { setShowTaskModal(false); setEditTask(null); }}
|
||||
@@ -1529,7 +1583,7 @@ export default function CalendarPage() {
|
||||
{detailEvent && detailAnchorRect && (
|
||||
<EventDetailPopover
|
||||
event={detailEvent}
|
||||
calendar={calendars.find(c => detailEvent.calendarIds[c.id])}
|
||||
calendar={displayCalendars.find(c => detailEvent.calendarIds[c.id])}
|
||||
anchorRect={detailAnchorRect}
|
||||
onEdit={handleEditFromDetail}
|
||||
onDelete={handleDeleteFromDetail}
|
||||
@@ -1549,7 +1603,7 @@ export default function CalendarPage() {
|
||||
<EventModal
|
||||
key={editEvent?.id ?? 'new'}
|
||||
event={editEvent}
|
||||
calendars={calendars}
|
||||
calendars={displayCalendars}
|
||||
defaultDate={defaultModalDate}
|
||||
defaultEndDate={defaultModalEndDate}
|
||||
defaultAllDay={defaultModalAllDay}
|
||||
@@ -1566,7 +1620,7 @@ export default function CalendarPage() {
|
||||
|
||||
{showImportModal && client && (
|
||||
<ICalImportModal
|
||||
calendars={calendars}
|
||||
calendars={displayCalendars}
|
||||
client={client}
|
||||
initialUrl={pendingSubscription?.url}
|
||||
onClose={() => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { ChevronDown, ChevronRight, Globe, ListTodo, Pencil, RefreshCw, Share2, Trash2, Cake, User, Users, Plus, Eraser, Palette } from "lucide-react";
|
||||
import { ChevronDown, ChevronRight, Globe, ListTodo, Pencil, RefreshCw, Share2, Trash2, Cake, User, Users, Plus, Eraser, Palette, Shuffle } from "lucide-react";
|
||||
import { cn, formatDateTime } from "@/lib/utils";
|
||||
import type { Calendar } from "@/lib/jmap/types";
|
||||
import { CalendarColorPicker } from "@/components/settings/calendar-management-settings";
|
||||
@@ -11,6 +11,7 @@ import { useSettingsStore } from "@/stores/settings-store";
|
||||
import { useTaskStore } from "@/stores/task-store";
|
||||
import { useAccountStore } from "@/stores/account-store";
|
||||
import { BIRTHDAY_CALENDAR_ID } from "@/lib/birthday-calendar";
|
||||
import { sharedCalendarColorKey } from "@/lib/shared-calendar-colors";
|
||||
import { toast } from "@/stores/toast-store";
|
||||
import { ContextMenu, ContextMenuItem, ContextMenuSeparator, ContextMenuSubMenu } from "@/components/ui/context-menu";
|
||||
import { useContextMenu } from "@/hooks/use-context-menu";
|
||||
@@ -50,6 +51,7 @@ interface CalendarSidebarPanelProps {
|
||||
selectedCalendarIds: string[];
|
||||
onToggleVisibility: (id: string) => void;
|
||||
onColorChange?: (calendarId: string, color: string) => void;
|
||||
onResetColor?: (calendar: Calendar) => void;
|
||||
onShareCalendar?: (calendar: Calendar) => void;
|
||||
onCreateEvent?: (calendar: Calendar) => void;
|
||||
onClearCalendar?: (calendar: Calendar) => void;
|
||||
@@ -71,6 +73,7 @@ export function CalendarSidebarPanel({
|
||||
selectedCalendarIds,
|
||||
onToggleVisibility,
|
||||
onColorChange,
|
||||
onResetColor,
|
||||
onShareCalendar,
|
||||
onCreateEvent,
|
||||
onClearCalendar,
|
||||
@@ -94,6 +97,7 @@ export function CalendarSidebarPanel({
|
||||
const refreshICalSubscription = useCalendarStore((s) => s.refreshICalSubscription);
|
||||
const removeICalSubscription = useCalendarStore((s) => s.removeICalSubscription);
|
||||
const timeFormat = useSettingsStore((s) => s.timeFormat);
|
||||
const sharedCalendarColors = useSettingsStore((s) => s.sharedCalendarColors);
|
||||
const enableCalendarTasks = useSettingsStore((s) => s.enableCalendarTasks);
|
||||
const tasks = useTaskStore((s) => s.tasks);
|
||||
const setViewMode = useCalendarStore((s) => s.setViewMode);
|
||||
@@ -303,9 +307,11 @@ export function CalendarSidebarPanel({
|
||||
const canCreate = onCreateEvent && !isBirthday && cal.myRights?.mayWriteOwn !== false;
|
||||
const canShare = onShareCalendar && cal.myRights?.mayShare && !cal.isShared;
|
||||
const canChangeColor = !!onColorChange;
|
||||
const hasColorOverride = !!cal.isShared && !!sharedCalendarColors[sharedCalendarColorKey(cal)];
|
||||
const canResetColor = !!onResetColor && hasColorOverride;
|
||||
const canClear = onClearCalendar && !isBirthday && cal.myRights?.mayDelete !== false;
|
||||
const canDelete = onDeleteCalendar && !isBirthday && !cal.isDefault && !cal.isShared;
|
||||
const showSeparator = (canCreate || canShare || canChangeColor) && (canClear || canDelete);
|
||||
const showSeparator = (canCreate || canShare || canChangeColor || canResetColor) && (canClear || canDelete);
|
||||
const color = cal.color || "#3b82f6";
|
||||
|
||||
return (
|
||||
@@ -335,6 +341,13 @@ export function CalendarSidebarPanel({
|
||||
</div>
|
||||
</ContextMenuSubMenu>
|
||||
)}
|
||||
{canResetColor && (
|
||||
<ContextMenuItem
|
||||
icon={Shuffle}
|
||||
label={tMgmt('random_color')}
|
||||
onClick={() => { closeContextMenu(); onResetColor!(cal); }}
|
||||
/>
|
||||
)}
|
||||
{showSeparator && <ContextMenuSeparator />}
|
||||
{canClear && (
|
||||
<ContextMenuItem
|
||||
|
||||
@@ -34,6 +34,11 @@ function sanitizeColor(color: string | null | undefined, fallback = "#3b82f6"):
|
||||
}
|
||||
|
||||
function getEventColor(event: CalendarEvent, calendar?: Calendar): string {
|
||||
// A local color override on a shared calendar wins over per-event colors,
|
||||
// so the whole shared calendar paints uniformly in the viewer's chosen hue.
|
||||
if (calendar?.colorIsLocalOverride && calendar.color) {
|
||||
return sanitizeColor(calendar.color);
|
||||
}
|
||||
return sanitizeColor(event.color, sanitizeColor(calendar?.color));
|
||||
}
|
||||
|
||||
|
||||
@@ -15,25 +15,7 @@ import { ICalImportModal } from '@/components/calendar/ical-import-modal';
|
||||
import { ICalSubscriptionModal } from '@/components/calendar/ical-subscription-modal';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
import { apiFetch } from '@/lib/browser-navigation';
|
||||
|
||||
const CALENDAR_COLORS = [
|
||||
"#3b82f6", // blue
|
||||
"#ef4444", // red
|
||||
"#22c55e", // green
|
||||
"#f59e0b", // amber
|
||||
"#8b5cf6", // violet
|
||||
"#ec4899", // pink
|
||||
"#14b8a6", // teal
|
||||
"#f97316", // orange
|
||||
"#06b6d4", // cyan
|
||||
"#84cc16", // lime
|
||||
"#6366f1", // indigo
|
||||
"#a855f7", // purple
|
||||
"#e11d48", // rose
|
||||
"#0ea5e9", // sky
|
||||
"#10b981", // emerald
|
||||
"#d946ef", // fuchsia
|
||||
];
|
||||
import { CALENDAR_COLORS, sharedCalendarColorKey } from '@/lib/shared-calendar-colors';
|
||||
|
||||
function CalendarColorPicker({
|
||||
value,
|
||||
@@ -182,6 +164,8 @@ export function CalendarManagementSettings() {
|
||||
const tImport = useTranslations('calendar.import');
|
||||
const tSub = useTranslations('calendar.subscription');
|
||||
const timeFormat = useSettingsStore((s) => s.timeFormat);
|
||||
const sharedCalendarColors = useSettingsStore((s) => s.sharedCalendarColors);
|
||||
const setSharedCalendarColor = useSettingsStore((s) => s.setSharedCalendarColor);
|
||||
const colorPickerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Load calendars if not yet loaded
|
||||
@@ -329,6 +313,15 @@ export function CalendarManagementSettings() {
|
||||
|
||||
const handleColorChange = async (calendarId: string, color: string) => {
|
||||
if (!client) return;
|
||||
// Shared calendars: recolor locally only - the viewer usually can't write
|
||||
// the owner's calendar and it'd recolor it for everyone (see #345).
|
||||
const cal = calendars.find((c) => c.id === calendarId);
|
||||
if (cal?.isShared) {
|
||||
setSharedCalendarColor(sharedCalendarColorKey(cal), color);
|
||||
toast.success(t('color_updated'));
|
||||
setColorPickerId(null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await updateCalendar(client, calendarId, { color });
|
||||
toast.success(t('color_updated'));
|
||||
@@ -396,7 +389,7 @@ export function CalendarManagementSettings() {
|
||||
<SettingsSection title={t('title')} description={t('description')}>
|
||||
<div className="space-y-2">
|
||||
{calendars.filter(cal => !isSubscriptionCalendar(cal.id)).map((cal) => {
|
||||
const color = cal.color || '#3b82f6';
|
||||
const color = (cal.isShared && sharedCalendarColors[sharedCalendarColorKey(cal)]) || cal.color || '#3b82f6';
|
||||
|
||||
if (editingId === cal.id) {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { Calendar } from '@/lib/jmap/types';
|
||||
import {
|
||||
CALENDAR_COLORS,
|
||||
sharedCalendarColorKey,
|
||||
pickUnusedCalendarColor,
|
||||
} from '../shared-calendar-colors';
|
||||
|
||||
function makeCal(overrides: Partial<Calendar>): Calendar {
|
||||
return {
|
||||
id: 'cal-1',
|
||||
name: 'Cal',
|
||||
description: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
isSubscribed: true,
|
||||
isVisible: true,
|
||||
isDefault: false,
|
||||
includeInAvailability: 'all',
|
||||
defaultAlertsWithTime: null,
|
||||
defaultAlertsWithoutTime: null,
|
||||
timeZone: null,
|
||||
shareWith: null,
|
||||
myRights: {} as Calendar['myRights'],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('sharedCalendarColorKey', () => {
|
||||
it('is built from local account, JMAP account, and the original id', () => {
|
||||
const cal = makeCal({
|
||||
id: 'acct-9:cal-7',
|
||||
originalId: 'cal-7',
|
||||
accountId: 'acct-9',
|
||||
localAccountId: 'slot-2',
|
||||
});
|
||||
expect(sharedCalendarColorKey(cal)).toBe('slot-2|acct-9|cal-7');
|
||||
});
|
||||
|
||||
it('is stable regardless of the Pro-shell id prefix', () => {
|
||||
// Same underlying calendar, shown once under the active account (bare id)
|
||||
// and once cross-account (prefixed id) - both must map to one key.
|
||||
const active = makeCal({ id: 'acct-9:cal-7', originalId: 'cal-7', accountId: 'acct-9', localAccountId: 'slot-2' });
|
||||
const prefixed = makeCal({ id: 'slot-2::acct-9:cal-7', originalId: 'cal-7', accountId: 'acct-9', localAccountId: 'slot-2' });
|
||||
expect(sharedCalendarColorKey(active)).toBe(sharedCalendarColorKey(prefixed));
|
||||
});
|
||||
|
||||
it('falls back to the id when originalId is absent', () => {
|
||||
const cal = makeCal({ id: 'cal-7', accountId: 'acct-9' });
|
||||
expect(sharedCalendarColorKey(cal)).toBe('|acct-9|cal-7');
|
||||
});
|
||||
});
|
||||
|
||||
describe('pickUnusedCalendarColor', () => {
|
||||
it('returns a palette color not present in usedColors', () => {
|
||||
const used = CALENDAR_COLORS.slice(0, CALENDAR_COLORS.length - 1);
|
||||
const picked = pickUnusedCalendarColor(used);
|
||||
expect(picked).toBe(CALENDAR_COLORS[CALENDAR_COLORS.length - 1]);
|
||||
});
|
||||
|
||||
it('ignores case when comparing used colors', () => {
|
||||
const used = CALENDAR_COLORS.slice(0, -1).map((c) => c.toUpperCase());
|
||||
expect(pickUnusedCalendarColor(used)).toBe(CALENDAR_COLORS[CALENDAR_COLORS.length - 1]);
|
||||
});
|
||||
|
||||
it('still returns a palette color once every color is taken', () => {
|
||||
expect(CALENDAR_COLORS).toContain(pickUnusedCalendarColor(CALENDAR_COLORS));
|
||||
});
|
||||
|
||||
it('always returns a valid palette color for a small used set', () => {
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const picked = pickUnusedCalendarColor(['#3b82f6']);
|
||||
expect(CALENDAR_COLORS).toContain(picked);
|
||||
expect(picked).not.toBe('#3b82f6');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -488,6 +488,10 @@ export interface Calendar {
|
||||
// can route mutations to the right client. Distinct from `accountId`
|
||||
// which is the JMAP server's own account UUID.
|
||||
localAccountId?: string;
|
||||
// Set when `color` has been replaced by the viewer's local override for a
|
||||
// shared calendar (see lib/shared-calendar-colors). When true, the override
|
||||
// wins over per-event colors so the whole shared calendar paints uniformly.
|
||||
colorIsLocalOverride?: boolean;
|
||||
}
|
||||
|
||||
export interface CalendarRights {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { Calendar } from '@/lib/jmap/types';
|
||||
|
||||
/**
|
||||
* Palette of calendar colors offered in the color picker. Defined here (rather
|
||||
* than in the settings UI component) so non-React modules can reuse it without
|
||||
* pulling in component code. The settings color picker re-exports this.
|
||||
*/
|
||||
export const CALENDAR_COLORS = [
|
||||
"#3b82f6", // blue
|
||||
"#ef4444", // red
|
||||
"#22c55e", // green
|
||||
"#f59e0b", // amber
|
||||
"#8b5cf6", // violet
|
||||
"#ec4899", // pink
|
||||
"#14b8a6", // teal
|
||||
"#f97316", // orange
|
||||
"#06b6d4", // cyan
|
||||
"#84cc16", // lime
|
||||
"#6366f1", // indigo
|
||||
"#a855f7", // purple
|
||||
"#e11d48", // rose
|
||||
"#0ea5e9", // sky
|
||||
"#10b981", // emerald
|
||||
"#d946ef", // fuchsia
|
||||
];
|
||||
|
||||
/**
|
||||
* Stable key for a shared calendar's local color override. Independent of the
|
||||
* Pro-shell id prefix (which changes with the active account), so the override
|
||||
* survives shell-mode and account switches. Built from the owning JMAP account
|
||||
* + the calendar's original server id.
|
||||
*/
|
||||
export function sharedCalendarColorKey(
|
||||
cal: Pick<Calendar, 'id' | 'originalId' | 'accountId' | 'localAccountId'>,
|
||||
): string {
|
||||
const localAccount = cal.localAccountId ?? '';
|
||||
const account = cal.accountId ?? '';
|
||||
const id = cal.originalId ?? cal.id;
|
||||
return `${localAccount}|${account}|${id}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick a random palette color not present in `usedColors`. Once every palette
|
||||
* entry is taken, fall back to a random palette color (collisions are
|
||||
* unavoidable past CALENDAR_COLORS.length calendars).
|
||||
*/
|
||||
export function pickUnusedCalendarColor(usedColors: Iterable<string>): string {
|
||||
const used = new Set<string>();
|
||||
for (const c of usedColors) {
|
||||
if (c) used.add(c.toLowerCase());
|
||||
}
|
||||
const available = CALENDAR_COLORS.filter((c) => !used.has(c.toLowerCase()));
|
||||
const pool = available.length > 0 ? available : CALENDAR_COLORS;
|
||||
return pool[Math.floor(Math.random() * pool.length)];
|
||||
}
|
||||
@@ -2642,6 +2642,7 @@
|
||||
"name_placeholder": "Název kalendáře",
|
||||
"color": "Barva",
|
||||
"change_color": "Změnit barvu",
|
||||
"random_color": "Nová náhodná barva",
|
||||
"add_calendar": "Přidat kalendář",
|
||||
"edit": "Upravit",
|
||||
"delete": "Odstranit",
|
||||
|
||||
@@ -2656,6 +2656,7 @@
|
||||
"name_placeholder": "Kalendernavn",
|
||||
"color": "Farve",
|
||||
"change_color": "Skift farve",
|
||||
"random_color": "Ny tilfældig farve",
|
||||
"add_calendar": "Tilføj kalender",
|
||||
"edit": "Redigér",
|
||||
"delete": "Slet",
|
||||
|
||||
@@ -2642,6 +2642,7 @@
|
||||
"name_placeholder": "Kalendername",
|
||||
"color": "Farbe",
|
||||
"change_color": "Farbe ändern",
|
||||
"random_color": "Neue zufällige Farbe",
|
||||
"add_calendar": "Kalender hinzufügen",
|
||||
"edit": "Bearbeiten",
|
||||
"delete": "Löschen",
|
||||
|
||||
@@ -2657,6 +2657,7 @@
|
||||
"name_placeholder": "Calendar name",
|
||||
"color": "Color",
|
||||
"change_color": "Change color",
|
||||
"random_color": "New random color",
|
||||
"add_calendar": "Add calendar",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
|
||||
@@ -2642,6 +2642,7 @@
|
||||
"name_placeholder": "Nombre del calendario",
|
||||
"color": "Color",
|
||||
"change_color": "Cambiar color",
|
||||
"random_color": "Nuevo color aleatorio",
|
||||
"add_calendar": "Añadir calendario",
|
||||
"edit": "Editar",
|
||||
"delete": "Eliminar",
|
||||
|
||||
@@ -2656,6 +2656,7 @@
|
||||
"name_placeholder": "Nom du calendrier",
|
||||
"color": "Couleur",
|
||||
"change_color": "Changer la couleur",
|
||||
"random_color": "Nouvelle couleur aléatoire",
|
||||
"add_calendar": "Ajouter un calendrier",
|
||||
"edit": "Modifier",
|
||||
"delete": "Supprimer",
|
||||
|
||||
@@ -2642,6 +2642,7 @@
|
||||
"name_placeholder": "Nome del calendario",
|
||||
"color": "Colore",
|
||||
"change_color": "Cambia colore",
|
||||
"random_color": "Nuovo colore casuale",
|
||||
"add_calendar": "Aggiungi calendario",
|
||||
"edit": "Modifica",
|
||||
"delete": "Elimina",
|
||||
|
||||
@@ -2642,6 +2642,7 @@
|
||||
"name_placeholder": "カレンダー名",
|
||||
"color": "色",
|
||||
"change_color": "色を変更",
|
||||
"random_color": "新しいランダムな色",
|
||||
"add_calendar": "カレンダーを追加",
|
||||
"edit": "編集",
|
||||
"delete": "削除",
|
||||
|
||||
@@ -2642,6 +2642,7 @@
|
||||
"name_placeholder": "캘린더 이름",
|
||||
"color": "색상",
|
||||
"change_color": "색상 변경",
|
||||
"random_color": "새 무작위 색상",
|
||||
"add_calendar": "캘린더 추가",
|
||||
"edit": "수정",
|
||||
"delete": "삭제",
|
||||
|
||||
@@ -2641,6 +2641,7 @@
|
||||
"name_placeholder": "Kalendāra nosaukums",
|
||||
"color": "Krāsa",
|
||||
"change_color": "Mainīt krāsu",
|
||||
"random_color": "Jauna nejauša krāsa",
|
||||
"add_calendar": "Pievienot kalendāru",
|
||||
"edit": "Rediģēt",
|
||||
"delete": "Dzēst",
|
||||
|
||||
@@ -2642,6 +2642,7 @@
|
||||
"name_placeholder": "Agendanaam",
|
||||
"color": "Kleur",
|
||||
"change_color": "Kleur wijzigen",
|
||||
"random_color": "Nieuwe willekeurige kleur",
|
||||
"add_calendar": "Agenda toevoegen",
|
||||
"edit": "Bewerken",
|
||||
"delete": "Verwijderen",
|
||||
|
||||
@@ -2642,6 +2642,7 @@
|
||||
"name_placeholder": "Nazwa kalendarza",
|
||||
"color": "Kolor",
|
||||
"change_color": "Zmień kolor",
|
||||
"random_color": "Nowy losowy kolor",
|
||||
"add_calendar": "Dodaj kalendarz",
|
||||
"edit": "Edytuj",
|
||||
"delete": "Usuń",
|
||||
|
||||
@@ -2656,6 +2656,7 @@
|
||||
"name_placeholder": "Nome do calendário",
|
||||
"color": "Cor",
|
||||
"change_color": "Alterar cor",
|
||||
"random_color": "Nova cor aleatória",
|
||||
"add_calendar": "Adicionar calendário",
|
||||
"edit": "Editar",
|
||||
"delete": "Excluir",
|
||||
|
||||
@@ -2642,6 +2642,7 @@
|
||||
"name_placeholder": "Название календаря",
|
||||
"color": "Цвет",
|
||||
"change_color": "Изменить цвет",
|
||||
"random_color": "Новый случайный цвет",
|
||||
"add_calendar": "Добавить календарь",
|
||||
"edit": "Редактировать",
|
||||
"delete": "Удалить",
|
||||
|
||||
@@ -2656,6 +2656,7 @@
|
||||
"name_placeholder": "Takvim adı",
|
||||
"color": "Renk",
|
||||
"change_color": "Rengi değiştir",
|
||||
"random_color": "Yeni rastgele renk",
|
||||
"add_calendar": "Takvim ekle",
|
||||
"edit": "Düzenle",
|
||||
"delete": "Sil",
|
||||
|
||||
@@ -2642,6 +2642,7 @@
|
||||
"name_placeholder": "Назва календаря",
|
||||
"color": "колір",
|
||||
"change_color": "Змінити колір",
|
||||
"random_color": "Новий випадковий колір",
|
||||
"add_calendar": "Додати календар",
|
||||
"edit": "Редагувати",
|
||||
"delete": "Видалити",
|
||||
|
||||
@@ -2642,6 +2642,7 @@
|
||||
"name_placeholder": "日历名称",
|
||||
"color": "颜色",
|
||||
"change_color": "更改颜色",
|
||||
"random_color": "随机新颜色",
|
||||
"add_calendar": "添加日历",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
|
||||
@@ -183,6 +183,11 @@ interface SettingsState {
|
||||
showBirthdayCalendar: boolean;
|
||||
birthdayCalendarColor: string;
|
||||
|
||||
// Per-viewer color overrides for shared calendars, keyed by
|
||||
// sharedCalendarColorKey(). Lets each recipient recolor calendars shared
|
||||
// with them without changing the owner's color (see #345).
|
||||
sharedCalendarColors: Record<string, string>;
|
||||
|
||||
// Contacts Display
|
||||
groupContactsByLetter: boolean;
|
||||
|
||||
@@ -274,6 +279,10 @@ interface SettingsState {
|
||||
setFolderIcon: (mailboxId: string, icon: string) => void;
|
||||
removeFolderIcon: (mailboxId: string) => void;
|
||||
|
||||
// Shared-calendar color overrides
|
||||
setSharedCalendarColor: (key: string, color: string) => void;
|
||||
removeSharedCalendarColor: (key: string) => void;
|
||||
|
||||
// Trusted senders
|
||||
addTrustedSender: (email: string) => void;
|
||||
removeTrustedSender: (email: string) => void;
|
||||
@@ -360,6 +369,8 @@ const DEFAULT_SETTINGS = {
|
||||
showBirthdayCalendar: false,
|
||||
birthdayCalendarColor: '#eab308',
|
||||
|
||||
sharedCalendarColors: {} as Record<string, string>,
|
||||
|
||||
// Contacts Display
|
||||
groupContactsByLetter: true,
|
||||
|
||||
@@ -546,6 +557,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
showTasksOnCalendar: state.showTasksOnCalendar,
|
||||
showBirthdayCalendar: state.showBirthdayCalendar,
|
||||
birthdayCalendarColor: state.birthdayCalendarColor,
|
||||
sharedCalendarColors: state.sharedCalendarColors,
|
||||
groupContactsByLetter: state.groupContactsByLetter,
|
||||
expandedFilterView: state.expandedFilterView,
|
||||
showTimeInMonthView: state.showTimeInMonthView,
|
||||
@@ -642,6 +654,16 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
set({ folderIcons: rest });
|
||||
},
|
||||
|
||||
// Shared-calendar color override methods
|
||||
setSharedCalendarColor: (key: string, color: string) => {
|
||||
set({ sharedCalendarColors: { ...get().sharedCalendarColors, [key]: color } });
|
||||
},
|
||||
|
||||
removeSharedCalendarColor: (key: string) => {
|
||||
const { [key]: _, ...rest } = get().sharedCalendarColors;
|
||||
set({ sharedCalendarColors: rest });
|
||||
},
|
||||
|
||||
// Trusted senders methods
|
||||
addTrustedSender: (email: string) => {
|
||||
const normalizedEmail = email.toLowerCase().trim();
|
||||
|
||||
Reference in New Issue
Block a user