237 lines
8.1 KiB
JavaScript
237 lines
8.1 KiB
JavaScript
// Einstellungen des angemeldeten Kontos: Anzeigename, Passwort,
|
||
// Benachrichtigungen.
|
||
"use strict";
|
||
|
||
import { del, patch, post } from "../api.js";
|
||
import { el, mount } from "../dom.js";
|
||
import * as push from "../push.js";
|
||
import { set, state } from "../store.js";
|
||
import { appBar } from "../appbar.js";
|
||
|
||
function formatDate(iso) {
|
||
return new Date(iso).toLocaleDateString("de-DE", {
|
||
day: "2-digit", month: "2-digit", year: "numeric",
|
||
});
|
||
}
|
||
|
||
export async function settingsView(root, { back, admin }) {
|
||
let banner = null;
|
||
let pushStatus = null;
|
||
let devices = [];
|
||
|
||
async function reload() {
|
||
pushStatus = await push.status();
|
||
devices = pushStatus.serverEnabled
|
||
? await push.listDevices().catch(() => [])
|
||
: [];
|
||
}
|
||
|
||
function say(message, kind = "notice") {
|
||
banner = { message, kind };
|
||
render();
|
||
setTimeout(() => { banner = null; render(); }, 8000);
|
||
}
|
||
|
||
async function guarded(fn) {
|
||
try {
|
||
await fn();
|
||
await reload();
|
||
render();
|
||
} catch (err) {
|
||
say(err.message, "error");
|
||
}
|
||
}
|
||
|
||
// -------------------------------------------------------------------
|
||
// Benachrichtigungen
|
||
// -------------------------------------------------------------------
|
||
|
||
function pushSection() {
|
||
if (!pushStatus.serverEnabled) {
|
||
return el("section.card", {},
|
||
el("h2", {}, "Benachrichtigungen"),
|
||
el("p.lead", {},
|
||
"Auf diesem Server sind Push-Benachrichtigungen nicht eingerichtet. " +
|
||
"Dafür wird ein VAPID-Schlüsselpaar in der Konfiguration benötigt."));
|
||
}
|
||
|
||
// Auf iOS gibt es Push nur in der installierten App - das ist eine
|
||
// Einschränkung von Apple, kein Fehler der Anwendung.
|
||
if (pushStatus.needsInstall) {
|
||
return el("section.card", {},
|
||
el("h2", {}, "Benachrichtigungen"),
|
||
el("p.lead.warn", {},
|
||
"Auf iPhone und iPad funktionieren Benachrichtigungen nur, wenn die " +
|
||
"App über „Teilen → Zum Home-Bildschirm“ installiert wurde. " +
|
||
"Öffne sie danach über das Symbol auf dem Startbildschirm und " +
|
||
"komm hierher zurück."));
|
||
}
|
||
|
||
if (!pushStatus.supported) {
|
||
return el("section.card", {},
|
||
el("h2", {}, "Benachrichtigungen"),
|
||
el("p.lead", {},
|
||
"Dieser Browser unterstützt keine Push-Benachrichtigungen. " +
|
||
"Über eine unverschlüsselte Verbindung sind sie ebenfalls nicht " +
|
||
"möglich – dann hilft der Zugriff über HTTPS."));
|
||
}
|
||
|
||
const blocked = pushStatus.permission === "denied";
|
||
|
||
return el("section.card", {},
|
||
el("h2", {}, "Benachrichtigungen"),
|
||
el("p.lead", {},
|
||
"Wenn jemand eine geteilte Liste ändert, bekommst du eine Meldung – ",
|
||
el("strong", {}, `höchstens eine je Liste alle ${pushStatus.throttleHours} Stunden`),
|
||
". Wer im Laden steht und abhakt, erzeugt sonst im Minutentakt " +
|
||
"Benachrichtigungen."),
|
||
|
||
blocked
|
||
? el("p.lead.warn", {},
|
||
"Benachrichtigungen wurden für diese Seite abgelehnt. Das lässt " +
|
||
"sich nur in den Einstellungen des Browsers wieder ändern – " +
|
||
"meist über das Schloss- oder Info-Symbol in der Adresszeile.")
|
||
: null,
|
||
|
||
pushStatus.subscribed
|
||
? el("div.menu-actions", {},
|
||
el("button", {
|
||
type: "button",
|
||
onclick: () => guarded(async () => {
|
||
await push.sendTest();
|
||
say("Testnachricht unterwegs. Kommt sie nicht an, steht der " +
|
||
"Grund im Log des api-Containers.");
|
||
}),
|
||
}, "Testnachricht senden"),
|
||
el("button.danger", {
|
||
type: "button",
|
||
onclick: () => guarded(async () => {
|
||
await push.unsubscribe();
|
||
say("Dieses Gerät bekommt keine Benachrichtigungen mehr.");
|
||
}),
|
||
}, "Auf diesem Gerät abschalten"))
|
||
: el("button.primary", {
|
||
type: "button",
|
||
disabled: blocked,
|
||
onclick: () => guarded(async () => {
|
||
const result = await push.subscribe();
|
||
if (!result.ok) {
|
||
say(result.reason, "error");
|
||
return;
|
||
}
|
||
say("Benachrichtigungen für dieses Gerät eingeschaltet.");
|
||
}),
|
||
}, "Auf diesem Gerät einschalten"),
|
||
|
||
devices.length
|
||
? el("div", {},
|
||
el("h3.device-heading", {}, "Angemeldete Geräte"),
|
||
el("ul.share-list", {}, devices.map((device) =>
|
||
el("li", {},
|
||
el("div.who-block", {},
|
||
el("span.name", {}, device.label || "Unbenanntes Gerät"),
|
||
el("span.sub", {},
|
||
`seit ${formatDate(device.created_at)}`,
|
||
device.last_success_at
|
||
? ` · zuletzt erreicht ${formatDate(device.last_success_at)}`
|
||
: " · noch nichts zugestellt")),
|
||
el("div.member-actions", {},
|
||
el("button.danger", {
|
||
type: "button",
|
||
onclick: () => guarded(() =>
|
||
del(`/api/push/subscriptions/${device.id}`)),
|
||
}, "Entfernen"))))))
|
||
: null
|
||
);
|
||
}
|
||
|
||
// -------------------------------------------------------------------
|
||
// Profil und Passwort
|
||
// -------------------------------------------------------------------
|
||
|
||
function profileSection() {
|
||
const nameField = el("input", {
|
||
type: "text",
|
||
value: state.user.display_name || "",
|
||
maxLength: 80,
|
||
placeholder: "wird anderen Mitgliedern angezeigt",
|
||
});
|
||
|
||
return el("section.card", {},
|
||
el("h2", {}, "Profil"),
|
||
el("p.lead", {}, state.user.email),
|
||
el("label", {}, "Anzeigename ", el("span.hint", {}, "(freiwillig)")),
|
||
el("div.row", {}, nameField,
|
||
el("button.primary", {
|
||
type: "button",
|
||
onclick: () => guarded(async () => {
|
||
const user = await patch("/api/auth/me", {
|
||
display_name: nameField.value.trim() || null,
|
||
});
|
||
set({ user });
|
||
say("Gespeichert.");
|
||
}),
|
||
}, "Speichern"))
|
||
);
|
||
}
|
||
|
||
function passwordSection() {
|
||
const current = el("input", { type: "password", autocomplete: "current-password" });
|
||
const next = el("input", { type: "password", autocomplete: "new-password" });
|
||
const repeat = el("input", { type: "password", autocomplete: "new-password" });
|
||
|
||
return el("section.card", {},
|
||
el("h2", {}, "Passwort ändern"),
|
||
el("p.lead", {},
|
||
"Nach der Änderung werden alle anderen Geräte abgemeldet."),
|
||
el("label", {}, "Bisheriges Passwort"), current,
|
||
el("label", {}, "Neues Passwort ", el("span.hint", {}, "(mindestens 12 Zeichen)")), next,
|
||
el("label", {}, "Wiederholen"), repeat,
|
||
el("button.primary", {
|
||
type: "button",
|
||
onclick: () => guarded(async () => {
|
||
if (next.value !== repeat.value) {
|
||
throw new Error("Die beiden Eingaben stimmen nicht überein.");
|
||
}
|
||
const result = await post("/api/auth/password/change", {
|
||
current_password: current.value,
|
||
new_password: next.value,
|
||
});
|
||
current.value = next.value = repeat.value = "";
|
||
say(result.detail);
|
||
}),
|
||
}, "Passwort ändern")
|
||
);
|
||
}
|
||
|
||
function render() {
|
||
mount(root,
|
||
appBar({
|
||
back: { label: "Listen", onClick: back },
|
||
title: "Einstellungen",
|
||
actions: state.user.is_admin
|
||
? [{ label: "Benutzerverwaltung", onClick: admin,
|
||
hint: "Konten, Einladungen, Bereinigung" }]
|
||
: [],
|
||
}),
|
||
|
||
banner ? el(banner.kind === "error" ? "p.error" : "p.notice", {}, banner.message) : null,
|
||
|
||
// Die Benutzerverwaltung steht im Menü der Kopfleiste - nur für
|
||
// Administratoren, und der Endpunkt prüft zusätzlich.
|
||
el("section.card", {},
|
||
el("h1", {}, state.user.display_name || state.user.email),
|
||
el("p.lead", {},
|
||
state.user.email,
|
||
state.user.is_admin ? " · Administrator" : "")),
|
||
pushSection(),
|
||
profileSection(),
|
||
passwordSection()
|
||
);
|
||
}
|
||
|
||
await reload();
|
||
render();
|
||
return render;
|
||
}
|