v0.0.9: Group management — leave, kick, members

Server:
- POST /groups/:name/leave — remove self from group
- POST /groups/:name/kick — creator can kick members
- GET /groups/:name/members — list with aliases + creator badge

CLI TUI:
- /gleave — leave current group
- /gkick <fp_or_alias> — kick (creator only)
- /gmembers — show member list with aliases and ★ for creator

Web client:
- Same commands: /gleave, /gkick, /gmembers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-27 12:04:28 +04:00
parent 2599ce956a
commit 4fb3973403
5 changed files with 224 additions and 9 deletions

View File

@@ -160,7 +160,7 @@ let pollTimer = null;
let ws = null; // WebSocket connection
let wasmReady = false;
const VERSION = '0.0.6';
const VERSION = '0.0.9';
let DEBUG = true; // toggle with /debug command
// ── Receipt tracking ──
@@ -568,7 +568,7 @@ async function enterChat() {
addSys('Identity loaded: ' + myFingerprint);
addSys('Key registered with server');
addSys('v' + VERSION + ' | DM: paste peer fingerprint or @alias above');
addSys('/alias · /g · /glist · /info · /selftest · /reset · /debug');
addSys('/alias · /g · /gleave · /gkick · /gmembers · /glist · /file · /info');
const savedPeer = localStorage.getItem('wz-peer');
if (savedPeer) $peerInput.value = savedPeer;
@@ -733,6 +733,33 @@ async function doSend() {
}
if (text.startsWith('/gcreate ')) { await groupCreate(text.slice(9).trim()); return; }
if (text.startsWith('/gjoin ')) { await groupJoin(text.slice(7).trim()); return; }
if (text === '/gleave') {
if (!currentGroup) { addSys('Not in a group'); return; }
const r = await fetch(SERVER+'/v1/groups/'+currentGroup+'/leave',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({fingerprint:normFP(myFingerprint)})});
const d = await r.json();
if (d.error) addSys('Error: '+d.error); else { addSys('Left group "'+currentGroup+'"'); currentGroup=null; $peerInput.value=''; }
return;
}
if (text.startsWith('/gkick ')) {
if (!currentGroup) { addSys('Not in a group'); return; }
const target = text.slice(7).trim();
const r = await fetch(SERVER+'/v1/groups/'+currentGroup+'/kick',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({fingerprint:normFP(myFingerprint),target:target})});
const d = await r.json();
if (d.error) addSys('Error: '+d.error); else addSys('Kicked '+d.kicked);
return;
}
if (text === '/gmembers') {
if (!currentGroup) { addSys('Not in a group'); return; }
const r = await fetch(SERVER+'/v1/groups/'+currentGroup+'/members');
const d = await r.json();
if (d.error) { addSys('Error: '+d.error); return; }
addSys('Members of #'+currentGroup+':');
for (const m of d.members) {
const a = m.alias ? '@'+m.alias : m.fingerprint.slice(0,12)+'...';
addSys(' '+a+(m.is_creator?' ★':''));
}
return;
}
if (text.startsWith('/g ')) { await groupSwitch(text.slice(3).trim()); return; }
// Send to group or DM