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:
1 parent
dbfd351ee4
commit
8b81da4069
46 files changed
+855
-599
No files matched your search
@@ -296,10 +296,10 @@ void main() {
|
||||
findsOneWidget);
|
||||
expect(find.descendant(of: find.byKey(const ValueKey('member-m2')), matching: find.text('host')),
|
||||
findsNothing);
|
||||
// The host cannot be kicked: no Kick in its menu.
|
||||
// The host can be sat out like anyone.
|
||||
await tester.tap(find.descendant(of: find.byKey(const ValueKey('member-m1')), matching: find.byTooltip('More')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Kick'), findsNothing);
|
||||
expect(find.text('Sit out'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('hosting: Copy invite address lists this machine\'s addresses', (tester) async {
|
||||
|
||||
@@ -26,8 +26,9 @@ final png = Uint8List.fromList(const [
|
||||
0x42, 0x60, 0x82,
|
||||
]);
|
||||
|
||||
MemberView mv(String id, String name, {bool ready = false, bool? inSet, double buffered = 30, bool stale = false}) =>
|
||||
MemberView(id: id, name: name, ready: ready, inSet: inSet ?? ready, buffered: buffered, stale: stale);
|
||||
MemberView mv(String id, String name,
|
||||
{bool ready = false, bool? inSet, double buffered = 30, bool stale = false, bool out = false}) =>
|
||||
MemberView(id: id, name: name, ready: ready, inSet: inSet ?? ready, buffered: buffered, stale: stale, out: out);
|
||||
|
||||
StateBroadcast held(List<MemberView> members, {String transport = 'held', double position = 0, int mediaId = 1}) =>
|
||||
StateBroadcast(
|
||||
@@ -55,17 +56,8 @@ void main() {
|
||||
group('status', () {
|
||||
final me = mv('me', 'Me', ready: true);
|
||||
|
||||
test('problems first: removed, reconnecting, health', () {
|
||||
var v = playerStatusFor(
|
||||
health: const HealthStatus(HealthSeverity.error, 'kicked: spam'),
|
||||
kickReason: 'spam',
|
||||
reconnecting: true,
|
||||
state: held([me]));
|
||||
expect(v!.kind, StatusKind.kicked);
|
||||
expect(v.text, 'You were removed: spam');
|
||||
expect(v.tone, HealthSeverity.error);
|
||||
expect(v.action, isNull);
|
||||
v = playerStatusFor(health: const HealthStatus(HealthSeverity.warn, 'connection lost — retrying…'),
|
||||
test('problems first: reconnecting, health', () {
|
||||
var v = playerStatusFor(health: const HealthStatus(HealthSeverity.warn, 'connection lost — retrying…'),
|
||||
reconnecting: true);
|
||||
expect(v!.text, 'Reconnecting…');
|
||||
expect(v.action, StatusAction.cancel);
|
||||
@@ -109,11 +101,14 @@ void main() {
|
||||
expect(v.self, isTrue);
|
||||
final w = playerStatusFor(
|
||||
health: ok,
|
||||
state: held([me, mv('b', 'Bob'), mv('m', 'Mika'), mv('z', 'Zed', stale: true)]),
|
||||
state: held([me, mv('b', 'Bob'), mv('m', 'Mika'), mv('z', 'Zed', stale: true), mv('o', 'Ola', out: true)]),
|
||||
self: me)!;
|
||||
expect(w.kind, StatusKind.waiting);
|
||||
expect(w.text, 'Waiting for Bob and Mika');
|
||||
expect(w.text, 'Waiting for Bob and Mika', reason: 'nobody waits for who sits out');
|
||||
expect(w.detail, '1/3 ready');
|
||||
final outMe = mv('me', 'Me', out: true);
|
||||
expect(playerStatusFor(health: ok, state: held([outMe, mv('b', 'Bob')]), self: outMe), isNull,
|
||||
reason: 'sitting out: the join-back card speaks');
|
||||
expect(w.who.map((m) => m.name), ['Bob', 'Mika']);
|
||||
});
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
test('host path: isolate server, host unkickable, crash leaves the session',
|
||||
test('host path: isolate server, host sat out and nudging, crash leaves the session',
|
||||
timeout: const Timeout(Duration(minutes: 2)), () async {
|
||||
MediaKit.ensureInitialized();
|
||||
Hive.init(
|
||||
@@ -131,16 +131,16 @@ void main() {
|
||||
final hostId = ((state['members'] as List)
|
||||
.cast<Map>()
|
||||
.firstWhere((m) => m['name'] == 'host'))['id'];
|
||||
friend.ws.add(jsonEncode(KickRequest(target: '$hostId').toJson()));
|
||||
final err = await friend.next('err');
|
||||
expect(err['message'], "can't kick the host");
|
||||
// Anyone may sit the host out; its player unloads, the server serves on.
|
||||
friend.ws.add(jsonEncode(OutIntent(target: '$hostId', out: true).toJson()));
|
||||
await _poll(() => session.coordinator.selfOut.peek(), 'host sat out');
|
||||
|
||||
// The host can kick the friend (plain loopback peers are not hosts).
|
||||
// The host nudges the friend: only the friend hears it.
|
||||
final friendId = ((state['members'] as List)
|
||||
.cast<Map>()
|
||||
.firstWhere((m) => m['name'] == 'friend'))['id'];
|
||||
session.coordinator.kick('$friendId');
|
||||
await friend.next('kicked');
|
||||
session.coordinator.nudgeMember('$friendId');
|
||||
expect((await friend.next('nudge'))['name'], 'host');
|
||||
|
||||
session.hostedServer!.isolate.kill(priority: Isolate.immediate);
|
||||
await _poll(() => session.route.peek() == AppRoute.home, 'leave after crash');
|
||||
|
||||
@@ -166,13 +166,13 @@ void main() {
|
||||
expect(find.descendant(of: card, matching: find.text(m.name)), findsOneWidget);
|
||||
}
|
||||
expect(find.descendant(of: card, matching: find.text('reconnecting…')), findsOneWidget);
|
||||
// The kick menu asks before kicking.
|
||||
// Someone else: nudge or sit them out (no kicking).
|
||||
await tester.tap(find.descendant(of: find.byKey(const ValueKey('member-b')), matching: find.byTooltip('More')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Kick'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Remove Bob?'), findsOneWidget);
|
||||
await tester.tap(find.text('Cancel'));
|
||||
expect(find.text('Nudge'), findsOneWidget);
|
||||
expect(find.text('Sit out'), findsOneWidget);
|
||||
expect(find.text('Kick'), findsNothing);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapAt(const Offset(40, 700));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
Reference in new issue
Block a user