♻️ Replace livekit with jitsi in calling
This commit is contained in:
		| @@ -1,289 +0,0 @@ | ||||
| import 'dart:math' as math; | ||||
|  | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:livekit_client/livekit_client.dart' as livekit; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/providers/chat_call.dart'; | ||||
| import 'package:surface/widgets/chat/call/call_controls.dart'; | ||||
| import 'package:surface/widgets/chat/call/call_participant.dart'; | ||||
| import 'package:surface/widgets/navigation/app_scaffold.dart'; | ||||
|  | ||||
| class CallRoomScreen extends StatefulWidget { | ||||
|   final String scope; | ||||
|   final String alias; | ||||
|  | ||||
|   const CallRoomScreen({super.key, required this.scope, required this.alias}); | ||||
|  | ||||
|   @override | ||||
|   State<CallRoomScreen> createState() => _CallRoomScreenState(); | ||||
| } | ||||
|  | ||||
| class _CallRoomScreenState extends State<CallRoomScreen> { | ||||
|   int _layoutMode = 0; | ||||
|  | ||||
|   void _switchLayout() { | ||||
|     if (_layoutMode < 1) { | ||||
|       setState(() => _layoutMode++); | ||||
|     } else { | ||||
|       setState(() => _layoutMode = 0); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Widget _buildMeetLayout() { | ||||
|     final call = context.read<ChatCallProvider>(); | ||||
|     return Stack( | ||||
|       children: [ | ||||
|         Container( | ||||
|           color: | ||||
|               Theme.of(context).colorScheme.surfaceContainer.withOpacity(0.75), | ||||
|           child: call.focusTrack != null | ||||
|               ? InteractiveParticipantWidget( | ||||
|                   participant: call.focusTrack!, | ||||
|                 ) | ||||
|               : const SizedBox.shrink(), | ||||
|         ), | ||||
|         Positioned( | ||||
|           left: 0, | ||||
|           right: 0, | ||||
|           top: 0, | ||||
|           child: SizedBox( | ||||
|             height: 128, | ||||
|             child: ListView.builder( | ||||
|               scrollDirection: Axis.horizontal, | ||||
|               itemCount: math.max(0, call.participantTracks.length), | ||||
|               itemBuilder: (BuildContext context, int index) { | ||||
|                 final track = call.participantTracks[index]; | ||||
|                 if (track.participant.sid == call.focusTrack?.participant.sid) { | ||||
|                   return Container(); | ||||
|                 } | ||||
|  | ||||
|                 return SizedBox( | ||||
|                   height: 128, | ||||
|                   width: 128, | ||||
|                   child: InteractiveParticipantWidget( | ||||
|                     participant: track, | ||||
|                     avatarSize: 32, | ||||
|                     onTap: () { | ||||
|                       if (track.participant.sid != | ||||
|                           call.focusTrack?.participant.sid) { | ||||
|                         call.setFocusTrack(track); | ||||
|                       } | ||||
|                     }, | ||||
|                   ), | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|       ], | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Widget _buildListLayout() { | ||||
|     final call = context.read<ChatCallProvider>(); | ||||
|  | ||||
|     return LayoutBuilder( | ||||
|       builder: (context, constraints) { | ||||
|         return ListView.builder( | ||||
|           padding: EdgeInsets.zero, | ||||
|           itemCount: math.max(0, call.participantTracks.length), | ||||
|           itemBuilder: (BuildContext context, int index) { | ||||
|             final track = call.participantTracks[index]; | ||||
|             return InteractiveParticipantWidget( | ||||
|               padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), | ||||
|               isList: true, | ||||
|               avatarSize: 24, | ||||
|               participant: track, | ||||
|             ); | ||||
|           }, | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     super.initState(); | ||||
|     final call = context.read<ChatCallProvider>(); | ||||
|  | ||||
|     Future.delayed(Duration.zero, () { | ||||
|       call | ||||
|         ..setupRoom() | ||||
|         ..enableDurationUpdater(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final call = context.read<ChatCallProvider>(); | ||||
|  | ||||
|     return ListenableBuilder( | ||||
|         listenable: call, | ||||
|         builder: (context, _) { | ||||
|           return AppScaffold( | ||||
|             noBackground: ResponsiveScaffold.getIsExpand(context), | ||||
|             appBar: AppBar( | ||||
|               title: RichText( | ||||
|                 textAlign: TextAlign.center, | ||||
|                 text: TextSpan(children: [ | ||||
|                   TextSpan( | ||||
|                     text: 'call'.tr(), | ||||
|                     style: Theme.of(context).textTheme.titleLarge!.copyWith( | ||||
|                           color: Theme.of(context).appBarTheme.foregroundColor, | ||||
|                         ), | ||||
|                   ), | ||||
|                   const TextSpan(text: '\n'), | ||||
|                   TextSpan( | ||||
|                     text: call.lastDuration.toString(), | ||||
|                     style: Theme.of(context).textTheme.bodySmall!.copyWith( | ||||
|                           color: Theme.of(context).appBarTheme.foregroundColor, | ||||
|                         ), | ||||
|                   ), | ||||
|                 ]), | ||||
|               ), | ||||
|             ), | ||||
|             body: Column( | ||||
|               children: [ | ||||
|                 SizedBox( | ||||
|                   width: MediaQuery.of(context).size.width, | ||||
|                   height: 64, | ||||
|                   child: Row( | ||||
|                     mainAxisAlignment: MainAxisAlignment.spaceBetween, | ||||
|                     crossAxisAlignment: CrossAxisAlignment.center, | ||||
|                     children: [ | ||||
|                       Builder(builder: (context) { | ||||
|                         final call = context.read<ChatCallProvider>(); | ||||
|                         final connectionQuality = | ||||
|                             call.room.localParticipant?.connectionQuality ?? | ||||
|                                 livekit.ConnectionQuality.unknown; | ||||
|                         return Expanded( | ||||
|                           child: Column( | ||||
|                             mainAxisSize: MainAxisSize.min, | ||||
|                             crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                             children: [ | ||||
|                               Row( | ||||
|                                 children: [ | ||||
|                                   Text( | ||||
|                                     call.channel?.name ?? 'unknown'.tr(), | ||||
|                                     style: const TextStyle( | ||||
|                                       fontWeight: FontWeight.bold, | ||||
|                                     ), | ||||
|                                   ), | ||||
|                                   const Gap(6), | ||||
|                                   Text(call.lastDuration.toString()) | ||||
|                                 ], | ||||
|                               ), | ||||
|                               Row( | ||||
|                                 children: [ | ||||
|                                   Text( | ||||
|                                     { | ||||
|                                       livekit.ConnectionState.disconnected: | ||||
|                                           'callStatusDisconnected'.tr(), | ||||
|                                       livekit.ConnectionState.connected: | ||||
|                                           'callStatusConnected'.tr(), | ||||
|                                       livekit.ConnectionState.connecting: | ||||
|                                           'callStatusConnecting'.tr(), | ||||
|                                       livekit.ConnectionState.reconnecting: | ||||
|                                           'callStatusReconnecting'.tr(), | ||||
|                                     }[call.room.connectionState]!, | ||||
|                                   ), | ||||
|                                   const Gap(6), | ||||
|                                   if (connectionQuality != | ||||
|                                       livekit.ConnectionQuality.unknown) | ||||
|                                     Icon( | ||||
|                                       { | ||||
|                                         livekit.ConnectionQuality.excellent: | ||||
|                                             Icons.signal_cellular_alt, | ||||
|                                         livekit.ConnectionQuality.good: | ||||
|                                             Icons.signal_cellular_alt_2_bar, | ||||
|                                         livekit.ConnectionQuality.poor: | ||||
|                                             Icons.signal_cellular_alt_1_bar, | ||||
|                                       }[connectionQuality], | ||||
|                                       color: { | ||||
|                                         livekit.ConnectionQuality.excellent: | ||||
|                                             Colors.green, | ||||
|                                         livekit.ConnectionQuality.good: | ||||
|                                             Colors.orange, | ||||
|                                         livekit.ConnectionQuality.poor: | ||||
|                                             Colors.red, | ||||
|                                       }[connectionQuality], | ||||
|                                       size: 16, | ||||
|                                     ) | ||||
|                                   else | ||||
|                                     const SizedBox( | ||||
|                                       width: 12, | ||||
|                                       height: 12, | ||||
|                                       child: CircularProgressIndicator( | ||||
|                                         color: Colors.white, | ||||
|                                         strokeWidth: 2, | ||||
|                                         padding: EdgeInsets.zero, | ||||
|                                       ), | ||||
|                                     ).padding(all: 3), | ||||
|                                 ], | ||||
|                               ), | ||||
|                             ], | ||||
|                           ), | ||||
|                         ); | ||||
|                       }), | ||||
|                       Row( | ||||
|                         children: [ | ||||
|                           IconButton( | ||||
|                             icon: _layoutMode == 0 | ||||
|                                 ? const Icon(Icons.view_list) | ||||
|                                 : const Icon(Icons.grid_view), | ||||
|                             onPressed: () { | ||||
|                               _switchLayout(); | ||||
|                             }, | ||||
|                           ), | ||||
|                         ], | ||||
|                       ), | ||||
|                     ], | ||||
|                   ).padding(left: 20, right: 16), | ||||
|                 ), | ||||
|                 Expanded( | ||||
|                   child: Material( | ||||
|                     color: Theme.of(context).colorScheme.surfaceContainerLow, | ||||
|                     child: Builder( | ||||
|                       builder: (context) { | ||||
|                         switch (_layoutMode) { | ||||
|                           case 1: | ||||
|                             return _buildListLayout(); | ||||
|                           default: | ||||
|                             return _buildMeetLayout(); | ||||
|                         } | ||||
|                       }, | ||||
|                     ), | ||||
|                   ), | ||||
|                 ), | ||||
|                 if (call.room.localParticipant != null) | ||||
|                   SizedBox( | ||||
|                     width: MediaQuery.of(context).size.width, | ||||
|                     child: ControlsWidget( | ||||
|                       call.room, | ||||
|                       call.room.localParticipant!, | ||||
|                     ), | ||||
|                   ), | ||||
|                 Gap(MediaQuery.of(context).padding.bottom), | ||||
|               ], | ||||
|             ), | ||||
|           ); | ||||
|         }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void deactivate() { | ||||
|     final call = context.read<ChatCallProvider>(); | ||||
|     call.disableDurationUpdater(); | ||||
|     super.deactivate(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void activate() { | ||||
|     final call = context.read<ChatCallProvider>(); | ||||
|     call.enableDurationUpdater(); | ||||
|     super.activate(); | ||||
|   } | ||||
| } | ||||
| @@ -2,18 +2,17 @@ import 'dart:async'; | ||||
| import 'dart:convert'; | ||||
| import 'dart:developer'; | ||||
|  | ||||
| import 'package:dio/dio.dart'; | ||||
| import 'package:easy_localization/easy_localization.dart'; | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:gap/gap.dart'; | ||||
| import 'package:go_router/go_router.dart'; | ||||
| import 'package:jitsi_meet_flutter_sdk/jitsi_meet_flutter_sdk.dart'; | ||||
| import 'package:material_symbols_icons/symbols.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:styled_widget/styled_widget.dart'; | ||||
| import 'package:surface/controllers/chat_message_controller.dart'; | ||||
| import 'package:surface/controllers/post_write_controller.dart'; | ||||
| import 'package:surface/providers/channel.dart'; | ||||
| import 'package:surface/providers/chat_call.dart'; | ||||
| import 'package:surface/providers/notification.dart'; | ||||
| import 'package:surface/providers/sn_network.dart'; | ||||
| import 'package:surface/providers/user_directory.dart'; | ||||
| @@ -21,7 +20,6 @@ import 'package:surface/providers/userinfo.dart'; | ||||
| import 'package:surface/providers/websocket.dart'; | ||||
| import 'package:surface/types/chat.dart'; | ||||
| import 'package:surface/types/websocket.dart'; | ||||
| import 'package:surface/widgets/chat/call/call_prejoin.dart'; | ||||
| import 'package:surface/widgets/chat/chat_message.dart'; | ||||
| import 'package:surface/widgets/chat/chat_message_input.dart'; | ||||
| import 'package:surface/widgets/chat/chat_typing_indicator.dart'; | ||||
| @@ -51,13 +49,11 @@ class ChatRoomScreen extends StatefulWidget { | ||||
|  | ||||
| class _ChatRoomScreenState extends State<ChatRoomScreen> { | ||||
|   bool _isBusy = false; | ||||
|   bool _isCalling = false; | ||||
|   bool _isJoining = false; | ||||
|  | ||||
|   SnChannel? _channel; | ||||
|   SnChannelMember? _currentMember; | ||||
|   SnChannelMember? _otherMember; | ||||
|   SnChatCall? _ongoingCall; | ||||
|  | ||||
|   final GlobalKey<ChatMessageInputState> _inputGlobalKey = GlobalKey(); | ||||
|   late final ChatMessageController _messageController; | ||||
| @@ -139,88 +135,25 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _fetchOngoingCall() async { | ||||
|     setState(() => _isCalling = true); | ||||
|  | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       final resp = await sn.client.get( | ||||
|         '/cgi/im/channels/${_messageController.channel!.keyPath}/calls/ongoing', | ||||
|         options: Options( | ||||
|           validateStatus: (status) => status != null && status < 500, | ||||
|           receiveTimeout: const Duration(seconds: 60), | ||||
|           sendTimeout: const Duration(seconds: 60), | ||||
|         ), | ||||
|       ); | ||||
|       if (resp.statusCode == 200) { | ||||
|         _ongoingCall = SnChatCall.fromJson(resp.data); | ||||
|       } | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isCalling = false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _makeCall() async { | ||||
|     setState(() => _isCalling = true); | ||||
|  | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.client.post( | ||||
|         '/cgi/im/channels/${_messageController.channel!.keyPath}/calls', | ||||
|         options: Options( | ||||
|           sendTimeout: const Duration(seconds: 30), | ||||
|           receiveTimeout: const Duration(seconds: 30), | ||||
|         ), | ||||
|       ); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       if (_ongoingCall == null) { | ||||
|         // ignore the error because the call is already ongoing | ||||
|         context.showErrorDialog(err); | ||||
|       } | ||||
|     } finally { | ||||
|       setState(() => _isCalling = false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _endCall() async { | ||||
|     setState(() => _isCalling = true); | ||||
|  | ||||
|     try { | ||||
|       final sn = context.read<SnNetworkProvider>(); | ||||
|       await sn.client.delete( | ||||
|         '/cgi/im/channels/${_messageController.channel!.keyPath}/calls/ongoing', | ||||
|       ); | ||||
|     } catch (err) { | ||||
|       if (!mounted) return; | ||||
|       context.showErrorDialog(err); | ||||
|     } finally { | ||||
|       setState(() => _isCalling = false); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _onCallJoin() async { | ||||
|     await showModalBottomSheet( | ||||
|       context: context, | ||||
|       builder: (context) => ChatCallPrejoinPopup( | ||||
|         ongoingCall: _ongoingCall!, | ||||
|         channel: _channel!, | ||||
|         onJoin: _onCallResume, | ||||
|     final sn = context.read<SnNetworkProvider>(); | ||||
|     final ua = context.read<UserProvider>(); | ||||
|     final meet = JitsiMeet(); | ||||
|     final confOpts = JitsiMeetConferenceOptions( | ||||
|       room: 'sn-chat-${_channel!.id}', | ||||
|       serverURL: | ||||
|           'https://meet.element.io', // TODO fetch this as config from remote | ||||
|       configOverrides: { | ||||
|         "subject": _channel!.name, | ||||
|       }, | ||||
|       userInfo: JitsiMeetUserInfo( | ||||
|         avatar: ua.user!.avatar.isNotEmpty | ||||
|             ? sn.getAttachmentUrl(ua.user!.avatar) | ||||
|             : null, | ||||
|         displayName: _currentMember!.nick ?? ua.user!.nick, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   void _onCallResume() { | ||||
|     GoRouter.of(context).pushNamed( | ||||
|       'chatCallRoom', | ||||
|       pathParameters: { | ||||
|         'scope': _channel!.realm?.alias ?? 'global', | ||||
|         'alias': _channel!.alias, | ||||
|       }, | ||||
|     ); | ||||
|     meet.join(confOpts); | ||||
|   } | ||||
|  | ||||
|   bool _checkMessageMergeable(SnChatMessage? a, SnChatMessage? b) { | ||||
| @@ -248,10 +181,7 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> { | ||||
|         }); | ||||
|       } | ||||
|  | ||||
|       await Future.wait([ | ||||
|         _messageController.checkUpdate(), | ||||
|         _fetchOngoingCall(), | ||||
|       ]); | ||||
|       await _messageController.checkUpdate(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
| @@ -260,23 +190,6 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> { | ||||
|     super.initState(); | ||||
|     _messageController = ChatMessageController(context); | ||||
|     _initializeChat(); | ||||
|  | ||||
|     _wsSubscription = _ws.pk.stream.listen((event) { | ||||
|       switch (event.method) { | ||||
|         case 'calls.new': | ||||
|           final payload = SnChatCall.fromJson(event.payload!); | ||||
|           if (payload.channelId == _channel?.id) { | ||||
|             setState(() => _ongoingCall = payload); | ||||
|           } | ||||
|           break; | ||||
|         case 'calls.end': | ||||
|           final payload = SnChatCall.fromJson(event.payload!); | ||||
|           if (payload.channelId == _channel?.id) { | ||||
|             setState(() => _ongoingCall = null); | ||||
|           } | ||||
|           break; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
| @@ -300,7 +213,6 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final call = context.watch<ChatCallProvider>(); | ||||
|     final ud = context.read<UserDirectoryProvider>(); | ||||
|  | ||||
|     return AppScaffold( | ||||
| @@ -324,14 +236,8 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> { | ||||
|             ), | ||||
|           if (_currentMember != null) | ||||
|             IconButton( | ||||
|               icon: _ongoingCall == null | ||||
|                   ? const Icon(Symbols.call) | ||||
|                   : const Icon(Symbols.call_end), | ||||
|               onPressed: _isCalling | ||||
|                   ? null | ||||
|                   : _ongoingCall == null | ||||
|                       ? _makeCall | ||||
|                       : _endCall, | ||||
|               icon: const Icon(Symbols.video_call), | ||||
|               onPressed: _onCallJoin, | ||||
|             ), | ||||
|           IconButton( | ||||
|             icon: const Icon(Symbols.more_vert), | ||||
| @@ -359,28 +265,6 @@ class _ChatRoomScreenState extends State<ChatRoomScreen> { | ||||
|               LoadingIndicator( | ||||
|                 isActive: _isBusy || _messageController.isAggressiveLoading, | ||||
|               ), | ||||
|               SingleChildScrollView( | ||||
|                 physics: const NeverScrollableScrollPhysics(), | ||||
|                 child: MaterialBanner( | ||||
|                   dividerColor: Colors.transparent, | ||||
|                   leading: const Icon(Symbols.call_received), | ||||
|                   content: Text('callOngoingNotice').tr().padding(top: 2), | ||||
|                   actions: [ | ||||
|                     if (call.current == null) | ||||
|                       TextButton( | ||||
|                         onPressed: _onCallJoin, | ||||
|                         child: Text('callJoin').tr(), | ||||
|                       ) | ||||
|                     else if (call.current?.channelId == _channel?.id) | ||||
|                       TextButton( | ||||
|                         onPressed: _onCallResume, | ||||
|                         child: Text('callResume').tr(), | ||||
|                       ) | ||||
|                   ], | ||||
|                 ), | ||||
|               ).height(_ongoingCall != null ? 54 : 0, animate: true).animate( | ||||
|                   const Duration(milliseconds: 300), | ||||
|                   Curves.fastLinearToSlowEaseIn), | ||||
|               if (_currentMember == null && !_isBusy) | ||||
|                 Expanded( | ||||
|                   child: Center( | ||||
|   | ||||
		Reference in New Issue
	
	Block a user