storebound/apps/desktop/src/lib/notify.ts

42 lines
2.7 KiB
TypeScript

// What deserves a desktop notification after a read (Settings > Notifications). Pure: compares the previous read with
// the new one and never fires on a first read (nothing to compare with = no flood).
import type { PortfolioResult } from '../api';
import { sameVersion } from '../../../../cli/lib/portfolio';
import { healthSeries, type HealthSnapshot, isError, PLAY_THRESHOLDS, type Review } from './model';
export interface ReleaseChange { project: string; name: string; store: 'ios' | 'android'; version: string; from: string; to: string }
/** Phase changes of the same version on the same store that the owner waits for: review in / out, approved, rejected, live, halted. */
export function releaseChanges(prev: PortfolioResult | null | undefined, next: PortfolioResult): ReleaseChange[] {
if (!prev) return [];
const WATCH = new Set(['review', 'sent', 'released', 'approved', 'rejected', 'live', 'halted']);
const out: ReleaseChange[] = [];
for (const a of next.apps) {
const before = prev.apps.find(p => p.project === a.project);
if (!before || before.release.source !== 'live' || a.release.source !== 'live') continue;
for (const r of a.release.all ?? []) {
if (!r.version) continue;
const old = (before.release.all ?? []).find(x => x.store === r.store && (x.version === r.version || sameVersion(x.version, r.version)));
if (!old || old.phase === r.phase) continue;
if (WATCH.has(r.phase) || WATCH.has(old.phase)) out.push({ project: a.project, name: a.app?.name ?? a.project, store: r.store, version: r.version, from: old.phase, to: r.phase });
}
}
return out;
}
/** New 1-2 star reviews since the previous read. */
export function newLowReviews(prev: Review[] | null | undefined, next: Review[]): Review[] {
if (!prev) return [];
const seen = new Set(prev.map(r => `${r.store}:${r.id}`));
return next.filter(r => r.rating <= 2 && !seen.has(`${r.store}:${r.id}`));
}
/** Crash or ANR rate that went over the Play bad behavior threshold since the previous read. */
export function crashCrossed(prev: HealthSnapshot | null | undefined, next: HealthSnapshot): ('crash' | 'anr')[] {
if (!prev?.play || isError(prev.play) || !next.play || isError(next.play)) return [];
const out: ('crash' | 'anr')[] = [];
const was = { crash: healthSeries(prev.play.crash, 'userPerceivedCrashRate', PLAY_THRESHOLDS.crash), anr: healthSeries(prev.play.anr, 'userPerceivedAnrRate', PLAY_THRESHOLDS.anr) };
const now = { crash: healthSeries(next.play.crash, 'userPerceivedCrashRate', PLAY_THRESHOLDS.crash), anr: healthSeries(next.play.anr, 'userPerceivedAnrRate', PLAY_THRESHOLDS.anr) };
for (const k of ['crash', 'anr'] as const) if (now[k].tone === 'bad' && was[k].tone !== 'bad') out.push(k);
return out;
}