Protocol 11: sit out replaces kicking; nudge

Anyone can sit anyone out (host included); only you bring yourself back.
Out members stay in chat and the party, never block or count, and load no
media; a card with the live position and Join back (Space) replaces the
video. The mini player's x sits you out; optionally idle does too. Nudge:
the server forwards it to the target only (3/s, 20/min per sender); the
target hears the nudge sound, gets a toast and a taskbar attention
request. Kick, its messages, errors and dialog are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-09 21:05:35 +03:00
1 parent dbfd351ee4
commit 8b81da4069
46 files changed
+855 -599

No files matched your search

+49
View File
@@ -36,6 +36,55 @@ void main() {
});
});
group('sitting out', () {
test('an out member never blocks, is never ready, its buffer is ignored', () {
join('a', 'alice');
final bob = join('b', 'bob');
engine.applyReady('a', true);
buffer('a', 4.0);
expect(engine.transport, Transport.held, reason: 'bob is in the set');
engine.applyOut('a', bob, true);
final b = engine.members['b']!;
expect([b.out, b.inSet, b.ready], [true, false, false]);
expect(engine.transport, Transport.playing);
final feed = engine.feed.drain().lastWhere((f) => f.action == 'out');
expect([feed.actor, feed.target], ['alice', 'bob']);
expect(engine.broadcast().members.last.toJson()['out'], isTrue);
expect(engine.broadcast().members.first.toJson().containsKey('out'), isFalse);
// Ready, force, swap and reports leave it out.
engine.applyReady('b', true);
engine.applyForce('a', 'all', true);
buffer('b', 9);
engine.applySwap('a', const HelloMedia(name: 'n', size: 1, duration: 0, url: 'u'));
expect([b.ready, b.inSet, b.buffered], [false, false, 0]);
expect(engine.members['a']!.inSet, isTrue);
});
test('only oneself comes back: in the set unready, ready follows', () {
final alice = join('a', 'alice');
final bob = join('b', 'bob');
engine.applyOut('b', bob, true);
expect(engine.feed.drain().last.target, isNull, reason: 'sat out by itself');
engine.applyReady('a', true);
buffer('a', 4.0);
expect(engine.transport, Transport.playing);
engine.applyOut('a', bob, false);
expect(engine.members['b']!.out, isTrue, reason: 'others cannot bring bob back');
engine.applyOut('b', bob, false);
final b = engine.members['b']!;
expect([b.out, b.inSet, b.ready], [false, false, false], reason: 'playing: in from its ready');
expect(engine.transport, Transport.playing);
engine.applyReady('b', true);
expect(engine.transport, Transport.held, reason: 'now the gate waits for bob');
buffer('b', 4.0);
expect(engine.transport, Transport.playing);
// The host can be sat out like anyone.
engine.members['a']!.host = true;
engine.applyOut('b', alice, true);
expect(engine.members['a']!.out, isTrue);
});
});
group('membership + gate', () {
test('join while held lands in the set, playback stays held', () {
join('a', 'alice');