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

+13 -11
View File
@@ -3,21 +3,31 @@ import 'dart:math';
import 'package:sync_relay_core/core.dart';
enum MediaGate { ok, gone, unauthorized, forbidden, offline }
enum MediaGate { ok, gone, unauthorized, offline }
class ViewerToken {
final String token;
/// Cleaned ([cleanMemberName]); follows `profile` renames.
String name;
bool kicked = false;
/// Sent the server's host key in hello: the hosting app's own client.
bool isHost = false;
int bytesServed = 0;
DateTime lastSeen;
Future<void> Function(String reason)? onKick;
/// When this member's recent nudges went out, for the rate cap.
final List<DateTime> nudges = [];
ViewerToken({required this.token, required this.name})
: lastSeen = DateTime.now().toUtc();
/// Takes one nudge at [now] if under the caps
/// ([RelayConstants.nudgesPerSec] / [RelayConstants.nudgesPerMin]).
bool takeNudge(DateTime now) {
nudges.removeWhere((t) => now.difference(t) >= const Duration(minutes: 1));
final lastSec = nudges.where((t) => now.difference(t) < const Duration(seconds: 1)).length;
if (nudges.length >= RelayConstants.nudgesPerMin || lastSec >= RelayConstants.nudgesPerSec) return false;
nudges.add(now);
return true;
}
bool isStale(Duration timeout, [DateTime? now]) =>
(now ?? DateTime.now().toUtc()).difference(lastSeen) > timeout;
@@ -66,17 +76,9 @@ class Session {
MediaGate gate(String? token) {
if (token == null || !tokens.containsKey(token)) return MediaGate.unauthorized;
if (tokens[token]!.kicked) return MediaGate.forbidden;
return MediaGate.ok;
}
Future<void> kick(String token, {String reason = ''}) async {
final viewer = tokens[token];
if (viewer == null) throw const AuthException('unknown token');
viewer.kicked = true;
await viewer.onKick?.call(reason);
}
void addBytes(String? token, int n) {
if (token != null) tokens[token]?.bytesServed += n;
}