mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-03 16:07:32 +00:00
93 lines
3.7 KiB
TypeScript
93 lines
3.7 KiB
TypeScript
import type { CalendarEvent } from '../api/types';
|
|
import { getEventEndDate, getEventStartDate } from './calendar-utils';
|
|
|
|
// Event search in the calendar (webmail components/calendar/calendar-search
|
|
// and hooks/use-calendar-event-search): the loaded events answer at once,
|
|
// the server's CalendarEvent/query `text` filter adds the rest.
|
|
|
|
/** The words of a search, lower case; every one has to match. */
|
|
export function searchTerms(query: string): string[] {
|
|
return query.toLowerCase().split(/\s+/).map((w) => w.trim()).filter(Boolean);
|
|
}
|
|
|
|
/** The CalendarEvent/query filter for the words: one `text` condition each. */
|
|
export function searchFilter(terms: string[]): Record<string, unknown> | null {
|
|
if (terms.length === 0) return null;
|
|
if (terms.length === 1) return { text: terms[0] };
|
|
return { operator: 'AND', conditions: terms.map((text) => ({ text })) };
|
|
}
|
|
|
|
/** What a search looks at: title, description, places and participants. */
|
|
function eventFields(event: CalendarEvent): string[] {
|
|
const fields = [event.title ?? '', event.description ?? ''];
|
|
for (const location of Object.values(event.locations ?? {})) if (location?.name) fields.push(location.name);
|
|
for (const p of Object.values(event.participants ?? {})) {
|
|
if (p.name) fields.push(p.name);
|
|
if (p.email) fields.push(p.email);
|
|
if (p.calendarAddress) fields.push(p.calendarAddress.replace(/^mailto:/i, ''));
|
|
}
|
|
return fields.map((f) => f.toLowerCase());
|
|
}
|
|
|
|
/** Whether every word of the search is in one of the event's fields. */
|
|
export function eventMatches(event: CalendarEvent, terms: string[]): boolean {
|
|
if (terms.length === 0) return false;
|
|
const fields = eventFields(event);
|
|
return terms.every((term) => fields.some((field) => field.includes(term)));
|
|
}
|
|
|
|
export interface EventSearchResults {
|
|
/** Still to come or going on, soonest first. */
|
|
upcoming: CalendarEvent[];
|
|
/** Over, latest first. */
|
|
past: CalendarEvent[];
|
|
}
|
|
|
|
// One row per event: a series found as loaded occurrences and as the
|
|
// server's base event shows once. An occurrence renamed on its own keeps a
|
|
// row of its own.
|
|
const seriesKey = (event: CalendarEvent) =>
|
|
`${event.accountId ?? ''} ${event.uid || event.originalId || event.id} ${event.title ?? ''}`;
|
|
|
|
/**
|
|
* The rows of a search: of a series' loaded occurrences the next one to
|
|
* come, else the last; the server's events add what the loaded window
|
|
* lacks. Tasks and events without a start are left out.
|
|
*/
|
|
export function orderSearchResults(
|
|
local: CalendarEvent[],
|
|
remote: CalendarEvent[],
|
|
now: number,
|
|
): EventSearchResults {
|
|
const ends = (event: CalendarEvent) => getEventEndDate(event).getTime();
|
|
const starts = (event: CalendarEvent) => getEventStartDate(event).getTime();
|
|
const bySeries = new Map<string, CalendarEvent>();
|
|
for (const event of local) {
|
|
if (!event.start) continue;
|
|
const key = seriesKey(event);
|
|
const kept = bySeries.get(key);
|
|
if (!kept) {
|
|
bySeries.set(key, event);
|
|
continue;
|
|
}
|
|
const keptUpcoming = ends(kept) >= now;
|
|
const upcoming = ends(event) >= now;
|
|
if (upcoming && (!keptUpcoming || starts(event) < starts(kept))) bySeries.set(key, event);
|
|
else if (!upcoming && !keptUpcoming && starts(event) > starts(kept)) bySeries.set(key, event);
|
|
}
|
|
for (const event of remote) {
|
|
if (!event.start) continue;
|
|
const key = seriesKey(event);
|
|
if (!bySeries.has(key)) bySeries.set(key, event);
|
|
}
|
|
const upcoming: CalendarEvent[] = [];
|
|
const past: CalendarEvent[] = [];
|
|
for (const event of bySeries.values()) {
|
|
if (Number.isNaN(starts(event))) continue;
|
|
(ends(event) >= now ? upcoming : past).push(event);
|
|
}
|
|
upcoming.sort((a, b) => starts(a) - starts(b));
|
|
past.sort((a, b) => starts(b) - starts(a));
|
|
return { upcoming, past };
|
|
}
|