Compare commits
	
		
			14 Commits
		
	
	
		
			7c1f24b824
			...
			refactor/w
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 0622498f4e | |||
| 844efcda1a | |||
| 98e39cce6a | |||
| 0c459bf7e3 | |||
| a2576abee0 | |||
| f4b28c3fa2 | |||
| 
						
						
							
						
						43d767bc03
	
				 | 
					
					
						|||
| 
						
						
							
						
						0910be88ef
	
				 | 
					
					
						|||
| 
						
						
							
						
						e96b1fd9d4
	
				 | 
					
					
						|||
| 
						
						
							
						
						3f83bbc1d8
	
				 | 
					
					
						|||
| 001549b190 | |||
| 4595865ad3 | |||
| 
						 | 
					1834643167 | ||
| 
						 | 
					0e816eaa3e | 
@@ -2,8 +2,6 @@ PODS:
 | 
				
			|||||||
  - Alamofire (5.10.2)
 | 
					  - Alamofire (5.10.2)
 | 
				
			||||||
  - app_links (6.4.1):
 | 
					  - app_links (6.4.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - connectivity_plus (0.0.1):
 | 
					 | 
				
			||||||
    - Flutter
 | 
					 | 
				
			||||||
  - croppy (0.0.1):
 | 
					  - croppy (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - device_info_plus (0.0.1):
 | 
					  - device_info_plus (0.0.1):
 | 
				
			||||||
@@ -219,10 +217,6 @@ PODS:
 | 
				
			|||||||
  - irondash_engine_context (0.0.1):
 | 
					  - irondash_engine_context (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
  - Kingfisher (8.6.0)
 | 
					  - Kingfisher (8.6.0)
 | 
				
			||||||
  - livekit_client (2.5.0):
 | 
					 | 
				
			||||||
    - Flutter
 | 
					 | 
				
			||||||
    - flutter_webrtc
 | 
					 | 
				
			||||||
    - WebRTC-SDK (= 137.7151.04)
 | 
					 | 
				
			||||||
  - local_auth_darwin (0.0.1):
 | 
					  - local_auth_darwin (0.0.1):
 | 
				
			||||||
    - Flutter
 | 
					    - Flutter
 | 
				
			||||||
    - FlutterMacOS
 | 
					    - FlutterMacOS
 | 
				
			||||||
@@ -309,7 +303,6 @@ PODS:
 | 
				
			|||||||
DEPENDENCIES:
 | 
					DEPENDENCIES:
 | 
				
			||||||
  - Alamofire
 | 
					  - Alamofire
 | 
				
			||||||
  - app_links (from `.symlinks/plugins/app_links/ios`)
 | 
					  - app_links (from `.symlinks/plugins/app_links/ios`)
 | 
				
			||||||
  - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
 | 
					 | 
				
			||||||
  - croppy (from `.symlinks/plugins/croppy/ios`)
 | 
					  - croppy (from `.symlinks/plugins/croppy/ios`)
 | 
				
			||||||
  - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
 | 
					  - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
 | 
				
			||||||
  - file_picker (from `.symlinks/plugins/file_picker/ios`)
 | 
					  - file_picker (from `.symlinks/plugins/file_picker/ios`)
 | 
				
			||||||
@@ -333,7 +326,6 @@ DEPENDENCIES:
 | 
				
			|||||||
  - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
 | 
					  - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
 | 
				
			||||||
  - irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
 | 
					  - irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
 | 
				
			||||||
  - Kingfisher (~> 8.0)
 | 
					  - Kingfisher (~> 8.0)
 | 
				
			||||||
  - livekit_client (from `.symlinks/plugins/livekit_client/ios`)
 | 
					 | 
				
			||||||
  - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
 | 
					  - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
 | 
				
			||||||
  - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
 | 
					  - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`)
 | 
				
			||||||
  - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
 | 
					  - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`)
 | 
				
			||||||
@@ -388,8 +380,6 @@ SPEC REPOS:
 | 
				
			|||||||
EXTERNAL SOURCES:
 | 
					EXTERNAL SOURCES:
 | 
				
			||||||
  app_links:
 | 
					  app_links:
 | 
				
			||||||
    :path: ".symlinks/plugins/app_links/ios"
 | 
					    :path: ".symlinks/plugins/app_links/ios"
 | 
				
			||||||
  connectivity_plus:
 | 
					 | 
				
			||||||
    :path: ".symlinks/plugins/connectivity_plus/ios"
 | 
					 | 
				
			||||||
  croppy:
 | 
					  croppy:
 | 
				
			||||||
    :path: ".symlinks/plugins/croppy/ios"
 | 
					    :path: ".symlinks/plugins/croppy/ios"
 | 
				
			||||||
  device_info_plus:
 | 
					  device_info_plus:
 | 
				
			||||||
@@ -434,8 +424,6 @@ EXTERNAL SOURCES:
 | 
				
			|||||||
    :path: ".symlinks/plugins/image_picker_ios/ios"
 | 
					    :path: ".symlinks/plugins/image_picker_ios/ios"
 | 
				
			||||||
  irondash_engine_context:
 | 
					  irondash_engine_context:
 | 
				
			||||||
    :path: ".symlinks/plugins/irondash_engine_context/ios"
 | 
					    :path: ".symlinks/plugins/irondash_engine_context/ios"
 | 
				
			||||||
  livekit_client:
 | 
					 | 
				
			||||||
    :path: ".symlinks/plugins/livekit_client/ios"
 | 
					 | 
				
			||||||
  local_auth_darwin:
 | 
					  local_auth_darwin:
 | 
				
			||||||
    :path: ".symlinks/plugins/local_auth_darwin/darwin"
 | 
					    :path: ".symlinks/plugins/local_auth_darwin/darwin"
 | 
				
			||||||
  media_kit_libs_ios_video:
 | 
					  media_kit_libs_ios_video:
 | 
				
			||||||
@@ -480,7 +468,6 @@ EXTERNAL SOURCES:
 | 
				
			|||||||
SPEC CHECKSUMS:
 | 
					SPEC CHECKSUMS:
 | 
				
			||||||
  Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
 | 
					  Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
 | 
				
			||||||
  app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
 | 
					  app_links: 3dbc685f76b1693c66a6d9dd1e9ab6f73d97dc0a
 | 
				
			||||||
  connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
 | 
					 | 
				
			||||||
  croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
 | 
					  croppy: 979e8ddc254f4642bffe7d52dc7193354b27ba30
 | 
				
			||||||
  device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
 | 
					  device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
 | 
				
			||||||
  DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
 | 
					  DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
 | 
				
			||||||
@@ -520,7 +507,6 @@ SPEC CHECKSUMS:
 | 
				
			|||||||
  image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
 | 
					  image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
 | 
				
			||||||
  irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
 | 
					  irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
 | 
				
			||||||
  Kingfisher: 64278f126a815d0e2d391cdf71311b85882c4de0
 | 
					  Kingfisher: 64278f126a815d0e2d391cdf71311b85882c4de0
 | 
				
			||||||
  livekit_client: a6f5fa86ac28ccd7ded53626a5379961db311ab4
 | 
					 | 
				
			||||||
  local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
 | 
					  local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
 | 
				
			||||||
  media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
 | 
					  media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
 | 
				
			||||||
  media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
 | 
					  media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -130,7 +130,6 @@ void main() async {
 | 
				
			|||||||
          debugPrint('[Wayland] setAsFrameless failed: $e');
 | 
					          debugPrint('[Wayland] setAsFrameless failed: $e');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      await windowManager.setAsFrameless();
 | 
					 | 
				
			||||||
      await windowManager.setMinimumSize(defaultSize);
 | 
					      await windowManager.setMinimumSize(defaultSize);
 | 
				
			||||||
      await windowManager.show();
 | 
					      await windowManager.show();
 | 
				
			||||||
      await windowManager.focus();
 | 
					      await windowManager.focus();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -149,6 +149,8 @@ sealed class CallParticipant with _$CallParticipant {
 | 
				
			|||||||
  const factory CallParticipant({
 | 
					  const factory CallParticipant({
 | 
				
			||||||
    required String identity,
 | 
					    required String identity,
 | 
				
			||||||
    required String name,
 | 
					    required String name,
 | 
				
			||||||
 | 
					    required String accountId,
 | 
				
			||||||
 | 
					    @Default(null) SnAccount? account,
 | 
				
			||||||
    required DateTime joinedAt,
 | 
					    required DateTime joinedAt,
 | 
				
			||||||
  }) = _CallParticipant;
 | 
					  }) = _CallParticipant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2241,7 +2241,7 @@ as List<CallParticipant>,
 | 
				
			|||||||
/// @nodoc
 | 
					/// @nodoc
 | 
				
			||||||
mixin _$CallParticipant {
 | 
					mixin _$CallParticipant {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 String get identity; String get name; DateTime get joinedAt;
 | 
					 String get identity; String get name; String get accountId; SnAccount? get account; DateTime get joinedAt;
 | 
				
			||||||
/// Create a copy of CallParticipant
 | 
					/// Create a copy of CallParticipant
 | 
				
			||||||
/// with the given fields replaced by the non-null parameter values.
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
@@ -2254,16 +2254,16 @@ $CallParticipantCopyWith<CallParticipant> get copyWith => _$CallParticipantCopyW
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@override
 | 
					@override
 | 
				
			||||||
bool operator ==(Object other) {
 | 
					bool operator ==(Object other) {
 | 
				
			||||||
  return identical(this, other) || (other.runtimeType == runtimeType&&other is CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt));
 | 
					  return identical(this, other) || (other.runtimeType == runtimeType&&other is CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
@override
 | 
					@override
 | 
				
			||||||
int get hashCode => Object.hash(runtimeType,identity,name,joinedAt);
 | 
					int get hashCode => Object.hash(runtimeType,identity,name,accountId,account,joinedAt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@override
 | 
					@override
 | 
				
			||||||
String toString() {
 | 
					String toString() {
 | 
				
			||||||
  return 'CallParticipant(identity: $identity, name: $name, joinedAt: $joinedAt)';
 | 
					  return 'CallParticipant(identity: $identity, name: $name, accountId: $accountId, account: $account, joinedAt: $joinedAt)';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -2274,11 +2274,11 @@ abstract mixin class $CallParticipantCopyWith<$Res>  {
 | 
				
			|||||||
  factory $CallParticipantCopyWith(CallParticipant value, $Res Function(CallParticipant) _then) = _$CallParticipantCopyWithImpl;
 | 
					  factory $CallParticipantCopyWith(CallParticipant value, $Res Function(CallParticipant) _then) = _$CallParticipantCopyWithImpl;
 | 
				
			||||||
@useResult
 | 
					@useResult
 | 
				
			||||||
$Res call({
 | 
					$Res call({
 | 
				
			||||||
 String identity, String name, DateTime joinedAt
 | 
					 String identity, String name, String accountId, SnAccount? account, DateTime joinedAt
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$SnAccountCopyWith<$Res>? get account;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/// @nodoc
 | 
					/// @nodoc
 | 
				
			||||||
@@ -2291,15 +2291,29 @@ class _$CallParticipantCopyWithImpl<$Res>
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/// Create a copy of CallParticipant
 | 
					/// Create a copy of CallParticipant
 | 
				
			||||||
/// with the given fields replaced by the non-null parameter values.
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
@pragma('vm:prefer-inline') @override $Res call({Object? identity = null,Object? name = null,Object? joinedAt = null,}) {
 | 
					@pragma('vm:prefer-inline') @override $Res call({Object? identity = null,Object? name = null,Object? accountId = null,Object? account = freezed,Object? joinedAt = null,}) {
 | 
				
			||||||
  return _then(_self.copyWith(
 | 
					  return _then(_self.copyWith(
 | 
				
			||||||
identity: null == identity ? _self.identity : identity // ignore: cast_nullable_to_non_nullable
 | 
					identity: null == identity ? _self.identity : identity // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
 | 
					as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,joinedAt: null == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable
 | 
					as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as SnAccount?,joinedAt: null == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as DateTime,
 | 
					as DateTime,
 | 
				
			||||||
  ));
 | 
					  ));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					/// Create a copy of CallParticipant
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline')
 | 
				
			||||||
 | 
					$SnAccountCopyWith<$Res>? get account {
 | 
				
			||||||
 | 
					    if (_self.account == null) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return $SnAccountCopyWith<$Res>(_self.account!, (value) {
 | 
				
			||||||
 | 
					    return _then(_self.copyWith(account: value));
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -2378,10 +2392,10 @@ return $default(_that);case _:
 | 
				
			|||||||
/// }
 | 
					/// }
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String identity,  String name,  DateTime joinedAt)?  $default,{required TResult orElse(),}) {final _that = this;
 | 
					@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String identity,  String name,  String accountId,  SnAccount? account,  DateTime joinedAt)?  $default,{required TResult orElse(),}) {final _that = this;
 | 
				
			||||||
switch (_that) {
 | 
					switch (_that) {
 | 
				
			||||||
case _CallParticipant() when $default != null:
 | 
					case _CallParticipant() when $default != null:
 | 
				
			||||||
return $default(_that.identity,_that.name,_that.joinedAt);case _:
 | 
					return $default(_that.identity,_that.name,_that.accountId,_that.account,_that.joinedAt);case _:
 | 
				
			||||||
  return orElse();
 | 
					  return orElse();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -2399,10 +2413,10 @@ return $default(_that.identity,_that.name,_that.joinedAt);case _:
 | 
				
			|||||||
/// }
 | 
					/// }
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String identity,  String name,  DateTime joinedAt)  $default,) {final _that = this;
 | 
					@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String identity,  String name,  String accountId,  SnAccount? account,  DateTime joinedAt)  $default,) {final _that = this;
 | 
				
			||||||
switch (_that) {
 | 
					switch (_that) {
 | 
				
			||||||
case _CallParticipant():
 | 
					case _CallParticipant():
 | 
				
			||||||
return $default(_that.identity,_that.name,_that.joinedAt);}
 | 
					return $default(_that.identity,_that.name,_that.accountId,_that.account,_that.joinedAt);}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/// A variant of `when` that fallback to returning `null`
 | 
					/// A variant of `when` that fallback to returning `null`
 | 
				
			||||||
///
 | 
					///
 | 
				
			||||||
@@ -2416,10 +2430,10 @@ return $default(_that.identity,_that.name,_that.joinedAt);}
 | 
				
			|||||||
/// }
 | 
					/// }
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String identity,  String name,  DateTime joinedAt)?  $default,) {final _that = this;
 | 
					@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String identity,  String name,  String accountId,  SnAccount? account,  DateTime joinedAt)?  $default,) {final _that = this;
 | 
				
			||||||
switch (_that) {
 | 
					switch (_that) {
 | 
				
			||||||
case _CallParticipant() when $default != null:
 | 
					case _CallParticipant() when $default != null:
 | 
				
			||||||
return $default(_that.identity,_that.name,_that.joinedAt);case _:
 | 
					return $default(_that.identity,_that.name,_that.accountId,_that.account,_that.joinedAt);case _:
 | 
				
			||||||
  return null;
 | 
					  return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -2431,11 +2445,13 @@ return $default(_that.identity,_that.name,_that.joinedAt);case _:
 | 
				
			|||||||
@JsonSerializable()
 | 
					@JsonSerializable()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _CallParticipant implements CallParticipant {
 | 
					class _CallParticipant implements CallParticipant {
 | 
				
			||||||
  const _CallParticipant({required this.identity, required this.name, required this.joinedAt});
 | 
					  const _CallParticipant({required this.identity, required this.name, required this.accountId, this.account = null, required this.joinedAt});
 | 
				
			||||||
  factory _CallParticipant.fromJson(Map<String, dynamic> json) => _$CallParticipantFromJson(json);
 | 
					  factory _CallParticipant.fromJson(Map<String, dynamic> json) => _$CallParticipantFromJson(json);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@override final  String identity;
 | 
					@override final  String identity;
 | 
				
			||||||
@override final  String name;
 | 
					@override final  String name;
 | 
				
			||||||
 | 
					@override final  String accountId;
 | 
				
			||||||
 | 
					@override@JsonKey() final  SnAccount? account;
 | 
				
			||||||
@override final  DateTime joinedAt;
 | 
					@override final  DateTime joinedAt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Create a copy of CallParticipant
 | 
					/// Create a copy of CallParticipant
 | 
				
			||||||
@@ -2451,16 +2467,16 @@ Map<String, dynamic> toJson() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@override
 | 
					@override
 | 
				
			||||||
bool operator ==(Object other) {
 | 
					bool operator ==(Object other) {
 | 
				
			||||||
  return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt));
 | 
					  return identical(this, other) || (other.runtimeType == runtimeType&&other is _CallParticipant&&(identical(other.identity, identity) || other.identity == identity)&&(identical(other.name, name) || other.name == name)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
@override
 | 
					@override
 | 
				
			||||||
int get hashCode => Object.hash(runtimeType,identity,name,joinedAt);
 | 
					int get hashCode => Object.hash(runtimeType,identity,name,accountId,account,joinedAt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@override
 | 
					@override
 | 
				
			||||||
String toString() {
 | 
					String toString() {
 | 
				
			||||||
  return 'CallParticipant(identity: $identity, name: $name, joinedAt: $joinedAt)';
 | 
					  return 'CallParticipant(identity: $identity, name: $name, accountId: $accountId, account: $account, joinedAt: $joinedAt)';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -2471,11 +2487,11 @@ abstract mixin class _$CallParticipantCopyWith<$Res> implements $CallParticipant
 | 
				
			|||||||
  factory _$CallParticipantCopyWith(_CallParticipant value, $Res Function(_CallParticipant) _then) = __$CallParticipantCopyWithImpl;
 | 
					  factory _$CallParticipantCopyWith(_CallParticipant value, $Res Function(_CallParticipant) _then) = __$CallParticipantCopyWithImpl;
 | 
				
			||||||
@override @useResult
 | 
					@override @useResult
 | 
				
			||||||
$Res call({
 | 
					$Res call({
 | 
				
			||||||
 String identity, String name, DateTime joinedAt
 | 
					 String identity, String name, String accountId, SnAccount? account, DateTime joinedAt
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override $SnAccountCopyWith<$Res>? get account;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/// @nodoc
 | 
					/// @nodoc
 | 
				
			||||||
@@ -2488,16 +2504,30 @@ class __$CallParticipantCopyWithImpl<$Res>
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/// Create a copy of CallParticipant
 | 
					/// Create a copy of CallParticipant
 | 
				
			||||||
/// with the given fields replaced by the non-null parameter values.
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
@override @pragma('vm:prefer-inline') $Res call({Object? identity = null,Object? name = null,Object? joinedAt = null,}) {
 | 
					@override @pragma('vm:prefer-inline') $Res call({Object? identity = null,Object? name = null,Object? accountId = null,Object? account = freezed,Object? joinedAt = null,}) {
 | 
				
			||||||
  return _then(_CallParticipant(
 | 
					  return _then(_CallParticipant(
 | 
				
			||||||
identity: null == identity ? _self.identity : identity // ignore: cast_nullable_to_non_nullable
 | 
					identity: null == identity ? _self.identity : identity // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
 | 
					as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as String,joinedAt: null == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable
 | 
					as String,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as SnAccount?,joinedAt: null == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as DateTime,
 | 
					as DateTime,
 | 
				
			||||||
  ));
 | 
					  ));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create a copy of CallParticipant
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline')
 | 
				
			||||||
 | 
					$SnAccountCopyWith<$Res>? get account {
 | 
				
			||||||
 | 
					    if (_self.account == null) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return $SnAccountCopyWith<$Res>(_self.account!, (value) {
 | 
				
			||||||
 | 
					    return _then(_self.copyWith(account: value));
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -275,6 +275,11 @@ _CallParticipant _$CallParticipantFromJson(Map<String, dynamic> json) =>
 | 
				
			|||||||
    _CallParticipant(
 | 
					    _CallParticipant(
 | 
				
			||||||
      identity: json['identity'] as String,
 | 
					      identity: json['identity'] as String,
 | 
				
			||||||
      name: json['name'] as String,
 | 
					      name: json['name'] as String,
 | 
				
			||||||
 | 
					      accountId: json['account_id'] as String,
 | 
				
			||||||
 | 
					      account:
 | 
				
			||||||
 | 
					          json['account'] == null
 | 
				
			||||||
 | 
					              ? null
 | 
				
			||||||
 | 
					              : SnAccount.fromJson(json['account'] as Map<String, dynamic>),
 | 
				
			||||||
      joinedAt: DateTime.parse(json['joined_at'] as String),
 | 
					      joinedAt: DateTime.parse(json['joined_at'] as String),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -282,6 +287,8 @@ Map<String, dynamic> _$CallParticipantToJson(_CallParticipant instance) =>
 | 
				
			|||||||
    <String, dynamic>{
 | 
					    <String, dynamic>{
 | 
				
			||||||
      'identity': instance.identity,
 | 
					      'identity': instance.identity,
 | 
				
			||||||
      'name': instance.name,
 | 
					      'name': instance.name,
 | 
				
			||||||
 | 
					      'account_id': instance.accountId,
 | 
				
			||||||
 | 
					      'account': instance.account?.toJson(),
 | 
				
			||||||
      'joined_at': instance.joinedAt.toIso8601String(),
 | 
					      'joined_at': instance.joinedAt.toIso8601String(),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,13 +3,15 @@ import 'dart:io';
 | 
				
			|||||||
import 'package:flutter/foundation.dart';
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
 | 
					import 'package:flutter_webrtc/flutter_webrtc.dart';
 | 
				
			||||||
import 'package:island/widgets/alert.dart';
 | 
					import 'package:island/pods/config.dart';
 | 
				
			||||||
 | 
					import 'package:island/pods/userinfo.dart';
 | 
				
			||||||
import 'package:island/widgets/chat/call_button.dart';
 | 
					import 'package:island/widgets/chat/call_button.dart';
 | 
				
			||||||
import 'package:livekit_client/livekit_client.dart' as lk;
 | 
					 | 
				
			||||||
import 'package:freezed_annotation/freezed_annotation.dart';
 | 
					import 'package:freezed_annotation/freezed_annotation.dart';
 | 
				
			||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
					import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
import 'package:island/pods/network.dart';
 | 
					import 'package:island/pods/network.dart';
 | 
				
			||||||
import 'package:island/models/chat.dart';
 | 
					import 'package:island/models/chat.dart';
 | 
				
			||||||
 | 
					import 'package:island/models/account.dart';
 | 
				
			||||||
 | 
					import 'package:island/pods/chat/webrtc_manager.dart';
 | 
				
			||||||
import 'package:wakelock_plus/wakelock_plus.dart';
 | 
					import 'package:wakelock_plus/wakelock_plus.dart';
 | 
				
			||||||
import 'package:island/talker.dart';
 | 
					import 'package:island/talker.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -43,193 +45,212 @@ sealed class CallParticipantLive with _$CallParticipantLive {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const factory CallParticipantLive({
 | 
					  const factory CallParticipantLive({
 | 
				
			||||||
    required CallParticipant participant,
 | 
					    required CallParticipant participant,
 | 
				
			||||||
    required lk.Participant remoteParticipant,
 | 
					    required WebRTCParticipant remoteParticipant,
 | 
				
			||||||
  }) = _CallParticipantLive;
 | 
					  }) = _CallParticipantLive;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool get isSpeaking => remoteParticipant.isSpeaking;
 | 
					  bool get isSpeaking {
 | 
				
			||||||
  bool get isMuted =>
 | 
					    // Use the actual audio level from WebRTC monitoring
 | 
				
			||||||
      remoteParticipant.isMuted || !remoteParticipant.isMicrophoneEnabled();
 | 
					    return remoteParticipant.audioLevel > 0.1; // Threshold for speaking
 | 
				
			||||||
  bool get isScreenSharing => remoteParticipant.isScreenShareEnabled();
 | 
					  }
 | 
				
			||||||
  bool get isScreenSharingWithAudio =>
 | 
					 | 
				
			||||||
      remoteParticipant.isScreenShareAudioEnabled();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bool get hasVideo => remoteParticipant.hasVideo;
 | 
					  double get audioLevel => remoteParticipant.audioLevel;
 | 
				
			||||||
  bool get hasAudio => remoteParticipant.hasAudio;
 | 
					
 | 
				
			||||||
 | 
					  bool get isMuted => !remoteParticipant.isAudioEnabled;
 | 
				
			||||||
 | 
					  bool get isScreenSharing => remoteParticipant.isVideoEnabled; // Simplified
 | 
				
			||||||
 | 
					  bool get isScreenSharingWithAudio => false; // TODO: Implement screen sharing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  bool get hasVideo => remoteParticipant.isVideoEnabled;
 | 
				
			||||||
 | 
					  bool get hasAudio => remoteParticipant.isAudioEnabled;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Riverpod(keepAlive: true)
 | 
					@Riverpod(keepAlive: true)
 | 
				
			||||||
class CallNotifier extends _$CallNotifier {
 | 
					class CallNotifier extends _$CallNotifier {
 | 
				
			||||||
  lk.Room? _room;
 | 
					  WebRTCManager? _webrtcManager;
 | 
				
			||||||
  lk.LocalParticipant? _localParticipant;
 | 
					 | 
				
			||||||
  List<CallParticipantLive> _participants = [];
 | 
					  List<CallParticipantLive> _participants = [];
 | 
				
			||||||
  final Map<String, CallParticipant> _participantInfoByIdentity = {};
 | 
					  final Map<String, CallParticipant> _participantInfoByIdentity = {};
 | 
				
			||||||
  lk.EventsListener? _roomListener;
 | 
					  StreamSubscription<WebRTCParticipant>? _participantJoinedSubscription;
 | 
				
			||||||
 | 
					  StreamSubscription<String>? _participantLeftSubscription;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  List<CallParticipantLive> get participants =>
 | 
					  List<CallParticipantLive> get participants =>
 | 
				
			||||||
      List.unmodifiable(_participants);
 | 
					      List.unmodifiable(_participants);
 | 
				
			||||||
  lk.LocalParticipant? get localParticipant => _localParticipant;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, double> participantsVolumes = {};
 | 
					  Map<String, double> participantsVolumes = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Timer? _durationTimer;
 | 
					  Timer? _durationTimer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  lk.Room? get room => _room;
 | 
					  String? _roomId;
 | 
				
			||||||
 | 
					  String? get roomId => _roomId;
 | 
				
			||||||
 | 
					  WebRTCManager? get webrtcManager => _webrtcManager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  CallState build() {
 | 
					  CallState build() {
 | 
				
			||||||
    // Subscribe to websocket updates
 | 
					    // Subscribe to websocket updates
 | 
				
			||||||
    return const CallState(
 | 
					    return const CallState(
 | 
				
			||||||
      isConnected: false,
 | 
					      isConnected: false,
 | 
				
			||||||
      isMicrophoneEnabled: true,
 | 
					      isMicrophoneEnabled:
 | 
				
			||||||
      isCameraEnabled: false,
 | 
					          true, // Audio enabled by default (matches WebRTC init)
 | 
				
			||||||
 | 
					      isCameraEnabled: true, // Video enabled by default (matches WebRTC init)
 | 
				
			||||||
      isScreenSharing: false,
 | 
					      isScreenSharing: false,
 | 
				
			||||||
      isSpeakerphone: true,
 | 
					      isSpeakerphone: true,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _initRoomListeners() {
 | 
					  void _initWebRTCListeners() {
 | 
				
			||||||
    if (_room == null) return;
 | 
					    _participantJoinedSubscription?.cancel();
 | 
				
			||||||
    _roomListener?.dispose();
 | 
					    _participantLeftSubscription?.cancel();
 | 
				
			||||||
    _roomListener = _room!.createListener();
 | 
					 | 
				
			||||||
    _room!.addListener(_onRoomChange);
 | 
					 | 
				
			||||||
    _roomListener!
 | 
					 | 
				
			||||||
      ..on<lk.ParticipantConnectedEvent>((e) {
 | 
					 | 
				
			||||||
        _refreshLiveParticipants();
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
      ..on<lk.RoomDisconnectedEvent>((e) {
 | 
					 | 
				
			||||||
        _participants = [];
 | 
					 | 
				
			||||||
        state = state.copyWith();
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _onRoomChange() {
 | 
					    _participantJoinedSubscription = _webrtcManager?.onParticipantJoined.listen(
 | 
				
			||||||
    _refreshLiveParticipants();
 | 
					      (participant) {
 | 
				
			||||||
  }
 | 
					        _updateLiveParticipantsFromWebRTC();
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
  void _refreshLiveParticipants() {
 | 
					 | 
				
			||||||
    if (_room == null) return;
 | 
					 | 
				
			||||||
    final remoteParticipants = _room!.remoteParticipants;
 | 
					 | 
				
			||||||
    _participants = [];
 | 
					 | 
				
			||||||
    // Add local participant first if available
 | 
					 | 
				
			||||||
    if (_localParticipant != null) {
 | 
					 | 
				
			||||||
      final localInfo = _buildParticipant();
 | 
					 | 
				
			||||||
      _participants.add(
 | 
					 | 
				
			||||||
        CallParticipantLive(
 | 
					 | 
				
			||||||
          participant: localInfo,
 | 
					 | 
				
			||||||
          remoteParticipant: _localParticipant!,
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    // Add remote participants
 | 
					 | 
				
			||||||
    _participants.addAll(
 | 
					 | 
				
			||||||
      remoteParticipants.values.map((remote) {
 | 
					 | 
				
			||||||
        final match =
 | 
					 | 
				
			||||||
            _participantInfoByIdentity[remote.identity] ??
 | 
					 | 
				
			||||||
            CallParticipant(
 | 
					 | 
				
			||||||
              identity: remote.identity,
 | 
					 | 
				
			||||||
              name: remote.identity,
 | 
					 | 
				
			||||||
              joinedAt: DateTime.now(),
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
        return CallParticipantLive(
 | 
					 | 
				
			||||||
          participant: match,
 | 
					 | 
				
			||||||
          remoteParticipant: remote,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }),
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _participantLeftSubscription = _webrtcManager?.onParticipantLeft.listen((
 | 
				
			||||||
 | 
					      participantId,
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      _participants.removeWhere((p) => p.remoteParticipant.id == participantId);
 | 
				
			||||||
 | 
					      state = state.copyWith();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Add local participant immediately when WebRTC is initialized
 | 
				
			||||||
 | 
					    final userinfo = ref.watch(userInfoProvider);
 | 
				
			||||||
 | 
					    if (userinfo.value != null) {
 | 
				
			||||||
 | 
					      _addLocalParticipant(userinfo.value!);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _addLocalParticipant(SnAccount userinfo) {
 | 
				
			||||||
 | 
					    if (_webrtcManager == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Remove any existing local participant first
 | 
				
			||||||
 | 
					    _participants.removeWhere((p) => p.participant.identity == userinfo.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Add local participant (current user)
 | 
				
			||||||
 | 
					    final localParticipant = CallParticipantLive(
 | 
				
			||||||
 | 
					      participant: CallParticipant(
 | 
				
			||||||
 | 
					        identity: userinfo.id, // Use roomId as local identity
 | 
				
			||||||
 | 
					        name: userinfo.name,
 | 
				
			||||||
 | 
					        accountId: userinfo.id,
 | 
				
			||||||
 | 
					        account: userinfo,
 | 
				
			||||||
 | 
					        joinedAt: DateTime.now(),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      remoteParticipant: WebRTCParticipant(
 | 
				
			||||||
 | 
					        id: _webrtcManager!.roomId,
 | 
				
			||||||
 | 
					        name: userinfo.nick,
 | 
				
			||||||
 | 
					        userinfo: userinfo,
 | 
				
			||||||
 | 
					        isLocal: true,
 | 
				
			||||||
 | 
					      )..remoteStream = _webrtcManager!.localStream, // Access local stream
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _participants.insert(0, localParticipant); // Add at the beginning
 | 
				
			||||||
    state = state.copyWith();
 | 
					    state = state.copyWith();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /// Builds the CallParticipant object for the local participant.
 | 
					  void _updateLiveParticipantsFromWebRTC() {
 | 
				
			||||||
  /// Optionally, pass [participants] if you want to prioritize info from the latest list.
 | 
					    if (_webrtcManager == null) return;
 | 
				
			||||||
  CallParticipant _buildParticipant({List<CallParticipant>? participants}) {
 | 
					 | 
				
			||||||
    if (_localParticipant == null) {
 | 
					 | 
				
			||||||
      throw StateError('No local participant available');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    // Prefer info from the latest participants list if available
 | 
					 | 
				
			||||||
    if (participants != null) {
 | 
					 | 
				
			||||||
      final idx = participants.indexWhere(
 | 
					 | 
				
			||||||
        (p) => p.identity == _localParticipant!.identity,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      if (idx != -1) return participants[idx];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Otherwise, use info from the identity map or fallback to minimal
 | 
					    final webrtcParticipants = _webrtcManager!.participants;
 | 
				
			||||||
    return _participantInfoByIdentity[_localParticipant!.identity] ??
 | 
					 | 
				
			||||||
        CallParticipant(
 | 
					 | 
				
			||||||
          identity: _localParticipant!.identity,
 | 
					 | 
				
			||||||
          name: _localParticipant!.identity,
 | 
					 | 
				
			||||||
          joinedAt: DateTime.now(),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _updateLiveParticipants(List<CallParticipant> participants) {
 | 
					    // Always ensure local participant exists
 | 
				
			||||||
    // Update the info map for lookup
 | 
					    final existingLocalParticipant =
 | 
				
			||||||
    for (final p in participants) {
 | 
					        _participants.isNotEmpty &&
 | 
				
			||||||
      _participantInfoByIdentity[p.identity] = p;
 | 
					                _participants[0].remoteParticipant.id == _webrtcManager!.roomId
 | 
				
			||||||
    }
 | 
					            ? _participants[0]
 | 
				
			||||||
    if (_room == null) {
 | 
					 | 
				
			||||||
      // Can't build live objects, just store empty
 | 
					 | 
				
			||||||
      _participants = [];
 | 
					 | 
				
			||||||
      state = state.copyWith();
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    final remoteParticipants = _room!.remoteParticipants;
 | 
					 | 
				
			||||||
    final remotes = remoteParticipants.values.toList();
 | 
					 | 
				
			||||||
    _participants = [];
 | 
					 | 
				
			||||||
    // Add local participant if present in the list
 | 
					 | 
				
			||||||
    if (_localParticipant != null) {
 | 
					 | 
				
			||||||
      final localInfo = _buildParticipant(participants: participants);
 | 
					 | 
				
			||||||
      _participants.add(
 | 
					 | 
				
			||||||
        CallParticipantLive(
 | 
					 | 
				
			||||||
          participant: localInfo,
 | 
					 | 
				
			||||||
          remoteParticipant: _localParticipant!,
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      state = state.copyWith();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    // Add remote participants
 | 
					 | 
				
			||||||
    _participants.addAll(
 | 
					 | 
				
			||||||
      participants.map((p) {
 | 
					 | 
				
			||||||
        lk.RemoteParticipant? remote;
 | 
					 | 
				
			||||||
        for (final r in remotes) {
 | 
					 | 
				
			||||||
          if (r.identity == p.identity) {
 | 
					 | 
				
			||||||
            remote = r;
 | 
					 | 
				
			||||||
            break;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (_localParticipant != null &&
 | 
					 | 
				
			||||||
            p.identity == _localParticipant!.identity) {
 | 
					 | 
				
			||||||
          return null; // Already added local
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return remote != null
 | 
					 | 
				
			||||||
            ? CallParticipantLive(participant: p, remoteParticipant: remote)
 | 
					 | 
				
			||||||
            : null;
 | 
					            : null;
 | 
				
			||||||
      }).whereType<CallParticipantLive>(),
 | 
					
 | 
				
			||||||
    );
 | 
					    final localParticipant =
 | 
				
			||||||
 | 
					        existingLocalParticipant ?? _createLocalParticipant();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Add remote participants
 | 
				
			||||||
 | 
					    final remoteParticipants =
 | 
				
			||||||
 | 
					        webrtcParticipants.map((p) {
 | 
				
			||||||
 | 
					          final participantInfo =
 | 
				
			||||||
 | 
					              _participantInfoByIdentity[p.id] ??
 | 
				
			||||||
 | 
					              CallParticipant(
 | 
				
			||||||
 | 
					                identity: p.id,
 | 
				
			||||||
 | 
					                name: p.name,
 | 
				
			||||||
 | 
					                accountId: p.userinfo.id,
 | 
				
			||||||
 | 
					                account: p.userinfo,
 | 
				
			||||||
 | 
					                joinedAt: DateTime.now(),
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					          return CallParticipantLive(
 | 
				
			||||||
 | 
					            participant: participantInfo,
 | 
				
			||||||
 | 
					            remoteParticipant: p,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }).toList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Combine local participant with remote participants
 | 
				
			||||||
 | 
					    _participants = [localParticipant, ...remoteParticipants];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    state = state.copyWith();
 | 
					    state = state.copyWith();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String? _roomId;
 | 
					  CallParticipantLive _createLocalParticipant() {
 | 
				
			||||||
  String? get roomId => _roomId;
 | 
					    return CallParticipantLive(
 | 
				
			||||||
 | 
					      participant: CallParticipant(
 | 
				
			||||||
 | 
					        identity: _webrtcManager!.roomId, // Use roomId as local identity
 | 
				
			||||||
 | 
					        name: 'You',
 | 
				
			||||||
 | 
					        accountId: '',
 | 
				
			||||||
 | 
					        account: null,
 | 
				
			||||||
 | 
					        joinedAt: DateTime.now(),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					      remoteParticipant: WebRTCParticipant(
 | 
				
			||||||
 | 
					        id: _webrtcManager!.roomId,
 | 
				
			||||||
 | 
					        name: 'You',
 | 
				
			||||||
 | 
					        userinfo: SnAccount(
 | 
				
			||||||
 | 
					          id: '',
 | 
				
			||||||
 | 
					          name: '',
 | 
				
			||||||
 | 
					          nick: '',
 | 
				
			||||||
 | 
					          language: '',
 | 
				
			||||||
 | 
					          isSuperuser: false,
 | 
				
			||||||
 | 
					          automatedId: null,
 | 
				
			||||||
 | 
					          profile: SnAccountProfile(
 | 
				
			||||||
 | 
					            id: '',
 | 
				
			||||||
 | 
					            firstName: '',
 | 
				
			||||||
 | 
					            middleName: '',
 | 
				
			||||||
 | 
					            lastName: '',
 | 
				
			||||||
 | 
					            bio: '',
 | 
				
			||||||
 | 
					            gender: '',
 | 
				
			||||||
 | 
					            pronouns: '',
 | 
				
			||||||
 | 
					            location: '',
 | 
				
			||||||
 | 
					            timeZone: '',
 | 
				
			||||||
 | 
					            links: [],
 | 
				
			||||||
 | 
					            experience: 0,
 | 
				
			||||||
 | 
					            level: 0,
 | 
				
			||||||
 | 
					            socialCredits: 0,
 | 
				
			||||||
 | 
					            socialCreditsLevel: 0,
 | 
				
			||||||
 | 
					            levelingProgress: 0,
 | 
				
			||||||
 | 
					            picture: null,
 | 
				
			||||||
 | 
					            background: null,
 | 
				
			||||||
 | 
					            verification: null,
 | 
				
			||||||
 | 
					            usernameColor: null,
 | 
				
			||||||
 | 
					            createdAt: DateTime.now(),
 | 
				
			||||||
 | 
					            updatedAt: DateTime.now(),
 | 
				
			||||||
 | 
					            deletedAt: null,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          perkSubscription: null,
 | 
				
			||||||
 | 
					          createdAt: DateTime.now(),
 | 
				
			||||||
 | 
					          updatedAt: DateTime.now(),
 | 
				
			||||||
 | 
					          deletedAt: null,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      )..remoteStream = _webrtcManager!.localStream, // Access local stream
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> joinRoom(String roomId) async {
 | 
					  Future<void> joinRoom(String roomId) async {
 | 
				
			||||||
    if (_roomId == roomId && _room != null) {
 | 
					    if (_roomId == roomId && _webrtcManager != null) {
 | 
				
			||||||
      talker.info('[Call] Call skipped. Already has data');
 | 
					      talker.info('[Call] Call skipped. Already connected to this room');
 | 
				
			||||||
      return;
 | 
					      // Ensure state is connected even if we skip the join process
 | 
				
			||||||
    } else if (_room != null) {
 | 
					      if (!state.isConnected) {
 | 
				
			||||||
      if (!_room!.isDisposed &&
 | 
					        state = state.copyWith(isConnected: true);
 | 
				
			||||||
          _room!.connectionState != lk.ConnectionState.disconnected) {
 | 
					 | 
				
			||||||
        throw Exception('Call already connected');
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    _roomId = roomId;
 | 
					    _roomId = roomId;
 | 
				
			||||||
    if (_room != null) {
 | 
					
 | 
				
			||||||
      await _room!.disconnect();
 | 
					    // Clean up existing connection
 | 
				
			||||||
      await _room!.dispose();
 | 
					    await disconnect();
 | 
				
			||||||
      _room = null;
 | 
					
 | 
				
			||||||
      _localParticipant = null;
 | 
					 | 
				
			||||||
      _participants = [];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      final apiClient = ref.read(apiClientProvider);
 | 
					      final apiClient = ref.read(apiClientProvider);
 | 
				
			||||||
      final ongoingCall = await ref.read(ongoingCallProvider(roomId).future);
 | 
					      final ongoingCall = await ref.read(ongoingCallProvider(roomId).future);
 | 
				
			||||||
@@ -241,8 +262,11 @@ class CallNotifier extends _$CallNotifier {
 | 
				
			|||||||
        // Parse join response
 | 
					        // Parse join response
 | 
				
			||||||
        final joinResponse = ChatRealtimeJoinResponse.fromJson(data);
 | 
					        final joinResponse = ChatRealtimeJoinResponse.fromJson(data);
 | 
				
			||||||
        final participants = joinResponse.participants;
 | 
					        final participants = joinResponse.participants;
 | 
				
			||||||
        final String endpoint = joinResponse.endpoint;
 | 
					
 | 
				
			||||||
        final String token = joinResponse.token;
 | 
					        // Update participant info map
 | 
				
			||||||
 | 
					        for (final p in participants) {
 | 
				
			||||||
 | 
					          _participantInfoByIdentity[p.identity] = p;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Setup duration timer
 | 
					        // Setup duration timer
 | 
				
			||||||
        _durationTimer?.cancel();
 | 
					        _durationTimer?.cancel();
 | 
				
			||||||
@@ -257,47 +281,18 @@ class CallNotifier extends _$CallNotifier {
 | 
				
			|||||||
          );
 | 
					          );
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Connect to LiveKit
 | 
					        // Initialize WebRTC manager
 | 
				
			||||||
        _room = lk.Room();
 | 
					        final serverUrl = ref.watch(serverUrlProvider);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await _room!.connect(
 | 
					        _webrtcManager = WebRTCManager(roomId: roomId, serverUrl: serverUrl);
 | 
				
			||||||
          endpoint,
 | 
					 | 
				
			||||||
          token,
 | 
					 | 
				
			||||||
          connectOptions: lk.ConnectOptions(autoSubscribe: true),
 | 
					 | 
				
			||||||
          roomOptions: lk.RoomOptions(adaptiveStream: true, dynacast: true),
 | 
					 | 
				
			||||||
          fastConnectOptions: lk.FastConnectOptions(
 | 
					 | 
				
			||||||
            microphone: lk.TrackOption(enabled: true),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        _localParticipant = _room!.localParticipant;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        _initRoomListeners();
 | 
					        await _webrtcManager!.initialize(ref);
 | 
				
			||||||
        _updateLiveParticipants(participants);
 | 
					        _initWebRTCListeners();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
 | 
					        if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
 | 
				
			||||||
          lk.Hardware.instance.setSpeakerphoneOn(true);
 | 
					          // TODO: Implement speakerphone control for WebRTC
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Listen for connection updates
 | 
					 | 
				
			||||||
        _room!.addListener(() {
 | 
					 | 
				
			||||||
          final wasConnected = state.isConnected;
 | 
					 | 
				
			||||||
          final isNowConnected =
 | 
					 | 
				
			||||||
              _room!.connectionState == lk.ConnectionState.connected;
 | 
					 | 
				
			||||||
          state = state.copyWith(
 | 
					 | 
				
			||||||
            isConnected: isNowConnected,
 | 
					 | 
				
			||||||
            isMicrophoneEnabled: _localParticipant!.isMicrophoneEnabled(),
 | 
					 | 
				
			||||||
            isCameraEnabled: _localParticipant!.isCameraEnabled(),
 | 
					 | 
				
			||||||
            isScreenSharing: _localParticipant!.isScreenShareEnabled(),
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          // Enable wakelock when call connects
 | 
					 | 
				
			||||||
          if (!wasConnected && isNowConnected) {
 | 
					 | 
				
			||||||
            WakelockPlus.enable();
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          // Disable wakelock when call disconnects
 | 
					 | 
				
			||||||
          else if (wasConnected && !isNowConnected) {
 | 
					 | 
				
			||||||
            WakelockPlus.disable();
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        state = state.copyWith(isConnected: true);
 | 
					        state = state.copyWith(isConnected: true);
 | 
				
			||||||
        // Enable wakelock when call connects
 | 
					        // Enable wakelock when call connects
 | 
				
			||||||
        WakelockPlus.enable();
 | 
					        WakelockPlus.enable();
 | 
				
			||||||
@@ -310,104 +305,114 @@ class CallNotifier extends _$CallNotifier {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> toggleMicrophone() async {
 | 
					  Future<void> toggleMicrophone() async {
 | 
				
			||||||
    if (_localParticipant != null) {
 | 
					    final target = !state.isMicrophoneEnabled;
 | 
				
			||||||
      const autostop = true;
 | 
					    state = state.copyWith(isMicrophoneEnabled: target);
 | 
				
			||||||
      final target = !_localParticipant!.isMicrophoneEnabled();
 | 
					    await _webrtcManager?.toggleMicrophone(target);
 | 
				
			||||||
      state = state.copyWith(isMicrophoneEnabled: target);
 | 
					
 | 
				
			||||||
      if (target) {
 | 
					    // Update local participant's audio state
 | 
				
			||||||
        await _localParticipant!.audioTrackPublications.firstOrNull?.unmute(
 | 
					    if (_participants.isNotEmpty) {
 | 
				
			||||||
          stopOnMute: autostop,
 | 
					      _participants[0].remoteParticipant.isAudioEnabled = target;
 | 
				
			||||||
        );
 | 
					      state = state.copyWith(); // Trigger UI update
 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        await _localParticipant!.audioTrackPublications.firstOrNull?.mute(
 | 
					 | 
				
			||||||
          stopOnMute: autostop,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      state = state.copyWith();
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> toggleCamera() async {
 | 
					  Future<void> toggleCamera() async {
 | 
				
			||||||
    if (_localParticipant != null) {
 | 
					    final target = !state.isCameraEnabled;
 | 
				
			||||||
      final target = !_localParticipant!.isCameraEnabled();
 | 
					    state = state.copyWith(isCameraEnabled: target);
 | 
				
			||||||
      state = state.copyWith(isCameraEnabled: target);
 | 
					    await _webrtcManager?.toggleCamera(target);
 | 
				
			||||||
      await _localParticipant!.setCameraEnabled(target);
 | 
					
 | 
				
			||||||
      state = state.copyWith();
 | 
					    // Update local participant's video state
 | 
				
			||||||
 | 
					    if (_participants.isNotEmpty) {
 | 
				
			||||||
 | 
					      _participants[0].remoteParticipant.isVideoEnabled = target;
 | 
				
			||||||
 | 
					      state = state.copyWith(); // Trigger UI update
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> toggleScreenShare(BuildContext context) async {
 | 
					  Future<void> toggleScreenShare(BuildContext context) async {
 | 
				
			||||||
    if (_localParticipant != null) {
 | 
					    if (_webrtcManager == null) return;
 | 
				
			||||||
      final target = !_localParticipant!.isScreenShareEnabled();
 | 
					 | 
				
			||||||
      state = state.copyWith(isScreenSharing: target);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (target && lk.lkPlatformIsDesktop()) {
 | 
					    try {
 | 
				
			||||||
        try {
 | 
					      if (state.isScreenSharing) {
 | 
				
			||||||
          final source = await showDialog<DesktopCapturerSource>(
 | 
					        // Stop screen sharing - switch back to camera
 | 
				
			||||||
            context: context,
 | 
					        await _webrtcManager!.toggleCamera(state.isCameraEnabled);
 | 
				
			||||||
            builder: (context) => lk.ScreenSelectDialog(),
 | 
					        state = state.copyWith(isScreenSharing: false);
 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          if (source == null) {
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          var track = await lk.LocalVideoTrack.createScreenShareTrack(
 | 
					 | 
				
			||||||
            lk.ScreenShareCaptureOptions(
 | 
					 | 
				
			||||||
              sourceId: source.id,
 | 
					 | 
				
			||||||
              maxFrameRate: 30.0,
 | 
					 | 
				
			||||||
              captureScreenAudio: true,
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          await _localParticipant!.publishVideoTrack(track);
 | 
					 | 
				
			||||||
        } catch (err) {
 | 
					 | 
				
			||||||
          showErrorAlert(err);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        await _localParticipant!.setScreenShareEnabled(target);
 | 
					        // Start screen sharing
 | 
				
			||||||
      }
 | 
					        if (WebRTC.platformIsDesktop) {
 | 
				
			||||||
 | 
					          // For desktop, we need to get screen capture source
 | 
				
			||||||
 | 
					          // This would require implementing a screen selection dialog
 | 
				
			||||||
 | 
					          // For now, just toggle the state
 | 
				
			||||||
 | 
					          state = state.copyWith(isScreenSharing: true);
 | 
				
			||||||
 | 
					        } else if (WebRTC.platformIsWeb) {
 | 
				
			||||||
 | 
					          // For web, get display media directly
 | 
				
			||||||
 | 
					          await navigator.mediaDevices.getDisplayMedia({
 | 
				
			||||||
 | 
					            'video': true,
 | 
				
			||||||
 | 
					            'audio':
 | 
				
			||||||
 | 
					                false, // Screen sharing typically doesn't include system audio
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      state = state.copyWith();
 | 
					          // Replace video track with screen sharing track
 | 
				
			||||||
 | 
					          // This is a simplified implementation
 | 
				
			||||||
 | 
					          state = state.copyWith(isScreenSharing: true);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      talker.error('[Call] Screen sharing error: $e');
 | 
				
			||||||
 | 
					      state = state.copyWith(error: 'Failed to toggle screen sharing: $e');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> toggleSpeakerphone() async {
 | 
					  Future<void> toggleSpeakerphone() async {
 | 
				
			||||||
    state = state.copyWith(isSpeakerphone: !state.isSpeakerphone);
 | 
					    if (!kIsWeb && (Platform.isIOS || Platform.isAndroid)) {
 | 
				
			||||||
    await lk.Hardware.instance.setSpeakerphoneOn(state.isSpeakerphone);
 | 
					      try {
 | 
				
			||||||
    state = state.copyWith();
 | 
					        // For mobile platforms, we can control audio routing
 | 
				
			||||||
 | 
					        // This is a simplified implementation
 | 
				
			||||||
 | 
					        final newSpeakerphoneState = !state.isSpeakerphone;
 | 
				
			||||||
 | 
					        state = state.copyWith(isSpeakerphone: newSpeakerphoneState);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Note: Actual speakerphone control would require platform-specific code
 | 
				
			||||||
 | 
					        // For a full implementation, you'd need to use platform channels
 | 
				
			||||||
 | 
					        // to control audio routing on iOS/Android
 | 
				
			||||||
 | 
					        talker.info('[Call] Speakerphone toggled to: $newSpeakerphoneState');
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        talker.error('[Call] Speakerphone control error: $e');
 | 
				
			||||||
 | 
					        state = state.copyWith(error: 'Failed to toggle speakerphone: $e');
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      // For web/desktop, speakerphone control is handled by the browser/OS
 | 
				
			||||||
 | 
					      state = state.copyWith(isSpeakerphone: !state.isSpeakerphone);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<void> disconnect() async {
 | 
					  Future<void> disconnect() async {
 | 
				
			||||||
    if (_room != null) {
 | 
					    _webrtcManager?.dispose();
 | 
				
			||||||
      await _room!.disconnect();
 | 
					    _webrtcManager = null;
 | 
				
			||||||
      state = state.copyWith(
 | 
					    _participantJoinedSubscription?.cancel();
 | 
				
			||||||
        isConnected: false,
 | 
					    _participantLeftSubscription?.cancel();
 | 
				
			||||||
        isMicrophoneEnabled: false,
 | 
					    _participants.clear();
 | 
				
			||||||
        isCameraEnabled: false,
 | 
					    state = state.copyWith(
 | 
				
			||||||
        isScreenSharing: false,
 | 
					      isConnected: false,
 | 
				
			||||||
      );
 | 
					      isMicrophoneEnabled: false,
 | 
				
			||||||
      // Disable wakelock when call disconnects
 | 
					      isCameraEnabled: false,
 | 
				
			||||||
      WakelockPlus.disable();
 | 
					      isScreenSharing: false,
 | 
				
			||||||
    }
 | 
					    );
 | 
				
			||||||
 | 
					    // Disable wakelock when call disconnects
 | 
				
			||||||
 | 
					    WakelockPlus.disable();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void setParticipantVolume(CallParticipantLive live, double volume) {
 | 
					  void setParticipantVolume(CallParticipantLive live, double volume) {
 | 
				
			||||||
    if (participantsVolumes[live.remoteParticipant.sid] == null) {
 | 
					    // Store volume setting for this participant
 | 
				
			||||||
      participantsVolumes[live.remoteParticipant.sid] = 1;
 | 
					    // Note: WebRTC doesn't have built-in per-participant volume control
 | 
				
			||||||
    }
 | 
					    // This is just storing the preference for UI purposes
 | 
				
			||||||
    Helper.setVolume(
 | 
					    // Actual volume control would need to be implemented at the audio rendering level
 | 
				
			||||||
      volume,
 | 
					    participantsVolumes[live.remoteParticipant.id] = volume.clamp(0.0, 1.0);
 | 
				
			||||||
      live
 | 
					    talker.info(
 | 
				
			||||||
          .remoteParticipant
 | 
					      '[Call] Volume set to $volume for participant ${live.remoteParticipant.id}',
 | 
				
			||||||
          .audioTrackPublications
 | 
					 | 
				
			||||||
          .first
 | 
					 | 
				
			||||||
          .track!
 | 
					 | 
				
			||||||
          .mediaStreamTrack,
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    participantsVolumes[live.remoteParticipant.sid] = volume;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  double getParticipantVolume(CallParticipantLive live) {
 | 
					  double getParticipantVolume(CallParticipantLive live) {
 | 
				
			||||||
    return participantsVolumes[live.remoteParticipant.sid] ?? 1;
 | 
					    return participantsVolumes[live.remoteParticipant.id] ?? 1.0;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void dispose() {
 | 
					  void dispose() {
 | 
				
			||||||
@@ -418,9 +423,10 @@ class CallNotifier extends _$CallNotifier {
 | 
				
			|||||||
      isCameraEnabled: false,
 | 
					      isCameraEnabled: false,
 | 
				
			||||||
      isScreenSharing: false,
 | 
					      isScreenSharing: false,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    _roomListener?.dispose();
 | 
					    _participantJoinedSubscription?.cancel();
 | 
				
			||||||
    _room?.removeListener(_onRoomChange);
 | 
					    _participantLeftSubscription?.cancel();
 | 
				
			||||||
    _room?.dispose();
 | 
					    _webrtcManager?.dispose();
 | 
				
			||||||
 | 
					    _webrtcManager = null;
 | 
				
			||||||
    _durationTimer?.cancel();
 | 
					    _durationTimer?.cancel();
 | 
				
			||||||
    _roomId = null;
 | 
					    _roomId = null;
 | 
				
			||||||
    participantsVolumes = {};
 | 
					    participantsVolumes = {};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -295,7 +295,7 @@ as String?,
 | 
				
			|||||||
/// @nodoc
 | 
					/// @nodoc
 | 
				
			||||||
mixin _$CallParticipantLive implements DiagnosticableTreeMixin {
 | 
					mixin _$CallParticipantLive implements DiagnosticableTreeMixin {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 CallParticipant get participant; lk.Participant get remoteParticipant;
 | 
					 CallParticipant get participant; WebRTCParticipant get remoteParticipant;
 | 
				
			||||||
/// Create a copy of CallParticipantLive
 | 
					/// Create a copy of CallParticipantLive
 | 
				
			||||||
/// with the given fields replaced by the non-null parameter values.
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
@@ -332,7 +332,7 @@ abstract mixin class $CallParticipantLiveCopyWith<$Res>  {
 | 
				
			|||||||
  factory $CallParticipantLiveCopyWith(CallParticipantLive value, $Res Function(CallParticipantLive) _then) = _$CallParticipantLiveCopyWithImpl;
 | 
					  factory $CallParticipantLiveCopyWith(CallParticipantLive value, $Res Function(CallParticipantLive) _then) = _$CallParticipantLiveCopyWithImpl;
 | 
				
			||||||
@useResult
 | 
					@useResult
 | 
				
			||||||
$Res call({
 | 
					$Res call({
 | 
				
			||||||
 CallParticipant participant, lk.Participant remoteParticipant
 | 
					 CallParticipant participant, WebRTCParticipant remoteParticipant
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -353,7 +353,7 @@ class _$CallParticipantLiveCopyWithImpl<$Res>
 | 
				
			|||||||
  return _then(_self.copyWith(
 | 
					  return _then(_self.copyWith(
 | 
				
			||||||
participant: null == participant ? _self.participant : participant // ignore: cast_nullable_to_non_nullable
 | 
					participant: null == participant ? _self.participant : participant // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as CallParticipant,remoteParticipant: null == remoteParticipant ? _self.remoteParticipant : remoteParticipant // ignore: cast_nullable_to_non_nullable
 | 
					as CallParticipant,remoteParticipant: null == remoteParticipant ? _self.remoteParticipant : remoteParticipant // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as lk.Participant,
 | 
					as WebRTCParticipant,
 | 
				
			||||||
  ));
 | 
					  ));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/// Create a copy of CallParticipantLive
 | 
					/// Create a copy of CallParticipantLive
 | 
				
			||||||
@@ -444,7 +444,7 @@ return $default(_that);case _:
 | 
				
			|||||||
/// }
 | 
					/// }
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( CallParticipant participant,  lk.Participant remoteParticipant)?  $default,{required TResult orElse(),}) {final _that = this;
 | 
					@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( CallParticipant participant,  WebRTCParticipant remoteParticipant)?  $default,{required TResult orElse(),}) {final _that = this;
 | 
				
			||||||
switch (_that) {
 | 
					switch (_that) {
 | 
				
			||||||
case _CallParticipantLive() when $default != null:
 | 
					case _CallParticipantLive() when $default != null:
 | 
				
			||||||
return $default(_that.participant,_that.remoteParticipant);case _:
 | 
					return $default(_that.participant,_that.remoteParticipant);case _:
 | 
				
			||||||
@@ -465,7 +465,7 @@ return $default(_that.participant,_that.remoteParticipant);case _:
 | 
				
			|||||||
/// }
 | 
					/// }
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( CallParticipant participant,  lk.Participant remoteParticipant)  $default,) {final _that = this;
 | 
					@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( CallParticipant participant,  WebRTCParticipant remoteParticipant)  $default,) {final _that = this;
 | 
				
			||||||
switch (_that) {
 | 
					switch (_that) {
 | 
				
			||||||
case _CallParticipantLive():
 | 
					case _CallParticipantLive():
 | 
				
			||||||
return $default(_that.participant,_that.remoteParticipant);}
 | 
					return $default(_that.participant,_that.remoteParticipant);}
 | 
				
			||||||
@@ -482,7 +482,7 @@ return $default(_that.participant,_that.remoteParticipant);}
 | 
				
			|||||||
/// }
 | 
					/// }
 | 
				
			||||||
/// ```
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( CallParticipant participant,  lk.Participant remoteParticipant)?  $default,) {final _that = this;
 | 
					@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( CallParticipant participant,  WebRTCParticipant remoteParticipant)?  $default,) {final _that = this;
 | 
				
			||||||
switch (_that) {
 | 
					switch (_that) {
 | 
				
			||||||
case _CallParticipantLive() when $default != null:
 | 
					case _CallParticipantLive() when $default != null:
 | 
				
			||||||
return $default(_that.participant,_that.remoteParticipant);case _:
 | 
					return $default(_that.participant,_that.remoteParticipant);case _:
 | 
				
			||||||
@@ -501,7 +501,7 @@ class _CallParticipantLive extends CallParticipantLive with DiagnosticableTreeMi
 | 
				
			|||||||
  
 | 
					  
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@override final  CallParticipant participant;
 | 
					@override final  CallParticipant participant;
 | 
				
			||||||
@override final  lk.Participant remoteParticipant;
 | 
					@override final  WebRTCParticipant remoteParticipant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Create a copy of CallParticipantLive
 | 
					/// Create a copy of CallParticipantLive
 | 
				
			||||||
/// with the given fields replaced by the non-null parameter values.
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
@@ -539,7 +539,7 @@ abstract mixin class _$CallParticipantLiveCopyWith<$Res> implements $CallPartici
 | 
				
			|||||||
  factory _$CallParticipantLiveCopyWith(_CallParticipantLive value, $Res Function(_CallParticipantLive) _then) = __$CallParticipantLiveCopyWithImpl;
 | 
					  factory _$CallParticipantLiveCopyWith(_CallParticipantLive value, $Res Function(_CallParticipantLive) _then) = __$CallParticipantLiveCopyWithImpl;
 | 
				
			||||||
@override @useResult
 | 
					@override @useResult
 | 
				
			||||||
$Res call({
 | 
					$Res call({
 | 
				
			||||||
 CallParticipant participant, lk.Participant remoteParticipant
 | 
					 CallParticipant participant, WebRTCParticipant remoteParticipant
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -560,7 +560,7 @@ class __$CallParticipantLiveCopyWithImpl<$Res>
 | 
				
			|||||||
  return _then(_CallParticipantLive(
 | 
					  return _then(_CallParticipantLive(
 | 
				
			||||||
participant: null == participant ? _self.participant : participant // ignore: cast_nullable_to_non_nullable
 | 
					participant: null == participant ? _self.participant : participant // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as CallParticipant,remoteParticipant: null == remoteParticipant ? _self.remoteParticipant : remoteParticipant // ignore: cast_nullable_to_non_nullable
 | 
					as CallParticipant,remoteParticipant: null == remoteParticipant ? _self.remoteParticipant : remoteParticipant // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
as lk.Participant,
 | 
					as WebRTCParticipant,
 | 
				
			||||||
  ));
 | 
					  ));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@ part of 'call.dart';
 | 
				
			|||||||
// RiverpodGenerator
 | 
					// RiverpodGenerator
 | 
				
			||||||
// **************************************************************************
 | 
					// **************************************************************************
 | 
				
			||||||
 | 
					
 | 
				
			||||||
String _$callNotifierHash() => r'a8ca3f625c0db3ad9992033ae70864ce15efc281';
 | 
					String _$callNotifierHash() => r'4015d326388553c46859fe537e84d2c9da4236c9';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// See also [CallNotifier].
 | 
					/// See also [CallNotifier].
 | 
				
			||||||
@ProviderFor(CallNotifier)
 | 
					@ProviderFor(CallNotifier)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										476
									
								
								lib/pods/chat/webrtc_manager.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										476
									
								
								lib/pods/chat/webrtc_manager.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,476 @@
 | 
				
			|||||||
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_webrtc/flutter_webrtc.dart';
 | 
				
			||||||
 | 
					import 'package:island/models/account.dart';
 | 
				
			||||||
 | 
					import 'package:island/pods/chat/webrtc_signaling.dart';
 | 
				
			||||||
 | 
					import 'package:island/pods/userinfo.dart';
 | 
				
			||||||
 | 
					import 'package:island/talker.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class WebRTCParticipant {
 | 
				
			||||||
 | 
					  final String id;
 | 
				
			||||||
 | 
					  final String name;
 | 
				
			||||||
 | 
					  final SnAccount userinfo;
 | 
				
			||||||
 | 
					  RTCPeerConnection? peerConnection;
 | 
				
			||||||
 | 
					  MediaStream? remoteStream;
 | 
				
			||||||
 | 
					  List<RTCIceCandidate> remoteCandidates = [];
 | 
				
			||||||
 | 
					  bool isAudioEnabled = true;
 | 
				
			||||||
 | 
					  bool isVideoEnabled = false;
 | 
				
			||||||
 | 
					  bool isConnected = false;
 | 
				
			||||||
 | 
					  bool isLocal = false;
 | 
				
			||||||
 | 
					  double audioLevel = 0.0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  WebRTCParticipant({
 | 
				
			||||||
 | 
					    required this.id,
 | 
				
			||||||
 | 
					    required this.name,
 | 
				
			||||||
 | 
					    required this.userinfo,
 | 
				
			||||||
 | 
					    this.isAudioEnabled = true,
 | 
				
			||||||
 | 
					    this.isVideoEnabled = false,
 | 
				
			||||||
 | 
					    this.isLocal = false,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class WebRTCManager {
 | 
				
			||||||
 | 
					  final String roomId;
 | 
				
			||||||
 | 
					  final String serverUrl;
 | 
				
			||||||
 | 
					  late WebRTCSignaling _signaling;
 | 
				
			||||||
 | 
					  final Map<String, WebRTCParticipant> _participants = {};
 | 
				
			||||||
 | 
					  final Map<String, RTCPeerConnection> _peerConnections = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MediaStream? _localStream;
 | 
				
			||||||
 | 
					  Timer? _audioLevelTimer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  MediaStream? get localStream => _localStream;
 | 
				
			||||||
 | 
					  final StreamController<WebRTCParticipant> _participantController =
 | 
				
			||||||
 | 
					      StreamController<WebRTCParticipant>.broadcast();
 | 
				
			||||||
 | 
					  final StreamController<String> _participantLeftController =
 | 
				
			||||||
 | 
					      StreamController<String>.broadcast();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Stream<WebRTCParticipant> get onParticipantJoined =>
 | 
				
			||||||
 | 
					      _participantController.stream;
 | 
				
			||||||
 | 
					  Stream<String> get onParticipantLeft => _participantLeftController.stream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  WebRTCManager({required this.roomId, required this.serverUrl}) {
 | 
				
			||||||
 | 
					    _signaling = WebRTCSignaling(roomId: roomId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> initialize(Ref ref) async {
 | 
				
			||||||
 | 
					    final user = ref.watch(userInfoProvider).value!;
 | 
				
			||||||
 | 
					    _signaling.userId = user.id;
 | 
				
			||||||
 | 
					    _signaling.userName = user.name;
 | 
				
			||||||
 | 
					    _signaling.user = user;
 | 
				
			||||||
 | 
					    await _initializeLocalStream();
 | 
				
			||||||
 | 
					    _setupSignalingListeners();
 | 
				
			||||||
 | 
					    await _signaling.connect(ref);
 | 
				
			||||||
 | 
					    _startAudioLevelMonitoring();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _initializeLocalStream() async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      _localStream = await navigator.mediaDevices.getUserMedia({
 | 
				
			||||||
 | 
					        'audio': true,
 | 
				
			||||||
 | 
					        'video': true,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      talker.info('[WebRTC] Local stream initialized');
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      talker.error('[WebRTC] Failed to initialize local stream: $e');
 | 
				
			||||||
 | 
					      rethrow;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _setupSignalingListeners() {
 | 
				
			||||||
 | 
					    _signaling.messages.listen((message) async {
 | 
				
			||||||
 | 
					      switch (message.type) {
 | 
				
			||||||
 | 
					        case 'offer':
 | 
				
			||||||
 | 
					          await _handleOffer(message.accountId, message.account, message.data);
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        case 'answer':
 | 
				
			||||||
 | 
					          await _handleAnswer(message.accountId, message.data);
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        case 'ice-candidate':
 | 
				
			||||||
 | 
					          await _handleIceCandidate(message.accountId, message.data);
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        // CHANGED: Listen for new users joining the room.
 | 
				
			||||||
 | 
					        case 'user-joined':
 | 
				
			||||||
 | 
					          await _handleUserJoined(message.accountId, message.account);
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        default:
 | 
				
			||||||
 | 
					          talker.warning(
 | 
				
			||||||
 | 
					            '[WebRTC Manager] Receieved an unknown type singaling message: ${message.type} with ${message.data}',
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // CHANGED: The welcome message now drives connection initiation.
 | 
				
			||||||
 | 
					    _signaling.welcomeMessages.listen((welcome) {
 | 
				
			||||||
 | 
					      talker.info('[WebRTC Manager] Connected to room: ${welcome.roomId}');
 | 
				
			||||||
 | 
					      final existingParticipants =
 | 
				
			||||||
 | 
					          welcome.participants; // Assuming the server sends this.
 | 
				
			||||||
 | 
					      talker.info(
 | 
				
			||||||
 | 
					        '[WebRTC Manager] Existing participants: $existingParticipants',
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // The newcomer is responsible for initiating the connection to everyone else.
 | 
				
			||||||
 | 
					      for (final participant in existingParticipants) {
 | 
				
			||||||
 | 
					        if (participant.identity != _signaling.userId) {
 | 
				
			||||||
 | 
					          if (!_participants.containsKey(participant.identity)) {
 | 
				
			||||||
 | 
					            final webrtcParticipant = WebRTCParticipant(
 | 
				
			||||||
 | 
					              id: participant.identity,
 | 
				
			||||||
 | 
					              name: participant.name,
 | 
				
			||||||
 | 
					              userinfo: participant.account!,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            _participants[participant.identity] = webrtcParticipant;
 | 
				
			||||||
 | 
					            _participantController.add(webrtcParticipant);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          _createPeerConnection(participant.identity, isInitiator: true);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // CHANGED: New handler for when an existing user is notified of a new peer.
 | 
				
			||||||
 | 
					  Future<void> _handleUserJoined(
 | 
				
			||||||
 | 
					    String participantId,
 | 
				
			||||||
 | 
					    SnAccount account,
 | 
				
			||||||
 | 
					  ) async {
 | 
				
			||||||
 | 
					    talker.info(
 | 
				
			||||||
 | 
					      '[WebRTC Manager] User joined: $participantId. Waiting for their offer.',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    // We don't need to be the initiator here. The newcomer will send us an offer.
 | 
				
			||||||
 | 
					    // We just create the peer connection to be ready for it.
 | 
				
			||||||
 | 
					    if (!_peerConnections.containsKey(participantId)) {
 | 
				
			||||||
 | 
					      // Create a participant object to represent the new user
 | 
				
			||||||
 | 
					      if (!_participants.containsKey(participantId)) {
 | 
				
			||||||
 | 
					        final participant = WebRTCParticipant(
 | 
				
			||||||
 | 
					          id: participantId,
 | 
				
			||||||
 | 
					          name: participantId,
 | 
				
			||||||
 | 
					          userinfo: account,
 | 
				
			||||||
 | 
					        ); // Placeholder name
 | 
				
			||||||
 | 
					        _participants[participantId] = participant;
 | 
				
			||||||
 | 
					        _participantController.add(participant);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      await _createPeerConnection(participantId, isInitiator: false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _createPeerConnection(
 | 
				
			||||||
 | 
					    String participantId, {
 | 
				
			||||||
 | 
					    bool isInitiator = false,
 | 
				
			||||||
 | 
					  }) async {
 | 
				
			||||||
 | 
					    talker.info(
 | 
				
			||||||
 | 
					      '[WebRTC] Creating peer connection to $participantId (initiator: $isInitiator)',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    final configuration = {
 | 
				
			||||||
 | 
					      'iceServers': [
 | 
				
			||||||
 | 
					        {'urls': 'stun:stun.l.google.com:19302'},
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final peerConnection = await createPeerConnection(configuration);
 | 
				
			||||||
 | 
					    _peerConnections[participantId] = peerConnection;
 | 
				
			||||||
 | 
					    _participants[participantId]!.peerConnection = peerConnection;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (_localStream != null) {
 | 
				
			||||||
 | 
					      for (final track in _localStream!.getTracks()) {
 | 
				
			||||||
 | 
					        await peerConnection.addTrack(track, _localStream!);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    peerConnection.onTrack = (event) {
 | 
				
			||||||
 | 
					      if (event.streams.isNotEmpty) {
 | 
				
			||||||
 | 
					        final participant = _participants[participantId];
 | 
				
			||||||
 | 
					        if (participant != null) {
 | 
				
			||||||
 | 
					          participant.remoteStream = event.streams[0];
 | 
				
			||||||
 | 
					          participant.isConnected = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // Detect video tracks and update video enabled state
 | 
				
			||||||
 | 
					          final videoTracks = event.streams[0].getVideoTracks();
 | 
				
			||||||
 | 
					          if (videoTracks.isNotEmpty) {
 | 
				
			||||||
 | 
					            participant.isVideoEnabled = true;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          _participantController.add(participant);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    peerConnection.onIceCandidate = (candidate) {
 | 
				
			||||||
 | 
					      // CHANGED: Send candidate to the specific participant
 | 
				
			||||||
 | 
					      _signaling.sendIceCandidate(participantId, candidate);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    peerConnection.onConnectionState = (state) {
 | 
				
			||||||
 | 
					      talker.info('[WebRTC] Connection state for $participantId: $state');
 | 
				
			||||||
 | 
					      final participant = _participants[participantId];
 | 
				
			||||||
 | 
					      if (participant != null) {
 | 
				
			||||||
 | 
					        participant.isConnected =
 | 
				
			||||||
 | 
					            state == RTCPeerConnectionState.RTCPeerConnectionStateConnected;
 | 
				
			||||||
 | 
					        _participantController.add(participant);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (isInitiator) {
 | 
				
			||||||
 | 
					      final offer = await peerConnection.createOffer();
 | 
				
			||||||
 | 
					      await peerConnection.setLocalDescription(offer);
 | 
				
			||||||
 | 
					      // CHANGED: Send offer to the specific participant
 | 
				
			||||||
 | 
					      _signaling.sendOffer(participantId, offer);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _handleOffer(
 | 
				
			||||||
 | 
					    String from,
 | 
				
			||||||
 | 
					    SnAccount account,
 | 
				
			||||||
 | 
					    Map<String, dynamic> data,
 | 
				
			||||||
 | 
					  ) async {
 | 
				
			||||||
 | 
					    final participantId = from;
 | 
				
			||||||
 | 
					    talker.info('[WebRTC Manager] Handling offer from $participantId');
 | 
				
			||||||
 | 
					    final offer = RTCSessionDescription(data['sdp'], data['type']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!_peerConnections.containsKey(participantId)) {
 | 
				
			||||||
 | 
					      if (!_participants.containsKey(participantId)) {
 | 
				
			||||||
 | 
					        final participant = WebRTCParticipant(
 | 
				
			||||||
 | 
					          id: participantId,
 | 
				
			||||||
 | 
					          name: participantId,
 | 
				
			||||||
 | 
					          userinfo: account,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        _participants[participantId] = participant;
 | 
				
			||||||
 | 
					        _participantController.add(participant);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      await _createPeerConnection(participantId, isInitiator: false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final peerConnection = _peerConnections[participantId]!;
 | 
				
			||||||
 | 
					    await peerConnection.setRemoteDescription(offer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final answer = await peerConnection.createAnswer();
 | 
				
			||||||
 | 
					    await peerConnection.setLocalDescription(answer);
 | 
				
			||||||
 | 
					    // CHANGED: Send answer to the specific participant
 | 
				
			||||||
 | 
					    _signaling.sendAnswer(participantId, answer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Process any queued ICE candidates
 | 
				
			||||||
 | 
					    final participant = _participants[participantId];
 | 
				
			||||||
 | 
					    if (participant != null) {
 | 
				
			||||||
 | 
					      for (final candidate in participant.remoteCandidates) {
 | 
				
			||||||
 | 
					        await peerConnection.addCandidate(candidate);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      participant.remoteCandidates.clear();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _handleAnswer(String from, Map<String, dynamic> data) async {
 | 
				
			||||||
 | 
					    final participantId = from;
 | 
				
			||||||
 | 
					    talker.info('[WebRTC Manager] Handling answer from $participantId');
 | 
				
			||||||
 | 
					    final answer = RTCSessionDescription(data['sdp'], data['type']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final peerConnection = _peerConnections[participantId];
 | 
				
			||||||
 | 
					    if (peerConnection != null) {
 | 
				
			||||||
 | 
					      await peerConnection.setRemoteDescription(answer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Process any queued ICE candidates
 | 
				
			||||||
 | 
					      final participant = _participants[participantId];
 | 
				
			||||||
 | 
					      if (participant != null) {
 | 
				
			||||||
 | 
					        for (final candidate in participant.remoteCandidates) {
 | 
				
			||||||
 | 
					          await peerConnection.addCandidate(candidate);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        participant.remoteCandidates.clear();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _handleIceCandidate(
 | 
				
			||||||
 | 
					    String from,
 | 
				
			||||||
 | 
					    Map<String, dynamic> data,
 | 
				
			||||||
 | 
					  ) async {
 | 
				
			||||||
 | 
					    final participantId = from;
 | 
				
			||||||
 | 
					    final candidate = RTCIceCandidate(
 | 
				
			||||||
 | 
					      data['candidate'],
 | 
				
			||||||
 | 
					      data['sdpMid'],
 | 
				
			||||||
 | 
					      data['sdpMLineIndex'],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final participant = _participants[participantId];
 | 
				
			||||||
 | 
					    if (participant != null) {
 | 
				
			||||||
 | 
					      final pc = participant.peerConnection;
 | 
				
			||||||
 | 
					      if (pc != null) {
 | 
				
			||||||
 | 
					        await pc.addCandidate(candidate);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        participant.remoteCandidates.add(candidate);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> replaceMediaStream(Map<String, dynamic> constraints) async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final newStream = await navigator.mediaDevices.getUserMedia(constraints);
 | 
				
			||||||
 | 
					      final newVideoTrack = newStream.getVideoTracks().firstOrNull;
 | 
				
			||||||
 | 
					      final newAudioTrack = newStream.getAudioTracks().firstOrNull;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (_localStream != null) {
 | 
				
			||||||
 | 
					        final oldVideoTrack = _localStream!.getVideoTracks().firstOrNull;
 | 
				
			||||||
 | 
					        final oldAudioTrack = _localStream!.getAudioTracks().firstOrNull;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Replace tracks in all existing peer connections
 | 
				
			||||||
 | 
					        for (final pc in _peerConnections.values) {
 | 
				
			||||||
 | 
					          final senders = await pc.getSenders();
 | 
				
			||||||
 | 
					          for (final sender in senders) {
 | 
				
			||||||
 | 
					            if (newVideoTrack != null && sender.track == oldVideoTrack) {
 | 
				
			||||||
 | 
					              await sender.replaceTrack(newVideoTrack);
 | 
				
			||||||
 | 
					            } else if (newAudioTrack != null && sender.track == oldAudioTrack) {
 | 
				
			||||||
 | 
					              await sender.replaceTrack(newAudioTrack);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Stop old tracks and update local stream
 | 
				
			||||||
 | 
					        for (final track in _localStream!.getTracks()) {
 | 
				
			||||||
 | 
					          track.stop();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      _localStream = newStream;
 | 
				
			||||||
 | 
					      talker.info('[WebRTC] Media stream replaced with new constraints');
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      talker.error('[WebRTC] Failed to replace media stream: $e');
 | 
				
			||||||
 | 
					      rethrow;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> toggleMicrophone(bool enabled) async {
 | 
				
			||||||
 | 
					    if (_localStream != null) {
 | 
				
			||||||
 | 
					      final audioTracks = _localStream!.getAudioTracks();
 | 
				
			||||||
 | 
					      for (final track in audioTracks) {
 | 
				
			||||||
 | 
					        track.enabled = enabled;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> toggleCamera(bool enabled) async {
 | 
				
			||||||
 | 
					    if (_localStream != null) {
 | 
				
			||||||
 | 
					      _localStream!.getVideoTracks().forEach((track) {
 | 
				
			||||||
 | 
					        track.enabled = enabled;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> switchCamera(String deviceId) async {
 | 
				
			||||||
 | 
					    await replaceMediaStream({
 | 
				
			||||||
 | 
					      'audio': _localStream?.getAudioTracks().isNotEmpty ?? true,
 | 
				
			||||||
 | 
					      'video': {'deviceId': deviceId},
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    talker.info('[WebRTC] Switched to camera device: $deviceId');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> switchMicrophone(String deviceId) async {
 | 
				
			||||||
 | 
					    await replaceMediaStream({
 | 
				
			||||||
 | 
					      'audio': {'deviceId': deviceId},
 | 
				
			||||||
 | 
					      'video': _localStream?.getVideoTracks().isNotEmpty ?? true,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    talker.info('[WebRTC] Switched to microphone device: $deviceId');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<List<MediaDeviceInfo>> getVideoDevices() async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final devices = await navigator.mediaDevices.enumerateDevices();
 | 
				
			||||||
 | 
					      return devices.where((device) => device.kind == 'videoinput').toList();
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      talker.error('[WebRTC] Failed to enumerate video devices: $e');
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<List<MediaDeviceInfo>> getAudioDevices() async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final devices = await navigator.mediaDevices.enumerateDevices();
 | 
				
			||||||
 | 
					      return devices.where((device) => device.kind == 'audioinput').toList();
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      talker.error('[WebRTC] Failed to enumerate audio devices: $e');
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _startAudioLevelMonitoring() {
 | 
				
			||||||
 | 
					    _audioLevelTimer?.cancel();
 | 
				
			||||||
 | 
					    _audioLevelTimer = Timer.periodic(const Duration(milliseconds: 100), (_) {
 | 
				
			||||||
 | 
					      _updateAudioLevels();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _stopAudioLevelMonitoring() {
 | 
				
			||||||
 | 
					    _audioLevelTimer?.cancel();
 | 
				
			||||||
 | 
					    _audioLevelTimer = null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _updateAudioLevels() async {
 | 
				
			||||||
 | 
					    bool hasUpdates = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (final participant in _participants.values) {
 | 
				
			||||||
 | 
					      if (participant.remoteStream != null && participant.isAudioEnabled) {
 | 
				
			||||||
 | 
					        final audioTracks = participant.remoteStream!.getAudioTracks();
 | 
				
			||||||
 | 
					        if (audioTracks.isNotEmpty) {
 | 
				
			||||||
 | 
					          try {
 | 
				
			||||||
 | 
					            // Try to get stats for more accurate audio level detection
 | 
				
			||||||
 | 
					            final pc = participant.peerConnection;
 | 
				
			||||||
 | 
					            if (pc != null) {
 | 
				
			||||||
 | 
					              final stats = await pc.getStats();
 | 
				
			||||||
 | 
					              double maxAudioLevel = 0.0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              // Look for audio receiver stats
 | 
				
			||||||
 | 
					              for (var report in stats) {
 | 
				
			||||||
 | 
					                if (report.type == 'inbound-rtp' &&
 | 
				
			||||||
 | 
					                    report.values['mediaType'] == 'audio') {
 | 
				
			||||||
 | 
					                  final audioLevel = report.values['audioLevel'] as double?;
 | 
				
			||||||
 | 
					                  if (audioLevel != null && audioLevel > maxAudioLevel) {
 | 
				
			||||||
 | 
					                    maxAudioLevel = audioLevel;
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              // If we got stats, use them; otherwise use a simple heuristic
 | 
				
			||||||
 | 
					              if (maxAudioLevel > 0) {
 | 
				
			||||||
 | 
					                participant.audioLevel = maxAudioLevel.clamp(0.0, 1.0);
 | 
				
			||||||
 | 
					              } else {
 | 
				
			||||||
 | 
					                // Simple heuristic: if audio track is enabled, assume some level
 | 
				
			||||||
 | 
					                // In a real app, you'd analyze the actual audio data
 | 
				
			||||||
 | 
					                participant.audioLevel = audioTracks[0].enabled ? 0.5 : 0.0;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              // Fallback for local participant or when no PC available
 | 
				
			||||||
 | 
					              participant.audioLevel = participant.isLocal ? 0.0 : 0.3;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            hasUpdates = true;
 | 
				
			||||||
 | 
					          } catch (e) {
 | 
				
			||||||
 | 
					            talker.warning('[WebRTC] Failed to update audio level for ${participant.id}: $e');
 | 
				
			||||||
 | 
					            participant.audioLevel = 0.0;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          participant.audioLevel = 0.0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        participant.audioLevel = 0.0;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Notify listeners if there were updates (throttled to avoid excessive updates)
 | 
				
			||||||
 | 
					    if (hasUpdates) {
 | 
				
			||||||
 | 
					      // This will trigger UI updates for speaking indicators
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  List<WebRTCParticipant> get participants => _participants.values.toList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void dispose() {
 | 
				
			||||||
 | 
					    _stopAudioLevelMonitoring();
 | 
				
			||||||
 | 
					    _signaling.disconnect();
 | 
				
			||||||
 | 
					    for (final pc in _peerConnections.values) {
 | 
				
			||||||
 | 
					      pc.close();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    _peerConnections.clear();
 | 
				
			||||||
 | 
					    for (var p in _participants.values) {
 | 
				
			||||||
 | 
					      p.remoteCandidates.clear();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    _participants.clear();
 | 
				
			||||||
 | 
					    _localStream?.dispose();
 | 
				
			||||||
 | 
					    _participantController.close();
 | 
				
			||||||
 | 
					    _participantLeftController.close();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										211
									
								
								lib/pods/chat/webrtc_signaling.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										211
									
								
								lib/pods/chat/webrtc_signaling.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,211 @@
 | 
				
			|||||||
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					import 'dart:convert';
 | 
				
			||||||
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_webrtc/flutter_webrtc.dart';
 | 
				
			||||||
 | 
					import 'package:freezed_annotation/freezed_annotation.dart';
 | 
				
			||||||
 | 
					import 'package:island/models/account.dart';
 | 
				
			||||||
 | 
					import 'package:island/models/chat.dart';
 | 
				
			||||||
 | 
					import 'package:island/pods/config.dart';
 | 
				
			||||||
 | 
					import 'package:island/pods/network.dart';
 | 
				
			||||||
 | 
					import 'package:island/pods/websocket.dart';
 | 
				
			||||||
 | 
					import 'package:web_socket_channel/io.dart';
 | 
				
			||||||
 | 
					import 'package:web_socket_channel/web_socket_channel.dart';
 | 
				
			||||||
 | 
					import 'package:island/talker.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					part 'webrtc_signaling.freezed.dart';
 | 
				
			||||||
 | 
					part 'webrtc_signaling.g.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@freezed
 | 
				
			||||||
 | 
					sealed class SignalingMessage with _$SignalingMessage {
 | 
				
			||||||
 | 
					  const factory SignalingMessage({
 | 
				
			||||||
 | 
					    required String type,
 | 
				
			||||||
 | 
					    // CHANGED: Added 'to' field for directed messaging
 | 
				
			||||||
 | 
					    String? to,
 | 
				
			||||||
 | 
					    required String accountId,
 | 
				
			||||||
 | 
					    required SnAccount account,
 | 
				
			||||||
 | 
					    required Map<String, dynamic> data,
 | 
				
			||||||
 | 
					  }) = _SignalingMessage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory SignalingMessage.fromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
 | 
					      _$SignalingMessageFromJson(json);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@freezed
 | 
				
			||||||
 | 
					sealed class WebRTCWelcomeMessage with _$WebRTCWelcomeMessage {
 | 
				
			||||||
 | 
					  const factory WebRTCWelcomeMessage({
 | 
				
			||||||
 | 
					    required String userId,
 | 
				
			||||||
 | 
					    required String roomId,
 | 
				
			||||||
 | 
					    required String message,
 | 
				
			||||||
 | 
					    required String timestamp,
 | 
				
			||||||
 | 
					    // CHANGED: Added participants list
 | 
				
			||||||
 | 
					    @Default([]) List<CallParticipant> participants,
 | 
				
			||||||
 | 
					  }) = _WebRTCWelcomeMessage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory WebRTCWelcomeMessage.fromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
 | 
					      _$WebRTCWelcomeMessageFromJson(json);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class WebRTCSignaling {
 | 
				
			||||||
 | 
					  final String roomId;
 | 
				
			||||||
 | 
					  late final String userId;
 | 
				
			||||||
 | 
					  late final String userName;
 | 
				
			||||||
 | 
					  late SnAccount user;
 | 
				
			||||||
 | 
					  final StreamController<SignalingMessage> _messageController =
 | 
				
			||||||
 | 
					      StreamController<SignalingMessage>.broadcast();
 | 
				
			||||||
 | 
					  final StreamController<WebRTCWelcomeMessage> _welcomeController =
 | 
				
			||||||
 | 
					      StreamController<WebRTCWelcomeMessage>.broadcast();
 | 
				
			||||||
 | 
					  WebSocketChannel? _channel;
 | 
				
			||||||
 | 
					  Timer? _heartbeatTimer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Stream<SignalingMessage> get messages => _messageController.stream;
 | 
				
			||||||
 | 
					  Stream<WebRTCWelcomeMessage> get welcomeMessages => _welcomeController.stream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  WebRTCSignaling({required this.roomId});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> connect(Ref ref) async {
 | 
				
			||||||
 | 
					    final baseUrl = ref.watch(serverUrlProvider);
 | 
				
			||||||
 | 
					    final token = await getToken(ref.watch(tokenProvider));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final url = '$baseUrl/sphere/chat/realtime/$roomId'.replaceFirst(
 | 
				
			||||||
 | 
					      'http',
 | 
				
			||||||
 | 
					      'ws',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    talker.info('[WebRTC Signaling] Trying connecting to $url');
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      if (kIsWeb) {
 | 
				
			||||||
 | 
					        _channel = WebSocketChannel.connect(Uri.parse('$url?tk=$token'));
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        _channel = IOWebSocketChannel.connect(
 | 
				
			||||||
 | 
					          Uri.parse(url),
 | 
				
			||||||
 | 
					          headers: {'Authorization': 'AtField $token'},
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      await _channel!.ready;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Start heartbeat timer
 | 
				
			||||||
 | 
					      _heartbeatTimer = Timer.periodic(const Duration(seconds: 30), (timer) => _sendHeartbeat());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      _channel!.stream.listen(
 | 
				
			||||||
 | 
					        (data) {
 | 
				
			||||||
 | 
					          final dataStr =
 | 
				
			||||||
 | 
					              data is Uint8List ? utf8.decode(data) : data.toString();
 | 
				
			||||||
 | 
					          final packet = WebSocketPacket.fromJson(jsonDecode(dataStr));
 | 
				
			||||||
 | 
					          talker.info(
 | 
				
			||||||
 | 
					            '[WebRTC Signaling] Recieved a singal message with packet type: ${packet.type}',
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          if (packet.type == 'webrtc') {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					              final welcomeMessage = WebRTCWelcomeMessage.fromJson(
 | 
				
			||||||
 | 
					                packet.data!,
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					              _welcomeController.add(welcomeMessage);
 | 
				
			||||||
 | 
					              talker.info(
 | 
				
			||||||
 | 
					                '[WebRTC Signaling] Welcome message received: ${welcomeMessage.message}',
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            } catch (e) {
 | 
				
			||||||
 | 
					              talker.error(
 | 
				
			||||||
 | 
					                '[WebRTC Signaling] Failed to parse welcome message: $e',
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          } else if (packet.type == 'webrtc.signal') {
 | 
				
			||||||
 | 
					            try {
 | 
				
			||||||
 | 
					              final signalingMessage = SignalingMessage.fromJson(packet.data!);
 | 
				
			||||||
 | 
					              // CHANGED: Ensure we only process messages intended for us if the 'to' field is present
 | 
				
			||||||
 | 
					              if (signalingMessage.to == null ||
 | 
				
			||||||
 | 
					                  signalingMessage.to == userId) {
 | 
				
			||||||
 | 
					                _messageController.add(signalingMessage);
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            } catch (e) {
 | 
				
			||||||
 | 
					              talker.error(
 | 
				
			||||||
 | 
					                '[WebRTC Signaling] Failed to parse signaling message: $e',
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        onError: (error) {
 | 
				
			||||||
 | 
					          talker.error('[WebRTC Signaling] WebSocket error: $error');
 | 
				
			||||||
 | 
					          _messageController.addError(error);
 | 
				
			||||||
 | 
					          _welcomeController.addError(error);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        onDone: () {
 | 
				
			||||||
 | 
					          talker.info('[WebRTC Signaling] WebSocket connection closed');
 | 
				
			||||||
 | 
					          _messageController.close();
 | 
				
			||||||
 | 
					          _welcomeController.close();
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      talker.error('[WebRTC Signaling] Failed to connect: $err');
 | 
				
			||||||
 | 
					      _messageController.addError(err);
 | 
				
			||||||
 | 
					      _welcomeController.addError(err);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void sendMessage(SignalingMessage message) {
 | 
				
			||||||
 | 
					    if (_channel == null) return;
 | 
				
			||||||
 | 
					    talker.info(
 | 
				
			||||||
 | 
					      '[WebRTC Signaling] Sending a message with message type: ${message.type} to ${message.to}',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    final packet = WebSocketPacket(
 | 
				
			||||||
 | 
					      type: 'webrtc.signal',
 | 
				
			||||||
 | 
					      data: message.toJson(),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    _channel!.sink.add(jsonEncode(packet.toJson()));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // CHANGED: All send methods now correctly use the `to` parameter
 | 
				
			||||||
 | 
					  void sendOffer(String to, RTCSessionDescription offer) {
 | 
				
			||||||
 | 
					    sendMessage(
 | 
				
			||||||
 | 
					      SignalingMessage(
 | 
				
			||||||
 | 
					        type: 'offer',
 | 
				
			||||||
 | 
					        to: to,
 | 
				
			||||||
 | 
					        accountId: userId,
 | 
				
			||||||
 | 
					        account: user,
 | 
				
			||||||
 | 
					        data: {'sdp': offer.sdp, 'type': offer.type},
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void sendAnswer(String to, RTCSessionDescription answer) {
 | 
				
			||||||
 | 
					    sendMessage(
 | 
				
			||||||
 | 
					      SignalingMessage(
 | 
				
			||||||
 | 
					        type: 'answer',
 | 
				
			||||||
 | 
					        to: to,
 | 
				
			||||||
 | 
					        accountId: userId,
 | 
				
			||||||
 | 
					        account: user,
 | 
				
			||||||
 | 
					        data: {'sdp': answer.sdp, 'type': answer.type},
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void sendIceCandidate(String to, RTCIceCandidate candidate) {
 | 
				
			||||||
 | 
					    sendMessage(
 | 
				
			||||||
 | 
					      SignalingMessage(
 | 
				
			||||||
 | 
					        type: 'ice-candidate',
 | 
				
			||||||
 | 
					        to: to,
 | 
				
			||||||
 | 
					        accountId: userId,
 | 
				
			||||||
 | 
					        account: user,
 | 
				
			||||||
 | 
					        data: {
 | 
				
			||||||
 | 
					          'candidate': candidate.candidate,
 | 
				
			||||||
 | 
					          'sdpMid': candidate.sdpMid,
 | 
				
			||||||
 | 
					          'sdpMLineIndex': candidate.sdpMLineIndex,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _sendHeartbeat() {
 | 
				
			||||||
 | 
					    if (_channel == null) return;
 | 
				
			||||||
 | 
					    talker.info('[WebRTC Signaling] Sending heartbeat');
 | 
				
			||||||
 | 
					    final packet = WebSocketPacket(type: 'heartbeat', data: null);
 | 
				
			||||||
 | 
					    _channel!.sink.add(jsonEncode(packet.toJson()));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void disconnect() {
 | 
				
			||||||
 | 
					    _heartbeatTimer?.cancel();
 | 
				
			||||||
 | 
					    _channel?.sink.close();
 | 
				
			||||||
 | 
					    _messageController.close();
 | 
				
			||||||
 | 
					    _welcomeController.close();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										611
									
								
								lib/pods/chat/webrtc_signaling.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										611
									
								
								lib/pods/chat/webrtc_signaling.freezed.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,611 @@
 | 
				
			|||||||
 | 
					// GENERATED CODE - DO NOT MODIFY BY HAND
 | 
				
			||||||
 | 
					// coverage:ignore-file
 | 
				
			||||||
 | 
					// ignore_for_file: type=lint
 | 
				
			||||||
 | 
					// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					part of 'webrtc_signaling.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// **************************************************************************
 | 
				
			||||||
 | 
					// FreezedGenerator
 | 
				
			||||||
 | 
					// **************************************************************************
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// dart format off
 | 
				
			||||||
 | 
					T _$identity<T>(T value) => value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					mixin _$SignalingMessage implements DiagnosticableTreeMixin {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 String get type;// CHANGED: Added 'to' field for directed messaging
 | 
				
			||||||
 | 
					 String? get to; String get accountId; SnAccount get account; Map<String, dynamic> get data;
 | 
				
			||||||
 | 
					/// Create a copy of SignalingMessage
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline')
 | 
				
			||||||
 | 
					$SignalingMessageCopyWith<SignalingMessage> get copyWith => _$SignalingMessageCopyWithImpl<SignalingMessage>(this as SignalingMessage, _$identity);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Serializes this SignalingMessage to a JSON map.
 | 
				
			||||||
 | 
					  Map<String, dynamic> toJson();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					void debugFillProperties(DiagnosticPropertiesBuilder properties) {
 | 
				
			||||||
 | 
					  properties
 | 
				
			||||||
 | 
					    ..add(DiagnosticsProperty('type', 'SignalingMessage'))
 | 
				
			||||||
 | 
					    ..add(DiagnosticsProperty('type', type))..add(DiagnosticsProperty('to', to))..add(DiagnosticsProperty('accountId', accountId))..add(DiagnosticsProperty('account', account))..add(DiagnosticsProperty('data', data));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					bool operator ==(Object other) {
 | 
				
			||||||
 | 
					  return identical(this, other) || (other.runtimeType == runtimeType&&other is SignalingMessage&&(identical(other.type, type) || other.type == type)&&(identical(other.to, to) || other.to == to)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&const DeepCollectionEquality().equals(other.data, data));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					int get hashCode => Object.hash(runtimeType,type,to,accountId,account,const DeepCollectionEquality().hash(data));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
 | 
				
			||||||
 | 
					  return 'SignalingMessage(type: $type, to: $to, accountId: $accountId, account: $account, data: $data)';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					abstract mixin class $SignalingMessageCopyWith<$Res>  {
 | 
				
			||||||
 | 
					  factory $SignalingMessageCopyWith(SignalingMessage value, $Res Function(SignalingMessage) _then) = _$SignalingMessageCopyWithImpl;
 | 
				
			||||||
 | 
					@useResult
 | 
				
			||||||
 | 
					$Res call({
 | 
				
			||||||
 | 
					 String type, String? to, String accountId, SnAccount account, Map<String, dynamic> data
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$SnAccountCopyWith<$Res> get account;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					class _$SignalingMessageCopyWithImpl<$Res>
 | 
				
			||||||
 | 
					    implements $SignalingMessageCopyWith<$Res> {
 | 
				
			||||||
 | 
					  _$SignalingMessageCopyWithImpl(this._self, this._then);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final SignalingMessage _self;
 | 
				
			||||||
 | 
					  final $Res Function(SignalingMessage) _then;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create a copy of SignalingMessage
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? to = freezed,Object? accountId = null,Object? account = null,Object? data = null,}) {
 | 
				
			||||||
 | 
					  return _then(_self.copyWith(
 | 
				
			||||||
 | 
					type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,to: freezed == to ? _self.to : to // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,account: null == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as SnAccount,data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as Map<String, dynamic>,
 | 
				
			||||||
 | 
					  ));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// Create a copy of SignalingMessage
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline')
 | 
				
			||||||
 | 
					$SnAccountCopyWith<$Res> get account {
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  return $SnAccountCopyWith<$Res>(_self.account, (value) {
 | 
				
			||||||
 | 
					    return _then(_self.copyWith(account: value));
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Adds pattern-matching-related methods to [SignalingMessage].
 | 
				
			||||||
 | 
					extension SignalingMessagePatterns on SignalingMessage {
 | 
				
			||||||
 | 
					/// A variant of `map` that fallback to returning `orElse`.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// It is equivalent to doing:
 | 
				
			||||||
 | 
					/// ```dart
 | 
				
			||||||
 | 
					/// switch (sealedClass) {
 | 
				
			||||||
 | 
					///   case final Subclass value:
 | 
				
			||||||
 | 
					///     return ...;
 | 
				
			||||||
 | 
					///   case _:
 | 
				
			||||||
 | 
					///     return orElse();
 | 
				
			||||||
 | 
					/// }
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SignalingMessage value)?  $default,{required TResult orElse(),}){
 | 
				
			||||||
 | 
					final _that = this;
 | 
				
			||||||
 | 
					switch (_that) {
 | 
				
			||||||
 | 
					case _SignalingMessage() when $default != null:
 | 
				
			||||||
 | 
					return $default(_that);case _:
 | 
				
			||||||
 | 
					  return orElse();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// A `switch`-like method, using callbacks.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Callbacks receives the raw object, upcasted.
 | 
				
			||||||
 | 
					/// It is equivalent to doing:
 | 
				
			||||||
 | 
					/// ```dart
 | 
				
			||||||
 | 
					/// switch (sealedClass) {
 | 
				
			||||||
 | 
					///   case final Subclass value:
 | 
				
			||||||
 | 
					///     return ...;
 | 
				
			||||||
 | 
					///   case final Subclass2 value:
 | 
				
			||||||
 | 
					///     return ...;
 | 
				
			||||||
 | 
					/// }
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SignalingMessage value)  $default,){
 | 
				
			||||||
 | 
					final _that = this;
 | 
				
			||||||
 | 
					switch (_that) {
 | 
				
			||||||
 | 
					case _SignalingMessage():
 | 
				
			||||||
 | 
					return $default(_that);}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// A variant of `map` that fallback to returning `null`.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// It is equivalent to doing:
 | 
				
			||||||
 | 
					/// ```dart
 | 
				
			||||||
 | 
					/// switch (sealedClass) {
 | 
				
			||||||
 | 
					///   case final Subclass value:
 | 
				
			||||||
 | 
					///     return ...;
 | 
				
			||||||
 | 
					///   case _:
 | 
				
			||||||
 | 
					///     return null;
 | 
				
			||||||
 | 
					/// }
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SignalingMessage value)?  $default,){
 | 
				
			||||||
 | 
					final _that = this;
 | 
				
			||||||
 | 
					switch (_that) {
 | 
				
			||||||
 | 
					case _SignalingMessage() when $default != null:
 | 
				
			||||||
 | 
					return $default(_that);case _:
 | 
				
			||||||
 | 
					  return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// A variant of `when` that fallback to an `orElse` callback.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// It is equivalent to doing:
 | 
				
			||||||
 | 
					/// ```dart
 | 
				
			||||||
 | 
					/// switch (sealedClass) {
 | 
				
			||||||
 | 
					///   case Subclass(:final field):
 | 
				
			||||||
 | 
					///     return ...;
 | 
				
			||||||
 | 
					///   case _:
 | 
				
			||||||
 | 
					///     return orElse();
 | 
				
			||||||
 | 
					/// }
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String type,  String? to,  String accountId,  SnAccount account,  Map<String, dynamic> data)?  $default,{required TResult orElse(),}) {final _that = this;
 | 
				
			||||||
 | 
					switch (_that) {
 | 
				
			||||||
 | 
					case _SignalingMessage() when $default != null:
 | 
				
			||||||
 | 
					return $default(_that.type,_that.to,_that.accountId,_that.account,_that.data);case _:
 | 
				
			||||||
 | 
					  return orElse();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// A `switch`-like method, using callbacks.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// As opposed to `map`, this offers destructuring.
 | 
				
			||||||
 | 
					/// It is equivalent to doing:
 | 
				
			||||||
 | 
					/// ```dart
 | 
				
			||||||
 | 
					/// switch (sealedClass) {
 | 
				
			||||||
 | 
					///   case Subclass(:final field):
 | 
				
			||||||
 | 
					///     return ...;
 | 
				
			||||||
 | 
					///   case Subclass2(:final field2):
 | 
				
			||||||
 | 
					///     return ...;
 | 
				
			||||||
 | 
					/// }
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String type,  String? to,  String accountId,  SnAccount account,  Map<String, dynamic> data)  $default,) {final _that = this;
 | 
				
			||||||
 | 
					switch (_that) {
 | 
				
			||||||
 | 
					case _SignalingMessage():
 | 
				
			||||||
 | 
					return $default(_that.type,_that.to,_that.accountId,_that.account,_that.data);}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// A variant of `when` that fallback to returning `null`
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// It is equivalent to doing:
 | 
				
			||||||
 | 
					/// ```dart
 | 
				
			||||||
 | 
					/// switch (sealedClass) {
 | 
				
			||||||
 | 
					///   case Subclass(:final field):
 | 
				
			||||||
 | 
					///     return ...;
 | 
				
			||||||
 | 
					///   case _:
 | 
				
			||||||
 | 
					///     return null;
 | 
				
			||||||
 | 
					/// }
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String type,  String? to,  String accountId,  SnAccount account,  Map<String, dynamic> data)?  $default,) {final _that = this;
 | 
				
			||||||
 | 
					switch (_that) {
 | 
				
			||||||
 | 
					case _SignalingMessage() when $default != null:
 | 
				
			||||||
 | 
					return $default(_that.type,_that.to,_that.accountId,_that.account,_that.data);case _:
 | 
				
			||||||
 | 
					  return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					@JsonSerializable()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _SignalingMessage with DiagnosticableTreeMixin implements SignalingMessage {
 | 
				
			||||||
 | 
					  const _SignalingMessage({required this.type, this.to, required this.accountId, required this.account, required final  Map<String, dynamic> data}): _data = data;
 | 
				
			||||||
 | 
					  factory _SignalingMessage.fromJson(Map<String, dynamic> json) => _$SignalingMessageFromJson(json);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override final  String type;
 | 
				
			||||||
 | 
					// CHANGED: Added 'to' field for directed messaging
 | 
				
			||||||
 | 
					@override final  String? to;
 | 
				
			||||||
 | 
					@override final  String accountId;
 | 
				
			||||||
 | 
					@override final  SnAccount account;
 | 
				
			||||||
 | 
					 final  Map<String, dynamic> _data;
 | 
				
			||||||
 | 
					@override Map<String, dynamic> get data {
 | 
				
			||||||
 | 
					  if (_data is EqualUnmodifiableMapView) return _data;
 | 
				
			||||||
 | 
					  // ignore: implicit_dynamic_type
 | 
				
			||||||
 | 
					  return EqualUnmodifiableMapView(_data);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create a copy of SignalingMessage
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@override @JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline')
 | 
				
			||||||
 | 
					_$SignalingMessageCopyWith<_SignalingMessage> get copyWith => __$SignalingMessageCopyWithImpl<_SignalingMessage>(this, _$identity);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					Map<String, dynamic> toJson() {
 | 
				
			||||||
 | 
					  return _$SignalingMessageToJson(this, );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					void debugFillProperties(DiagnosticPropertiesBuilder properties) {
 | 
				
			||||||
 | 
					  properties
 | 
				
			||||||
 | 
					    ..add(DiagnosticsProperty('type', 'SignalingMessage'))
 | 
				
			||||||
 | 
					    ..add(DiagnosticsProperty('type', type))..add(DiagnosticsProperty('to', to))..add(DiagnosticsProperty('accountId', accountId))..add(DiagnosticsProperty('account', account))..add(DiagnosticsProperty('data', data));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					bool operator ==(Object other) {
 | 
				
			||||||
 | 
					  return identical(this, other) || (other.runtimeType == runtimeType&&other is _SignalingMessage&&(identical(other.type, type) || other.type == type)&&(identical(other.to, to) || other.to == to)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&const DeepCollectionEquality().equals(other._data, _data));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					int get hashCode => Object.hash(runtimeType,type,to,accountId,account,const DeepCollectionEquality().hash(_data));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
 | 
				
			||||||
 | 
					  return 'SignalingMessage(type: $type, to: $to, accountId: $accountId, account: $account, data: $data)';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					abstract mixin class _$SignalingMessageCopyWith<$Res> implements $SignalingMessageCopyWith<$Res> {
 | 
				
			||||||
 | 
					  factory _$SignalingMessageCopyWith(_SignalingMessage value, $Res Function(_SignalingMessage) _then) = __$SignalingMessageCopyWithImpl;
 | 
				
			||||||
 | 
					@override @useResult
 | 
				
			||||||
 | 
					$Res call({
 | 
				
			||||||
 | 
					 String type, String? to, String accountId, SnAccount account, Map<String, dynamic> data
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override $SnAccountCopyWith<$Res> get account;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					class __$SignalingMessageCopyWithImpl<$Res>
 | 
				
			||||||
 | 
					    implements _$SignalingMessageCopyWith<$Res> {
 | 
				
			||||||
 | 
					  __$SignalingMessageCopyWithImpl(this._self, this._then);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final _SignalingMessage _self;
 | 
				
			||||||
 | 
					  final $Res Function(_SignalingMessage) _then;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create a copy of SignalingMessage
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? to = freezed,Object? accountId = null,Object? account = null,Object? data = null,}) {
 | 
				
			||||||
 | 
					  return _then(_SignalingMessage(
 | 
				
			||||||
 | 
					type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,to: freezed == to ? _self.to : to // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,account: null == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as SnAccount,data: null == data ? _self._data : data // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as Map<String, dynamic>,
 | 
				
			||||||
 | 
					  ));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create a copy of SignalingMessage
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline')
 | 
				
			||||||
 | 
					$SnAccountCopyWith<$Res> get account {
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  return $SnAccountCopyWith<$Res>(_self.account, (value) {
 | 
				
			||||||
 | 
					    return _then(_self.copyWith(account: value));
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					mixin _$WebRTCWelcomeMessage implements DiagnosticableTreeMixin {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 String get userId; String get roomId; String get message; String get timestamp;// CHANGED: Added participants list
 | 
				
			||||||
 | 
					 List<CallParticipant> get participants;
 | 
				
			||||||
 | 
					/// Create a copy of WebRTCWelcomeMessage
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline')
 | 
				
			||||||
 | 
					$WebRTCWelcomeMessageCopyWith<WebRTCWelcomeMessage> get copyWith => _$WebRTCWelcomeMessageCopyWithImpl<WebRTCWelcomeMessage>(this as WebRTCWelcomeMessage, _$identity);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /// Serializes this WebRTCWelcomeMessage to a JSON map.
 | 
				
			||||||
 | 
					  Map<String, dynamic> toJson();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					void debugFillProperties(DiagnosticPropertiesBuilder properties) {
 | 
				
			||||||
 | 
					  properties
 | 
				
			||||||
 | 
					    ..add(DiagnosticsProperty('type', 'WebRTCWelcomeMessage'))
 | 
				
			||||||
 | 
					    ..add(DiagnosticsProperty('userId', userId))..add(DiagnosticsProperty('roomId', roomId))..add(DiagnosticsProperty('message', message))..add(DiagnosticsProperty('timestamp', timestamp))..add(DiagnosticsProperty('participants', participants));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					bool operator ==(Object other) {
 | 
				
			||||||
 | 
					  return identical(this, other) || (other.runtimeType == runtimeType&&other is WebRTCWelcomeMessage&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.roomId, roomId) || other.roomId == roomId)&&(identical(other.message, message) || other.message == message)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&const DeepCollectionEquality().equals(other.participants, participants));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					int get hashCode => Object.hash(runtimeType,userId,roomId,message,timestamp,const DeepCollectionEquality().hash(participants));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
 | 
				
			||||||
 | 
					  return 'WebRTCWelcomeMessage(userId: $userId, roomId: $roomId, message: $message, timestamp: $timestamp, participants: $participants)';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					abstract mixin class $WebRTCWelcomeMessageCopyWith<$Res>  {
 | 
				
			||||||
 | 
					  factory $WebRTCWelcomeMessageCopyWith(WebRTCWelcomeMessage value, $Res Function(WebRTCWelcomeMessage) _then) = _$WebRTCWelcomeMessageCopyWithImpl;
 | 
				
			||||||
 | 
					@useResult
 | 
				
			||||||
 | 
					$Res call({
 | 
				
			||||||
 | 
					 String userId, String roomId, String message, String timestamp, List<CallParticipant> participants
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					class _$WebRTCWelcomeMessageCopyWithImpl<$Res>
 | 
				
			||||||
 | 
					    implements $WebRTCWelcomeMessageCopyWith<$Res> {
 | 
				
			||||||
 | 
					  _$WebRTCWelcomeMessageCopyWithImpl(this._self, this._then);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final WebRTCWelcomeMessage _self;
 | 
				
			||||||
 | 
					  final $Res Function(WebRTCWelcomeMessage) _then;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create a copy of WebRTCWelcomeMessage
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline') @override $Res call({Object? userId = null,Object? roomId = null,Object? message = null,Object? timestamp = null,Object? participants = null,}) {
 | 
				
			||||||
 | 
					  return _then(_self.copyWith(
 | 
				
			||||||
 | 
					userId: null == userId ? _self.userId : userId // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,roomId: null == roomId ? _self.roomId : roomId // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,participants: null == participants ? _self.participants : participants // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as List<CallParticipant>,
 | 
				
			||||||
 | 
					  ));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Adds pattern-matching-related methods to [WebRTCWelcomeMessage].
 | 
				
			||||||
 | 
					extension WebRTCWelcomeMessagePatterns on WebRTCWelcomeMessage {
 | 
				
			||||||
 | 
					/// A variant of `map` that fallback to returning `orElse`.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// It is equivalent to doing:
 | 
				
			||||||
 | 
					/// ```dart
 | 
				
			||||||
 | 
					/// switch (sealedClass) {
 | 
				
			||||||
 | 
					///   case final Subclass value:
 | 
				
			||||||
 | 
					///     return ...;
 | 
				
			||||||
 | 
					///   case _:
 | 
				
			||||||
 | 
					///     return orElse();
 | 
				
			||||||
 | 
					/// }
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _WebRTCWelcomeMessage value)?  $default,{required TResult orElse(),}){
 | 
				
			||||||
 | 
					final _that = this;
 | 
				
			||||||
 | 
					switch (_that) {
 | 
				
			||||||
 | 
					case _WebRTCWelcomeMessage() when $default != null:
 | 
				
			||||||
 | 
					return $default(_that);case _:
 | 
				
			||||||
 | 
					  return orElse();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// A `switch`-like method, using callbacks.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Callbacks receives the raw object, upcasted.
 | 
				
			||||||
 | 
					/// It is equivalent to doing:
 | 
				
			||||||
 | 
					/// ```dart
 | 
				
			||||||
 | 
					/// switch (sealedClass) {
 | 
				
			||||||
 | 
					///   case final Subclass value:
 | 
				
			||||||
 | 
					///     return ...;
 | 
				
			||||||
 | 
					///   case final Subclass2 value:
 | 
				
			||||||
 | 
					///     return ...;
 | 
				
			||||||
 | 
					/// }
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _WebRTCWelcomeMessage value)  $default,){
 | 
				
			||||||
 | 
					final _that = this;
 | 
				
			||||||
 | 
					switch (_that) {
 | 
				
			||||||
 | 
					case _WebRTCWelcomeMessage():
 | 
				
			||||||
 | 
					return $default(_that);}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// A variant of `map` that fallback to returning `null`.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// It is equivalent to doing:
 | 
				
			||||||
 | 
					/// ```dart
 | 
				
			||||||
 | 
					/// switch (sealedClass) {
 | 
				
			||||||
 | 
					///   case final Subclass value:
 | 
				
			||||||
 | 
					///     return ...;
 | 
				
			||||||
 | 
					///   case _:
 | 
				
			||||||
 | 
					///     return null;
 | 
				
			||||||
 | 
					/// }
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _WebRTCWelcomeMessage value)?  $default,){
 | 
				
			||||||
 | 
					final _that = this;
 | 
				
			||||||
 | 
					switch (_that) {
 | 
				
			||||||
 | 
					case _WebRTCWelcomeMessage() when $default != null:
 | 
				
			||||||
 | 
					return $default(_that);case _:
 | 
				
			||||||
 | 
					  return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// A variant of `when` that fallback to an `orElse` callback.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// It is equivalent to doing:
 | 
				
			||||||
 | 
					/// ```dart
 | 
				
			||||||
 | 
					/// switch (sealedClass) {
 | 
				
			||||||
 | 
					///   case Subclass(:final field):
 | 
				
			||||||
 | 
					///     return ...;
 | 
				
			||||||
 | 
					///   case _:
 | 
				
			||||||
 | 
					///     return orElse();
 | 
				
			||||||
 | 
					/// }
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String userId,  String roomId,  String message,  String timestamp,  List<CallParticipant> participants)?  $default,{required TResult orElse(),}) {final _that = this;
 | 
				
			||||||
 | 
					switch (_that) {
 | 
				
			||||||
 | 
					case _WebRTCWelcomeMessage() when $default != null:
 | 
				
			||||||
 | 
					return $default(_that.userId,_that.roomId,_that.message,_that.timestamp,_that.participants);case _:
 | 
				
			||||||
 | 
					  return orElse();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// A `switch`-like method, using callbacks.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// As opposed to `map`, this offers destructuring.
 | 
				
			||||||
 | 
					/// It is equivalent to doing:
 | 
				
			||||||
 | 
					/// ```dart
 | 
				
			||||||
 | 
					/// switch (sealedClass) {
 | 
				
			||||||
 | 
					///   case Subclass(:final field):
 | 
				
			||||||
 | 
					///     return ...;
 | 
				
			||||||
 | 
					///   case Subclass2(:final field2):
 | 
				
			||||||
 | 
					///     return ...;
 | 
				
			||||||
 | 
					/// }
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String userId,  String roomId,  String message,  String timestamp,  List<CallParticipant> participants)  $default,) {final _that = this;
 | 
				
			||||||
 | 
					switch (_that) {
 | 
				
			||||||
 | 
					case _WebRTCWelcomeMessage():
 | 
				
			||||||
 | 
					return $default(_that.userId,_that.roomId,_that.message,_that.timestamp,_that.participants);}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// A variant of `when` that fallback to returning `null`
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// It is equivalent to doing:
 | 
				
			||||||
 | 
					/// ```dart
 | 
				
			||||||
 | 
					/// switch (sealedClass) {
 | 
				
			||||||
 | 
					///   case Subclass(:final field):
 | 
				
			||||||
 | 
					///     return ...;
 | 
				
			||||||
 | 
					///   case _:
 | 
				
			||||||
 | 
					///     return null;
 | 
				
			||||||
 | 
					/// }
 | 
				
			||||||
 | 
					/// ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String userId,  String roomId,  String message,  String timestamp,  List<CallParticipant> participants)?  $default,) {final _that = this;
 | 
				
			||||||
 | 
					switch (_that) {
 | 
				
			||||||
 | 
					case _WebRTCWelcomeMessage() when $default != null:
 | 
				
			||||||
 | 
					return $default(_that.userId,_that.roomId,_that.message,_that.timestamp,_that.participants);case _:
 | 
				
			||||||
 | 
					  return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					@JsonSerializable()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _WebRTCWelcomeMessage with DiagnosticableTreeMixin implements WebRTCWelcomeMessage {
 | 
				
			||||||
 | 
					  const _WebRTCWelcomeMessage({required this.userId, required this.roomId, required this.message, required this.timestamp, final  List<CallParticipant> participants = const []}): _participants = participants;
 | 
				
			||||||
 | 
					  factory _WebRTCWelcomeMessage.fromJson(Map<String, dynamic> json) => _$WebRTCWelcomeMessageFromJson(json);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override final  String userId;
 | 
				
			||||||
 | 
					@override final  String roomId;
 | 
				
			||||||
 | 
					@override final  String message;
 | 
				
			||||||
 | 
					@override final  String timestamp;
 | 
				
			||||||
 | 
					// CHANGED: Added participants list
 | 
				
			||||||
 | 
					 final  List<CallParticipant> _participants;
 | 
				
			||||||
 | 
					// CHANGED: Added participants list
 | 
				
			||||||
 | 
					@override@JsonKey() List<CallParticipant> get participants {
 | 
				
			||||||
 | 
					  if (_participants is EqualUnmodifiableListView) return _participants;
 | 
				
			||||||
 | 
					  // ignore: implicit_dynamic_type
 | 
				
			||||||
 | 
					  return EqualUnmodifiableListView(_participants);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create a copy of WebRTCWelcomeMessage
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@override @JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
 | 
					@pragma('vm:prefer-inline')
 | 
				
			||||||
 | 
					_$WebRTCWelcomeMessageCopyWith<_WebRTCWelcomeMessage> get copyWith => __$WebRTCWelcomeMessageCopyWithImpl<_WebRTCWelcomeMessage>(this, _$identity);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					Map<String, dynamic> toJson() {
 | 
				
			||||||
 | 
					  return _$WebRTCWelcomeMessageToJson(this, );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					void debugFillProperties(DiagnosticPropertiesBuilder properties) {
 | 
				
			||||||
 | 
					  properties
 | 
				
			||||||
 | 
					    ..add(DiagnosticsProperty('type', 'WebRTCWelcomeMessage'))
 | 
				
			||||||
 | 
					    ..add(DiagnosticsProperty('userId', userId))..add(DiagnosticsProperty('roomId', roomId))..add(DiagnosticsProperty('message', message))..add(DiagnosticsProperty('timestamp', timestamp))..add(DiagnosticsProperty('participants', participants));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					bool operator ==(Object other) {
 | 
				
			||||||
 | 
					  return identical(this, other) || (other.runtimeType == runtimeType&&other is _WebRTCWelcomeMessage&&(identical(other.userId, userId) || other.userId == userId)&&(identical(other.roomId, roomId) || other.roomId == roomId)&&(identical(other.message, message) || other.message == message)&&(identical(other.timestamp, timestamp) || other.timestamp == timestamp)&&const DeepCollectionEquality().equals(other._participants, _participants));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					int get hashCode => Object.hash(runtimeType,userId,roomId,message,timestamp,const DeepCollectionEquality().hash(_participants));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
 | 
				
			||||||
 | 
					  return 'WebRTCWelcomeMessage(userId: $userId, roomId: $roomId, message: $message, timestamp: $timestamp, participants: $participants)';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					abstract mixin class _$WebRTCWelcomeMessageCopyWith<$Res> implements $WebRTCWelcomeMessageCopyWith<$Res> {
 | 
				
			||||||
 | 
					  factory _$WebRTCWelcomeMessageCopyWith(_WebRTCWelcomeMessage value, $Res Function(_WebRTCWelcomeMessage) _then) = __$WebRTCWelcomeMessageCopyWithImpl;
 | 
				
			||||||
 | 
					@override @useResult
 | 
				
			||||||
 | 
					$Res call({
 | 
				
			||||||
 | 
					 String userId, String roomId, String message, String timestamp, List<CallParticipant> participants
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// @nodoc
 | 
				
			||||||
 | 
					class __$WebRTCWelcomeMessageCopyWithImpl<$Res>
 | 
				
			||||||
 | 
					    implements _$WebRTCWelcomeMessageCopyWith<$Res> {
 | 
				
			||||||
 | 
					  __$WebRTCWelcomeMessageCopyWithImpl(this._self, this._then);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final _WebRTCWelcomeMessage _self;
 | 
				
			||||||
 | 
					  final $Res Function(_WebRTCWelcomeMessage) _then;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create a copy of WebRTCWelcomeMessage
 | 
				
			||||||
 | 
					/// with the given fields replaced by the non-null parameter values.
 | 
				
			||||||
 | 
					@override @pragma('vm:prefer-inline') $Res call({Object? userId = null,Object? roomId = null,Object? message = null,Object? timestamp = null,Object? participants = null,}) {
 | 
				
			||||||
 | 
					  return _then(_WebRTCWelcomeMessage(
 | 
				
			||||||
 | 
					userId: null == userId ? _self.userId : userId // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,roomId: null == roomId ? _self.roomId : roomId // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,timestamp: null == timestamp ? _self.timestamp : timestamp // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as String,participants: null == participants ? _self._participants : participants // ignore: cast_nullable_to_non_nullable
 | 
				
			||||||
 | 
					as List<CallParticipant>,
 | 
				
			||||||
 | 
					  ));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// dart format on
 | 
				
			||||||
							
								
								
									
										49
									
								
								lib/pods/chat/webrtc_signaling.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								lib/pods/chat/webrtc_signaling.g.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
				
			|||||||
 | 
					// GENERATED CODE - DO NOT MODIFY BY HAND
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					part of 'webrtc_signaling.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// **************************************************************************
 | 
				
			||||||
 | 
					// JsonSerializableGenerator
 | 
				
			||||||
 | 
					// **************************************************************************
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					_SignalingMessage _$SignalingMessageFromJson(Map<String, dynamic> json) =>
 | 
				
			||||||
 | 
					    _SignalingMessage(
 | 
				
			||||||
 | 
					      type: json['type'] as String,
 | 
				
			||||||
 | 
					      to: json['to'] as String?,
 | 
				
			||||||
 | 
					      accountId: json['account_id'] as String,
 | 
				
			||||||
 | 
					      account: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
 | 
				
			||||||
 | 
					      data: json['data'] as Map<String, dynamic>,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Map<String, dynamic> _$SignalingMessageToJson(_SignalingMessage instance) =>
 | 
				
			||||||
 | 
					    <String, dynamic>{
 | 
				
			||||||
 | 
					      'type': instance.type,
 | 
				
			||||||
 | 
					      'to': instance.to,
 | 
				
			||||||
 | 
					      'account_id': instance.accountId,
 | 
				
			||||||
 | 
					      'account': instance.account.toJson(),
 | 
				
			||||||
 | 
					      'data': instance.data,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					_WebRTCWelcomeMessage _$WebRTCWelcomeMessageFromJson(
 | 
				
			||||||
 | 
					  Map<String, dynamic> json,
 | 
				
			||||||
 | 
					) => _WebRTCWelcomeMessage(
 | 
				
			||||||
 | 
					  userId: json['user_id'] as String,
 | 
				
			||||||
 | 
					  roomId: json['room_id'] as String,
 | 
				
			||||||
 | 
					  message: json['message'] as String,
 | 
				
			||||||
 | 
					  timestamp: json['timestamp'] as String,
 | 
				
			||||||
 | 
					  participants:
 | 
				
			||||||
 | 
					      (json['participants'] as List<dynamic>?)
 | 
				
			||||||
 | 
					          ?.map((e) => CallParticipant.fromJson(e as Map<String, dynamic>))
 | 
				
			||||||
 | 
					          .toList() ??
 | 
				
			||||||
 | 
					      const [],
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Map<String, dynamic> _$WebRTCWelcomeMessageToJson(
 | 
				
			||||||
 | 
					  _WebRTCWelcomeMessage instance,
 | 
				
			||||||
 | 
					) => <String, dynamic>{
 | 
				
			||||||
 | 
					  'user_id': instance.userId,
 | 
				
			||||||
 | 
					  'room_id': instance.roomId,
 | 
				
			||||||
 | 
					  'message': instance.message,
 | 
				
			||||||
 | 
					  'timestamp': instance.timestamp,
 | 
				
			||||||
 | 
					  'participants': instance.participants.map((e) => e.toJson()).toList(),
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -100,12 +100,16 @@ class WebSocketService {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        onDone: () {
 | 
					        onDone: () {
 | 
				
			||||||
          talker.info('[WebSocket] Connection closed, attempting to reconnect...');
 | 
					          talker.info(
 | 
				
			||||||
 | 
					            '[WebSocket] Connection closed, attempting to reconnect...',
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
          _scheduleReconnect();
 | 
					          _scheduleReconnect();
 | 
				
			||||||
          _statusStreamController.sink.add(WebSocketState.disconnected());
 | 
					          _statusStreamController.sink.add(WebSocketState.disconnected());
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        onError: (error) {
 | 
					        onError: (error) {
 | 
				
			||||||
          talker.error('[WebSocket] Error occurred: $error, attempting to reconnect...');
 | 
					          talker.error(
 | 
				
			||||||
 | 
					            '[WebSocket] Error occurred: $error, attempting to reconnect...',
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
          _scheduleReconnect();
 | 
					          _scheduleReconnect();
 | 
				
			||||||
          _statusStreamController.sink.add(
 | 
					          _statusStreamController.sink.add(
 | 
				
			||||||
            WebSocketState.error(error.toString()),
 | 
					            WebSocketState.error(error.toString()),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,7 +25,6 @@ import 'package:island/screens/tabs.dart';
 | 
				
			|||||||
import 'package:island/screens/explore.dart';
 | 
					import 'package:island/screens/explore.dart';
 | 
				
			||||||
import 'package:island/screens/discovery/article_detail.dart';
 | 
					import 'package:island/screens/discovery/article_detail.dart';
 | 
				
			||||||
import 'package:island/screens/account.dart';
 | 
					import 'package:island/screens/account.dart';
 | 
				
			||||||
import 'package:island/screens/notification.dart';
 | 
					 | 
				
			||||||
import 'package:island/screens/wallet.dart';
 | 
					import 'package:island/screens/wallet.dart';
 | 
				
			||||||
import 'package:island/screens/account/relationship.dart';
 | 
					import 'package:island/screens/account/relationship.dart';
 | 
				
			||||||
import 'package:island/screens/account/profile.dart';
 | 
					import 'package:island/screens/account/profile.dart';
 | 
				
			||||||
@@ -392,11 +391,6 @@ final routerProvider = Provider<GoRouter>((ref) {
 | 
				
			|||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                    ],
 | 
					                    ],
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  GoRoute(
 | 
					 | 
				
			||||||
                    name: 'notifications',
 | 
					 | 
				
			||||||
                    path: '/account/notifications',
 | 
					 | 
				
			||||||
                    builder: (context, state) => const NotificationScreen(),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                  GoRoute(
 | 
					                  GoRoute(
 | 
				
			||||||
                    name: 'wallet',
 | 
					                    name: 'wallet',
 | 
				
			||||||
                    path: '/account/wallet',
 | 
					                    path: '/account/wallet',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -285,7 +285,8 @@ class AccountScreen extends HookConsumerWidget {
 | 
				
			|||||||
                showModalBottomSheet(
 | 
					                showModalBottomSheet(
 | 
				
			||||||
                  context: context,
 | 
					                  context: context,
 | 
				
			||||||
                  isScrollControlled: true,
 | 
					                  isScrollControlled: true,
 | 
				
			||||||
                  builder: (context) => const NotificationScreen(),
 | 
					                  useRootNavigator: true,
 | 
				
			||||||
 | 
					                  builder: (context) => const NotificationSheet(),
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
              },
 | 
					              },
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/material.dart' hide ConnectionState;
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
import 'package:gap/gap.dart';
 | 
					import 'package:gap/gap.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
@@ -9,8 +9,6 @@ import 'package:island/widgets/app_scaffold.dart';
 | 
				
			|||||||
import 'package:island/widgets/chat/call_button.dart';
 | 
					import 'package:island/widgets/chat/call_button.dart';
 | 
				
			||||||
import 'package:island/widgets/chat/call_overlay.dart';
 | 
					import 'package:island/widgets/chat/call_overlay.dart';
 | 
				
			||||||
import 'package:island/widgets/chat/call_participant_tile.dart';
 | 
					import 'package:island/widgets/chat/call_participant_tile.dart';
 | 
				
			||||||
import 'package:island/widgets/alert.dart';
 | 
					 | 
				
			||||||
import 'package:livekit_client/livekit_client.dart';
 | 
					 | 
				
			||||||
import 'package:material_symbols_icons/symbols.dart';
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -26,32 +24,13 @@ class CallScreen extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    useEffect(() {
 | 
					    useEffect(() {
 | 
				
			||||||
      talker.info('[Call] Joining the call...');
 | 
					      talker.info('[Call] Joining the call...');
 | 
				
			||||||
      callNotifier.joinRoom(roomId).catchError((_) {
 | 
					      Future(() {
 | 
				
			||||||
        showConfirmAlert(
 | 
					        callNotifier.joinRoom(roomId);
 | 
				
			||||||
          'Seems there already has a call connected, do you want override it?',
 | 
					 | 
				
			||||||
          'Call already connected',
 | 
					 | 
				
			||||||
        ).then((value) {
 | 
					 | 
				
			||||||
          if (value != true) return;
 | 
					 | 
				
			||||||
          talker.info('[Call] Joining the call... with overrides');
 | 
					 | 
				
			||||||
          callNotifier.disconnect();
 | 
					 | 
				
			||||||
          callNotifier.dispose();
 | 
					 | 
				
			||||||
          callNotifier.joinRoom(roomId);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
    }, []);
 | 
					    }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final allAudioOnly = callNotifier.participants.every(
 | 
					    final allAudioOnly = callNotifier.participants.every((p) => !p.hasVideo);
 | 
				
			||||||
      (p) =>
 | 
					 | 
				
			||||||
          !(p.hasVideo &&
 | 
					 | 
				
			||||||
              p.remoteParticipant.trackPublications.values.any(
 | 
					 | 
				
			||||||
                (pub) =>
 | 
					 | 
				
			||||||
                    pub.track != null &&
 | 
					 | 
				
			||||||
                    pub.kind == TrackType.VIDEO &&
 | 
					 | 
				
			||||||
                    !pub.muted &&
 | 
					 | 
				
			||||||
                    !pub.isDisposed,
 | 
					 | 
				
			||||||
              )),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AppScaffold(
 | 
					    return AppScaffold(
 | 
				
			||||||
      isNoBackground: false,
 | 
					      isNoBackground: false,
 | 
				
			||||||
@@ -67,12 +46,7 @@ class CallScreen extends HookConsumerWidget {
 | 
				
			|||||||
            Text(
 | 
					            Text(
 | 
				
			||||||
              callState.isConnected
 | 
					              callState.isConnected
 | 
				
			||||||
                  ? formatDuration(callState.duration)
 | 
					                  ? formatDuration(callState.duration)
 | 
				
			||||||
                  : (switch (callNotifier.room?.connectionState) {
 | 
					                  : 'connecting'.tr(),
 | 
				
			||||||
                    ConnectionState.connected => 'connected',
 | 
					 | 
				
			||||||
                    ConnectionState.connecting => 'connecting',
 | 
					 | 
				
			||||||
                    ConnectionState.reconnecting => 'reconnecting',
 | 
					 | 
				
			||||||
                    _ => 'disconnected',
 | 
					 | 
				
			||||||
                  }).tr(),
 | 
					 | 
				
			||||||
              style: const TextStyle(fontSize: 14),
 | 
					              style: const TextStyle(fontSize: 14),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
@@ -159,19 +133,7 @@ class CallScreen extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                        // Stage view: show main speaker(s) large, others in row
 | 
					                        // Stage view: show main speaker(s) large, others in row
 | 
				
			||||||
                        final mainSpeakers =
 | 
					                        final mainSpeakers =
 | 
				
			||||||
                            participants
 | 
					                            participants.where((p) => p.hasVideo).toList();
 | 
				
			||||||
                                .where(
 | 
					 | 
				
			||||||
                                  (p) => p
 | 
					 | 
				
			||||||
                                      .remoteParticipant
 | 
					 | 
				
			||||||
                                      .trackPublications
 | 
					 | 
				
			||||||
                                      .values
 | 
					 | 
				
			||||||
                                      .any(
 | 
					 | 
				
			||||||
                                        (pub) =>
 | 
					 | 
				
			||||||
                                            pub.track != null &&
 | 
					 | 
				
			||||||
                                            pub.kind == TrackType.VIDEO,
 | 
					 | 
				
			||||||
                                      ),
 | 
					 | 
				
			||||||
                                )
 | 
					 | 
				
			||||||
                                .toList();
 | 
					 | 
				
			||||||
                        if (mainSpeakers.isEmpty && participants.isNotEmpty) {
 | 
					                        if (mainSpeakers.isEmpty && participants.isNotEmpty) {
 | 
				
			||||||
                          mainSpeakers.add(participants.first);
 | 
					                          mainSpeakers.add(participants.first);
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -57,7 +57,8 @@ Widget notificationIndicatorWidget(
 | 
				
			|||||||
      showModalBottomSheet(
 | 
					      showModalBottomSheet(
 | 
				
			||||||
        context: context,
 | 
					        context: context,
 | 
				
			||||||
        isScrollControlled: true,
 | 
					        isScrollControlled: true,
 | 
				
			||||||
        builder: (context) => const NotificationScreen(),
 | 
					        useRootNavigator: true,
 | 
				
			||||||
 | 
					        builder: (context) => const NotificationSheet(),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  ),
 | 
					  ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -112,8 +112,8 @@ class NotificationListNotifier extends _$NotificationListNotifier
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NotificationScreen extends HookConsumerWidget {
 | 
					class NotificationSheet extends HookConsumerWidget {
 | 
				
			||||||
  const NotificationScreen({super.key});
 | 
					  const NotificationSheet({super.key});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -374,10 +374,10 @@ class PageBackButton extends StatelessWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    final isDesktop =
 | 
					    final hasPageAction =
 | 
				
			||||||
        !kIsWeb && (Platform.isMacOS || Platform.isLinux || Platform.isWindows);
 | 
					        !kIsWeb && Platform.isMacOS;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (isDesktop && isWideScreen(context)) return const SizedBox.shrink();
 | 
					    if (hasPageAction && isWideScreen(context)) return const SizedBox.shrink();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return IconButton(
 | 
					    return IconButton(
 | 
				
			||||||
      onPressed: () {
 | 
					      onPressed: () {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,7 @@ Future<SnRealtimeCall?> ongoingCall(Ref ref, String roomId) async {
 | 
				
			|||||||
  if (roomId.isEmpty) return null;
 | 
					  if (roomId.isEmpty) return null;
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    final apiClient = ref.watch(apiClientProvider);
 | 
					    final apiClient = ref.watch(apiClientProvider);
 | 
				
			||||||
    final resp = await apiClient.get('/sphere/chat/realtime/$roomId');
 | 
					    final resp = await apiClient.get('/sphere/chat/realtime/$roomId/status');
 | 
				
			||||||
    return SnRealtimeCall.fromJson(resp.data);
 | 
					    return SnRealtimeCall.fromJson(resp.data);
 | 
				
			||||||
  } catch (e) {
 | 
					  } catch (e) {
 | 
				
			||||||
    if (e is DioException && e.response?.statusCode == 404) {
 | 
					    if (e is DioException && e.response?.statusCode == 404) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@ part of 'call_button.dart';
 | 
				
			|||||||
// RiverpodGenerator
 | 
					// RiverpodGenerator
 | 
				
			||||||
// **************************************************************************
 | 
					// **************************************************************************
 | 
				
			||||||
 | 
					
 | 
				
			||||||
String _$ongoingCallHash() => r'48031badb79efa07aefb3a4fc51635be457bd3f9';
 | 
					String _$ongoingCallHash() => r'0f14b36393276720a06190cab3dc8d5e4c88cd57';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Copied from Dart SDK
 | 
					/// Copied from Dart SDK
 | 
				
			||||||
class _SystemHash {
 | 
					class _SystemHash {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,7 @@ import 'package:island/widgets/chat/call_participant_tile.dart';
 | 
				
			|||||||
import 'package:island/widgets/content/sheet.dart';
 | 
					import 'package:island/widgets/content/sheet.dart';
 | 
				
			||||||
import 'package:material_symbols_icons/symbols.dart';
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
import 'package:livekit_client/livekit_client.dart';
 | 
					import 'package:flutter_webrtc/flutter_webrtc.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CallControlsBar extends HookConsumerWidget {
 | 
					class CallControlsBar extends HookConsumerWidget {
 | 
				
			||||||
  const CallControlsBar({super.key});
 | 
					  const CallControlsBar({super.key});
 | 
				
			||||||
@@ -194,9 +194,16 @@ class CallControlsBar extends HookConsumerWidget {
 | 
				
			|||||||
    String deviceType,
 | 
					    String deviceType,
 | 
				
			||||||
  ) async {
 | 
					  ) async {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      final devices = await Hardware.instance.enumerateDevices(
 | 
					      final devices = await navigator.mediaDevices.enumerateDevices();
 | 
				
			||||||
        type: deviceType,
 | 
					      final filteredDevices =
 | 
				
			||||||
      );
 | 
					          devices.where((device) {
 | 
				
			||||||
 | 
					            if (deviceType == 'videoinput') {
 | 
				
			||||||
 | 
					              return device.kind == 'videoinput';
 | 
				
			||||||
 | 
					            } else if (deviceType == 'audioinput') {
 | 
				
			||||||
 | 
					              return device.kind == 'audioinput';
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					          }).toList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!context.mounted) return;
 | 
					      if (!context.mounted) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -209,9 +216,9 @@ class CallControlsBar extends HookConsumerWidget {
 | 
				
			|||||||
                    ? 'selectCamera'.tr()
 | 
					                    ? 'selectCamera'.tr()
 | 
				
			||||||
                    : 'selectMicrophone'.tr(),
 | 
					                    : 'selectMicrophone'.tr(),
 | 
				
			||||||
            child: ListView.builder(
 | 
					            child: ListView.builder(
 | 
				
			||||||
              itemCount: devices.length,
 | 
					              itemCount: filteredDevices.length,
 | 
				
			||||||
              itemBuilder: (context, index) {
 | 
					              itemBuilder: (context, index) {
 | 
				
			||||||
                final device = devices[index];
 | 
					                final device = filteredDevices[index];
 | 
				
			||||||
                return ListTile(
 | 
					                return ListTile(
 | 
				
			||||||
                  title: Text(
 | 
					                  title: Text(
 | 
				
			||||||
                    device.label.isNotEmpty
 | 
					                    device.label.isNotEmpty
 | 
				
			||||||
@@ -236,33 +243,17 @@ class CallControlsBar extends HookConsumerWidget {
 | 
				
			|||||||
  Future<void> _switchDevice(
 | 
					  Future<void> _switchDevice(
 | 
				
			||||||
    BuildContext context,
 | 
					    BuildContext context,
 | 
				
			||||||
    WidgetRef ref,
 | 
					    WidgetRef ref,
 | 
				
			||||||
    MediaDevice device,
 | 
					    MediaDeviceInfo device,
 | 
				
			||||||
    String deviceType,
 | 
					    String deviceType,
 | 
				
			||||||
  ) async {
 | 
					  ) async {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      final callNotifier = ref.read(callNotifierProvider.notifier);
 | 
					      final callNotifier = ref.read(callNotifierProvider.notifier);
 | 
				
			||||||
 | 
					      if (callNotifier.webrtcManager == null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (deviceType == 'videoinput') {
 | 
					      if (deviceType == 'videoinput') {
 | 
				
			||||||
        // Switch camera device
 | 
					        await callNotifier.webrtcManager!.switchCamera(device.deviceId);
 | 
				
			||||||
        final localParticipant = callNotifier.room?.localParticipant;
 | 
					 | 
				
			||||||
        final videoTrack =
 | 
					 | 
				
			||||||
            localParticipant?.videoTrackPublications.firstOrNull?.track;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (videoTrack is LocalVideoTrack) {
 | 
					 | 
				
			||||||
          await videoTrack.switchCamera(device.deviceId);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      } else if (deviceType == 'audioinput') {
 | 
					      } else if (deviceType == 'audioinput') {
 | 
				
			||||||
        // Switch microphone device
 | 
					        await callNotifier.webrtcManager!.switchMicrophone(device.deviceId);
 | 
				
			||||||
        final localParticipant = callNotifier.room?.localParticipant;
 | 
					 | 
				
			||||||
        final audioTrack =
 | 
					 | 
				
			||||||
            localParticipant?.audioTrackPublications.firstOrNull?.track;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (audioTrack is LocalAudioTrack) {
 | 
					 | 
				
			||||||
          // For audio devices, we need to restart the track with new device
 | 
					 | 
				
			||||||
          await audioTrack.restartTrack(
 | 
					 | 
				
			||||||
            AudioCaptureOptions(deviceId: device.deviceId),
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (context.mounted) {
 | 
					      if (context.mounted) {
 | 
				
			||||||
@@ -289,31 +280,9 @@ class CallOverlayBar extends HookConsumerWidget {
 | 
				
			|||||||
    if (!callState.isConnected) return const SizedBox.shrink();
 | 
					    if (!callState.isConnected) return const SizedBox.shrink();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final lastSpeaker =
 | 
					    final lastSpeaker =
 | 
				
			||||||
        callNotifier.participants
 | 
					        callNotifier.participants.isNotEmpty
 | 
				
			||||||
                .where(
 | 
					 | 
				
			||||||
                  (element) => element.remoteParticipant.lastSpokeAt != null,
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .isEmpty
 | 
					 | 
				
			||||||
            ? callNotifier.participants.first
 | 
					            ? callNotifier.participants.first
 | 
				
			||||||
            : callNotifier.participants
 | 
					            : null;
 | 
				
			||||||
                .where(
 | 
					 | 
				
			||||||
                  (element) => element.remoteParticipant.lastSpokeAt != null,
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .fold(
 | 
					 | 
				
			||||||
                  callNotifier.participants.first,
 | 
					 | 
				
			||||||
                  (value, element) =>
 | 
					 | 
				
			||||||
                      element.remoteParticipant.lastSpokeAt != null &&
 | 
					 | 
				
			||||||
                              (value.remoteParticipant.lastSpokeAt == null ||
 | 
					 | 
				
			||||||
                                  element.remoteParticipant.lastSpokeAt!
 | 
					 | 
				
			||||||
                                          .compareTo(
 | 
					 | 
				
			||||||
                                            value
 | 
					 | 
				
			||||||
                                                .remoteParticipant
 | 
					 | 
				
			||||||
                                                .lastSpokeAt!,
 | 
					 | 
				
			||||||
                                          ) >
 | 
					 | 
				
			||||||
                                      0)
 | 
					 | 
				
			||||||
                          ? element
 | 
					 | 
				
			||||||
                          : value,
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final actionButtonStyle = ButtonStyle(
 | 
					    final actionButtonStyle = ButtonStyle(
 | 
				
			||||||
      minimumSize: const MaterialStatePropertyAll(Size(24, 24)),
 | 
					      minimumSize: const MaterialStatePropertyAll(Size(24, 24)),
 | 
				
			||||||
@@ -330,17 +299,16 @@ class CallOverlayBar extends HookConsumerWidget {
 | 
				
			|||||||
                children: [
 | 
					                children: [
 | 
				
			||||||
                  Builder(
 | 
					                  Builder(
 | 
				
			||||||
                    builder: (context) {
 | 
					                    builder: (context) {
 | 
				
			||||||
                      if (callNotifier.localParticipant == null) {
 | 
					                      if (lastSpeaker == null) {
 | 
				
			||||||
                        return CircularProgressIndicator().center();
 | 
					                        return const CircularProgressIndicator();
 | 
				
			||||||
                      }
 | 
					                      }
 | 
				
			||||||
                      return SizedBox(
 | 
					                      return SizedBox(
 | 
				
			||||||
                        width: 40,
 | 
					                        width: 40,
 | 
				
			||||||
                        height: 40,
 | 
					                        height: 40,
 | 
				
			||||||
                        child:
 | 
					                        child: SpeakingRippleAvatar(
 | 
				
			||||||
                            SpeakingRippleAvatar(
 | 
					                          live: lastSpeaker,
 | 
				
			||||||
                              live: lastSpeaker,
 | 
					                          size: 36,
 | 
				
			||||||
                              size: 36,
 | 
					                        ),
 | 
				
			||||||
                            ).center(),
 | 
					 | 
				
			||||||
                      );
 | 
					                      );
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
@@ -348,7 +316,9 @@ class CallOverlayBar extends HookConsumerWidget {
 | 
				
			|||||||
                  Column(
 | 
					                  Column(
 | 
				
			||||||
                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
					                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
                    children: [
 | 
					                    children: [
 | 
				
			||||||
                      Text('@${lastSpeaker.participant.identity}').bold(),
 | 
					                      Text(
 | 
				
			||||||
 | 
					                        '@${lastSpeaker?.participant.identity ?? 'Unknown'}',
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
                      Text(
 | 
					                      Text(
 | 
				
			||||||
                        formatDuration(callState.duration),
 | 
					                        formatDuration(callState.duration),
 | 
				
			||||||
                        style: Theme.of(context).textTheme.bodySmall,
 | 
					                        style: Theme.of(context).textTheme.bodySmall,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,6 @@ import 'package:gap/gap.dart';
 | 
				
			|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:island/pods/chat/call.dart';
 | 
					import 'package:island/pods/chat/call.dart';
 | 
				
			||||||
import 'package:island/widgets/account/account_nameplate.dart';
 | 
					import 'package:island/widgets/account/account_nameplate.dart';
 | 
				
			||||||
import 'package:livekit_client/livekit_client.dart';
 | 
					 | 
				
			||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
 | 
					import 'package:material_symbols_icons/material_symbols_icons.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -66,19 +65,17 @@ class CallParticipantCard extends HookConsumerWidget {
 | 
				
			|||||||
                  children: [
 | 
					                  children: [
 | 
				
			||||||
                    const Icon(Symbols.wifi, size: 16),
 | 
					                    const Icon(Symbols.wifi, size: 16),
 | 
				
			||||||
                    const Gap(8),
 | 
					                    const Gap(8),
 | 
				
			||||||
                    Text(switch (live.remoteParticipant.connectionQuality) {
 | 
					                    Text(
 | 
				
			||||||
                      ConnectionQuality.excellent => 'Excellent',
 | 
					                      live.remoteParticipant.isConnected
 | 
				
			||||||
                      ConnectionQuality.good => 'Good',
 | 
					                          ? 'Connected'
 | 
				
			||||||
                      ConnectionQuality.poor => 'Bad',
 | 
					                          : 'Connecting',
 | 
				
			||||||
                      ConnectionQuality.lost => 'Lost',
 | 
					                    ),
 | 
				
			||||||
                      _ => 'Connecting',
 | 
					 | 
				
			||||||
                    }),
 | 
					 | 
				
			||||||
                  ],
 | 
					                  ],
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ).padding(horizontal: 20, top: 16),
 | 
					            ).padding(horizontal: 20, top: 16),
 | 
				
			||||||
            AccountNameplate(
 | 
					            AccountNameplate(
 | 
				
			||||||
              name: live.participant.identity,
 | 
					              name: live.remoteParticipant.userinfo.name,
 | 
				
			||||||
              isOutlined: false,
 | 
					              isOutlined: false,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +1,9 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:island/pods/chat/call.dart';
 | 
					import 'package:island/pods/chat/call.dart';
 | 
				
			||||||
import 'package:island/screens/account/profile.dart';
 | 
					 | 
				
			||||||
import 'package:island/widgets/chat/call_participant_card.dart';
 | 
					import 'package:island/widgets/chat/call_participant_card.dart';
 | 
				
			||||||
import 'package:island/widgets/content/cloud_files.dart';
 | 
					import 'package:island/widgets/content/cloud_files.dart';
 | 
				
			||||||
import 'package:livekit_client/livekit_client.dart';
 | 
					import 'package:flutter_webrtc/flutter_webrtc.dart';
 | 
				
			||||||
import 'package:material_symbols_icons/symbols.dart';
 | 
					import 'package:material_symbols_icons/symbols.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -16,10 +15,8 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    final account = ref.watch(accountProvider(live.participant.identity));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final avatarRadius = size / 2;
 | 
					    final avatarRadius = size / 2;
 | 
				
			||||||
    final clampedLevel = live.remoteParticipant.audioLevel.clamp(0.0, 1.0);
 | 
					    final clampedLevel = live.audioLevel.clamp(0.0, 1.0);
 | 
				
			||||||
    final rippleRadius = avatarRadius + clampedLevel * (size * 0.333);
 | 
					    final rippleRadius = avatarRadius + clampedLevel * (size * 0.333);
 | 
				
			||||||
    return SizedBox(
 | 
					    return SizedBox(
 | 
				
			||||||
      width: size + 8,
 | 
					      width: size + 8,
 | 
				
			||||||
@@ -27,7 +24,7 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
 | 
				
			|||||||
      child: TweenAnimationBuilder<double>(
 | 
					      child: TweenAnimationBuilder<double>(
 | 
				
			||||||
        tween: Tween<double>(
 | 
					        tween: Tween<double>(
 | 
				
			||||||
          begin: avatarRadius,
 | 
					          begin: avatarRadius,
 | 
				
			||||||
          end: live.remoteParticipant.isSpeaking ? rippleRadius : avatarRadius,
 | 
					          end: live.isSpeaking ? rippleRadius : avatarRadius,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        duration: const Duration(milliseconds: 250),
 | 
					        duration: const Duration(milliseconds: 250),
 | 
				
			||||||
        curve: Curves.easeOut,
 | 
					        curve: Curves.easeOut,
 | 
				
			||||||
@@ -35,7 +32,7 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
 | 
				
			|||||||
          return Stack(
 | 
					          return Stack(
 | 
				
			||||||
            alignment: Alignment.center,
 | 
					            alignment: Alignment.center,
 | 
				
			||||||
            children: [
 | 
					            children: [
 | 
				
			||||||
              if (live.remoteParticipant.isSpeaking)
 | 
					              if (live.isSpeaking)
 | 
				
			||||||
                Container(
 | 
					                Container(
 | 
				
			||||||
                  width: animatedRadius * 2,
 | 
					                  width: animatedRadius * 2,
 | 
				
			||||||
                  height: animatedRadius * 2,
 | 
					                  height: animatedRadius * 2,
 | 
				
			||||||
@@ -49,28 +46,15 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
 | 
				
			|||||||
                height: size,
 | 
					                height: size,
 | 
				
			||||||
                alignment: Alignment.center,
 | 
					                alignment: Alignment.center,
 | 
				
			||||||
                decoration: BoxDecoration(shape: BoxShape.circle),
 | 
					                decoration: BoxDecoration(shape: BoxShape.circle),
 | 
				
			||||||
                child: account.when(
 | 
					                child: CallParticipantGestureDetector(
 | 
				
			||||||
                  data:
 | 
					                  participant: live,
 | 
				
			||||||
                      (value) => CallParticipantGestureDetector(
 | 
					                  child: ProfilePictureWidget(
 | 
				
			||||||
                        participant: live,
 | 
					                    file: live.remoteParticipant.userinfo.profile.picture,
 | 
				
			||||||
                        child: ProfilePictureWidget(
 | 
					                    radius: size / 2,
 | 
				
			||||||
                          file: value.profile.picture,
 | 
					                  ),
 | 
				
			||||||
                          radius: size / 2,
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
                  error:
 | 
					 | 
				
			||||||
                      (_, _) => CircleAvatar(
 | 
					 | 
				
			||||||
                        radius: size / 2,
 | 
					 | 
				
			||||||
                        child: const Icon(Symbols.person_remove),
 | 
					 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
                  loading:
 | 
					 | 
				
			||||||
                      () => CircleAvatar(
 | 
					 | 
				
			||||||
                        radius: size / 2,
 | 
					 | 
				
			||||||
                        child: CircularProgressIndicator(),
 | 
					 | 
				
			||||||
                      ),
 | 
					 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              if (live.remoteParticipant.isMuted)
 | 
					              if (live.isMuted)
 | 
				
			||||||
                Positioned(
 | 
					                Positioned(
 | 
				
			||||||
                  bottom: 4,
 | 
					                  bottom: 4,
 | 
				
			||||||
                  right: 4,
 | 
					                  right: 4,
 | 
				
			||||||
@@ -96,40 +80,65 @@ class SpeakingRippleAvatar extends HookConsumerWidget {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CallParticipantTile extends HookConsumerWidget {
 | 
					class CallParticipantTile extends StatefulWidget {
 | 
				
			||||||
  final CallParticipantLive live;
 | 
					  final CallParticipantLive live;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const CallParticipantTile({super.key, required this.live});
 | 
					  const CallParticipantTile({super.key, required this.live});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  State<CallParticipantTile> createState() => _CallParticipantTileState();
 | 
				
			||||||
    final hasVideo =
 | 
					}
 | 
				
			||||||
        live.hasVideo &&
 | 
					 | 
				
			||||||
        live.remoteParticipant.trackPublications.values
 | 
					 | 
				
			||||||
            .where((pub) => pub.track != null && pub.kind == TrackType.VIDEO)
 | 
					 | 
				
			||||||
            .isNotEmpty;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (hasVideo) {
 | 
					class _CallParticipantTileState extends State<CallParticipantTile> {
 | 
				
			||||||
 | 
					  RTCVideoRenderer? _renderer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void initState() {
 | 
				
			||||||
 | 
					    super.initState();
 | 
				
			||||||
 | 
					    _initRenderer();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void didUpdateWidget(CallParticipantTile oldWidget) {
 | 
				
			||||||
 | 
					    super.didUpdateWidget(oldWidget);
 | 
				
			||||||
 | 
					    // Update renderer source when the stream changes
 | 
				
			||||||
 | 
					    if (_renderer != null &&
 | 
				
			||||||
 | 
					        widget.live.remoteParticipant.remoteStream !=
 | 
				
			||||||
 | 
					            oldWidget.live.remoteParticipant.remoteStream) {
 | 
				
			||||||
 | 
					      _renderer!.srcObject = widget.live.remoteParticipant.remoteStream;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _initRenderer() async {
 | 
				
			||||||
 | 
					    _renderer = RTCVideoRenderer();
 | 
				
			||||||
 | 
					    await _renderer!.initialize();
 | 
				
			||||||
 | 
					    _renderer!.srcObject = widget.live.remoteParticipant.remoteStream;
 | 
				
			||||||
 | 
					    if (mounted) {
 | 
				
			||||||
 | 
					      setState(() {});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void dispose() {
 | 
				
			||||||
 | 
					    _renderer?.dispose();
 | 
				
			||||||
 | 
					    super.dispose();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
 | 
					    if (widget.live.hasVideo &&
 | 
				
			||||||
 | 
					        widget.live.remoteParticipant.remoteStream != null &&
 | 
				
			||||||
 | 
					        _renderer != null) {
 | 
				
			||||||
      return Stack(
 | 
					      return Stack(
 | 
				
			||||||
        fit: StackFit.loose,
 | 
					        fit: StackFit.loose,
 | 
				
			||||||
        children: [
 | 
					        children: [
 | 
				
			||||||
          AspectRatio(
 | 
					          AspectRatio(aspectRatio: 16 / 9, child: RTCVideoView(_renderer!)),
 | 
				
			||||||
            aspectRatio: 16 / 9,
 | 
					 | 
				
			||||||
            child: VideoTrackRenderer(
 | 
					 | 
				
			||||||
              live.remoteParticipant.trackPublications.values
 | 
					 | 
				
			||||||
                      .where((track) => track.kind == TrackType.VIDEO)
 | 
					 | 
				
			||||||
                      .first
 | 
					 | 
				
			||||||
                      .track
 | 
					 | 
				
			||||||
                  as VideoTrack,
 | 
					 | 
				
			||||||
              renderMode: VideoRenderMode.platformView,
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          Positioned(
 | 
					          Positioned(
 | 
				
			||||||
            left: 8,
 | 
					            left: 8,
 | 
				
			||||||
            right: 8,
 | 
					            right: 8,
 | 
				
			||||||
            bottom: 8,
 | 
					            bottom: 8,
 | 
				
			||||||
            child: Text(
 | 
					            child: Text(
 | 
				
			||||||
              '@${live.participant.name}',
 | 
					              '@${widget.live.participant.name}',
 | 
				
			||||||
              textAlign: TextAlign.center,
 | 
					              textAlign: TextAlign.center,
 | 
				
			||||||
              style: const TextStyle(
 | 
					              style: const TextStyle(
 | 
				
			||||||
                fontSize: 14,
 | 
					                fontSize: 14,
 | 
				
			||||||
@@ -148,7 +157,7 @@ class CallParticipantTile extends HookConsumerWidget {
 | 
				
			|||||||
        ],
 | 
					        ],
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      return SpeakingRippleAvatar(size: 84, live: live);
 | 
					      return SpeakingRippleAvatar(size: 84, live: widget.live);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,6 @@ import 'package:island/pods/chat/messages_notifier.dart';
 | 
				
			|||||||
import 'package:island/pods/translate.dart';
 | 
					import 'package:island/pods/translate.dart';
 | 
				
			||||||
import 'package:island/pods/config.dart';
 | 
					import 'package:island/pods/config.dart';
 | 
				
			||||||
import 'package:island/widgets/account/account_pfc.dart';
 | 
					import 'package:island/widgets/account/account_pfc.dart';
 | 
				
			||||||
import 'package:island/widgets/app_scaffold.dart';
 | 
					 | 
				
			||||||
import 'package:island/widgets/chat/message_content.dart';
 | 
					import 'package:island/widgets/chat/message_content.dart';
 | 
				
			||||||
import 'package:island/widgets/chat/message_indicators.dart';
 | 
					import 'package:island/widgets/chat/message_indicators.dart';
 | 
				
			||||||
import 'package:island/widgets/chat/message_sender_info.dart';
 | 
					import 'package:island/widgets/chat/message_sender_info.dart';
 | 
				
			||||||
@@ -666,17 +665,11 @@ class MessageItemDisplayBubble extends HookConsumerWidget {
 | 
				
			|||||||
            ? Theme.of(context).colorScheme.primaryContainer.withOpacity(0.5)
 | 
					            ? Theme.of(context).colorScheme.primaryContainer.withOpacity(0.5)
 | 
				
			||||||
            : Theme.of(context).colorScheme.surfaceContainer;
 | 
					            : Theme.of(context).colorScheme.surfaceContainer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final hasBackground =
 | 
					 | 
				
			||||||
        ref.watch(backgroundImageFileProvider).valueOrNull != null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    final remoteMessage = message.toRemoteMessage();
 | 
					    final remoteMessage = message.toRemoteMessage();
 | 
				
			||||||
    final sender = remoteMessage.sender;
 | 
					    final sender = remoteMessage.sender;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Material(
 | 
					    return Material(
 | 
				
			||||||
      color:
 | 
					      color: Colors.transparent,
 | 
				
			||||||
          hasBackground
 | 
					 | 
				
			||||||
              ? Colors.transparent
 | 
					 | 
				
			||||||
              : Theme.of(context).colorScheme.surface,
 | 
					 | 
				
			||||||
      child: Padding(
 | 
					      child: Padding(
 | 
				
			||||||
        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
 | 
					        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
 | 
				
			||||||
        child: Column(
 | 
					        child: Column(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -44,10 +44,12 @@ void showInfoAlert(String message, String title) async {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Future<bool> showConfirmAlert(String message, String title) async {
 | 
					Future<bool> showConfirmAlert(String message, String title) async {
 | 
				
			||||||
  final result = await js.context.callMethod('swal', [
 | 
					  final result = await js.context.callMethod('swal', [
 | 
				
			||||||
    title,
 | 
					    js.JsObject.jsify({
 | 
				
			||||||
    message,
 | 
					      'title': title,
 | 
				
			||||||
    'question',
 | 
					      'text': message,
 | 
				
			||||||
    {'buttons': true},
 | 
					      'icon': 'info',
 | 
				
			||||||
 | 
					      'buttons': {'cancel': true, 'confirm': true},
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
  ]);
 | 
					  ]);
 | 
				
			||||||
  return result == true;
 | 
					  return result == true;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -193,10 +193,10 @@ class _PaymentContentState extends ConsumerState<_PaymentContent> {
 | 
				
			|||||||
      // Perform biometric authentication
 | 
					      // Perform biometric authentication
 | 
				
			||||||
      final bool didAuthenticate = await _localAuth.authenticate(
 | 
					      final bool didAuthenticate = await _localAuth.authenticate(
 | 
				
			||||||
        localizedReason: 'biometricPrompt'.tr(),
 | 
					        localizedReason: 'biometricPrompt'.tr(),
 | 
				
			||||||
        options: const AuthenticationOptions(
 | 
					        // options: const AuthenticationOptions(
 | 
				
			||||||
          biometricOnly: true,
 | 
					        //   biometricOnly: true,
 | 
				
			||||||
          stickyAuth: true,
 | 
					        //   stickyAuth: true,
 | 
				
			||||||
        ),
 | 
					        // ),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (didAuthenticate) {
 | 
					      if (didAuthenticate) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,7 +15,6 @@
 | 
				
			|||||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
 | 
					#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
 | 
				
			||||||
#include <gtk/gtk_plugin.h>
 | 
					#include <gtk/gtk_plugin.h>
 | 
				
			||||||
#include <irondash_engine_context/irondash_engine_context_plugin.h>
 | 
					#include <irondash_engine_context/irondash_engine_context_plugin.h>
 | 
				
			||||||
#include <livekit_client/live_kit_plugin.h>
 | 
					 | 
				
			||||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
 | 
					#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
 | 
				
			||||||
#include <media_kit_video/media_kit_video_plugin.h>
 | 
					#include <media_kit_video/media_kit_video_plugin.h>
 | 
				
			||||||
#include <pasteboard/pasteboard_plugin.h>
 | 
					#include <pasteboard/pasteboard_plugin.h>
 | 
				
			||||||
@@ -57,9 +56,6 @@ void fl_register_plugins(FlPluginRegistry* registry) {
 | 
				
			|||||||
  g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar =
 | 
					  g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar =
 | 
				
			||||||
      fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin");
 | 
					      fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin");
 | 
				
			||||||
  irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar);
 | 
					  irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar);
 | 
				
			||||||
  g_autoptr(FlPluginRegistrar) livekit_client_registrar =
 | 
					 | 
				
			||||||
      fl_plugin_registry_get_registrar_for_plugin(registry, "LiveKitPlugin");
 | 
					 | 
				
			||||||
  live_kit_plugin_register_with_registrar(livekit_client_registrar);
 | 
					 | 
				
			||||||
  g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar =
 | 
					  g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar =
 | 
				
			||||||
      fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin");
 | 
					      fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin");
 | 
				
			||||||
  media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar);
 | 
					  media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,7 +12,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
 | 
				
			|||||||
  flutter_webrtc
 | 
					  flutter_webrtc
 | 
				
			||||||
  gtk
 | 
					  gtk
 | 
				
			||||||
  irondash_engine_context
 | 
					  irondash_engine_context
 | 
				
			||||||
  livekit_client
 | 
					 | 
				
			||||||
  media_kit_libs_linux
 | 
					  media_kit_libs_linux
 | 
				
			||||||
  media_kit_video
 | 
					  media_kit_video
 | 
				
			||||||
  pasteboard
 | 
					  pasteboard
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,6 @@ import FlutterMacOS
 | 
				
			|||||||
import Foundation
 | 
					import Foundation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import app_links
 | 
					import app_links
 | 
				
			||||||
import connectivity_plus
 | 
					 | 
				
			||||||
import device_info_plus
 | 
					import device_info_plus
 | 
				
			||||||
import file_picker
 | 
					import file_picker
 | 
				
			||||||
import file_saver
 | 
					import file_saver
 | 
				
			||||||
@@ -24,7 +23,6 @@ import flutter_udid
 | 
				
			|||||||
import flutter_webrtc
 | 
					import flutter_webrtc
 | 
				
			||||||
import gal
 | 
					import gal
 | 
				
			||||||
import irondash_engine_context
 | 
					import irondash_engine_context
 | 
				
			||||||
import livekit_client
 | 
					 | 
				
			||||||
import local_auth_darwin
 | 
					import local_auth_darwin
 | 
				
			||||||
import media_kit_libs_macos_video
 | 
					import media_kit_libs_macos_video
 | 
				
			||||||
import media_kit_video
 | 
					import media_kit_video
 | 
				
			||||||
@@ -48,7 +46,6 @@ import window_manager
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
 | 
					func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
 | 
				
			||||||
  AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
 | 
					  AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
 | 
				
			||||||
  ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
 | 
					 | 
				
			||||||
  DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
 | 
					  DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
 | 
				
			||||||
  FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
 | 
					  FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
 | 
				
			||||||
  FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
 | 
					  FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin"))
 | 
				
			||||||
@@ -66,7 +63,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
 | 
				
			|||||||
  FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
 | 
					  FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
 | 
				
			||||||
  GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
 | 
					  GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
 | 
				
			||||||
  IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin"))
 | 
					  IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin"))
 | 
				
			||||||
  LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
 | 
					 | 
				
			||||||
  LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
 | 
					  LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
 | 
				
			||||||
  MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
 | 
					  MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
 | 
				
			||||||
  MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
 | 
					  MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										88
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										88
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -289,22 +289,6 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.19.1"
 | 
					    version: "1.19.1"
 | 
				
			||||||
  connectivity_plus:
 | 
					 | 
				
			||||||
    dependency: transitive
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: connectivity_plus
 | 
					 | 
				
			||||||
      sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec
 | 
					 | 
				
			||||||
      url: "https://pub.dev"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "6.1.5"
 | 
					 | 
				
			||||||
  connectivity_plus_platform_interface:
 | 
					 | 
				
			||||||
    dependency: transitive
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: connectivity_plus_platform_interface
 | 
					 | 
				
			||||||
      sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204"
 | 
					 | 
				
			||||||
      url: "https://pub.dev"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "2.0.1"
 | 
					 | 
				
			||||||
  console:
 | 
					  console:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -930,10 +914,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: flutter_local_notifications
 | 
					      name: flutter_local_notifications
 | 
				
			||||||
      sha256: "7ed76be64e8a7d01dfdf250b8434618e2a028c9dfa2a3c41dc9b531d4b3fc8a5"
 | 
					      sha256: "19ffb0a8bb7407875555e5e98d7343a633bb73707bae6c6a5f37c90014077875"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "19.4.2"
 | 
					    version: "19.5.0"
 | 
				
			||||||
  flutter_local_notifications_linux:
 | 
					  flutter_local_notifications_linux:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -991,10 +975,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: flutter_native_splash
 | 
					      name: flutter_native_splash
 | 
				
			||||||
      sha256: "8321a6d11a8d13977fa780c89de8d257cce3d841eecfb7a4cadffcc4f12d82dc"
 | 
					      sha256: "4fb9f4113350d3a80841ce05ebf1976a36de622af7d19aca0ca9a9911c7ff002"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.4.6"
 | 
					    version: "2.4.7"
 | 
				
			||||||
  flutter_otp_text_field:
 | 
					  flutter_otp_text_field:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1201,10 +1185,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: go_router
 | 
					      name: go_router
 | 
				
			||||||
      sha256: c752e2d08d088bf83742cb05bf83003f3e9d276ff1519b5c92f9d5e60e5ddd23
 | 
					      sha256: e1d7ffb0db475e6e845eb58b44768f50b830e23960e3df6908924acd8f7f70ea
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "16.2.4"
 | 
					    version: "16.2.5"
 | 
				
			||||||
  google_fonts:
 | 
					  google_fonts:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1321,10 +1305,10 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: image_picker_android
 | 
					      name: image_picker_android
 | 
				
			||||||
      sha256: dd7a61daaa5896cc34b7bc95f66c60225ae6bee0d167dde0e21a9d9016cac0dc
 | 
					      sha256: "58a85e6f09fe9c4484d53d18a0bd6271b72c53fce1d05e6f745ae36d8c18efca"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.8.13+4"
 | 
					    version: "0.8.13+5"
 | 
				
			||||||
  image_picker_for_web:
 | 
					  image_picker_for_web:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1469,38 +1453,30 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "6.0.0"
 | 
					    version: "6.0.0"
 | 
				
			||||||
  livekit_client:
 | 
					 | 
				
			||||||
    dependency: "direct main"
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: livekit_client
 | 
					 | 
				
			||||||
      sha256: "4c1663c1e6ac20a743d9a46c7bc71f17e1949db99d245750c68661d554e30cd2"
 | 
					 | 
				
			||||||
      url: "https://pub.dev"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "2.5.1"
 | 
					 | 
				
			||||||
  local_auth:
 | 
					  local_auth:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: local_auth
 | 
					      name: local_auth
 | 
				
			||||||
      sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b"
 | 
					      sha256: a4f1bf57f0236a4aeb5e8f0ec180e197f4b112a3456baa6c1e73b546630b0422
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.3.0"
 | 
					    version: "3.0.0"
 | 
				
			||||||
  local_auth_android:
 | 
					  local_auth_android:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: local_auth_android
 | 
					      name: local_auth_android
 | 
				
			||||||
      sha256: b2446c74fab1db37f828d4c54adaa3f003df80a29f5cbd710bbb8883d302e991
 | 
					      sha256: d836715ed95b16b2de3a8c47a88ba5e607976bb1e27c9446d193152ea1429fae
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.0.55"
 | 
					    version: "2.0.0"
 | 
				
			||||||
  local_auth_darwin:
 | 
					  local_auth_darwin:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: local_auth_darwin
 | 
					      name: local_auth_darwin
 | 
				
			||||||
      sha256: "699873970067a40ef2f2c09b4c72eb1cfef64224ef041b3df9fdc5c4c1f91f49"
 | 
					      sha256: "15d9db4ad4d58a11d7269e55d46ff8d49ed5e856226c8a5a91280f0d7c37b3a6"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.6.1"
 | 
					    version: "2.0.0"
 | 
				
			||||||
  local_auth_platform_interface:
 | 
					  local_auth_platform_interface:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1513,10 +1489,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: local_auth_windows
 | 
					      name: local_auth_windows
 | 
				
			||||||
      sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5
 | 
					      sha256: d95535a73eddf57ce5930d5e78a0fa4f294c31981fdeeee83325b797302be454
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.0.11"
 | 
					    version: "2.0.0"
 | 
				
			||||||
  logger:
 | 
					  logger:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1669,14 +1645,6 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "2.0.0"
 | 
					    version: "2.0.0"
 | 
				
			||||||
  mime_type:
 | 
					 | 
				
			||||||
    dependency: transitive
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: mime_type
 | 
					 | 
				
			||||||
      sha256: d652b613e84dac1af28030a9fba82c0999be05b98163f9e18a0849c6e63838bb
 | 
					 | 
				
			||||||
      url: "https://pub.dev"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "1.0.1"
 | 
					 | 
				
			||||||
  modal_bottom_sheet:
 | 
					  modal_bottom_sheet:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1709,14 +1677,6 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.0.0"
 | 
					    version: "1.0.0"
 | 
				
			||||||
  nm:
 | 
					 | 
				
			||||||
    dependency: transitive
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: nm
 | 
					 | 
				
			||||||
      sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254"
 | 
					 | 
				
			||||||
      url: "https://pub.dev"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "0.5.0"
 | 
					 | 
				
			||||||
  octo_image:
 | 
					  octo_image:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1933,14 +1893,6 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.2.4"
 | 
					    version: "1.2.4"
 | 
				
			||||||
  protobuf:
 | 
					 | 
				
			||||||
    dependency: transitive
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: protobuf
 | 
					 | 
				
			||||||
      sha256: de9c9eb2c33f8e933a42932fe1dc504800ca45ebc3d673e6ed7f39754ee4053e
 | 
					 | 
				
			||||||
      url: "https://pub.dev"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "4.2.0"
 | 
					 | 
				
			||||||
  provider:
 | 
					  provider:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -2206,14 +2158,6 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "3.0.1"
 | 
					    version: "3.0.1"
 | 
				
			||||||
  sdp_transform:
 | 
					 | 
				
			||||||
    dependency: transitive
 | 
					 | 
				
			||||||
    description:
 | 
					 | 
				
			||||||
      name: sdp_transform
 | 
					 | 
				
			||||||
      sha256: "73e412a5279a5c2de74001535208e20fff88f225c9a4571af0f7146202755e45"
 | 
					 | 
				
			||||||
      url: "https://pub.dev"
 | 
					 | 
				
			||||||
    source: hosted
 | 
					 | 
				
			||||||
    version: "0.3.2"
 | 
					 | 
				
			||||||
  share_plus:
 | 
					  share_plus:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										12
									
								
								pubspec.yaml
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								pubspec.yaml
									
									
									
									
									
								
							@@ -38,7 +38,7 @@ dependencies:
 | 
				
			|||||||
  cupertino_icons: ^1.0.8
 | 
					  cupertino_icons: ^1.0.8
 | 
				
			||||||
  flutter_hooks: ^0.21.3+1
 | 
					  flutter_hooks: ^0.21.3+1
 | 
				
			||||||
  hooks_riverpod: ^2.6.1
 | 
					  hooks_riverpod: ^2.6.1
 | 
				
			||||||
  go_router: ^16.2.4
 | 
					  go_router: ^16.2.5
 | 
				
			||||||
  styled_widget: ^0.4.1
 | 
					  styled_widget: ^0.4.1
 | 
				
			||||||
  shared_preferences: ^2.5.3
 | 
					  shared_preferences: ^2.5.3
 | 
				
			||||||
  flutter_riverpod: ^2.6.1
 | 
					  flutter_riverpod: ^2.6.1
 | 
				
			||||||
@@ -75,7 +75,7 @@ dependencies:
 | 
				
			|||||||
  file_picker: ^10.3.3
 | 
					  file_picker: ^10.3.3
 | 
				
			||||||
  riverpod_annotation: ^2.6.1
 | 
					  riverpod_annotation: ^2.6.1
 | 
				
			||||||
  image_picker_platform_interface: ^2.11.0
 | 
					  image_picker_platform_interface: ^2.11.0
 | 
				
			||||||
  image_picker_android: ^0.8.13+4
 | 
					  image_picker_android: ^0.8.13+5
 | 
				
			||||||
  super_context_menu: ^0.9.1
 | 
					  super_context_menu: ^0.9.1
 | 
				
			||||||
  modal_bottom_sheet: ^3.0.0
 | 
					  modal_bottom_sheet: ^3.0.0
 | 
				
			||||||
  firebase_messaging: ^16.0.3
 | 
					  firebase_messaging: ^16.0.3
 | 
				
			||||||
@@ -97,12 +97,12 @@ dependencies:
 | 
				
			|||||||
  avatar_stack: ^3.0.0
 | 
					  avatar_stack: ^3.0.0
 | 
				
			||||||
  markdown_widget: ^2.3.2+8
 | 
					  markdown_widget: ^2.3.2+8
 | 
				
			||||||
  visibility_detector: ^0.4.0+2
 | 
					  visibility_detector: ^0.4.0+2
 | 
				
			||||||
  flutter_native_splash: ^2.4.6
 | 
					  flutter_native_splash: ^2.4.7
 | 
				
			||||||
  photo_view: ^0.15.0
 | 
					  photo_view: ^0.15.0
 | 
				
			||||||
  gal: ^2.3.2
 | 
					  gal: ^2.3.2
 | 
				
			||||||
  dismissible_page: ^1.0.2
 | 
					  dismissible_page: ^1.0.2
 | 
				
			||||||
  super_sliver_list: ^0.4.1
 | 
					  super_sliver_list: ^0.4.1
 | 
				
			||||||
  livekit_client: ^2.5.1
 | 
					
 | 
				
			||||||
  pasteboard: ^0.4.0
 | 
					  pasteboard: ^0.4.0
 | 
				
			||||||
  flutter_colorpicker: ^1.1.0
 | 
					  flutter_colorpicker: ^1.1.0
 | 
				
			||||||
  image: ^4.5.4
 | 
					  image: ^4.5.4
 | 
				
			||||||
@@ -117,7 +117,7 @@ dependencies:
 | 
				
			|||||||
  sign_in_with_apple: ^7.0.1
 | 
					  sign_in_with_apple: ^7.0.1
 | 
				
			||||||
  flutter_svg: ^2.2.1
 | 
					  flutter_svg: ^2.2.1
 | 
				
			||||||
  native_exif: ^0.6.2
 | 
					  native_exif: ^0.6.2
 | 
				
			||||||
  local_auth: ^2.3.0
 | 
					  local_auth: ^3.0.0
 | 
				
			||||||
  flutter_secure_storage: ^9.2.4
 | 
					  flutter_secure_storage: ^9.2.4
 | 
				
			||||||
  flutter_math_fork: ^0.7.4
 | 
					  flutter_math_fork: ^0.7.4
 | 
				
			||||||
  share_plus: ^12.0.0
 | 
					  share_plus: ^12.0.0
 | 
				
			||||||
@@ -142,7 +142,7 @@ dependencies:
 | 
				
			|||||||
  file_saver: ^0.3.1
 | 
					  file_saver: ^0.3.1
 | 
				
			||||||
  tray_manager: ^0.5.1
 | 
					  tray_manager: ^0.5.1
 | 
				
			||||||
  flutter_webrtc: ^1.2.0
 | 
					  flutter_webrtc: ^1.2.0
 | 
				
			||||||
  flutter_local_notifications: ^19.4.2
 | 
					  flutter_local_notifications: ^19.5.0
 | 
				
			||||||
  wakelock_plus: ^1.4.0
 | 
					  wakelock_plus: ^1.4.0
 | 
				
			||||||
  slide_countdown: ^2.0.2
 | 
					  slide_countdown: ^2.0.2
 | 
				
			||||||
  shelf: ^1.4.2
 | 
					  shelf: ^1.4.2
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
; ==================================================
 | 
					; ==================================================
 | 
				
			||||||
#define AppVersion "3.2.0"
 | 
					#define AppVersion "3.3.0"
 | 
				
			||||||
#define BuildNumber "134"
 | 
					#define BuildNumber "136"
 | 
				
			||||||
; ==================================================
 | 
					; ==================================================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#define FullVersion AppVersion + "." + BuildNumber
 | 
					#define FullVersion AppVersion + "." + BuildNumber
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,6 @@
 | 
				
			|||||||
#include "generated_plugin_registrant.h"
 | 
					#include "generated_plugin_registrant.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <app_links/app_links_plugin_c_api.h>
 | 
					#include <app_links/app_links_plugin_c_api.h>
 | 
				
			||||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
 | 
					 | 
				
			||||||
#include <dart_ipc/dart_ipc_plugin_c_api.h>
 | 
					#include <dart_ipc/dart_ipc_plugin_c_api.h>
 | 
				
			||||||
#include <file_saver/file_saver_plugin.h>
 | 
					#include <file_saver/file_saver_plugin.h>
 | 
				
			||||||
#include <file_selector_windows/file_selector_windows.h>
 | 
					#include <file_selector_windows/file_selector_windows.h>
 | 
				
			||||||
@@ -20,7 +19,6 @@
 | 
				
			|||||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
 | 
					#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
 | 
				
			||||||
#include <gal/gal_plugin_c_api.h>
 | 
					#include <gal/gal_plugin_c_api.h>
 | 
				
			||||||
#include <irondash_engine_context/irondash_engine_context_plugin_c_api.h>
 | 
					#include <irondash_engine_context/irondash_engine_context_plugin_c_api.h>
 | 
				
			||||||
#include <livekit_client/live_kit_plugin.h>
 | 
					 | 
				
			||||||
#include <local_auth_windows/local_auth_plugin.h>
 | 
					#include <local_auth_windows/local_auth_plugin.h>
 | 
				
			||||||
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
 | 
					#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
 | 
				
			||||||
#include <media_kit_video/media_kit_video_plugin_c_api.h>
 | 
					#include <media_kit_video/media_kit_video_plugin_c_api.h>
 | 
				
			||||||
@@ -40,8 +38,6 @@
 | 
				
			|||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
 | 
					void RegisterPlugins(flutter::PluginRegistry* registry) {
 | 
				
			||||||
  AppLinksPluginCApiRegisterWithRegistrar(
 | 
					  AppLinksPluginCApiRegisterWithRegistrar(
 | 
				
			||||||
      registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
 | 
					      registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
 | 
				
			||||||
  ConnectivityPlusWindowsPluginRegisterWithRegistrar(
 | 
					 | 
				
			||||||
      registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
 | 
					 | 
				
			||||||
  DartIpcPluginCApiRegisterWithRegistrar(
 | 
					  DartIpcPluginCApiRegisterWithRegistrar(
 | 
				
			||||||
      registry->GetRegistrarForPlugin("DartIpcPluginCApi"));
 | 
					      registry->GetRegistrarForPlugin("DartIpcPluginCApi"));
 | 
				
			||||||
  FileSaverPluginRegisterWithRegistrar(
 | 
					  FileSaverPluginRegisterWithRegistrar(
 | 
				
			||||||
@@ -66,8 +62,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
 | 
				
			|||||||
      registry->GetRegistrarForPlugin("GalPluginCApi"));
 | 
					      registry->GetRegistrarForPlugin("GalPluginCApi"));
 | 
				
			||||||
  IrondashEngineContextPluginCApiRegisterWithRegistrar(
 | 
					  IrondashEngineContextPluginCApiRegisterWithRegistrar(
 | 
				
			||||||
      registry->GetRegistrarForPlugin("IrondashEngineContextPluginCApi"));
 | 
					      registry->GetRegistrarForPlugin("IrondashEngineContextPluginCApi"));
 | 
				
			||||||
  LiveKitPluginRegisterWithRegistrar(
 | 
					 | 
				
			||||||
      registry->GetRegistrarForPlugin("LiveKitPlugin"));
 | 
					 | 
				
			||||||
  LocalAuthPluginRegisterWithRegistrar(
 | 
					  LocalAuthPluginRegisterWithRegistrar(
 | 
				
			||||||
      registry->GetRegistrarForPlugin("LocalAuthPlugin"));
 | 
					      registry->GetRegistrarForPlugin("LocalAuthPlugin"));
 | 
				
			||||||
  MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(
 | 
					  MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
list(APPEND FLUTTER_PLUGIN_LIST
 | 
					list(APPEND FLUTTER_PLUGIN_LIST
 | 
				
			||||||
  app_links
 | 
					  app_links
 | 
				
			||||||
  connectivity_plus
 | 
					 | 
				
			||||||
  dart_ipc
 | 
					  dart_ipc
 | 
				
			||||||
  file_saver
 | 
					  file_saver
 | 
				
			||||||
  file_selector_windows
 | 
					  file_selector_windows
 | 
				
			||||||
@@ -17,7 +16,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
 | 
				
			|||||||
  flutter_webrtc
 | 
					  flutter_webrtc
 | 
				
			||||||
  gal
 | 
					  gal
 | 
				
			||||||
  irondash_engine_context
 | 
					  irondash_engine_context
 | 
				
			||||||
  livekit_client
 | 
					 | 
				
			||||||
  local_auth_windows
 | 
					  local_auth_windows
 | 
				
			||||||
  media_kit_libs_windows_video
 | 
					  media_kit_libs_windows_video
 | 
				
			||||||
  media_kit_video
 | 
					  media_kit_video
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user