diff --git a/packages/app/android/app/src/main/AndroidManifest.xml b/packages/app/android/app/src/main/AndroidManifest.xml index 29f4f8ff0e2..4c385816b5f 100644 --- a/packages/app/android/app/src/main/AndroidManifest.xml +++ b/packages/app/android/app/src/main/AndroidManifest.xml @@ -4,6 +4,15 @@ + + + + + + + + + appImplementations = { @@ -12,4 +13,5 @@ final Set appImplementations = { NewsApp(), NotesApp(), NotificationsApp(), + SpreedApp(), }; diff --git a/packages/app/linux/flutter/generated_plugin_registrant.cc b/packages/app/linux/flutter/generated_plugin_registrant.cc index 382b29c8027..3765eeab505 100644 --- a/packages/app/linux/flutter/generated_plugin_registrant.cc +++ b/packages/app/linux/flutter/generated_plugin_registrant.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +24,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_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) screen_retriever_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); diff --git a/packages/app/linux/flutter/generated_plugins.cmake b/packages/app/linux/flutter/generated_plugins.cmake index 64a9330bf03..f1c01d0d2ce 100644 --- a/packages/app/linux/flutter/generated_plugins.cmake +++ b/packages/app/linux/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST dynamic_color emoji_picker_flutter file_selector_linux + flutter_webrtc screen_retriever url_launcher_linux window_manager diff --git a/packages/app/pubspec.lock b/packages/app/pubspec.lock index 675d4f75226..e2f738e1ca6 100644 --- a/packages/app/pubspec.lock +++ b/packages/app/pubspec.lock @@ -201,6 +201,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + dart_webrtc: + dependency: transitive + description: + name: dart_webrtc + sha256: "5897a3bdd6c7fded07e80e250260ca4c9cd61f9080911aa308b516e1206745a9" + url: "https://pub.dev" + source: hosted + version: "1.1.3" dbus: dependency: transitive description: @@ -217,6 +225,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.3" + diffutil_dart: + dependency: transitive + description: + name: diffutil_dart + sha256: e0297e4600b9797edff228ed60f4169a778ea357691ec98408fa3b72994c7d06 + url: "https://pub.dev" + source: hosted + version: "3.0.0" dynamic_color: dependency: transitive description: @@ -240,6 +256,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.6.3" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -340,6 +364,23 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.1" + flutter_chat_types: + dependency: transitive + description: + name: flutter_chat_types + sha256: e285b588f6d19d907feb1f6d912deaf22e223656769c34093b64e1c59b094fb9 + url: "https://pub.dev" + source: hosted + version: "3.6.2" + flutter_chat_ui: + dependency: transitive + description: + path: "." + ref: ab50f411da781a078fc3c5197f14bbf9614d001c + resolved-ref: ab50f411da781a078fc3c5197f14bbf9614d001c + url: "https://github.com/flyerhq/flutter_chat_ui" + source: git + version: "1.6.9" flutter_driver: dependency: transitive description: flutter @@ -361,6 +402,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0-beta.2" + flutter_link_previewer: + dependency: transitive + description: + name: flutter_link_previewer + sha256: "007069e60f42419fb59872beb7a3cc3ea21e9f1bdff5d40239f376fa62ca9f20" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + flutter_linkify: + dependency: transitive + description: + name: flutter_linkify + sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073" + url: "https://pub.dev" + source: hosted + version: "6.0.0" flutter_local_notifications: dependency: transitive description: @@ -448,6 +505,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_webrtc: + dependency: transitive + description: + name: flutter_webrtc + sha256: "577216727181cb13776a65d3e7cb33e783e740c5496335011aed4a038b28c3fe" + url: "https://pub.dev" + source: hosted + version: "0.9.47" flutter_zxing: dependency: transitive description: @@ -618,6 +683,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" + linkify: + dependency: transitive + description: + name: linkify + sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832" + url: "https://pub.dev" + source: hosted + version: "5.0.0" list_counter: dependency: transitive description: @@ -723,6 +796,13 @@ packages: relative: true source: path version: "1.0.0" + neon_spreed: + dependency: "direct main" + description: + path: "../neon/neon_spreed" + relative: true + source: path + version: "1.0.0" nested: dependency: transitive description: @@ -890,6 +970,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + photo_view: + dependency: transitive + description: + name: photo_view + sha256: "8036802a00bae2a78fc197af8a158e3e2f7b500561ed23b4c458107685e645bb" + url: "https://pub.dev" + source: hosted + version: "0.14.0" platform: dependency: transitive description: @@ -898,6 +986,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + platform_detect: + dependency: transitive + description: + name: platform_detect + sha256: "08f4ee79c0e1c4858d37e06b22352a3ebdef5466b613749a3adb03e703d4f5b0" + url: "https://pub.dev" + source: hosted + version: "2.0.11" plugin_platform_interface: dependency: transitive description: @@ -1010,6 +1106,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.9" + scroll_to_index: + dependency: transitive + description: + name: scroll_to_index + sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176 + url: "https://pub.dev" + source: hosted + version: "3.0.1" scrollable_positioned_list: dependency: transitive description: @@ -1382,6 +1486,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + visibility_detector: + dependency: transitive + description: + name: visibility_detector + sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 + url: "https://pub.dev" + source: hosted + version: "0.4.0+2" vm_service: dependency: transitive description: @@ -1422,6 +1534,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + webrtc_interface: + dependency: transitive + description: + name: webrtc_interface + sha256: "2efbd3e4e5ebeb2914253bcc51dafd3053c4b87b43f3076c74835a9deecbae3a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" webview_flutter: dependency: transitive description: diff --git a/packages/app/pubspec.yaml b/packages/app/pubspec.yaml index 915f1af9369..f98867c4a89 100644 --- a/packages/app/pubspec.yaml +++ b/packages/app/pubspec.yaml @@ -33,6 +33,10 @@ dependencies: git: url: https://github.com/nextcloud/neon path: packages/neon/neon_notifications + neon_spreed: + git: + url: https://github.com/nextcloud/neon + path: packages/neon/neon_spreed universal_io: any vector_graphics: any diff --git a/packages/app/pubspec_overrides.yaml b/packages/app/pubspec_overrides.yaml index 43095c1170f..cd203d4c995 100644 --- a/packages/app/pubspec_overrides.yaml +++ b/packages/app/pubspec_overrides.yaml @@ -1,4 +1,4 @@ -# melos_managed_dependency_overrides: dynamite_runtime,neon_framework,neon_lints,nextcloud,sort_box +# melos_managed_dependency_overrides: dynamite_runtime,file_icons,neon_dashboard,neon_files,neon_framework,neon_lints,neon_news,neon_notes,neon_notifications,neon_spreed,nextcloud,sort_box dependency_overrides: dynamite_runtime: path: ../dynamite/dynamite_runtime @@ -18,6 +18,8 @@ dependency_overrides: path: ../neon/neon_notes neon_notifications: path: ../neon/neon_notifications + neon_spreed: + path: ../neon/neon_spreed nextcloud: path: ../nextcloud sort_box: diff --git a/packages/neon/neon_spreed/lib/src/blocs/call.dart b/packages/neon/neon_spreed/lib/src/blocs/call.dart index eae826ab6e9..6b4f70d29e2 100644 --- a/packages/neon/neon_spreed/lib/src/blocs/call.dart +++ b/packages/neon/neon_spreed/lib/src/blocs/call.dart @@ -9,7 +9,20 @@ import 'package:nextcloud/nextcloud.dart'; import 'package:nextcloud/spreed.dart' as spreed; import 'package:rxdart/rxdart.dart'; -abstract class SpreedCallBlocEvents { +sealed class SpreedCallBloc implements InteractiveBloc { + factory SpreedCallBloc( + spreed.SignalingSettings settings, + NextcloudClient client, + String roomToken, + String sessionID, + ) => + _SpreedCallBloc( + settings, + client, + roomToken, + sessionID, + ); + Future leaveCall(); // ignore: avoid_positional_boolean_parameters @@ -20,9 +33,7 @@ abstract class SpreedCallBlocEvents { // ignore: avoid_positional_boolean_parameters void changeScreen(bool enabled); -} -abstract class SpreedCallBlocStates { BehaviorSubject> get remoteParticipants; BehaviorSubject get audioEnabled; @@ -30,24 +41,27 @@ abstract class SpreedCallBlocStates { BehaviorSubject get videoEnabled; BehaviorSubject get screenEnabled; + + SpreedLocalCallParticipant get localParticipant; } -class SpreedCallBloc extends InteractiveBloc implements SpreedCallBlocEvents, SpreedCallBlocStates { - SpreedCallBloc( - this._settings, - this._client, - this._roomToken, - this._sessionID, +class _SpreedCallBloc extends InteractiveBloc implements SpreedCallBloc { + _SpreedCallBloc( + this.settings, + this.client, + this.roomToken, + this.sessionID, ) { unawaited(_setupLocalParticipant().then((_) => refresh())); } - final spreed.SignalingSettings _settings; - final NextcloudClient _client; - final String _roomToken; - final String _sessionID; + final spreed.SignalingSettings settings; + final NextcloudClient client; + final String roomToken; + final String sessionID; var _listeningSignalingMessages = false; + @override late SpreedLocalCallParticipant localParticipant; @override @@ -76,7 +90,7 @@ class SpreedCallBloc extends InteractiveBloc implements SpreedCallBlocEvents, Sp @override Future refresh() async { try { - await _client.spreed.call.joinCall(token: _roomToken); + await client.spreed.call.joinCall(token: roomToken); _listenForSignalingMessages(); } on Exception catch (e, s) { debugPrint(e.toString()); @@ -88,7 +102,7 @@ class SpreedCallBloc extends InteractiveBloc implements SpreedCallBlocEvents, Sp @override Future leaveCall() async { try { - await _client.spreed.call.leaveCall(token: _roomToken); + await client.spreed.call.leaveCall(token: roomToken); } on Exception catch (e, s) { debugPrint(e.toString()); debugPrint(s.toString()); @@ -126,8 +140,8 @@ class SpreedCallBloc extends InteractiveBloc implements SpreedCallBlocEvents, Sp final renderer = await _getInitializedRenderer(); renderer.srcObject = stream; localParticipant = SpreedLocalCallParticipant( - _settings.userId!, - _sessionID, + settings.userId!, + sessionID, renderer, stream, ); @@ -137,8 +151,8 @@ class SpreedCallBloc extends InteractiveBloc implements SpreedCallBlocEvents, Sp for (final message in messages) { // TODO: Send all messages at once, needs to send it over the body and not the URL, because that gets too long try { - await _client.spreed.signaling.sendMessages( - token: _roomToken, + await client.spreed.signaling.sendMessages( + token: roomToken, messages: ContentString( (b) => b ..content = BuiltList([ @@ -147,7 +161,7 @@ class SpreedCallBloc extends InteractiveBloc implements SpreedCallBlocEvents, Sp ..fn = ContentString( (b) => b..content = message, ).toBuilder() - ..sessionId = _sessionID, + ..sessionId = sessionID, ), ]), ), @@ -187,7 +201,7 @@ class SpreedCallBloc extends InteractiveBloc implements SpreedCallBlocEvents, Sp Stream> _pullSignalingMessages() async* { while (_listeningSignalingMessages) { try { - yield (await _client.spreed.signaling.pullMessages(token: _roomToken)).body.ocs.data.toList(); + yield (await client.spreed.signaling.pullMessages(token: roomToken)).body.ocs.data.toList(); } on Exception catch (e, s) { if (e is DynamiteApiException && e.statusCode >= 500) { continue; @@ -226,7 +240,7 @@ class SpreedCallBloc extends InteractiveBloc implements SpreedCallBlocEvents, Sp signalingICECandidateMessage: null, signalingMuteMessage: spreed.SignalingMuteMessage( (b) => b - ..from = _sessionID + ..from = sessionID ..to = remoteParticipant.sessionID ..type = entry.value ? spreed.SignalingMessageType.unmute : spreed.SignalingMessageType.mute ..payload = spreed.SignalingMuteMessage_Payload( @@ -238,7 +252,7 @@ class SpreedCallBloc extends InteractiveBloc implements SpreedCallBlocEvents, Sp ]; bool _isWeakerParticipant(SpreedRemoteCallParticipant remoteParticipant) => - _sessionID.compareTo(remoteParticipant.sessionID) > 0; + sessionID.compareTo(remoteParticipant.sessionID) > 0; Future _sendOffer(SpreedRemoteCallParticipant remoteParticipant) async { debugPrint('Sending offer to ${remoteParticipant.userID} ${remoteParticipant.sessionID}'); @@ -253,7 +267,7 @@ class SpreedCallBloc extends InteractiveBloc implements SpreedCallBlocEvents, Sp signalingMuteMessage: null, signalingSessionDescriptionMessage: spreed.SignalingSessionDescriptionMessage( (b) => b - ..from = _sessionID + ..from = sessionID ..to = remoteParticipant.sessionID ..type = spreed.SignalingMessageType.offer ..payload = spreed.SignalingSessionDescriptionMessage_Payload( @@ -273,8 +287,8 @@ class SpreedCallBloc extends InteractiveBloc implements SpreedCallBlocEvents, Sp { 'sdpSemantics': 'unified-plan', 'iceServers': [ - ..._settings.stunservers.map((s) => s.toJson()), - ..._settings.turnservers.map((s) => s.toJson()), + ...settings.stunservers.map((s) => s.toJson()), + ...settings.turnservers.map((s) => s.toJson()), ], }, ); @@ -297,7 +311,7 @@ class SpreedCallBloc extends InteractiveBloc implements SpreedCallBlocEvents, Sp ( signalingICECandidateMessage: spreed.SignalingICECandidateMessage( (b) => b - ..from = _sessionID + ..from = sessionID ..to = remoteParticipant.sessionID ..type = spreed.SignalingMessageType.answer ..payload = spreed.SignalingICECandidateMessage_Payload( @@ -353,7 +367,7 @@ class SpreedCallBloc extends InteractiveBloc implements SpreedCallBlocEvents, Sp for (final user in users) { if (currentParticipants.where((currentParticipant) => user.userId == currentParticipant.userID).isEmpty && - user.sessionId != _sessionID) { + user.sessionId != sessionID) { final remoteParticipant = SpreedRemoteCallParticipant( user.userId, user.sessionId, @@ -398,7 +412,7 @@ class SpreedCallBloc extends InteractiveBloc implements SpreedCallBlocEvents, Sp signalingMuteMessage: null, signalingSessionDescriptionMessage: spreed.SignalingSessionDescriptionMessage( (b) => b - ..from = _sessionID + ..from = sessionID ..to = remoteParticipant.sessionID ..type = spreed.SignalingMessageType.answer ..payload = spreed.SignalingSessionDescriptionMessage_Payload( diff --git a/packages/neon/neon_spreed/lib/src/blocs/room.dart b/packages/neon/neon_spreed/lib/src/blocs/room.dart index 5c31e11eada..cf825a3b451 100644 --- a/packages/neon/neon_spreed/lib/src/blocs/room.dart +++ b/packages/neon/neon_spreed/lib/src/blocs/room.dart @@ -9,15 +9,24 @@ import 'package:neon_spreed/src/options.dart'; import 'package:nextcloud/spreed.dart' as spreed; import 'package:rxdart/rxdart.dart'; -abstract class SpreedRoomBlocEvents { - void loadMoreMessages(); +sealed class SpreedRoomBloc implements InteractiveBloc { + factory SpreedRoomBloc( + SpreedAppSpecificOptions options, + Account account, + spreed.Room room, + ) => + _SpreedRoomBloc( + options, + account, + room, + ); + + Future loadMoreMessages(); void sendMessage(String message); Future leaveRoom(); -} -abstract class SpreedRoomBlocStates { BehaviorSubject> get room; BehaviorSubject>> get messages; @@ -27,10 +36,14 @@ abstract class SpreedRoomBlocStates { BehaviorSubject get sendingMessage; BehaviorSubject get lastCommonReadMessageId; + + String get roomToken; + + String get actorId; } -class SpreedRoomBloc extends InteractiveBloc implements SpreedRoomBlocEvents, SpreedRoomBlocStates { - SpreedRoomBloc( +class _SpreedRoomBloc extends InteractiveBloc implements SpreedRoomBloc { + _SpreedRoomBloc( this.options, this.account, spreed.Room r, @@ -43,11 +56,15 @@ class SpreedRoomBloc extends InteractiveBloc implements SpreedRoomBlocEvents, Sp final SpreedAppSpecificOptions options; final Account account; + @override late final String roomToken; final _limit = 100; int? _lastKnownMessageId; + @override + String get actorId => account.username; + @override void dispose() { unawaited(room.close()); diff --git a/packages/neon/neon_spreed/lib/src/blocs/spreed.dart b/packages/neon/neon_spreed/lib/src/blocs/spreed.dart index 341b19aeaea..f28dee89b62 100644 --- a/packages/neon/neon_spreed/lib/src/blocs/spreed.dart +++ b/packages/neon/neon_spreed/lib/src/blocs/spreed.dart @@ -1,29 +1,40 @@ import 'dart:async'; +import 'package:meta/meta.dart'; import 'package:neon_framework/blocs.dart'; import 'package:neon_framework/models.dart'; import 'package:neon_framework/utils.dart'; +import 'package:neon_spreed/src/blocs/room.dart'; import 'package:neon_spreed/src/options.dart'; import 'package:nextcloud/core.dart' as core; import 'package:nextcloud/spreed.dart' as spreed; import 'package:rxdart/rxdart.dart'; -abstract class SpreedBlocEvents { +sealed class SpreedBloc implements InteractiveBloc { + @internal + factory SpreedBloc( + SpreedAppSpecificOptions options, + Account account, + ) => + _SpreedBloc(options, account); + void createRoom( spreed.RoomType type, String? roomName, core.AutocompleteResult? invite, ); -} -abstract class SpreedBlocStates { BehaviorSubject>> get rooms; BehaviorSubject get unreadCounter; + + String get actorId; + + SpreedRoomBloc getRoomBloc(spreed.Room room); } -class SpreedBloc extends InteractiveBloc implements SpreedBlocEvents, SpreedBlocStates { - SpreedBloc( +class _SpreedBloc extends InteractiveBloc implements SpreedBloc { + _SpreedBloc( this.options, this.account, ) { @@ -41,6 +52,9 @@ class SpreedBloc extends InteractiveBloc implements SpreedBlocEvents, SpreedBloc final SpreedAppSpecificOptions options; final Account account; + @override + String get actorId => account.username; + @override void dispose() { unawaited(rooms.close()); @@ -80,4 +94,11 @@ class SpreedBloc extends InteractiveBloc implements SpreedBlocEvents, SpreedBloc ), ); } + + @override + SpreedRoomBloc getRoomBloc(spreed.Room room) => SpreedRoomBloc( + options, + account, + room, + ); } diff --git a/packages/neon/neon_spreed/lib/src/pages/main.dart b/packages/neon/neon_spreed/lib/src/pages/main.dart index a70b0fb5025..35c39d5a08d 100644 --- a/packages/neon/neon_spreed/lib/src/pages/main.dart +++ b/packages/neon/neon_spreed/lib/src/pages/main.dart @@ -4,7 +4,6 @@ import 'package:neon_framework/blocs.dart'; import 'package:neon_framework/utils.dart'; import 'package:neon_framework/widgets.dart'; import 'package:neon_spreed/l10n/localizations.dart'; -import 'package:neon_spreed/src/blocs/room.dart'; import 'package:neon_spreed/src/blocs/spreed.dart'; import 'package:neon_spreed/src/dialogs/create_room.dart'; import 'package:neon_spreed/src/pages/room.dart'; @@ -80,9 +79,9 @@ class _SpreedMainPageState extends State { room.lastMessage.chatMessage != null ? (room.type == spreed.RoomType.changelog.value || (room.type == spreed.RoomType.oneToOne.value && - room.lastMessage.chatMessage!.actorId != bloc.account.username) + room.lastMessage.chatMessage!.actorId != bloc.actorId) ? room.lastMessage.chatMessage!.message - : '${room.lastMessage.chatMessage!.actorId == bloc.account.username ? SpreedLocalizations.of(context).messageYou : room.lastMessage.chatMessage!.actorDisplayName}: ${room.lastMessage.chatMessage!.message}') + : '${room.lastMessage.chatMessage!.actorId == bloc.actorId ? SpreedLocalizations.of(context).messageYou : room.lastMessage.chatMessage!.actorDisplayName}: ${room.lastMessage.chatMessage!.message}') : '', overflow: TextOverflow.ellipsis, maxLines: 1, @@ -105,11 +104,7 @@ class _SpreedMainPageState extends State { ) : null, onTap: () async { - final roomBloc = SpreedRoomBloc( - bloc.options, - bloc.account, - room, - ); + final roomBloc = bloc.getRoomBloc(room); await Navigator.of(context).push( MaterialPageRoute( builder: (context) => SpreedRoomPage( diff --git a/packages/neon/neon_spreed/lib/src/pages/room.dart b/packages/neon/neon_spreed/lib/src/pages/room.dart index 6f6d1a8e5d4..43cecfbe87c 100644 --- a/packages/neon/neon_spreed/lib/src/pages/room.dart +++ b/packages/neon/neon_spreed/lib/src/pages/room.dart @@ -53,7 +53,7 @@ class _SpreedRoomPageState extends State { ); late final user = chat_types.User( - id: widget.bloc.account.username, + id: widget.bloc.actorId, ); void onSendPressed(chat_types.PartialText partialText) { diff --git a/packages/neon/neon_spreed/pubspec.yaml b/packages/neon/neon_spreed/pubspec.yaml index 8075f1dd73f..c3fc74341a4 100644 --- a/packages/neon/neon_spreed/pubspec.yaml +++ b/packages/neon/neon_spreed/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: flutter_webrtc: ^0.9.3 go_router: ^13.0.0 intersperse: ^2.0.0 + meta: ^1.0.0 neon_framework: git: url: https://github.com/nextcloud/neon