Skip to content
56 linesCodeBlameRaw
1/**
2 * When each person was last on a workspace's Home page, kept in their own
3 * feed (src/feed.ts) so it is the same on every device. Home reads it to
4 * show what happened while they were away, and marks it once they have
5 * looked (never on first paint), so a quick refresh does not wipe what
6 * they have not seen.
7 *
8 * Marks close together are one visit: a refresh, or coming back to Home
9 * from another page a few minutes later, keeps showing what happened
10 * before the visit began. A visit ends once no mark has come for
11 * `VISIT_GAP_MS`; the next one shows what happened since its last mark.
12 */
13
14/** The longest slug a visit is kept for. */
15const MAX_SLUG = 64;
16/** How far behind now a mark may be: Home marks the time the page loaded, at most a day before. */
17const MAX_BEHIND_MS = 24 * 60 * 60 * 1000;
18/** Marks closer than this are one visit. */
19export const VISIT_GAP_MS = 30 * 60 * 1000;
20
21/** What is kept per workspace: the latest mark, and the last mark of the visit before this one. */
22export type Visit = { seen_at: number; previous_at: number | null };
23
24/** A workspace slug as visits key it, or null when it is not one. */
25export function visitWorkspace(value: unknown): string | null {
26 if (typeof value !== "string") return null;
27 const slug = value.trim().toLowerCase();
28 return slug && slug.length <= MAX_SLUG && /^[a-z0-9][a-z0-9-]*$/.test(slug) ? slug : null;
29}
30
31/**
32 * The visit to keep after a mark at `at` (RFC 3339 or epoch ms): never
33 * later than now, never more than a day behind it, and never earlier than
34 * what is kept, so an old tab marking late does not move it back. Null
35 * when there is nothing to change.
36 */
37export function markVisit(kept: Visit | null, at: unknown, now: number): Visit | null {
38 const given = typeof at === "string" ? Date.parse(at) : typeof at === "number" ? at : Number.NaN;
39 if (!Number.isFinite(given)) return null;
40 const mark = Math.min(now, Math.max(given, now - MAX_BEHIND_MS));
41 if (!kept) return { seen_at: mark, previous_at: null };
42 if (kept.seen_at >= mark) return null;
43 // The same visit: what it shows still starts where it began.
44 if (mark - kept.seen_at < VISIT_GAP_MS) return { seen_at: mark, previous_at: kept.previous_at };
45 return { seen_at: mark, previous_at: kept.seen_at };
46}
47
48/**
49 * When the person was last here, as Home counts from: during a visit, the
50 * end of the one before it; after one, its last mark. Null when there was
51 * none before.
52 */
53export function lastVisit(kept: Visit | null, now: number): number | null {
54 if (!kept) return null;
55 return now - kept.seen_at < VISIT_GAP_MS ? kept.previous_at : kept.seen_at;
56}