diff --git a/lib/watch_together/providers/watch_together_provider.dart b/lib/watch_together/providers/watch_together_provider.dart index 09c96470..83c77ef9 100644 --- a/lib/watch_together/providers/watch_together_provider.dart +++ b/lib/watch_together/providers/watch_together_provider.dart @@ -304,15 +304,16 @@ class WatchTogetherProvider with ChangeNotifier { final probe = WatchTogetherPeerService(customBaseUrl: customRelayUrl); try { final becameHost = await probe.joinOrCreateSession(sessionId); + final shouldBeHost = becameHost || probe.connectedPeers.isEmpty; await probe.disconnect(); probe.dispose(); - if (becameHost) { + if (shouldBeHost) { await createSession(controlMode: controlMode, displayName: displayName, sessionId: sessionId); } else { await joinSession(sessionId, displayName: displayName); } - return becameHost; + return shouldBeHost; } catch (e) { await probe.disconnect(); probe.dispose(); @@ -460,14 +461,13 @@ class WatchTogetherProvider with ChangeNotifier { ); } - // If we're the host, send our join info back so the new peer - // adds us to their participant list. This is done at provider - // level (in addition to sync manager) so it works even when - // no player is attached yet. - if (isHost && _peerService != null) { + // Send our join info back so the new peer adds us to their + // participant list. Every peer does this (not just the host) + // so that late joiners learn about all existing participants. + if (_peerService != null) { _peerService!.sendTo( message.peerId!, - SyncMessage.join(peerId: _peerService!.myPeerId!, displayName: _displayName, isHost: true), + SyncMessage.join(peerId: _peerService!.myPeerId!, displayName: _displayName, isHost: isHost), ); } diff --git a/lib/watch_together/screens/watch_together_screen.dart b/lib/watch_together/screens/watch_together_screen.dart index 09e9bcea..7ff7a242 100644 --- a/lib/watch_together/screens/watch_together_screen.dart +++ b/lib/watch_together/screens/watch_together_screen.dart @@ -221,7 +221,7 @@ class _NotInSessionViewState extends State<_NotInSessionView> { controlMode: controlMode, displayName: _plexDisplayName, ); - await RecentRoomsService.addOrUpdateRoom(sessionId); + await RecentRoomsService.addOrUpdateRoom(sessionId, controlMode: controlMode); if (mounted) setState(() => _recentRooms = RecentRoomsService.getRecentRooms()); } catch (e) { appLogger.e('Failed to create session', error: e); @@ -299,7 +299,11 @@ class _NotInSessionViewState extends State<_NotInSessionView> { setState(() => _enteringRoomCode = room.code); try { - await widget.watchTogether.enterRoom(room.code, displayName: _plexDisplayName); + await widget.watchTogether.enterRoom( + room.code, + controlMode: room.controlMode ?? ControlMode.anyone, + displayName: _plexDisplayName, + ); await RecentRoomsService.addOrUpdateRoom(room.code); if (mounted) setState(() => _recentRooms = RecentRoomsService.getRecentRooms()); } catch (e) { diff --git a/lib/watch_together/services/recent_rooms_service.dart b/lib/watch_together/services/recent_rooms_service.dart index 94fa0209..230eac7e 100644 --- a/lib/watch_together/services/recent_rooms_service.dart +++ b/lib/watch_together/services/recent_rooms_service.dart @@ -1,30 +1,39 @@ import 'dart:convert'; import '../../services/settings_service.dart'; +import '../models/watch_session.dart'; class RecentRoom { final String code; final String? name; final DateTime lastUsed; + final ControlMode? controlMode; - const RecentRoom({required this.code, this.name, required this.lastUsed}); + const RecentRoom({required this.code, this.name, required this.lastUsed, this.controlMode}); Map toJson() => { 'code': code, if (name != null) 'name': name, 'lastUsed': lastUsed.millisecondsSinceEpoch, + if (controlMode != null) 'controlMode': controlMode!.index, }; - factory RecentRoom.fromJson(Map json) => RecentRoom( - code: json['code'] as String, - name: json['name'] as String?, - lastUsed: DateTime.fromMillisecondsSinceEpoch(json['lastUsed'] as int), - ); + factory RecentRoom.fromJson(Map json) { + final modeIndex = json['controlMode'] as int?; + return RecentRoom( + code: json['code'] as String, + name: json['name'] as String?, + lastUsed: DateTime.fromMillisecondsSinceEpoch(json['lastUsed'] as int), + controlMode: modeIndex != null ? ControlMode.values[modeIndex] : null, + ); + } - RecentRoom copyWith({String? code, String? name, DateTime? lastUsed, bool clearName = false}) => RecentRoom( + RecentRoom copyWith({String? code, String? name, DateTime? lastUsed, ControlMode? controlMode, bool clearName = false}) => + RecentRoom( code: code ?? this.code, name: clearName ? null : (name ?? this.name), lastUsed: lastUsed ?? this.lastUsed, + controlMode: controlMode ?? this.controlMode, ); } @@ -50,13 +59,17 @@ class RecentRoomsService { await SettingsService.instanceOrNull?.setRecentRooms(jsonEncode(rooms.map((r) => r.toJson()).toList())); } - static Future addOrUpdateRoom(String code, {String? name}) async { + static Future addOrUpdateRoom(String code, {String? name, ControlMode? controlMode}) async { final rooms = getRecentRooms(); final index = rooms.indexWhere((r) => r.code == code); if (index >= 0) { - rooms[index] = rooms[index].copyWith(lastUsed: DateTime.now(), name: name ?? rooms[index].name); + rooms[index] = rooms[index].copyWith( + lastUsed: DateTime.now(), + name: name ?? rooms[index].name, + controlMode: controlMode, + ); } else { - rooms.add(RecentRoom(code: code, name: name, lastUsed: DateTime.now())); + rooms.add(RecentRoom(code: code, name: name, lastUsed: DateTime.now(), controlMode: controlMode)); } await _save(rooms); } diff --git a/server/main.go b/server/main.go index 44cac0a9..0cc6d992 100644 --- a/server/main.go +++ b/server/main.go @@ -563,11 +563,18 @@ func (s *Server) handleWS(w http.ResponseWriter, r *http.Request) { continue } s.mu.Lock() - if _, exists := s.rooms[msg.SessionID]; exists { - s.mu.Unlock() - s.conns.releaseRoom(ip) - s.sendError(conn, "room_exists", "Room already exists") - continue + if existing, exists := s.rooms[msg.SessionID]; exists { + existing.mu.RLock() + empty := len(existing.Peers) == 0 + existing.mu.RUnlock() + if !empty { + s.mu.Unlock() + s.conns.releaseRoom(ip) + s.sendError(conn, "room_exists", "Room already exists") + continue + } + // Empty stale room — reclaim the ID + delete(s.rooms, msg.SessionID) } room := &Room{ SessionID: msg.SessionID,