fix: keep a shared calendar's own colour instead of a random one (bulwarkmail/webmail#1414)

This commit is contained in:
Linus Rath
2026-10-31 04:19:56 +01:00
parent 294023e2b2
commit 64eb97f4c3
3 changed files with 58 additions and 23 deletions
@@ -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({});
});
});
+32
View File
@@ -617,6 +617,38 @@ export function pickUnusedCalendarColor(usedColors: Iterable<string>): 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<Calendar, 'id' | 'color' | 'isShared' | 'accountId' | 'originalId'>[],
overrides: Record<string, string>,
): Record<string, string> {
// Every colour already on screen: personal and server colours plus the
// existing shared overrides.
const used = new Set<string>();
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<string, string> = {};
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<string, string>,
+7 -23
View File
@@ -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<string>();
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(