diff --git a/android/app/build.gradle b/android/app/build.gradle index 2f45f6b..aae5d75 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -15,12 +15,12 @@ android { ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8 + jvmTarget = JavaVersion.VERSION_17 } defaultConfig { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index af1d5fa..361f833 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,15 @@ + + + + + + + + + + + + android:name="io.flutter.embedding.android.NormalTheme" + android:resource="@style/NormalTheme" + /> - - + + - - + + - + \ No newline at end of file diff --git a/ios/Podfile b/ios/Podfile index 3e44f9c..ba9304f 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -40,5 +40,9 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + # Workaround for https://github.com/flutter/flutter/issues/64502 + config.build_settings['ONLY_ACTIVE_ARCH'] = 'YES' + end end end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index fd035a8..0378e06 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -105,6 +105,9 @@ PODS: - flutter_udid (0.0.1): - Flutter - SAMKeychain + - flutter_webrtc (0.11.8): + - Flutter + - WebRTC-SDK (= 125.6422.05) - GoogleAppMeasurement (11.4.0): - GoogleAppMeasurement/AdIdSupport (= 11.4.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0) @@ -202,6 +205,7 @@ PODS: - Flutter - wakelock_plus (0.0.1): - Flutter + - WebRTC-SDK (125.6422.05) DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) @@ -214,6 +218,7 @@ DEPENDENCIES: - Flutter (from `Flutter`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) + - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`) - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) @@ -249,6 +254,7 @@ SPEC REPOS: - SDWebImage - Sentry - SwiftyGif + - WebRTC-SDK EXTERNAL SOURCES: connectivity_plus: @@ -271,6 +277,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_native_splash/ios" flutter_udid: :path: ".symlinks/plugins/flutter_udid/ios" + flutter_webrtc: + :path: ".symlinks/plugins/flutter_webrtc/ios" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" isar_flutter_libs: @@ -321,6 +329,7 @@ SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_native_splash: e8a1e01082d97a8099d973f919f57904c925008a flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04 + flutter_webrtc: 4f730f3d58a28b0afdea039c8bf4a0f616a6b20c GoogleAppMeasurement: 987769c4ca6b968f2479fbcc9fe3ce34af454b8e GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d @@ -345,7 +354,8 @@ SPEC CHECKSUMS: url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 + WebRTC-SDK: 1990a1a595bd0b59c17485ce13ff17f575732c12 -PODFILE CHECKSUM: a57f30d18f102dd3ce366b1d62a55ecbef2158e5 +PODFILE CHECKSUM: d2bdaa1cc7915e14cf47235c34a21fcb07b00390 COCOAPODS: 1.15.2 diff --git a/lib/main.dart b/lib/main.dart index a14e41f..0926143 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:easy_localization_loader/easy_localization_loader.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:provider/provider.dart'; import 'package:relative_time/relative_time.dart'; @@ -22,6 +23,7 @@ import 'package:surface/providers/websocket.dart'; import 'package:surface/router.dart'; import 'package:surface/types/chat.dart'; import 'package:surface/types/realm.dart'; +import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -41,6 +43,9 @@ void main() async { debugInvertOversizedImages = true; } + GoRouter.optionURLReflectsImperativeAPIs = true; + usePathUrlStrategy(); + await SentryFlutter.init( (options) { options.dsn = diff --git a/lib/router.dart b/lib/router.dart index 3e5eaef..9ff4f60 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -10,6 +10,7 @@ import 'package:surface/screens/album.dart'; import 'package:surface/screens/auth/login.dart'; import 'package:surface/screens/auth/register.dart'; import 'package:surface/screens/chat.dart'; +import 'package:surface/screens/chat/call_room.dart'; import 'package:surface/screens/chat/manage.dart'; import 'package:surface/screens/chat/room.dart'; import 'package:surface/screens/explore.dart'; @@ -47,7 +48,7 @@ final _appRoutes = [ ), routes: [ GoRoute( - path: '/post/write/:mode', + path: '/write/:mode', name: 'postEditor', builder: (context, state) => AppBackground( isLessOptimization: true, @@ -66,14 +67,14 @@ final _appRoutes = [ ), ), GoRoute( - path: '/post/search', + path: '/search', name: 'postSearch', builder: (context, state) => const AppBackground( child: PostSearchScreen(), ), ), GoRoute( - path: '/post/:slug', + path: '/:slug', name: 'postDetail', builder: (context, state) => AppBackground( child: PostDetailScreen( @@ -99,7 +100,7 @@ final _appRoutes = [ ), routes: [ GoRoute( - path: '/chat/:scope/:alias', + path: '/:scope/:alias', name: 'chatRoom', builder: (context, state) => AppBackground( isLessOptimization: true, @@ -110,7 +111,18 @@ final _appRoutes = [ ), ), GoRoute( - path: '/chat/manage', + path: '/:scope/:alias/call', + name: 'chatCallRoom', + builder: (context, state) => AppBackground( + isLessOptimization: true, + child: CallRoomScreen( + scope: state.pathParameters['scope']!, + alias: state.pathParameters['alias']!, + ), + ), + ), + GoRoute( + path: '/manage', name: 'chatManage', pageBuilder: (context, state) => CustomTransitionPage( child: ChatManageScreen(), @@ -138,7 +150,7 @@ final _appRoutes = [ ), routes: [ GoRoute( - path: '/realm/manage', + path: '/manage', name: 'realmManage', pageBuilder: (context, state) => CustomTransitionPage( child: RealmManageScreen( diff --git a/lib/screens/chat/call_room.dart b/lib/screens/chat/call_room.dart new file mode 100644 index 0000000..e644c44 --- /dev/null +++ b/lib/screens/chat/call_room.dart @@ -0,0 +1,137 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_webrtc/flutter_webrtc.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +class CallRoomScreen extends StatefulWidget { + final String scope; + final String alias; + const CallRoomScreen({super.key, required this.scope, required this.alias}); + + @override + State createState() => _CallRoomScreenState(); +} + +const _kLocalWebRtcBaseUrl = 'http://localhost:8001'; + +class _CallRoomScreenState extends State { + RTCPeerConnection? _peerConnection; + MediaStream? _localStream; + WebSocketChannel? _wsChannel; + + @override + void initState() { + super.initState(); + _initWebRtc(); + } + + Future _initWebRtc() async { + final client = Dio(); + client.options.baseUrl = _kLocalWebRtcBaseUrl; + + final configResp = await client.get('/.well-known/webrtc'); + + // Get user media (audio only) + _localStream = await navigator.mediaDevices.getUserMedia({ + 'audio': true, + 'video': false, + }); + + // Configure Peer Connection + Map config = { + 'iceServers': configResp.data['ice_servers'] + }; + + _peerConnection = await createPeerConnection(config); + + // Add local stream to peer connection + _peerConnection?.addStream(_localStream!); + + // Listen for ICE candidates + _peerConnection?.onIceCandidate = (RTCIceCandidate candidate) { + print('New ICE candidate: ${candidate.candidate}'); + // Send the candidate to the signaling server + }; + + // Handle remote stream + _peerConnection?.onAddStream = (MediaStream stream) { + print('Remote stream added'); + // Play the remote stream + }; + + _wsChannel = WebSocketChannel.connect( + Uri.parse('$_kLocalWebRtcBaseUrl/webrtc'), + ); + await _wsChannel!.ready; + + _wsChannel!.stream.listen((event) { + final Map data = jsonDecode(event); + + switch (data['type']) { + case 'offer': + _handleOffer(data); + break; + case 'answer': + _handleAnswer(data); + break; + case 'candidate': + _handleCandidate(data); + break; + } + }); + } + + Future _handleOffer(Map data) async { + // Set remote description + final offer = RTCSessionDescription(data['sdp'], data['type']); + await _peerConnection?.setRemoteDescription(offer); + + // Create and send answer + final answer = await _peerConnection?.createAnswer(); + await _peerConnection?.setLocalDescription(answer!); + + _wsChannel?.sink.add({ + 'type': 'answer', + 'sdp': answer?.sdp, + }); + } + + Future _handleAnswer(Map data) async { + // Set remote description + final answer = RTCSessionDescription(data['sdp'], data['type']); + await _peerConnection?.setRemoteDescription(answer); + } + + Future _handleCandidate(Map data) async { + // Add ICE candidate + final candidate = RTCIceCandidate( + data['candidate'], + data['sdpMid'], + data['sdpMLineIndex'], + ); + await _peerConnection?.addCandidate(candidate); + } + + @override + void dispose() { + _wsChannel?.sink.close(); + _localStream?.dispose(); + _peerConnection?.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Voice Chat')), + body: Center( + child: ElevatedButton( + onPressed: () {}, + child: Text('Start Call'), + ), + ), + ); + } +} diff --git a/lib/screens/chat/room.dart b/lib/screens/chat/room.dart index f3bb3c6..5c570d6 100644 --- a/lib/screens/chat/room.dart +++ b/lib/screens/chat/room.dart @@ -1,5 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.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'; @@ -62,6 +64,18 @@ class _ChatRoomScreenState extends State { return Scaffold( appBar: AppBar( title: Text(_channel?.name ?? 'loading'.tr()), + actions: [ + IconButton( + onPressed: () { + GoRouter.of(context).pushNamed('chatCallRoom', pathParameters: { + 'scope': widget.scope, + 'alias': widget.alias, + }); + }, + icon: const Icon(Symbols.voice_chat), + ), + IconButton(onPressed: () {}, icon: const Icon(Symbols.more_vert)), + ], ), body: ListenableBuilder( listenable: _messageController, diff --git a/lib/screens/post/post_detail.dart b/lib/screens/post/post_detail.dart index 2b4876e..f1e7606 100644 --- a/lib/screens/post/post_detail.dart +++ b/lib/screens/post/post_detail.dart @@ -122,21 +122,25 @@ class _PostDetailScreenState extends State { const SliverToBoxAdapter(child: Divider(height: 1)), if (_data != null) SliverToBoxAdapter( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Icon(Symbols.comment, size: 24), - const Gap(16), - Text('postCommentsDetailed') - .plural(_data!.metric.replyCount) - .textStyle(Theme.of(context).textTheme.titleLarge!), - ], - ).padding(horizontal: 20, vertical: 12), + child: Container( + constraints: const BoxConstraints(maxWidth: 640), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon(Symbols.comment, size: 24), + const Gap(16), + Text('postCommentsDetailed') + .plural(_data!.metric.replyCount) + .textStyle(Theme.of(context).textTheme.titleLarge!), + ], + ).padding(horizontal: 20, vertical: 12).center(), + ), ), if (_data != null && ua.isAuthorized) SliverToBoxAdapter( child: Container( height: 240, + constraints: const BoxConstraints(maxWidth: 640), decoration: BoxDecoration( border: Border.symmetric( horizontal: BorderSide( @@ -158,7 +162,7 @@ class _PostDetailScreenState extends State { _childListKey.currentState!.refresh(); }, ), - ), + ).center(), ), if (_data != null) PostCommentSliverList( diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 66ab3fc..6e77527 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,7 +8,7 @@ #include #include -#include +#include #include #include #include @@ -22,9 +22,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_udid_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterUdidPlugin"); flutter_udid_plugin_register_with_registrar(flutter_udid_registrar); - g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin"); - isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin"); + flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar); g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 51673e7..38d7bf8 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,7 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux flutter_udid - isar_flutter_libs + flutter_webrtc media_kit_libs_linux media_kit_video pasteboard diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 2d9b468..517dd3b 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -11,7 +11,7 @@ import firebase_analytics import firebase_core import firebase_messaging import flutter_udid -import isar_flutter_libs +import flutter_webrtc import media_kit_libs_macos_video import media_kit_video import package_info_plus @@ -31,7 +31,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin")) - IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) + FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 7180182..f59db33 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -310,6 +310,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.7" + dart_webrtc: + dependency: transitive + description: + name: dart_webrtc + sha256: c664ad88d5646735753add421ee2118486c100febef5e92b7f59cdbabf6a51f6 + url: "https://pub.dev" + source: hosted + version: "1.4.9" dbus: dependency: transitive description: @@ -630,10 +638,18 @@ packages: source: hosted version: "3.0.0" flutter_web_plugins: - dependency: transitive + dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_webrtc: + dependency: "direct main" + description: + name: flutter_webrtc + sha256: "0b69ecab98211504c10d40c1c4cb48eb387e03ea8e732079bd0d2665d8c20d3f" + url: "https://pub.dev" + source: hosted + version: "0.12.1+hotfix.1" freezed: dependency: "direct dev" description: @@ -858,22 +874,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" - isar: - dependency: "direct main" - description: - name: isar - sha256: "99165dadb2cf2329d3140198363a7e7bff9bbd441871898a87e26914d25cf1ea" - url: "https://pub.dev" - source: hosted - version: "3.1.0+1" - isar_flutter_libs: - dependency: "direct main" - description: - name: isar_flutter_libs - sha256: bc6768cc4b9c61aabff77152e7f33b4b17d2fc93134f7af1c3dd51500fe8d5e8 - url: "https://pub.dev" - source: hosted - version: "3.1.0+1" jni: dependency: transitive description: @@ -1226,6 +1226,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.6" + platform_detect: + dependency: transitive + description: + name: platform_detect + sha256: a62f99417fc4fa2d099ce0ccdbb1bd3977920f2a64292c326271f049d4bc3a4f + url: "https://pub.dev" + source: hosted + version: "2.1.0" plugin_platform_interface: dependency: transitive description: @@ -1791,6 +1799,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + webrtc_interface: + dependency: transitive + description: + name: webrtc_interface + sha256: abec3ab7956bd5ac539cf34a42fa0c82ea26675847c0966bb85160400eea9388 + url: "https://pub.dev" + source: hosted + version: "1.2.0" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2c6d447..6f7346e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_web_plugins: + sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. @@ -73,8 +75,6 @@ dependencies: collection: ^1.18.0 mime: ^2.0.0 web_socket_channel: ^3.0.1 - isar: ^3.1.0+1 - isar_flutter_libs: ^3.1.0+1 hive: ^2.2.3 hive_flutter: ^1.1.0 swipe_to: ^1.0.6 @@ -88,6 +88,7 @@ dependencies: pasteboard: ^0.3.0 sentry_flutter: ^8.10.1 synchronized: ^3.3.0+3 + flutter_webrtc: ^0.12.1+hotfix.1 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index b0b33d1..e6374be 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include #include #include @@ -27,8 +27,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); FlutterUdidPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterUdidPluginCApi")); - IsarFlutterLibsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); + FlutterWebRTCPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); MediaKitVideoPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 75f22ee..4fe8285 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -7,7 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows firebase_core flutter_udid - isar_flutter_libs + flutter_webrtc media_kit_libs_windows_video media_kit_video pasteboard