fix tests and change invitation mechanics
This commit is contained in:
+78
-21
@@ -13,7 +13,9 @@ globalThis.Wegwichtel ??= {};
|
||||
const wheelStep = 96;
|
||||
const revisions = new Map();
|
||||
let selection = [];
|
||||
let codeChoice = null;
|
||||
let currentGroup = null;
|
||||
let currentExpiry = 0;
|
||||
let pending = false;
|
||||
let dialogGeneration = 0;
|
||||
const element = id => document.getElementById(id);
|
||||
@@ -42,6 +44,8 @@ globalThis.Wegwichtel ??= {};
|
||||
}
|
||||
|
||||
function errorMessage(error) {
|
||||
if (error.reason === 'code-taken') return t('error.code-taken');
|
||||
if (error.reason === 'invalid-code') return t('error.code');
|
||||
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');
|
||||
@@ -141,33 +145,68 @@ globalThis.Wegwichtel ??= {};
|
||||
event.preventDefault(); choose(event.key === 'Home' ? 0 : motifs.length - 1);
|
||||
}
|
||||
});
|
||||
paint(0);
|
||||
paint(selection[position]);
|
||||
column.append(up, viewport, down);
|
||||
return column;
|
||||
}
|
||||
|
||||
function openEntry() {
|
||||
function openEntry(chooseCode = false) {
|
||||
if (element(ids.dialog).open) element(ids.dialog).close();
|
||||
dialogGeneration += 1;
|
||||
selection = new Array(codeLength).fill(0);
|
||||
selection = Array.from({ length: codeLength }, () => chooseCode ? randomIndex(ns.GroupSymbols.list().length) : 0);
|
||||
element('group-entry-title').setAttribute('data-i18n', `groups.${chooseCode ? 'choose-code' : 'join-open'}`);
|
||||
element('group-entry-title').textContent = t(chooseCode ? 'choose-code' : 'join-open');
|
||||
element(ids.join).setAttribute('data-i18n', `groups.${chooseCode ? 'save-code' : 'join'}`);
|
||||
element(ids.join).disabled = false;
|
||||
element(ids.join).textContent = t(chooseCode ? 'save-code' : 'join');
|
||||
element(ids.entryStatus).textContent = '';
|
||||
if (!element(ids.entryDialog).open) element(ids.entryDialog).showModal();
|
||||
const wheels = element(ids.wheels);
|
||||
wheels.replaceChildren(...selection.map((_, index) => makeWheel(index)));
|
||||
if (!element(ids.entryDialog).open) element(ids.entryDialog).showModal();
|
||||
for (const [index, column] of Array.from(wheels.children).entries()) column.children[1].scrollTop = selection[index] * wheelStep;
|
||||
}
|
||||
|
||||
function randomIndex(size) {
|
||||
const values = new Uint32Array(1);
|
||||
const limit = Math.floor(0x100000000 / size) * size;
|
||||
do { globalThis.crypto.getRandomValues(values); } while (values[0] >= limit);
|
||||
return values[0] % size;
|
||||
}
|
||||
|
||||
function chooseCode() {
|
||||
if (codeChoice) return Promise.resolve(null);
|
||||
openEntry(true);
|
||||
return new Promise(resolve => { codeChoice = resolve; });
|
||||
}
|
||||
|
||||
function cancelChoice() {
|
||||
dialogGeneration += 1;
|
||||
const resolve = codeChoice;
|
||||
codeChoice = null;
|
||||
resolve?.(null);
|
||||
}
|
||||
|
||||
function updateCountdown() {
|
||||
if (!currentGroup || !element(ids.dialog).open || element(ids.invitation).hidden) return;
|
||||
const seconds = Math.max(0, Math.ceil(currentExpiry - Date.now() / 1000));
|
||||
const time = new Date(currentExpiry * 1000).toLocaleTimeString(ns.I18n.language, { hour: '2-digit', minute: '2-digit' });
|
||||
element(ids.expiry).textContent = seconds ? `${t('expires', { time })} ${t('remaining', {
|
||||
minutes: Math.floor(seconds / 60), seconds: String(seconds % 60).padStart(2, '0')
|
||||
})}` : t('code-expired');
|
||||
}
|
||||
|
||||
function showInvitation(payload) {
|
||||
if (!Array.isArray(payload.code) || payload.code.length !== codeLength) throw new Error(t('error.connection'));
|
||||
currentGroup = payload.group;
|
||||
element(ids.status).textContent = '';
|
||||
element(ids.status).textContent = payload.expired ? t('code-expired') : '';
|
||||
if (element(ids.entryDialog).open) element(ids.entryDialog).close();
|
||||
element(ids.invitation).hidden = false;
|
||||
element('group-route').textContent = payload.group.route.name || '';
|
||||
const motifs = payload.code.map(value => ns.GroupSymbols.get(value));
|
||||
element(ids.code).replaceChildren(...motifs.map(picture));
|
||||
const time = new Date(payload.expiresAt * 1000).toLocaleTimeString(ns.I18n.language, { hour: '2-digit', minute: '2-digit' });
|
||||
element(ids.expiry).textContent = t('expires', { time });
|
||||
currentExpiry = payload.expiresAt;
|
||||
if (!element(ids.dialog).open) element(ids.dialog).showModal();
|
||||
updateCountdown();
|
||||
}
|
||||
|
||||
let invitationPolling = false;
|
||||
@@ -179,14 +218,15 @@ globalThis.Wegwichtel ??= {};
|
||||
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') : '';
|
||||
element(ids.status).textContent = payload.expired ? t('code-expired') : '';
|
||||
} catch { /* Keep the invitation visible; expiry still applies on the server. */ }
|
||||
finally { invitationPolling = false; }
|
||||
}
|
||||
async function edit(group) {
|
||||
if (element(ids.dialog).open) element(ids.dialog).close();
|
||||
if (element(ids.entryDialog).open) element(ids.entryDialog).close();
|
||||
await ns.EditorUi.openGroup(group.route);
|
||||
const payload = await request(`/${group.id}/edit`, 'POST', {});
|
||||
await ns.EditorUi.openGroup(payload.group.route);
|
||||
}
|
||||
|
||||
async function renderPending() {
|
||||
@@ -228,8 +268,9 @@ globalThis.Wegwichtel ??= {};
|
||||
})));
|
||||
if (group.role === 'owner') {
|
||||
actions.appendChild(button(t('new-code'), () => guarded(async () => {
|
||||
if (!globalThis.confirm(t('new-code-confirm'))) return;
|
||||
showInvitation(await request(`/${group.id}/code`, 'POST', {}));
|
||||
const code = await chooseCode();
|
||||
if (!code) return;
|
||||
showInvitation(await request(`/${group.id}/code`, 'POST', { code }));
|
||||
await refresh();
|
||||
})));
|
||||
if (group.active) actions.appendChild(button(t('close-sharing'), () => guarded(async () => {
|
||||
@@ -238,6 +279,11 @@ globalThis.Wegwichtel ??= {};
|
||||
await refresh();
|
||||
})));
|
||||
}
|
||||
if (group.role === 'owner' && group.active) actions.appendChild(button(t('code-label'), () => guarded(async () => {
|
||||
const payload = await request(`/${group.id}/invitation`);
|
||||
if (payload.code) showInvitation(payload);
|
||||
else { const code = await chooseCode(); if (code) showInvitation(await request(`/${group.id}/code`, 'POST', { code })); }
|
||||
})));
|
||||
item.append(name, actions);
|
||||
list.appendChild(item);
|
||||
}
|
||||
@@ -249,11 +295,12 @@ globalThis.Wegwichtel ??= {};
|
||||
} catch { /* Existing drafts and forms stay usable without a connection. */ }
|
||||
}
|
||||
|
||||
async function share(routeId) {
|
||||
async function share(routeId, code = null) {
|
||||
code ??= await chooseCode();
|
||||
if (!code) return;
|
||||
try {
|
||||
const payload = await request('', 'POST', { routeId, creator_id: ns.LocalRoutes.creatorId() });
|
||||
const editor = document.getElementById('editor-dialog');
|
||||
if (!editor || editor.open) showInvitation(payload);
|
||||
const payload = await request('', 'POST', { routeId, code, creator_id: ns.LocalRoutes.creatorId() });
|
||||
showInvitation(payload);
|
||||
await refresh();
|
||||
} catch (error) { throw new Error(errorMessage(error)); }
|
||||
}
|
||||
@@ -287,8 +334,17 @@ globalThis.Wegwichtel ??= {};
|
||||
}
|
||||
|
||||
async function joinSelectedGroup() {
|
||||
const generation = dialogGeneration;
|
||||
if (!element(ids.entryDialog).open) return;
|
||||
const motifs = ns.GroupSymbols.list();
|
||||
if (codeChoice) {
|
||||
const resolve = codeChoice;
|
||||
codeChoice = null;
|
||||
const code = selection.map(index => motifs[index].unicode);
|
||||
element(ids.entryDialog).close();
|
||||
resolve(code);
|
||||
return;
|
||||
}
|
||||
const generation = dialogGeneration;
|
||||
let payload;
|
||||
try {
|
||||
payload = await request('/join', 'POST', {
|
||||
@@ -305,17 +361,18 @@ globalThis.Wegwichtel ??= {};
|
||||
|
||||
function init() {
|
||||
globalThis.setInterval?.(() => { refreshInvitation(); }, 30_000);
|
||||
globalThis.setInterval?.(updateCountdown, 1000);
|
||||
document.addEventListener('wegwichtel:point-drafts-changed', () => { renderPending(); ns.StorageUsage?.refresh(); });
|
||||
element(ids.entryDialog).addEventListener('cancel', () => { dialogGeneration += 1; });
|
||||
element(ids.entryDialog).addEventListener('close', () => { dialogGeneration += 1; });
|
||||
element(ids.entryDialog).addEventListener('cancel', cancelChoice);
|
||||
element(ids.entryDialog).addEventListener('close', cancelChoice);
|
||||
activate(element(ids.entryClose), () => element(ids.entryDialog).close());
|
||||
for (const id of [ids.joinOpen, ids.editorJoin]) activate(element(id), openEntry);
|
||||
for (const id of [ids.joinOpen, ids.editorJoin]) activate(element(id), () => openEntry());
|
||||
activate(element(ids.close), () => element(ids.dialog).close());
|
||||
activate(element(ids.join), () => guarded(joinSelectedGroup));
|
||||
activate(element(ids.join), () => codeChoice ? joinSelectedGroup() : guarded(joinSelectedGroup));
|
||||
activate(element(ids.edit), () => guarded(() => edit(currentGroup)));
|
||||
}
|
||||
|
||||
ns.Groups = Object.freeze({ share, shareButton, refresh, refreshInvitation, headersFor, observe });
|
||||
ns.Groups = Object.freeze({ share, chooseCode, shareButton, refresh, refreshInvitation, headersFor, observe });
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init, { once: true });
|
||||
else init();
|
||||
}(globalThis.Wegwichtel));
|
||||
|
||||
Reference in New Issue
Block a user