Drafts survive reload
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
import { findTask, onEach, state } from "./state.js";
|
||||
|
||||
type Saved = {
|
||||
composer: Record<string, { text: string; images: number }>;
|
||||
checked: Record<string, string[]>;
|
||||
texts: Record<string, string>;
|
||||
};
|
||||
|
||||
const composerTexts = new Map<string, string>();
|
||||
const composerImages = new Map<string, number>();
|
||||
type Send = { key: string; text: string; images: number };
|
||||
|
||||
const sending = new Set<Send>();
|
||||
const lostImages = new Map<string, number>();
|
||||
const checked = new Map<string, string[]>();
|
||||
const texts = new Map<string, string>();
|
||||
|
||||
let storeKey: string | undefined;
|
||||
let timer = 0;
|
||||
|
||||
export function loadDrafts(): void {
|
||||
storeKey = `byre.drafts.${state.me!.login}`;
|
||||
const saved = readSaved(storeKey);
|
||||
for (const [key, d] of Object.entries(saved.composer)) {
|
||||
if (d.text) {
|
||||
composerTexts.set(key, d.text);
|
||||
}
|
||||
if (d.images > 0) {
|
||||
lostImages.set(key, d.images);
|
||||
}
|
||||
}
|
||||
for (const [id, opts] of Object.entries(saved.checked)) {
|
||||
checked.set(id, opts);
|
||||
}
|
||||
for (const [id, text] of Object.entries(saved.texts)) {
|
||||
texts.set(id, text);
|
||||
}
|
||||
prune();
|
||||
onEach(["tasks", "inbox"], prune);
|
||||
addEventListener("pagehide", flushDrafts);
|
||||
}
|
||||
|
||||
function readSaved(key: string): Saved {
|
||||
const out: Saved = { composer: {}, checked: {}, texts: {} };
|
||||
let raw: unknown;
|
||||
try {
|
||||
raw = JSON.parse(localStorage.getItem(key) ?? "{}");
|
||||
} catch (err) {
|
||||
console.error("read drafts", err);
|
||||
return out;
|
||||
}
|
||||
if (!isRecord(raw)) {
|
||||
return out;
|
||||
}
|
||||
for (const [k, v] of Object.entries(isRecord(raw["composer"]) ? raw["composer"] : {})) {
|
||||
if (isRecord(v) && typeof v["text"] === "string" && typeof v["images"] === "number") {
|
||||
out.composer[k] = { text: v["text"], images: v["images"] };
|
||||
}
|
||||
}
|
||||
for (const [k, v] of Object.entries(isRecord(raw["checked"]) ? raw["checked"] : {})) {
|
||||
if (Array.isArray(v) && v.every((o) => typeof o === "string")) {
|
||||
out.checked[k] = v as string[];
|
||||
}
|
||||
}
|
||||
for (const [k, v] of Object.entries(isRecord(raw["texts"]) ? raw["texts"] : {})) {
|
||||
if (typeof v === "string") {
|
||||
out.texts[k] = v;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function isRecord(v: unknown): v is Record<string, unknown> {
|
||||
return typeof v === "object" && v !== null && !Array.isArray(v);
|
||||
}
|
||||
|
||||
function prune(): void {
|
||||
let changed = false;
|
||||
for (const map of [composerTexts, composerImages, lostImages]) {
|
||||
for (const key of map.keys()) {
|
||||
const taskId = key.replace(/^message:/, "");
|
||||
if (taskId !== "lead" && findTask(taskId)?.status !== "active") {
|
||||
map.delete(key);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const map of [checked, texts]) {
|
||||
for (const id of map.keys()) {
|
||||
if (!state.inbox.some((i) => i.id === id)) {
|
||||
map.delete(id);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
function save(): void {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(flushDrafts, 400);
|
||||
}
|
||||
|
||||
export function flushDrafts(): void {
|
||||
clearTimeout(timer);
|
||||
timer = 0;
|
||||
if (!storeKey) {
|
||||
return;
|
||||
}
|
||||
const out: Saved = { composer: {}, checked: Object.fromEntries(checked), texts: Object.fromEntries(texts) };
|
||||
const keys = new Set([...composerTexts.keys(), ...composerImages.keys(), ...[...sending].map((s) => s.key), ...lostImages.keys()]);
|
||||
for (const key of keys) {
|
||||
const inFlight = [...sending].filter((s) => s.key === key);
|
||||
const text = [...inFlight.map((s) => s.text), composerTexts.get(key) ?? ""].filter((t) => t).join("\n\n");
|
||||
const images = (composerImages.get(key) ?? 0) + inFlight.reduce((n, s) => n + s.images, 0) + (lostImages.get(key) ?? 0);
|
||||
if (text || images > 0) {
|
||||
out.composer[key] = { text, images };
|
||||
}
|
||||
}
|
||||
try {
|
||||
localStorage.setItem(storeKey, JSON.stringify(out));
|
||||
} catch (err) {
|
||||
console.error("save drafts", err);
|
||||
}
|
||||
}
|
||||
|
||||
export function composerText(key: string): string {
|
||||
return composerTexts.get(key) ?? "";
|
||||
}
|
||||
|
||||
export function setComposerText(key: string, text: string): void {
|
||||
if (text) {
|
||||
composerTexts.set(key, text);
|
||||
} else {
|
||||
composerTexts.delete(key);
|
||||
}
|
||||
save();
|
||||
}
|
||||
|
||||
export function setComposerImages(key: string, n: number): void {
|
||||
if (n > 0) {
|
||||
composerImages.set(key, n);
|
||||
} else {
|
||||
composerImages.delete(key);
|
||||
}
|
||||
save();
|
||||
}
|
||||
|
||||
export function lostImageCount(key: string): number {
|
||||
return lostImages.get(key) ?? 0;
|
||||
}
|
||||
|
||||
export function forgetLostImages(key: string): void {
|
||||
lostImages.delete(key);
|
||||
save();
|
||||
}
|
||||
|
||||
export function startSend(key: string, text: string, images: number): Send {
|
||||
const send = { key, text, images };
|
||||
sending.add(send);
|
||||
composerTexts.delete(key);
|
||||
return send;
|
||||
}
|
||||
|
||||
export function endSend(send: Send, ok: boolean): void {
|
||||
sending.delete(send);
|
||||
if (ok) {
|
||||
lostImages.delete(send.key);
|
||||
} else {
|
||||
const text = [send.text, composerTexts.get(send.key) ?? ""].filter((t) => t).join("\n\n");
|
||||
if (text) {
|
||||
composerTexts.set(send.key, text);
|
||||
}
|
||||
}
|
||||
flushDrafts();
|
||||
}
|
||||
|
||||
export function checkedAnswer(id: string): string[] {
|
||||
return checked.get(id) ?? [];
|
||||
}
|
||||
|
||||
export function setCheckedAnswer(id: string, options: string[]): void {
|
||||
checked.set(id, options);
|
||||
save();
|
||||
}
|
||||
|
||||
export function hasTextAnswer(id: string): boolean {
|
||||
return texts.has(id);
|
||||
}
|
||||
|
||||
export function textAnswer(id: string): string | undefined {
|
||||
return texts.get(id);
|
||||
}
|
||||
|
||||
export function setTextAnswer(id: string, text: string): void {
|
||||
texts.set(id, text);
|
||||
save();
|
||||
}
|
||||
|
||||
export function dropTextAnswer(id: string): void {
|
||||
texts.delete(id);
|
||||
save();
|
||||
}
|
||||
|
||||
export function clearAnswer(id: string): void {
|
||||
checked.delete(id);
|
||||
texts.delete(id);
|
||||
flushDrafts();
|
||||
}
|
||||
Reference in New Issue
Block a user