group code popup
This commit is contained in:
@@ -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; }
|
||||
|
||||
@@ -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
@@ -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));
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user