From 574db426526e7bd663fbe7a91adea8a5569330bb Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 04:31:14 +0100 Subject: [PATCH] feat: change the colour of the birthdays calendar --- src/components/calendar/CalendarSidebarDrawer.tsx | 5 +++-- src/lib/__tests__/birthday-calendar-color.test.ts | 11 +++++++++++ src/lib/birthday-calendar.ts | 4 ++-- src/screens/CalendarScreen.tsx | 9 +++++++-- src/stores/settings-store.ts | 7 +++++++ 5 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 src/lib/__tests__/birthday-calendar-color.test.ts diff --git a/src/components/calendar/CalendarSidebarDrawer.tsx b/src/components/calendar/CalendarSidebarDrawer.tsx index 59ec99e6..f725a8f6 100644 --- a/src/components/calendar/CalendarSidebarDrawer.tsx +++ b/src/components/calendar/CalendarSidebarDrawer.tsx @@ -322,8 +322,9 @@ function Section({ const isSubscription = !!isSubscriptionCalendar?.(cal.id); const isOwn = !cal.isShared && !isBirthday && !isSubscription; const canSetDefault = !!onSetDefault && isOwn && !cal.isDefault; - // Own calendars recolour server-side; shared ones locally (#345). - const canRecolor = !!onSetColor && (!!cal.isShared || isOwn); + // Own calendars recolour server-side; shared ones and the birthdays + // locally (#345). + const canRecolor = !!onSetColor && (!!cal.isShared || isOwn || isBirthday); const canRename = !!onRename && isOwn; const canShare = !!onShare && isOwn && (!cal.myRights || cal.myRights.mayShare !== false); const canClear = !!onClear && isOwn && isWritableCalendar(cal); diff --git a/src/lib/__tests__/birthday-calendar-color.test.ts b/src/lib/__tests__/birthday-calendar-color.test.ts new file mode 100644 index 00000000..8ea8c896 --- /dev/null +++ b/src/lib/__tests__/birthday-calendar-color.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from 'vitest'; +import { BIRTHDAY_CALENDAR_COLOR, createBirthdayCalendar } from '../birthday-calendar'; +import { useSettingsStore } from '../../stores/settings-store'; + +describe('birthday calendar colour', () => { + it('takes the colour the user picked and keeps yellow until then', () => { + expect(createBirthdayCalendar('Birthdays', '#ef4444').color).toBe('#ef4444'); + expect(createBirthdayCalendar('Birthdays').color).toBe(BIRTHDAY_CALENDAR_COLOR); + expect(useSettingsStore.getState().birthdayCalendarColor).toBe(BIRTHDAY_CALENDAR_COLOR); + }); +}); diff --git a/src/lib/birthday-calendar.ts b/src/lib/birthday-calendar.ts index e8a568f1..1ff8e577 100644 --- a/src/lib/birthday-calendar.ts +++ b/src/lib/birthday-calendar.ts @@ -6,11 +6,11 @@ export const BIRTHDAY_CALENDAR_ID = '__birthday-calendar__'; export const BIRTHDAY_CALENDAR_COLOR = '#eab308'; // yellow // A read-only virtual calendar holding generated birthday events. -export function createBirthdayCalendar(name = 'Birthdays'): Calendar { +export function createBirthdayCalendar(name = 'Birthdays', color?: string): Calendar { return { id: BIRTHDAY_CALENDAR_ID, name, - color: BIRTHDAY_CALENDAR_COLOR, + color: color || BIRTHDAY_CALENDAR_COLOR, sortOrder: 999, isSubscribed: true, isVisible: true, diff --git a/src/screens/CalendarScreen.tsx b/src/screens/CalendarScreen.tsx index 9a9b2f2b..77c857c8 100644 --- a/src/screens/CalendarScreen.tsx +++ b/src/screens/CalendarScreen.tsx @@ -165,6 +165,7 @@ export default function CalendarScreen() { const calendarFreeScroll = useSettingsStore((s) => s.calendarFreeScroll); const calendarTimeFormat = useSettingsStore((s) => s.calendarTimeFormat); const showBirthdayCalendar = useSettingsStore((s) => s.showBirthdayCalendar); + const birthdayCalendarColor = useSettingsStore((s) => s.birthdayCalendarColor); const enableCalendarTasks = useSettingsStore((s) => s.enableCalendarTasks); const sharedCalendarColors = useSettingsStore((s) => s.sharedCalendarColors); const setSharedCalendarColor = useSettingsStore((s) => s.setSharedCalendarColor); @@ -318,8 +319,8 @@ export default function CalendarScreen() { }, [storeCalendars, sharedCalendarColors, setSharedCalendarColor]); const allCalendars = React.useMemo( - () => (showBirthdayCalendar ? [...displayCalendars, createBirthdayCalendar()] : displayCalendars), - [displayCalendars, showBirthdayCalendar], + () => (showBirthdayCalendar ? [...displayCalendars, createBirthdayCalendar(undefined, birthdayCalendarColor)] : displayCalendars), + [displayCalendars, showBirthdayCalendar, birthdayCalendarColor], ); // Tasks with a due date are overlaid on the grid as chips (webmail's // showTasksOnCalendar); tapping one opens the tasks sheet on that task. @@ -884,6 +885,10 @@ export default function CalendarScreen() { const handleSetCalendarColor = React.useCallback( (cal: Calendar, color: string) => { + if (cal.id === BIRTHDAY_CALENDAR_ID) { + useSettingsStore.getState().updateSetting('birthdayCalendarColor', color); + return; + } if (cal.isShared) { // Per-viewer recolor (#345): the owner's colour is left alone. setSharedCalendarColor(sharedCalendarColorKey(cal), color); diff --git a/src/stores/settings-store.ts b/src/stores/settings-store.ts index bb0ffb33..4277ca31 100644 --- a/src/stores/settings-store.ts +++ b/src/stores/settings-store.ts @@ -309,6 +309,10 @@ interface PersistedSettings { // Hard cap on the on-disk body cache, in megabytes. When a sync pushes the // cache past this, the oldest messages are evicted to fit. offlineCacheMaxMB: number; + + // calendar: the colour of the virtual Birthdays calendar (webmail + // birthdayCalendarColor). + birthdayCalendarColor: string; } const DEFAULT_PERSISTED: PersistedSettings = { @@ -463,6 +467,9 @@ const DEFAULT_PERSISTED: PersistedSettings = { offlineCacheEnabled: false, offlineCacheDays: 7, offlineCacheMaxMB: 50, + + // calendar: + birthdayCalendarColor: '#eab308', }; export interface SettingsState extends PersistedSettings {