Een dating-app op basis van brieven, niet van gezichten!
Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.
import { Fragment, useEffect, useState } from "react";
import { INK, RED, SKY, FONT_MONO } from "../../theme";
import { POST_HOURS } from "../../constants";
import { postState, enablePost, disablePost, setPostHours } from "../../lib/postbode";
import { meldingFout } from "../../lib/plugin-fouten";
import { osNaam } from "../../lib/platform";
/*
* Notificaties, bij Mijn profiel: een schakelaar en de bezorgmomenten. Uit tot je hem aanzet — een
* melding is het enige in deze app dat uit zichzelf om aandacht vraagt.
*
* Kort gehouden met opzet. Hoe het werkt en waarom het zo werkt staat in de privacyverklaring en de
* README, niet hier: een instelscherm dat zichzelf verantwoordt is een instelscherm dat je niet
* leest. De momenten zijn hele uren (Android bepaalt zelf wanneer een app op de achtergrond aan de
* beurt is, dus minuten zouden precisie beloven die er niet is) en ook te verzetten terwijl de
* schakelaar uit staat. `getSince()` levert de hoogste brief die de app nu kent; zie lib/postbode.js.
*/
const uurLabel = (u) => String(u).padStart(2, "0") + ":00";
const ALLE_UREN = Array.from({ length: 24 }, (_, u) => u);
/* Wat "uit" is in een keuzemenu: een lege waarde, want een uur is het niet */
const UIT = "";
export default function PostSection({ token, getSince }) {
const [state, setState] = useState(null); // { enabled, allowed, hours } — null zolang onbekend
const [busy, setBusy] = useState(false);
const [denied, setDenied] = useState(false);
const [fout, setFout] = useState(null); // een afgewezen aanroep, al geduid (zie lib/plugin-fouten)
/*
* Elk antwoord van de plugin komt hier langs. Een `fout` is geen state: hij zegt niets over hoe de
* bezorging ervoor staat, alleen dat deze poging strandde — dus hij gaat apart, en wat er op het
* scherm stond blijft staan in plaats van te worden overschreven met een halve waarheid.
*/
const verwerk = (antwoord) => {
if (!antwoord) return false; // geen plugin (browser, of een app-versie die hem niet kent)
if (antwoord.fout) {
setFout(meldingFout(antwoord.fout));
return false;
}
setState((s) => ({ ...s, ...antwoord }));
return true;
};
useEffect(() => {
let levend = true;
postState().then((s) => levend && verwerk(s));
return () => { levend = false; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
/*
* Altijd evenveel keuzemenu's als POST_HOURS momenten kent, gevuld met wat er bewaard is en
* verder `null` — dat is een menu op "uit". De momenten zijn een verzameling tijdstippen, dus wat
* terugkomt is oplopend en ontdubbeld; de menu's volgen die volgorde.
*/
const bewaard = state?.hours ?? POST_HOURS.filter((u) => u !== null);
const uren = POST_HOURS.map((_, i) => bewaard[i] ?? null);
const actief = uren.filter((u) => u !== null);
const schakel = async (aan) => {
setBusy(true);
setDenied(false);
setFout(null);
const next = aan ? await enablePost({ token, since: getSince(), hours: actief }) : await disablePost();
if (next?.denied) setDenied(true);
verwerk(next);
setBusy(false);
};
/* Eén moment verzetten of uitzetten (uur = null). Het laatste moment kan niet uit, zie hieronder. */
const verzet = async (index, uur) => {
const volgende = uren.map((u, i) => (i === index ? uur : u)).filter((u) => u !== null);
setState((s) => ({ ...(s ?? {}), hours: volgende })); // meteen zichtbaar, ook als de plugin traag is
setFout(null);
verwerk(await setPostHours(volgende));
};
const aan = state?.enabled === true;
/* Aan in de app, uit in de systeeminstellingen: dan verschijnt er niets en hoort dat er te staan */
const gesmoord = aan && state?.allowed === false;
const keuze = {
padding: "6px 8px", border: `2px solid ${INK}`, background: "#fff", color: INK,
fontSize: 14, fontWeight: 600, fontFamily: FONT_MONO,
};
return (
<div style={{ marginTop: 36, paddingTop: 16, borderTop: `1px solid ${SKY}` }}>
<h3 style={{ margin: "0 0 10px", fontSize: 16 }}>Notificaties</h3>
<label style={{ display: "flex", gap: 8, alignItems: "center", fontSize: 14, marginBottom: 12 }}>
<input type="checkbox" checked={aan} disabled={busy} onChange={(e) => schakel(e.target.checked)} />
Zeg het als er post ligt
</label>
<div style={{ display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }}>
<span style={{ fontSize: 14 }}>Postbode komt langs om:</span>
{uren.map((uur, i) => (
<Fragment key={i}>
{i > 0 && <span style={{ fontSize: 14 }}>en</span>}
<select
value={uur ?? UIT}
onChange={(e) => verzet(i, e.target.value === UIT ? null : +e.target.value)}
style={keuze}
aria-label={`Bezorgmoment ${i + 1}`}
>
{/*
* "uit" staat er niet bij als dit het énige moment is dat nog aan staat: dan zou de
* schakelaar aan kunnen staan zonder dat er ooit iets langskomt. Geen bezorging wil
* je met de schakelaar zeggen, niet met een leeg rijtje tijdstippen.
*/}
{(uur === null || actief.length > 1) && <option value={UIT}>uit</option>}
{ALLE_UREN.map((u) => (
<option key={u} value={u}>{uurLabel(u)}</option>
))}
</select>
</Fragment>
))}
</div>
{fout && (
<p role="alert" style={{ fontSize: 13, color: RED, margin: "10px 0 0" }}>
Instellen lukte niet: {fout}
</p>
)}
{denied && (
<p role="alert" style={{ fontSize: 13, color: RED, margin: "10px 0 0" }}>
{osNaam()} liet de melding niet toe. Zet meldingen aan bij de instellingen van je toestel.
</p>
)}
{gesmoord && (
<p role="alert" style={{ fontSize: 13, color: RED, margin: "10px 0 0" }}>
Meldingen staan uit bij de instellingen van je toestel.
</p>
)}
</div>
);
}