group code popup
Sonarqube Scanner / Prepare environment (push) Successful in 6s
Sonarqube Scanner / Build and test (push) Successful in 27s
Sonarqube Scanner / SonarQube analysis (push) Failing after 1m14s
Sonarqube Scanner / Deploy to Server (push) Successful in 10s

This commit is contained in:
Florian Zumpe
2026-10-18 17:24:41 +02:00
parent 31768ae907
commit 0e4cdfbd54
18 changed files with 400 additions and 49 deletions
+9
View File
@@ -1652,6 +1652,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
if (shareInProgress) return;
shareInProgress = true;
try {
if (ns.Groups.confirmShare && !await ns.Groups.confirmShare(routeName(route))) return;
let sourceId = routeId(route);
if (routeSource(route) === 'local') {
if (!globalThis.confirm(ns.I18n.t('groups.upload-confirm'))) return;
@@ -1712,6 +1713,14 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
function bind() {
for (const eventName of ['input', 'change', 'pointerdown', 'keydown']) {
byId(ids.poiForm)?.addEventListener(eventName, () => {
if (state.currentSource === 'group') ns.GroupPoints?.noteActivity?.();
});
}
document.addEventListener('wegwichtel:point-lock-lost', () => {
if (state.currentSource === 'group') setStatus(ns.I18n.t('point-locks.lost'));
});
for (const eventName of ['cancel', 'close']) {
byId(ids.dialog)?.addEventListener(eventName, event => {
if (pointSaveInProgress && eventName === 'cancel') { event.preventDefault(); return; }
+22 -1
View File
@@ -5,6 +5,9 @@ globalThis.Wegwichtel ??= {};
let database;
let current = null;
let generation = 0;
let activity = 0;
let sentActivity = 0;
let touching = false;
const key = (routeId, poiId) => `${routeId}:${poiId}`;
async function store(mode, operation) {
if (!database) database = new Promise((resolve, reject) => {
@@ -45,6 +48,7 @@ globalThis.Wegwichtel ??= {};
method: 'POST', credentials: 'same-origin', cache: 'no-store',
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
});
if (response.status === 204) return {};
const payload = await response.json();
if (!response.ok) {
const error = new Error(t(response.status === 423 ? 'lost' : 'unavailable'));
@@ -158,6 +162,23 @@ globalThis.Wegwichtel ??= {};
if (savingGeneration !== generation) return false;
return remote;
}
function noteActivity() { activity += 1; }
async function keepActive() {
if (!current?.token || activity === sentActivity || touching) return;
const editing = current;
const observed = activity;
touching = true;
try {
await request(editing.routeId, editing.poiId, { heartbeat: true, token: editing.token });
if (editing === current) sentActivity = observed;
} catch (error) {
if (error.status === 423) {
sentActivity = observed;
document.dispatchEvent(new Event('wegwichtel:point-lock-lost'));
}
} finally { touching = false; }
}
globalThis.setInterval?.(() => { void keepActive(); }, 30_000);
function headersFor(path) {
const match = /^\/routes\/(\d+)\/pois\/(\d+)(?:\/|$)/.exec(path);
if (!match || !current || String(current.routeId) !== match[1] || String(current.poiId) !== match[2]) return {};
@@ -167,7 +188,7 @@ globalThis.Wegwichtel ??= {};
const version = response.headers?.get('X-Poi-Version');
if (current && String(current.poiId) === response.headers?.get('X-Poi-Id') && version) current.version = version;
}
ns.GroupPoints = Object.freeze({ begin, release, remember, read, list, remove, prepare, headersFor, observe,
ns.GroupPoints = Object.freeze({ begin, release, remember, read, list, remove, prepare, headersFor, observe, noteActivity, keepActive,
acceptNew: async (routeId, poi) => { current = { ...await acquire(routeId, poi.id), routeId, poiId: poi.id }; },
changed: () => document.dispatchEvent(new Event('wegwichtel:point-drafts-changed')) });
}(globalThis.Wegwichtel));
+44 -8
View File
@@ -8,7 +8,7 @@ globalThis.Wegwichtel ??= {};
entry: 'group-entry', invitation: 'group-invitation', code: 'group-code', join: 'group-join',
edit: 'group-edit', expiry: 'group-expiry'
};
const codeLength = 8;
const codeLength = 5;
const wheelStep = 52;
const revisions = new Map();
let selection = [];
@@ -43,6 +43,7 @@ globalThis.Wegwichtel ??= {};
function errorMessage(error) {
if (error.reason === 'group-limit') return t('error.limit');
if (error.reason === 'group-full') return t('error.full');
if (error.reason === 'group-editing') return t('code-wait');
if (error.status === 423) return ns.I18n.t('point-locks.busy');
if (error.status === 429) return t('error.rate');
if (error.status === 404) return t('error.code');
@@ -167,6 +168,19 @@ globalThis.Wegwichtel ??= {};
if (!element(ids.dialog).open) element(ids.dialog).showModal();
}
let invitationPolling = false;
async function refreshInvitation() {
const group = currentGroup;
if (!group || invitationPolling || !element(ids.dialog).open || element(ids.invitation).hidden) return;
invitationPolling = true;
try {
const payload = await request(`/${group.id}/invitation`);
if (currentGroup?.id !== group.id || !element(ids.dialog).open || element(ids.invitation).hidden) return;
if (payload.code) showInvitation(payload);
element(ids.status).textContent = payload.busy && payload.expired ? t('code-wait') : '';
} catch { /* Keep the invitation visible; expiry still applies on the server. */ }
finally { invitationPolling = false; }
}
async function edit(group) {
element(ids.dialog).close();
await ns.EditorUi.openGroup(group.route);
@@ -244,18 +258,34 @@ globalThis.Wegwichtel ??= {};
function shareButton(action) {
const result = document.createElement('button');
result.type = 'button';
result.className = 'editor-icon-button group-pair';
result.className = 'editor-icon-button';
result.dataset.editorAction = action;
result.title = t('share');
result.setAttribute('aria-label', t('share'));
for (const value of ['🐦', '🪨']) {
const image = picture(ns.GroupSymbols.get(value));
image.alt = '';
result.appendChild(image);
}
result.setAttribute('aria-haspopup', 'dialog');
result.textContent = '👥';
return result;
}
let shareDecision = null;
function confirmShare(name) {
if (shareDecision) return Promise.resolve(false);
const dialog = element('group-share-dialog');
element('group-share-route').textContent = name;
element('group-share-pictures').replaceChildren(...['🐦', '🪨'].map(value => picture(ns.GroupSymbols.get(value))));
return new Promise(resolve => {
shareDecision = resolve;
dialog.showModal();
});
}
function finishShare(accepted) {
const resolve = shareDecision;
shareDecision = null;
element('group-share-dialog').close();
resolve?.(accepted);
}
function headersFor(path) {
const id = /^\/routes\/(\d+)(?:\/|$)/.exec(path)?.[1];
return id && revisions.has(id) ? { 'If-Match': String(revisions.get(id)) } : {};
@@ -268,6 +298,12 @@ globalThis.Wegwichtel ??= {};
}
function init() {
globalThis.setInterval?.(() => { void refreshInvitation(); }, 30_000);
activate(element('group-share-start'), () => finishShare(true));
activate(element('group-share-close'), () => finishShare(false));
element('group-share-dialog').addEventListener('close', () => {
const resolve = shareDecision; shareDecision = null; resolve?.(false);
});
document.addEventListener('wegwichtel:point-drafts-changed', () => { void renderPending(); void ns.StorageUsage?.refresh(); });
element(ids.dialog).addEventListener('cancel', () => { dialogGeneration += 1; });
element(ids.dialog).addEventListener('close', () => { dialogGeneration += 1; });
@@ -292,7 +328,7 @@ globalThis.Wegwichtel ??= {};
activate(element(ids.edit), () => guarded(() => edit(currentGroup)));
}
ns.Groups = Object.freeze({ share, shareButton, refresh, headersFor, observe });
ns.Groups = Object.freeze({ share, shareButton, confirmShare, refresh, refreshInvitation, headersFor, observe });
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init, { once: true });
else init();
}(globalThis.Wegwichtel));
+4 -1
View File
@@ -134,12 +134,15 @@ globalThis.Wegwichtel ??= {};
"ready": "Alles ist bereit."
},
"groups": {
"code-wait": "In der Gruppe wird gerade eine Station bearbeitet. Ein neuer Code erscheint, sobald die Stationen wieder frei sind. Ein abgelaufener Code erlaubt keinen neuen Beitritt.",
"share-help": "Mit diesen beiden Wichteln öffnest du einen gemeinsamen Entwurf. Andere können mit einem Wichtelcode mitmachen. Das Original bleibt erhalten.",
"share-start": "Gemeinsamen Entwurf öffnen",
"title": "Gemeinsame Entwürfe",
"upload-confirm": "Dein Entwurf mit Bildern und Ton wird dafür an die Website gesendet und als gemeinsamer Entwurf kopiert. Fortfahren?",
"share": "Gemeinsam bearbeiten",
"join-open": "Gruppencode eingeben",
"online": "Gemeinsam bearbeiten braucht eine Internetverbindung.",
"wheel-help": "Schiebe die Wichtel nach oben oder unten. Die Wichtel in der Mitte ergeben den Code. Lies von links nach rechts, zuerst die obere Reihe.",
"wheel-help": "Schiebe die Wichtel nach oben oder unten. Die fünf Wichtel in der Mitte ergeben den Code. Lies von links nach rechts. Ein Wichtel darf mehrmals vorkommen.",
"code-label": "Gruppencode",
"join": "Mitmachen",
"invite-help": "Zeige diesen Code den anderen. Damit können sie deinen gemeinsamen Entwurf öffnen und bearbeiten. Das Original bleibt erhalten.",
+4 -1
View File
@@ -134,12 +134,15 @@ globalThis.Wegwichtel ??= {};
"ready": "Everything is ready."
},
"groups": {
"code-wait": "Someone in the group is editing a stop. A new code will appear once the stops are free. An expired code cannot be used to join.",
"share-help": "These two gnomes open a shared draft. Others can join using a gnome code. The original stays unchanged.",
"share-start": "Open a shared draft",
"title": "Shared drafts",
"upload-confirm": "Your draft, including pictures and recordings, will be sent to the website and copied into a shared draft. Continue?",
"share": "Edit together",
"join-open": "Enter group code",
"online": "You need an internet connection to edit together.",
"wheel-help": "Slide the gnomes up or down. The gnomes in the middle make the code. Read from left to right, starting with the top row.",
"wheel-help": "Slide the gnomes up or down. The gnomes in the middle make the code. Read the five gnomes from left to right. The same gnome can appear more than once.",
"code-label": "Group code",
"join": "Join in",
"invite-help": "Show this code to the others so they can open and edit your shared draft. The original stays as it is.",