diff --git a/src/lib/__tests__/shared-calendar-colors.test.ts b/src/lib/__tests__/shared-calendar-colors.test.ts new file mode 100644 index 00000000..2c306f1a --- /dev/null +++ b/src/lib/__tests__/shared-calendar-colors.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; +import { autoAssignSharedCalendarColors, sharedCalendarColorKey } from '../calendar-utils'; + +describe('autoAssignSharedCalendarColors (#1414)', () => { + const own = { id: 'c1', color: '#3b82f6' }; + const coloured = { id: 'b:c2', originalId: 'c2', accountId: 'b', isShared: true, color: '#10b981' }; + const plain = { id: 'b:c3', originalId: 'c3', accountId: 'b', isShared: true }; + + it('keeps the server colour of a shared calendar and colours only one without', () => { + const assigned = autoAssignSharedCalendarColors([own, coloured, plain], {}); + expect(Object.keys(assigned)).toEqual([sharedCalendarColorKey(plain)]); + const color = assigned[sharedCalendarColorKey(plain)].toLowerCase(); + expect(['#3b82f6', '#10b981']).not.toContain(color); + }); + + it('leaves a calendar the viewer coloured alone', () => { + expect(autoAssignSharedCalendarColors([plain], { [sharedCalendarColorKey(plain)]: '#ef4444' })).toEqual({}); + }); +}); diff --git a/src/lib/calendar-utils.ts b/src/lib/calendar-utils.ts index 9cbb7cbc..5afdedeb 100644 --- a/src/lib/calendar-utils.ts +++ b/src/lib/calendar-utils.ts @@ -617,6 +617,38 @@ export function pickUnusedCalendarColor(usedColors: Iterable): string { * via the `colorIsLocalOverride` flag (see getEventColor). Personal calendars * are passed through untouched. */ +/** + * A distinct colour for each shared calendar that has neither a colour from + * the server nor one the viewer picked, so several shared calendars don't + * collide on the default colour. A shared calendar the server gives a colour + * keeps it until the viewer picks their own (webmail #1414). Returns only the + * new overrides, keyed by sharedCalendarColorKey(). + */ +export function autoAssignSharedCalendarColors( + calendars: Pick[], + overrides: Record, +): Record { + // Every colour already on screen: personal and server colours plus the + // existing shared overrides. + const used = new Set(); + for (const cal of calendars) { + if (cal.color) used.add(cal.color.toLowerCase()); + } + for (const color of Object.values(overrides)) { + if (color) used.add(color.toLowerCase()); + } + const assigned: Record = {}; + for (const cal of calendars) { + if (!cal.isShared || cal.color) continue; + const key = sharedCalendarColorKey(cal); + if (overrides[key] || assigned[key]) continue; + const color = pickUnusedCalendarColor(used); + used.add(color.toLowerCase()); + assigned[key] = color; + } + return assigned; +} + export function applySharedCalendarColors( calendars: Calendar[], overrides: Record, diff --git a/src/screens/CalendarScreen.tsx b/src/screens/CalendarScreen.tsx index 51c5c672..9a9b2f2b 100644 --- a/src/screens/CalendarScreen.tsx +++ b/src/screens/CalendarScreen.tsx @@ -87,7 +87,7 @@ import { getEventStartDate, getPrimaryCalendarId, getTaskDueDate, - pickUnusedCalendarColor, + autoAssignSharedCalendarColors, sharedCalendarColorKey, reopenedEdit, } from '../lib/calendar-utils'; @@ -308,29 +308,13 @@ export default function CalendarScreen() { [storeCalendars, 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. + // Auto-assign a not-yet-used palette color to any freshly shared calendar + // without a server colour, so multiple shared calendars don't collide on + // one color (#1414: one with a server colour keeps it). Runs once per + // calendar, and the user can still overwrite it from the sidebar. React.useEffect(() => { - const missing = storeCalendars.filter( - (cal) => cal.isShared && !sharedCalendarColors[sharedCalendarColorKey(cal)], - ); - 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(); - for (const cal of storeCalendars) { - if (!cal.isShared && cal.color) used.add(cal.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); - } + const assigned = autoAssignSharedCalendarColors(storeCalendars, sharedCalendarColors); + for (const [key, color] of Object.entries(assigned)) setSharedCalendarColor(key, color); }, [storeCalendars, sharedCalendarColors, setSharedCalendarColor]); const allCalendars = React.useMemo(