mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-02 00:06:23 +00:00
Every widget tap runs as its own headless task in the same JS runtime, and the tasks interleave at every await. Each one loaded the stored snapshot, edited it and saved it back, then refreshed from the server. That went wrong in three ways: - two taps loaded the same snapshot and the second save dropped the first change; - the refresh of the first tap read the server before the second tap's archive landed and was stored anyway, so the message came back (seen on the emulator with a slow `Email/set`); - a redraw of an older snapshot could land after a newer one, leaving buttons that act on a message the widget no longer shows. The snapshot now only holds what the server last said. A button's change is kept as a pending op next to it and laid over it when a widget draws; it stays until a refresh that started after the server took it has been stored (calendar and task changes only count as caught up by a refresh that loaded calendar and tasks). A call that fails drops its op, which also undoes an archive made offline: before, the failed refresh kept the edited snapshot and the message stayed hidden although it was never archived. Reads, writes and redraws of the widget data run one at a time, and every redraw draws the latest view.
49 lines
1.8 KiB
TypeScript
49 lines
1.8 KiB
TypeScript
// Entry point of the headless task react-native-android-widget starts for
|
|
// every widget event (added, periodic update, resize, removed, tap). It can
|
|
// run in a cold JS runtime with no UI, so it only touches the stored snapshot
|
|
// and the widgets' own JMAP code.
|
|
|
|
import type { WidgetTaskHandlerProps } from 'react-native-android-widget';
|
|
import { handleWidgetAction } from './actions';
|
|
import { refreshSnapshot } from './build';
|
|
import { removeLocal } from './local-state';
|
|
import { drawOne, invalidatePlacedCache, redrawAll } from './render';
|
|
import { loadSnapshot } from './snapshot';
|
|
|
|
/** A snapshot younger than this is drawn as it is; older ones trigger a fetch. */
|
|
const FRESH_MS = 5 * 60 * 1000;
|
|
|
|
export async function widgetTaskHandler(props: WidgetTaskHandlerProps): Promise<void> {
|
|
const { widgetInfo, widgetAction, clickAction, clickActionData, renderWidget } = props;
|
|
const name = widgetInfo.widgetName;
|
|
|
|
switch (widgetAction) {
|
|
case 'WIDGET_ADDED':
|
|
case 'WIDGET_UPDATE':
|
|
case 'WIDGET_RESIZED': {
|
|
if (widgetAction === 'WIDGET_ADDED') invalidatePlacedCache();
|
|
renderWidget(await drawOne(name, widgetInfo));
|
|
if (widgetAction === 'WIDGET_RESIZED') return;
|
|
const stored = await loadSnapshot();
|
|
if (stored && Date.now() - stored.generatedAt < FRESH_MS) return;
|
|
// Every placed widget gets its own periodic update; they share one
|
|
// refresh (see refreshSnapshot) and the redraws that follow collapse.
|
|
await refreshSnapshot();
|
|
await redrawAll();
|
|
return;
|
|
}
|
|
|
|
case 'WIDGET_DELETED':
|
|
invalidatePlacedCache();
|
|
await removeLocal(widgetInfo.widgetId);
|
|
return;
|
|
|
|
case 'WIDGET_CLICK':
|
|
if (clickAction) await handleWidgetAction(clickAction, clickActionData ?? {}, widgetInfo.widgetId);
|
|
return;
|
|
|
|
default:
|
|
return;
|
|
}
|
|
}
|