mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 00:07:01 +00:00
feat: add a send-invitations switch and custom reminder offsets, keeping alerts the picker cannot show
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { formatReminder, preservedAlerts, remindersToAlerts } from '../calendar-alerts';
|
||||
|
||||
describe('preservedAlerts / remindersToAlerts', () => {
|
||||
const alerts = {
|
||||
a: { trigger: { '@type': 'OffsetTrigger', offset: '-PT15M' }, action: 'display' },
|
||||
abs: { trigger: { '@type': 'AbsoluteTrigger', when: '2026-03-01T08:00:00Z' }, action: 'display' },
|
||||
end: { trigger: { '@type': 'OffsetTrigger', offset: '-PT5M', relativeTo: 'end' }, action: 'display' },
|
||||
mail: { trigger: { '@type': 'OffsetTrigger', offset: '-PT1H' }, action: 'email' },
|
||||
};
|
||||
|
||||
it('keeps alerts the picker cannot represent', () => {
|
||||
expect(Object.keys(preservedAlerts(alerts)).sort()).toEqual(['abs', 'end', 'mail']);
|
||||
expect(preservedAlerts(undefined)).toEqual({});
|
||||
});
|
||||
|
||||
it('rebuilds the picker alerts on top of the preserved ones without key clashes', () => {
|
||||
const rebuilt = remindersToAlerts([{ minutesBefore: 10 }, { minutesBefore: 0 }], preservedAlerts(alerts))!;
|
||||
expect(Object.keys(rebuilt).sort()).toEqual(['abs', 'end', 'mail', 'reminder-1', 'reminder-2']);
|
||||
expect(rebuilt['reminder-1'].trigger).toEqual({ '@type': 'OffsetTrigger', offset: '-PT10M', relativeTo: 'start' });
|
||||
expect(rebuilt['reminder-2'].trigger.offset).toBe('PT0S');
|
||||
expect(rebuilt.abs).toBe(alerts.abs);
|
||||
});
|
||||
|
||||
it('returns undefined only when nothing is left', () => {
|
||||
expect(remindersToAlerts([], {})).toBeUndefined();
|
||||
expect(Object.keys(remindersToAlerts([], preservedAlerts(alerts))!)).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatReminder', () => {
|
||||
it('falls back to English plurals', () => {
|
||||
expect(formatReminder(0)).toBe('At time of event');
|
||||
expect(formatReminder(1)).toBe('1 minute before');
|
||||
expect(formatReminder(45)).toBe('45 minutes before');
|
||||
expect(formatReminder(120)).toBe('2 hours before');
|
||||
expect(formatReminder(60 * 24)).toBe('1 day before');
|
||||
expect(formatReminder(60 * 24 * 14)).toBe('2 weeks before');
|
||||
});
|
||||
|
||||
it('uses a plain translated string and ignores ICU plural templates it cannot expand', () => {
|
||||
const t = (key: string, fallback?: string) =>
|
||||
key === 'calendar.alerts.hours_before' ? '# Stunden vorher'
|
||||
: key === 'calendar.alerts.minutes_before' ? '{count, plural, one {# minute} other {# minutes}}'
|
||||
: fallback ?? key;
|
||||
expect(formatReminder(120, t)).toBe('2 Stunden vorher');
|
||||
expect(formatReminder(5, t)).toBe('5 minutes before');
|
||||
});
|
||||
});
|
||||
+65
-14
@@ -69,34 +69,85 @@ export function alertsToReminders(alerts: CalendarEvent['alerts']): Reminder[] {
|
||||
.sort((a, b) => b.minutesBefore - a.minutesBefore);
|
||||
}
|
||||
|
||||
export function remindersToAlerts(reminders: Reminder[]): Record<string, Alert> | undefined {
|
||||
if (reminders.length === 0) return undefined;
|
||||
const alerts: Record<string, Alert> = {};
|
||||
reminders.forEach((r, i) => {
|
||||
alerts[`reminder-${i + 1}`] = {
|
||||
trigger: { '@type': 'OffsetTrigger', offset: minutesBeforeToOffset(r.minutesBefore) },
|
||||
// Alerts the picker can't represent (absolute triggers, end-relative offsets,
|
||||
// non-display actions) — kept as-is so a save doesn't drop them.
|
||||
export function preservedAlerts(alerts: CalendarEvent['alerts']): Record<string, Alert> {
|
||||
const out: Record<string, Alert> = {};
|
||||
if (!alerts) return out;
|
||||
for (const [id, alert] of Object.entries(alerts)) {
|
||||
const trigger = alert?.trigger;
|
||||
if (!trigger) continue;
|
||||
const representable =
|
||||
trigger.offset !== undefined &&
|
||||
(trigger.relativeTo === undefined || trigger.relativeTo === 'start') &&
|
||||
(!alert.action || alert.action === 'display') &&
|
||||
offsetToMinutesBefore(trigger.offset) !== null;
|
||||
if (!representable) out[id] = alert;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function remindersToAlerts(
|
||||
reminders: Reminder[],
|
||||
preserved: Record<string, Alert> = {},
|
||||
): Record<string, Alert> | undefined {
|
||||
if (reminders.length === 0 && Object.keys(preserved).length === 0) return undefined;
|
||||
const alerts: Record<string, Alert> = { ...preserved };
|
||||
let n = 0;
|
||||
for (const r of reminders) {
|
||||
let key = `reminder-${++n}`;
|
||||
while (key in alerts) key = `reminder-${++n}`;
|
||||
alerts[key] = {
|
||||
'@type': 'Alert',
|
||||
trigger: { '@type': 'OffsetTrigger', offset: minutesBeforeToOffset(r.minutesBefore), relativeTo: 'start' },
|
||||
action: 'display',
|
||||
};
|
||||
});
|
||||
}
|
||||
return alerts;
|
||||
}
|
||||
|
||||
export function formatReminder(minutesBefore: number): string {
|
||||
if (minutesBefore === 0) return 'At time of event';
|
||||
if (minutesBefore < 0) return `${Math.abs(minutesBefore)} minutes after`;
|
||||
if (minutesBefore < 60) return `${minutesBefore} minute${minutesBefore === 1 ? '' : 's'} before`;
|
||||
type Translate = (key: string, fallback?: string) => string;
|
||||
const plain: Translate = (_key, fallback) => fallback ?? _key;
|
||||
|
||||
function plural(t: Translate, count: number, key: string, one: string, many: string): string {
|
||||
// Locale files carry ICU plurals ("{count, plural, one {# minute before} other {# minutes before}}")
|
||||
// that the mobile t() can't expand, so pick the English-style form and only
|
||||
// use the translation when it is a plain string.
|
||||
const raw = t(key, '');
|
||||
if (raw && !raw.includes('{')) return raw.replace('#', String(count));
|
||||
return (count === 1 ? one : many).replace('#', String(count));
|
||||
}
|
||||
|
||||
export function formatReminder(minutesBefore: number, t: Translate = plain): string {
|
||||
if (minutesBefore === 0) return t('calendar.alerts.at_time', 'At time of event');
|
||||
if (minutesBefore < 0) return `${Math.abs(minutesBefore)} ${t('calendar.alerts.unit_minutes_after', 'minutes after')}`;
|
||||
if (minutesBefore < 60) {
|
||||
return plural(t, minutesBefore, 'calendar.alerts.minutes_before', '# minute before', '# minutes before');
|
||||
}
|
||||
if (minutesBefore < 60 * 24) {
|
||||
const h = minutesBefore / 60;
|
||||
return Number.isInteger(h) ? `${h} hour${h === 1 ? '' : 's'} before` : `${minutesBefore} minutes before`;
|
||||
if (Number.isInteger(h)) return plural(t, h, 'calendar.alerts.hours_before', '# hour before', '# hours before');
|
||||
return plural(t, minutesBefore, 'calendar.alerts.minutes_before', '# minute before', '# minutes before');
|
||||
}
|
||||
if (minutesBefore < 60 * 24 * 7) {
|
||||
const d = minutesBefore / (60 * 24);
|
||||
return Number.isInteger(d) ? `${d} day${d === 1 ? '' : 's'} before` : `${(minutesBefore / 60).toFixed(0)} hours before`;
|
||||
if (Number.isInteger(d)) return plural(t, d, 'calendar.alerts.days_before', '# day before', '# days before');
|
||||
return plural(t, Math.round(minutesBefore / 60), 'calendar.alerts.hours_before', '# hour before', '# hours before');
|
||||
}
|
||||
const w = minutesBefore / (60 * 24 * 7);
|
||||
return Number.isInteger(w) ? `${w} week${w === 1 ? '' : 's'} before` : `${(minutesBefore / (60 * 24)).toFixed(0)} days before`;
|
||||
if (Number.isInteger(w)) return plural(t, w, 'calendar.alerts.weeks_before', '# week before', '# weeks before');
|
||||
return plural(t, Math.round(minutesBefore / (60 * 24)), 'calendar.alerts.days_before', '# day before', '# days before');
|
||||
}
|
||||
|
||||
export type ReminderUnit = 'minutes' | 'hours' | 'days' | 'weeks';
|
||||
|
||||
export const REMINDER_UNIT_MINUTES: Record<ReminderUnit, number> = {
|
||||
minutes: 1,
|
||||
hours: 60,
|
||||
days: 60 * 24,
|
||||
weeks: 60 * 24 * 7,
|
||||
};
|
||||
|
||||
// Presets offered by the reminder picker (minutes before start).
|
||||
export const REMINDER_PRESETS: number[] = [
|
||||
0, 5, 10, 15, 30, 60, 120, 60 * 24, 60 * 24 * 2, 60 * 24 * 7,
|
||||
|
||||
Reference in New Issue
Block a user