diff --git a/CHANGELOG.md b/CHANGELOG.md index 39b05b80ddb..46eb14cdf9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ All user visible changes to this project will be documented in this file. This p - Media panel: - Participants redialing. ([#241], [#233]) - Screen share display choosing on desktop. ([#228], [#222]) + - Opening in separate window on desktop. ([#93], [#87]) - Contacts tab: - Favorite contacts. ([#285], [#237], [#223]) - Searching. ([#323], [#310], [#260], [#259]) @@ -84,6 +85,8 @@ All user visible changes to this project will be documented in this file. This p - Context menu not opening over video previews. ([#198], [#196]) [#63]: /../../issues/63 +[#87]: /../../issues/87 +[#93]: /../../pull/93 [#102]: /../../issues/102 [#126]: /../../issues/126 [#134]: /../../issues/134 diff --git a/lib/domain/model/ongoing_call.dart b/lib/domain/model/ongoing_call.dart index 051e6c42232..c2baa3c65b6 100644 --- a/lib/domain/model/ongoing_call.dart +++ b/lib/domain/model/ongoing_call.dart @@ -1270,7 +1270,6 @@ class OngoingCall { for (Track t in members.values.expand((e) => e.tracks)) { t.dispose(); } - members.removeWhere((id, _) => id != _me); } /// Updates the local media settings with [audioDevice], [videoDevice] or diff --git a/lib/domain/repository/call.dart b/lib/domain/repository/call.dart index b4d1b669e39..9bc785b5d10 100644 --- a/lib/domain/repository/call.dart +++ b/lib/domain/repository/call.dart @@ -45,10 +45,10 @@ abstract class AbstractCallRepository { /// Adds the provided [ChatCall] to the [calls], if not already. Rx? add(ChatCall call); - /// Transforms the provided [WebStoredCall] into an [OngoingCall] and adds it, + /// Transforms the provided [StoredCall] into an [OngoingCall] and adds it, /// if not already. Rx addStored( - WebStoredCall stored, { + StoredCall stored, { bool withAudio = true, bool withVideo = true, bool withScreen = false, diff --git a/lib/domain/service/auth.dart b/lib/domain/service/auth.dart index fdd8df0f7a4..673dd297085 100644 --- a/lib/domain/service/auth.dart +++ b/lib/domain/service/auth.dart @@ -18,6 +18,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:get/get.dart'; import 'package:mutex/mutex.dart'; @@ -30,6 +31,7 @@ import '../repository/auth.dart'; import '/provider/gql/exceptions.dart'; import '/provider/hive/session.dart'; import '/routes.dart'; +import '/util/platform_utils.dart'; import '/util/web/web_utils.dart'; /// Authentication service exposing [credentials] of the authenticated session. @@ -118,31 +120,61 @@ class AuthService extends GetxService { RememberedSession? remembered = creds?.rememberedSession; // Listen to the [Credentials] changes if this window is a popup. - if (WebUtils.isPopup) { - _storageSubscription = WebUtils.onStorageChange.listen((e) { - if (e.key == 'credentials') { - if (e.newValue == null) { - _authRepository.token = null; - credentials.value = null; - _status.value = RxStatus.empty(); - } else { - Credentials creds = Credentials.fromJson(json.decode(e.newValue!)); - _authRepository.token = creds.session.token; - _authRepository.applyToken(); - credentials.value = creds; - _status.value = RxStatus.success(); - } + if (PlatformUtils.isPopup) { + onCredentials(String? newCredentials) { + if (newCredentials == null) { + _authRepository.token = null; + credentials.value = null; + _status.value = RxStatus.empty(); + } else { + Credentials creds = Credentials.fromJson(json.decode(newCredentials)); + _authRepository.token = creds.session.token; + _authRepository.applyToken(); + credentials.value = creds; + _status.value = RxStatus.success(); + } - if (_tokenGuard.isLocked) { - _tokenGuard.release(); - } + if (_tokenGuard.isLocked) { + _tokenGuard.release(); } - }); + } + + if (PlatformUtils.isWeb) { + _storageSubscription = WebUtils.onStorageChange.listen((e) { + if (e.key == 'credentials') { + onCredentials(e.newValue); + } + }); + } else { + DesktopMultiWindow.addMethodHandler((methodCall, fromWindowId) async { + if (methodCall.method == 'credentials') { + onCredentials(methodCall.arguments); + } + }); + } } else { // Update the [Credentials] otherwise. WebUtils.credentials = creds; - _sessionSubscription = _sessionProvider.boxEvents - .listen((e) => WebUtils.credentials = e.value?.credentials); + _sessionSubscription = _sessionProvider.boxEvents.listen((e) async { + if (PlatformUtils.isWeb) { + WebUtils.credentials = e.value?.credentials; + } else if (!PlatformUtils.isPopup) { + try { + List windows = await DesktopMultiWindow.getAllSubWindowIds(); + for (var w in windows) { + DesktopMultiWindow.invokeMethod( + w, + 'credentials', + e.value?.credentials == null + ? null + : json.encode(e.value?.credentials.toJson()), + ); + } + } catch (_) { + // No-op. + } + } + }); WebUtils.removeAllCalls(); } @@ -324,9 +356,9 @@ class AuthService extends GetxService { bool alreadyRenewing = _tokenGuard.isLocked; // Acquire the lock if this window is a popup. - if (WebUtils.isPopup) { + if (PlatformUtils.isPopup) { // The lock will be release once new [Credentials] are acquired via the - // [WebUtils.onStorageChange] stream. + // [WebUtils.onStorageChange] stream or method handler. await _tokenGuard.acquire(); alreadyRenewing = true; } diff --git a/lib/domain/service/call.dart b/lib/domain/service/call.dart index 09c8c2c8964..fad165f3304 100644 --- a/lib/domain/service/call.dart +++ b/lib/domain/service/call.dart @@ -33,6 +33,7 @@ import '/provider/gql/exceptions.dart' show TransformDialogCallIntoGroupCallException; import '/store/event/chat_call.dart'; import '/util/obs/obs.dart'; +import '/util/platform_utils.dart'; import '/util/web/web_utils.dart'; import 'disposable_service.dart'; @@ -68,7 +69,7 @@ class CallService extends DisposableService { }) async { final Rx? stored = _callsRepo[chatId]; - if (WebUtils.containsCall(chatId)) { + if (PlatformUtils.inPopup(chatId)) { throw CallIsInPopupException(); } else if (stored != null && stored.value.state.value != OngoingCallState.ended) { @@ -98,7 +99,7 @@ class CallService extends DisposableService { bool withVideo = false, bool withScreen = false, }) async { - if (WebUtils.containsCall(chatId) && !WebUtils.isPopup) { + if (PlatformUtils.inPopup(chatId)) { throw CallIsInPopupException(); } @@ -163,7 +164,7 @@ class CallService extends DisposableService { if (call != null) { // Closing the popup window will kill the pending requests, so it's // required to await the decline. - if (WebUtils.isPopup) { + if (PlatformUtils.isPopup) { await _callsRepo.decline(chatId); call.value.state.value = OngoingCallState.ended; call.value.dispose(); @@ -177,7 +178,7 @@ class CallService extends DisposableService { /// Constructs an [OngoingCall] from the provided [stored] call. Rx addStored( - WebStoredCall stored, { + StoredCall stored, { bool withAudio = true, bool withVideo = true, bool withScreen = false, @@ -250,9 +251,12 @@ class CallService extends DisposableService { if (call != null) { _callsRepo.move(chatId, newChatId); _callsRepo.moveCredentials(callId, newCallId); - if (WebUtils.isPopup) { - WebUtils.moveCall(chatId, newChatId, newState: call.value.toStored()); - } + + PlatformUtils.moveCall( + chatId, + newChatId, + newState: call.value.toStored(), + ); } } diff --git a/lib/main.dart b/lib/main.dart index 5459940b1d0..6c0e8a608c4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,7 +22,10 @@ library main; import 'dart:async'; +import 'dart:convert'; +import 'package:collection/collection.dart'; +import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart' @@ -35,6 +38,7 @@ import 'package:universal_io/io.dart'; import 'package:window_manager/window_manager.dart'; import 'config.dart'; +import '/domain/model/session.dart'; import 'domain/repository/auth.dart'; import 'domain/service/auth.dart'; import 'domain/service/notification.dart'; @@ -54,18 +58,60 @@ import 'util/platform_utils.dart'; import 'util/web/web_utils.dart'; /// Entry point of this application. -Future main() async { +Future main(List args) async { await Config.init(); + bool isSeparateWindow = args.firstOrNull == 'multi_window'; + StoredCall? call; + Credentials? credentials; + + if (isSeparateWindow) { + PlatformUtils.windowId = int.parse(args[1]); + final argument = json.decode(args[2]) as Map; + call = StoredCall.fromJson(json.decode(argument['call'] as String)); + credentials = + Credentials.fromJson(json.decode(argument['credentials'] as String)); + + WindowController windowController = + WindowController.fromWindowId(PlatformUtils.windowId!); + windowController.setOnWindowClose(() async { + await DesktopMultiWindow.invokeMethod( + DesktopMultiWindow.mainWindowId, + 'call_${call!.chatId.val}', + ); + }); + } + // Initializes and runs the [App]. Future appRunner() async { WebUtils.setPathUrlStrategy(); - await _initHive(); + String hiveDir = + 'hive${isSeparateWindow ? '/${PlatformUtils.windowId}' : ''}'; + await _initHive(hiveDir, credentials: credentials); - if (PlatformUtils.isDesktop && !PlatformUtils.isWeb) { + if (PlatformUtils.isDesktop && !PlatformUtils.isWeb && !isSeparateWindow) { await windowManager.ensureInitialized(); + WindowManager.instance.setPreventClose(true); + WindowManager.instance.addListener( + DesktopWindowListener( + onClose: () { + Future.sync(() async { + try { + var windows = await DesktopMultiWindow.getAllSubWindowIds(); + await Future.wait(windows + .map((e) => WindowController.fromWindowId(e).close())); + await Future.delayed(100.milliseconds); + } finally { + await WindowManager.instance.setPreventClose(false); + WindowManager.instance.close(); + } + }); + }, + ), + ); + final WindowPreferencesHiveProvider preferences = Get.find(); final WindowPreferences? prefs = preferences.get(); @@ -87,12 +133,17 @@ Future main() async { Get.put(AuthRepository(graphQlProvider)); final authService = Get.put(AuthService(AuthRepository(graphQlProvider), Get.find())); + await authService.init(); router = RouterState(authService); + if (isSeparateWindow) { + router.call(call!.chatId, call: call); + } - Get.put(NotificationService()) - .init(onNotificationResponse: onNotificationResponse); + if (!PlatformUtils.isPopup) { + Get.put(NotificationService()) + .init(onNotificationResponse: onNotificationResponse); + } - await authService.init(); await L10n.init(); Get.put(BackgroundWorker(Get.find())); @@ -176,8 +227,8 @@ class App extends StatelessWidget { /// Initializes a [Hive] storage and registers a [SessionDataHiveProvider] in /// the [Get]'s context. -Future _initHive() async { - await Hive.initFlutter('hive'); +Future _initHive(String dir, {Credentials? credentials}) async { + await Hive.initFlutter(dir); // Load and compare application version. Box box = await Hive.openBox('version'); @@ -187,15 +238,19 @@ Future _initHive() async { // If mismatch is detected, then clean the existing [Hive] cache. if (stored != version) { await Hive.close(); - await Hive.clean('hive'); - await Hive.initFlutter('hive'); + await Hive.clean(dir); + await Hive.initFlutter(dir); Hive.openBox('version').then((box) async { await box.put(0, version); await box.close(); }); } - await Get.put(SessionDataHiveProvider()).init(); + var sessionProvider = Get.put(SessionDataHiveProvider()); + await sessionProvider.init(); + if (credentials != null) { + await sessionProvider.setCredentials(credentials); + } await Get.put(WindowPreferencesHiveProvider()).init(); } diff --git a/lib/routes.dart b/lib/routes.dart index 0452c32af99..1bae492790f 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -343,6 +343,10 @@ class AppRouterDelegate extends RouterDelegate @override Future setNewRoutePath(RouteConfiguration configuration) async { + if (_state.route.startsWith(Routes.call)) { + return; + } + _state.routes.value = [configuration.route]; if (configuration.tab != null) { _state.tab = configuration.tab!; @@ -726,6 +730,13 @@ extension RouteLinks on RouterState { /// Changes router location to the [Routes.chatInfo] page. void chatInfo(ChatId id) => go('${Routes.chat}/$id${Routes.chatInfo}'); + + /// Changes router location to the [Routes.call] page. + void call(ChatId id, {StoredCall? call}) { + go('${Routes.call}/$id'); + + arguments = {'call': call}; + } } /// Extension adding helper methods to an [AppLifecycleState]. diff --git a/lib/store/call.dart b/lib/store/call.dart index b6722270d86..cddc0bcad44 100644 --- a/lib/store/call.dart +++ b/lib/store/call.dart @@ -139,7 +139,7 @@ class CallRepository extends DisposableInterface @override Rx addStored( - WebStoredCall stored, { + StoredCall stored, { bool withAudio = true, bool withVideo = true, bool withScreen = false, diff --git a/lib/ui/page/call/component/desktop.dart b/lib/ui/page/call/component/desktop.dart index d724ea01fc7..86a189ec15a 100644 --- a/lib/ui/page/call/component/desktop.dart +++ b/lib/ui/page/call/component/desktop.dart @@ -57,6 +57,10 @@ import 'common.dart'; Widget desktopCall(CallController c, BuildContext context) { return LayoutBuilder( builder: (context, constraints) { + if (c.state.value == OngoingCallState.ended) { + return Container(); + } + // Call stackable content. List content = [ SvgLoader.asset( @@ -563,7 +567,7 @@ Widget desktopCall(CallController c, BuildContext context) { width: 290, padding: EdgeInsets.only( top: 10 + - (WebUtils.isPopup + (PlatformUtils.isPopup ? 0 : CallController.titleHeight)), child: HintWidget( @@ -842,7 +846,7 @@ Widget desktopCall(CallController c, BuildContext context) { body: Column( mainAxisSize: MainAxisSize.min, children: [ - if (!WebUtils.isPopup) + if (!PlatformUtils.isPopup) GestureDetector( behavior: HitTestBehavior.translucent, onPanUpdate: (d) { @@ -1125,7 +1129,7 @@ Widget _titleBar(BuildContext context, CallController c) => Obx(() { child: ConstrainedBox( constraints: BoxConstraints(maxWidth: c.size.width - 60), child: InkWell( - onTap: WebUtils.isPopup + onTap: PlatformUtils.isPopup ? null : () { router.chat(c.chatId.value); @@ -1224,7 +1228,7 @@ Widget _primaryView(CallController c) { onOffset: () { if (c.minimized.value && !c.fullscreen.value) { return Offset(-c.left.value, -c.top.value - 30); - } else if (!WebUtils.isPopup) { + } else if (!PlatformUtils.isPopup) { return const Offset(0, -30); } @@ -1824,7 +1828,7 @@ Widget _secondaryView(CallController c, BuildContext context) { onOffset: () { if (c.minimized.value && !c.fullscreen.value) { return Offset(-c.left.value, -c.top.value - 30); - } else if (!WebUtils.isPopup) { + } else if (!PlatformUtils.isPopup) { return const Offset(0, -30); } diff --git a/lib/ui/page/call/controller.dart b/lib/ui/page/call/controller.dart index ab33c319434..1279805e22f 100644 --- a/lib/ui/page/call/controller.dart +++ b/lib/ui/page/call/controller.dart @@ -437,8 +437,8 @@ class CallController extends GetxController { // TODO: Account [BuildContext.mediaQueryPadding]. return router.context!.mediaQuerySize; } else { - // If not [WebUtils.isPopup], then subtract the title bar from the height. - if (fullscreen.isTrue && !WebUtils.isPopup) { + // If not [PlatformUtils.isPopup], then subtract the title bar from the height. + if (fullscreen.isTrue && !PlatformUtils.isPopup) { var size = router.context!.mediaQuerySize; return Size(size.width, size.height - titleHeight); } else { @@ -568,14 +568,14 @@ class CallController extends GetxController { secondaryBottomShifted = secondaryBottom.value; } - // Update the [WebUtils.title] if this call is in a popup. - if (WebUtils.isPopup) { + // Update the window title if this call is in a popup. + if (PlatformUtils.isPopup) { _titleSubscription?.cancel(); _durationSubscription?.cancel(); if (v != null) { void updateTitle() { - WebUtils.title( + PlatformUtils.setTitle( '\u205f​​​ \u205f​​​${'label_call_title'.l10nfmt(titleArguments)}\u205f​​​ \u205f​​​', ); } @@ -589,14 +589,6 @@ class CallController extends GetxController { } } - _chatService - .get(_currentCall.value.chatId.value) - .then(onChat) - .whenComplete(() { - members.forEach((_, value) => _putMember(value)); - _insureCorrectGrouping(); - }); - _chatWorker = ever( _currentCall.value.chatId, (ChatId id) => _chatService.get(id).then(onChat), @@ -802,39 +794,48 @@ class CallController extends GetxController { } } - _membersTracksSubscriptions = _currentCall.value.members.map( - (k, v) => - MapEntry(k, v.tracks.changes.listen((c) => onTracksChanged(v, c))), - ); + _chatService + .get(_currentCall.value.chatId.value) + .then(onChat) + .whenComplete(() { + members.forEach((_, value) => _putMember(value)); + _insureCorrectGrouping(); - _membersSubscription = _currentCall.value.members.changes.listen((e) { - switch (e.op) { - case OperationKind.added: - _putMember(e.value!); - _membersTracksSubscriptions[e.key!] = e.value!.tracks.changes.listen( - (c) => onTracksChanged(e.value!, c), - ); + _membersSubscription = _currentCall.value.members.changes.listen((e) { + switch (e.op) { + case OperationKind.added: + _putMember(e.value!); + _membersTracksSubscriptions[e.key!] = + e.value!.tracks.changes.listen( + (c) => onTracksChanged(e.value!, c), + ); - _insureCorrectGrouping(); - break; + _insureCorrectGrouping(); + break; + + case OperationKind.removed: + bool wasNotEmpty = primary.isNotEmpty; + paneled.removeWhere((m) => m.member.id == e.key); + locals.removeWhere((m) => m.member.id == e.key); + focused.removeWhere((m) => m.member.id == e.key); + remotes.removeWhere((m) => m.member.id == e.key); + _membersTracksSubscriptions.remove(e.key)?.cancel(); + _insureCorrectGrouping(); + if (wasNotEmpty && primary.isEmpty) { + focusAll(); + } + break; - case OperationKind.removed: - bool wasNotEmpty = primary.isNotEmpty; - paneled.removeWhere((m) => m.member.id == e.key); - locals.removeWhere((m) => m.member.id == e.key); - focused.removeWhere((m) => m.member.id == e.key); - remotes.removeWhere((m) => m.member.id == e.key); - _membersTracksSubscriptions.remove(e.key)?.cancel(); - _insureCorrectGrouping(); - if (wasNotEmpty && primary.isEmpty) { - focusAll(); - } - break; + case OperationKind.updated: + _insureCorrectGrouping(); + break; + } + }); - case OperationKind.updated: - _insureCorrectGrouping(); - break; - } + _membersTracksSubscriptions = _currentCall.value.members.map( + (k, v) => + MapEntry(k, v.tracks.changes.listen((c) => onTracksChanged(v, c))), + ); }); } @@ -1400,9 +1401,11 @@ class CallController extends GetxController { if (fullscreen.isTrue) { secondaryLeft.value = offset.dx - secondaryPanningOffset!.dx; secondaryTop.value = offset.dy - - ((WebUtils.isPopup || router.context!.isMobile) ? 0 : titleHeight) - + ((PlatformUtils.isPopup || router.context!.isMobile) + ? 0 + : titleHeight) - secondaryPanningOffset!.dy; - } else if (WebUtils.isPopup) { + } else if (PlatformUtils.isPopup) { secondaryLeft.value = offset.dx - secondaryPanningOffset!.dx; secondaryTop.value = offset.dy - secondaryPanningOffset!.dy; } else { diff --git a/lib/ui/page/call/view.dart b/lib/ui/page/call/view.dart index b77faf63854..6594503f7d5 100644 --- a/lib/ui/page/call/view.dart +++ b/lib/ui/page/call/view.dart @@ -20,7 +20,6 @@ import 'package:get/get.dart'; import '/domain/model/ongoing_call.dart'; import '/util/platform_utils.dart'; -import '/util/web/web_utils.dart'; import 'component/desktop.dart'; import 'component/mobile.dart'; import 'controller.dart'; @@ -44,7 +43,7 @@ class CallView extends StatelessWidget { ), tag: key?.hashCode.toString(), builder: (CallController c) { - if (WebUtils.isPopup) { + if (PlatformUtils.isPopup) { c.minimized.value = false; return Stack( clipBehavior: Clip.hardEdge, diff --git a/lib/ui/page/call/widget/reorderable_fit.dart b/lib/ui/page/call/widget/reorderable_fit.dart index 3763a000f3c..992f496c654 100644 --- a/lib/ui/page/call/widget/reorderable_fit.dart +++ b/lib/ui/page/call/widget/reorderable_fit.dart @@ -557,7 +557,11 @@ class _ReorderableFitState extends State<_ReorderableFit> { @override void dispose() { - _audioPlayer?.dispose(); + _audioPlayer?.dispose().onError((e, _) { + if (e is! MissingPluginException) { + throw e!; + } + }); AudioCache.instance.clear('audio/pop.mp3'); super.dispose(); @@ -640,12 +644,18 @@ class _ReorderableFitState extends State<_ReorderableFit> { onDoughBreak: () { _doughDragged = item; widget.onDoughBreak?.call(item.item); - _audioPlayer?.play( + _audioPlayer + ?.play( AssetSource('audio/pop.mp3'), volume: 0.3, position: Duration.zero, mode: PlayerMode.lowLatency, - ); + ) + .onError((e, _) { + if (e is! MissingPluginException) { + throw e!; + } + }); }, ), ), diff --git a/lib/ui/page/home/overlay/controller.dart b/lib/ui/page/home/overlay/controller.dart index 1f2b4a0e5f0..93497a7c1e3 100644 --- a/lib/ui/page/home/overlay/controller.dart +++ b/lib/ui/page/home/overlay/controller.dart @@ -16,17 +16,21 @@ // . import 'dart:async'; +import 'dart:convert'; +import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/widgets.dart'; import 'package:get/get.dart'; import '/domain/model/application_settings.dart'; -import '/domain/model/chat_call.dart'; import '/domain/model/chat.dart'; +import '/domain/model/chat_call.dart'; import '/domain/model/ongoing_call.dart'; import '/domain/repository/settings.dart'; +import '/domain/service/auth.dart'; import '/domain/service/call.dart'; import '/l10n/l10n.dart'; +import '/util/log.dart'; import '/util/obs/obs.dart'; import '/util/platform_utils.dart'; import '/util/web/web_utils.dart'; @@ -35,11 +39,18 @@ export 'view.dart'; /// Controller of an [OngoingCall]s overlay. class CallOverlayController extends GetxController { - CallOverlayController(this._callService, this._settingsRepo); + CallOverlayController( + this._callService, + this._authService, + this._settingsRepo, + ); /// Call service used to expose the [calls]. final CallService _callService; + /// Authentication service used to get the stored [Credentials]. + final AuthService _authService; + /// Settings repository, used to get the stored [ApplicationSettings]. final AbstractSettingsRepository _settingsRepo; @@ -58,57 +69,70 @@ class CallOverlayController extends GetxController { @override void onInit() { - _subscription = _callService.calls.changes.listen((event) { + _subscription = _callService.calls.changes.listen((event) async { switch (event.op) { case OperationKind.added: bool window = false; var ongoingCall = event.value!.value; - if (PlatformUtils.isWeb && - !PlatformUtils.isMobile && + if (PlatformUtils.isDesktop && _settings.value?.enablePopups != false) { - window = WebUtils.openPopupCall( - event.key!, - withAudio: - ongoingCall.audioState.value == LocalTrackState.enabling || - ongoingCall.audioState.value == LocalTrackState.enabled, - withVideo: - ongoingCall.videoState.value == LocalTrackState.enabling || - ongoingCall.videoState.value == LocalTrackState.enabled, - withScreen: ongoingCall.screenShareState.value == - LocalTrackState.enabling || - ongoingCall.screenShareState.value == LocalTrackState.enabled, - ); - - // If [window] is `true`, then a new popup window is created, so - // treat this call as a popup windowed call. - if (window) { - WebUtils.setCall(ongoingCall.toStored()); - if (ongoingCall.callChatItemId == null || - ongoingCall.deviceId == null) { - _workers[event.key!] = ever( - event.value!.value.call, - (ChatCall? call) { - WebUtils.setCall( - WebStoredCall( - chatId: ongoingCall.chatId.value, - call: call, - creds: ongoingCall.creds, - deviceId: ongoingCall.deviceId, - state: ongoingCall.state.value, - ), - ); - - if (call?.id != null) { - _workers[event.key!]?.dispose(); - } - }, + window = true; + try { + int? windowId = await PlatformUtils.openPopup( + ongoingCall, + _authService.credentials.value, + ); + + if (PlatformUtils.isWeb) { + WebUtils.setCall(ongoingCall.toStored()); + if (ongoingCall.callChatItemId == null || + ongoingCall.deviceId == null) { + _workers[event.key!] = ever( + ongoingCall.call, + (ChatCall? call) { + WebUtils.setCall(ongoingCall.toStored()); + + if (call?.id != null) { + _workers[event.key!]?.dispose(); + } + }, + ); + } + } else { + DesktopMultiWindow.invokeMethod( + windowId!, + 'call', + json.encode(ongoingCall.toStored().toJson()), ); + + if (ongoingCall.callChatItemId == null || + ongoingCall.deviceId == null) { + _workers[event.key!] = ever( + ongoingCall.call, + (ChatCall? call) { + DesktopMultiWindow.invokeMethod( + windowId, + 'call', + json.encode(ongoingCall.toStored()), + ); + + if (call?.id != null) { + _workers[event.key!]?.dispose(); + } + }, + ); + } } - } else { - Future.delayed(Duration.zero, () { - ongoingCall.addError('err_call_popup_was_blocked'.l10n); - }); + } catch (e) { + if (PlatformUtils.isWeb) { + Future.delayed(Duration.zero, () { + ongoingCall.addError('err_call_popup_was_blocked'.l10n); + }); + } + + Log.error(e); + window = false; } } @@ -122,11 +146,7 @@ class CallOverlayController extends GetxController { case OperationKind.removed: calls.removeWhere((e) => e.call == event.value!); - - OngoingCall call = event.value!.value; - if (call.callChatItemId == null || call.connected) { - WebUtils.removeCall(event.key!); - } + PlatformUtils.removeCall(event.value!.value); break; case OperationKind.updated: diff --git a/lib/ui/page/home/overlay/view.dart b/lib/ui/page/home/overlay/view.dart index 91de6f29466..9eacbd0dd89 100644 --- a/lib/ui/page/home/overlay/view.dart +++ b/lib/ui/page/home/overlay/view.dart @@ -34,7 +34,7 @@ class CallOverlayView extends StatelessWidget { @override Widget build(BuildContext context) { return GetBuilder( - init: CallOverlayController(Get.find(), Get.find()), + init: CallOverlayController(Get.find(), Get.find(), Get.find()), builder: (CallOverlayController c) => Obx( () { return Stack( diff --git a/lib/ui/page/home/page/my_profile/view.dart b/lib/ui/page/home/page/my_profile/view.dart index 78af89bd0f3..7bb470dbe83 100644 --- a/lib/ui/page/home/page/my_profile/view.dart +++ b/lib/ui/page/home/page/my_profile/view.dart @@ -183,7 +183,7 @@ class MyProfileView extends StatelessWidget { ); case ProfileTab.calls: - if (PlatformUtils.isDesktop && PlatformUtils.isWeb) { + if (PlatformUtils.isDesktop) { return Block( title: 'label_calls'.l10n, children: [_call(context, c)], diff --git a/lib/ui/page/home/tab/menu/view.dart b/lib/ui/page/home/tab/menu/view.dart index 1b2800246bb..e6e1723b4ae 100644 --- a/lib/ui/page/home/tab/menu/view.dart +++ b/lib/ui/page/home/tab/menu/view.dart @@ -243,7 +243,7 @@ class MenuTabView extends StatelessWidget { break; case ProfileTab.calls: - if (PlatformUtils.isDesktop && PlatformUtils.isWeb) { + if (PlatformUtils.isDesktop) { child = card( icon: Icons.call, title: 'label_calls'.l10n, diff --git a/lib/ui/page/popup_call/controller.dart b/lib/ui/page/popup_call/controller.dart index 4222dcd7d94..30bcde119a0 100644 --- a/lib/ui/page/popup_call/controller.dart +++ b/lib/ui/page/popup_call/controller.dart @@ -18,6 +18,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:get/get.dart'; import 'package:wakelock/wakelock.dart'; @@ -26,6 +27,7 @@ import '/domain/model/ongoing_call.dart'; import '/domain/model/user.dart'; import '/domain/service/call.dart'; import '/routes.dart'; +import '/util/platform_utils.dart'; import '/util/web/web_utils.dart'; export 'view.dart'; @@ -43,6 +45,9 @@ class PopupCallController extends GetxController { /// [CallService] maintaining the [call]. final CallService _calls; + /// Controller of a popup window on desktop. + WindowController? _windowController; + /// [StreamSubscription] to [WebUtils.onStorageChange] communicating with the /// main application. StreamSubscription? _storageSubscription; @@ -56,46 +61,113 @@ class PopupCallController extends GetxController { @override void onInit() { - Uri uri = Uri.parse(router.route); + if (!PlatformUtils.isWeb) { + _windowController = + WindowController.fromWindowId(PlatformUtils.windowId!); + } - WebStoredCall? stored = WebUtils.getCall(chatId); - if (stored == null || WebUtils.credentials == null) { - return WebUtils.closeWindow(); + StoredCall? stored; + if (PlatformUtils.isWeb) { + stored = WebUtils.getCall(chatId); + } else if (PlatformUtils.isDesktop) { + stored = router.arguments!['call']; + } + + if (stored == null || + (PlatformUtils.isWeb && WebUtils.credentials == null)) { + if (PlatformUtils.isWeb) { + WebUtils.closeWindow(); + } else { + _windowController?.close(); + } + return; + } + + bool withAudio, withVideo, withScreen; + if (PlatformUtils.isWeb) { + Uri uri = Uri.parse(router.route); + withAudio = uri.queryParameters['audio'] != 'false'; + withVideo = uri.queryParameters['video'] == 'true'; + withScreen = uri.queryParameters['screen'] == 'true'; + } else { + withAudio = stored.withAudio; + withVideo = stored.withVideo; + withScreen = stored.withScreen; } call = _calls.addStored( stored, - withAudio: uri.queryParameters['audio'] != 'false', - withVideo: uri.queryParameters['video'] == 'true', - withScreen: uri.queryParameters['screen'] == 'true', + withAudio: withAudio, + withVideo: withVideo, + withScreen: withScreen, ); _stateWorker = ever( call.value.state, (OngoingCallState state) { - WebUtils.setCall(call.value.toStored()); - if (state == OngoingCallState.ended) { - WebUtils.closeWindow(); + if (PlatformUtils.isWeb) { + WebUtils.setCall(call.value.toStored()); + if (state == OngoingCallState.ended) { + WebUtils.closeWindow(); + } + } else { + DesktopMultiWindow.invokeMethod( + DesktopMultiWindow.mainWindowId, + 'call_${call.value.chatId.value.val}', + json.encode(call.value.toStored().toJson()), + ); + if (state == OngoingCallState.ended) { + _windowController?.close(); + } } }, ); - _storageSubscription = WebUtils.onStorageChange.listen((e) { - if (e.key == null) { - WebUtils.closeWindow(); - } else if (e.newValue == null) { - if (e.key == 'credentials' || e.key == 'call_${call.value.chatId}') { + if (PlatformUtils.isWeb) { + _storageSubscription = WebUtils.onStorageChange.listen((e) { + if (e.key == null) { WebUtils.closeWindow(); + } else if (e.newValue == null) { + if (e.key == 'credentials' || e.key == 'call_${call.value.chatId}') { + WebUtils.closeWindow(); + } + } else if (e.key == 'call_${call.value.chatId}') { + var newValue = StoredCall.fromJson(json.decode(e.newValue!)); + call.value.call.value = newValue.call; + call.value.creds = call.value.creds ?? newValue.creds; + call.value.deviceId = call.value.deviceId ?? newValue.deviceId; + call.value.chatId.value = newValue.chatId; + _tryToConnect(); } - } else if (e.key == 'call_${call.value.chatId}') { - var stored = WebStoredCall.fromJson(json.decode(e.newValue!)); - call.value.call.value = stored.call; - call.value.creds = call.value.creds ?? stored.creds; - call.value.deviceId = call.value.deviceId ?? stored.deviceId; - call.value.chatId.value = stored.chatId; - _tryToConnect(); - } - }); + }); + } else { + _windowController!.setOnWindowClose(() async { + if (call.value.deviceId != null) { + _calls.leave(call.value.chatId.value, call.value.deviceId!); + } + await DesktopMultiWindow.invokeMethod( + DesktopMultiWindow.mainWindowId, + 'call_${call.value.chatId.value.val}', + ); + }); + + DesktopMultiWindow.addMethodHandler((methodCall, _) async { + if (methodCall.arguments == null) { + _windowController?.close(); + } + + if (methodCall.method == 'call') { + var newValue = StoredCall.fromJson(json.decode(methodCall.arguments)); + + call.value.call.value = newValue.call; + call.value.creds = call.value.creds ?? newValue.creds; + call.value.deviceId = call.value.deviceId ?? newValue.deviceId; + call.value.chatId.value = newValue.chatId; + _tryToConnect(); + } + }); + DesktopMultiWindow.setMethodHandlers(); + } _tryToConnect(); Wakelock.enable().onError((_, __) => false); @@ -105,7 +177,14 @@ class PopupCallController extends GetxController { @override void onClose() { Wakelock.disable().onError((_, __) => false); - WebUtils.removeCall(call.value.chatId.value); + if (PlatformUtils.isWeb) { + WebUtils.removeCall(call.value.chatId.value); + } else { + DesktopMultiWindow.invokeMethod( + DesktopMultiWindow.mainWindowId, + 'call_${call.value.chatId.value.val}', + ); + } _storageSubscription?.cancel(); _stateWorker.dispose(); _calls.leave(call.value.chatId.value); diff --git a/lib/ui/worker/call.dart b/lib/ui/worker/call.dart index c0c10f562f9..161f4d96518 100644 --- a/lib/ui/worker/call.dart +++ b/lib/ui/worker/call.dart @@ -20,6 +20,7 @@ import 'dart:convert'; import 'package:audioplayers/audioplayers.dart'; import 'package:callkeep/callkeep.dart'; +import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; @@ -364,28 +365,42 @@ class CallWorker extends DisposableService { /// Initializes [WebUtils] related functionality. void _initWebUtils() { - _storageSubscription = WebUtils.onStorageChange.listen((s) { - if (s.key == null) { - stop(); - } else if (s.key?.startsWith('call_') == true) { - ChatId chatId = ChatId(s.key!.replaceAll('call_', '')); - if (s.newValue == null) { - _callService.remove(chatId); + onCall(ChatId chatId, String? call) { + if (call == null) { + _callService.remove(chatId); + _workers.remove(chatId)?.dispose(); + if (_workers.isEmpty) { + stop(); + } + } else { + var storedCall = StoredCall.fromJson(json.decode(call)); + if (storedCall.state != OngoingCallState.local && + storedCall.state != OngoingCallState.pending) { _workers.remove(chatId)?.dispose(); if (_workers.isEmpty) { stop(); } - } else { - var call = WebStoredCall.fromJson(json.decode(s.newValue!)); - if (call.state != OngoingCallState.local && - call.state != OngoingCallState.pending) { - _workers.remove(chatId)?.dispose(); - if (_workers.isEmpty) { - stop(); - } - } } } - }); + } + + if (PlatformUtils.isWeb) { + _storageSubscription = WebUtils.onStorageChange.listen((s) { + if (s.key == null) { + stop(); + } else if (s.key!.startsWith('call_')) { + ChatId chatId = ChatId(s.key!.replaceAll('call_', '')); + onCall(chatId, s.newValue); + } + }); + } else if (PlatformUtils.isDesktop) { + DesktopMultiWindow.addMethodHandler((methodCall, fromWindowId) async { + if (methodCall.method.startsWith('call_')) { + ChatId chatId = ChatId(methodCall.method.replaceAll('call_', '')); + onCall(chatId, methodCall.arguments); + } + }); + DesktopMultiWindow.setMethodHandlers(); + } } } diff --git a/lib/util/platform_utils.dart b/lib/util/platform_utils.dart index 4cf1613a1bd..422f180f52b 100644 --- a/lib/util/platform_utils.dart +++ b/lib/util/platform_utils.dart @@ -16,9 +16,11 @@ // . import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:async/async.dart'; +import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:dio/dio.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; @@ -30,6 +32,9 @@ import 'package:share_plus/share_plus.dart'; import 'package:window_manager/window_manager.dart'; import '/config.dart'; +import '/domain/model/chat.dart'; +import '/domain/model/ongoing_call.dart'; +import '/domain/model/session.dart'; import '/routes.dart'; import 'web/web_utils.dart'; @@ -44,11 +49,22 @@ class PlatformUtilsImpl { /// Path to the downloads directory. String? _downloadDirectory; + /// Map of window IDs in that opened a call. + final Map _popupCalls = {}; + + /// ID of the windows this application opened. + /// + /// If not null it mean that application opened in separate window on desktop. + int? windowId; + /// [Dio] client to use in queries. /// /// May be overridden to be mocked in tests. Dio dio = Dio(); + /// Controller of this popup window. + WindowController? _windowController; + /// Indicates whether application is running in a web browser. bool get isWeb => GetPlatform.isWeb; @@ -76,6 +92,15 @@ class PlatformUtilsImpl { bool get isDesktop => PlatformUtils.isMacOS || GetPlatform.isWindows || GetPlatform.isLinux; + /// Indicates whether the current window is a popup. + bool get isPopup { + if (isWeb) { + return WebUtils.isPopup; + } else { + return windowId != null; + } + } + /// Returns a stream broadcasting the application's window focus changes. Stream get onFocusChanged { StreamController? controller; @@ -83,7 +108,7 @@ class PlatformUtilsImpl { if (isWeb) { return WebUtils.onFocusChanged; } else if (isDesktop) { - _WindowListener listener = _WindowListener( + DesktopWindowListener listener = DesktopWindowListener( onBlur: () => controller!.add(false), onFocus: () => controller!.add(true), ); @@ -111,7 +136,7 @@ class PlatformUtilsImpl { Stream> get onResized { StreamController>? controller; - final _WindowListener listener = _WindowListener( + final DesktopWindowListener listener = DesktopWindowListener( onResized: (pair) => controller!.add(pair), ); @@ -127,7 +152,7 @@ class PlatformUtilsImpl { Stream get onMoved { StreamController? controller; - final _WindowListener listener = _WindowListener( + final DesktopWindowListener listener = DesktopWindowListener( onMoved: (position) => controller!.add(position), ); @@ -157,7 +182,7 @@ class PlatformUtilsImpl { } else if (isDesktop) { StreamController? controller; - var windowListener = _WindowListener( + var windowListener = DesktopWindowListener( onEnterFullscreen: () => controller!.add(true), onLeaveFullscreen: () => controller!.add(false), ); @@ -192,6 +217,19 @@ class PlatformUtilsImpl { return _downloadDirectory!; } + /// Returns controller of this popup window. + WindowController get windowController { + if (_windowController != null) { + return _windowController!; + } + + if (windowId == null) { + throw ArgumentError.notNull('windowId'); + } + + return WindowController.fromWindowId(windowId!); + } + /// Enters fullscreen mode. Future enterFullscreen() async { if (isWeb) { @@ -364,6 +402,85 @@ class PlatformUtilsImpl { await Share.shareXFiles([XFile(path)]); File(path).delete(); } + + /// Sets [title] of the window. + Future setTitle(String title) async { + if (isWeb) { + WebUtils.title(title); + } else if (isDesktop) { + if (windowId != null) { + windowController.setTitle(title); + } else { + WindowManager.instance.setTitle(title); + } + } + } + + /// Opens a new popup window with the provided [call]. + Future openPopup(OngoingCall call, Credentials? credentials) async { + if (isWeb) { + bool window = WebUtils.openPopupCall( + call.chatId.value, + withAudio: call.audioState.value == LocalTrackState.enabling || + call.audioState.value == LocalTrackState.enabled, + withVideo: call.videoState.value == LocalTrackState.enabling || + call.videoState.value == LocalTrackState.enabled, + withScreen: call.screenShareState.value == LocalTrackState.enabling || + call.screenShareState.value == LocalTrackState.enabled, + ); + + if (!window) { + throw Exception('Popup window not opened'); + } + } else if (isDesktop) { + var desktopWindow = await DesktopMultiWindow.createWindow( + json.encode({ + 'call': json.encode(call.toStored().toJson()), + 'credentials': json.encode(credentials?.toJson()), + }), + ); + await desktopWindow.setFrame(const Offset(0, 0) & const Size(700, 700)); + await desktopWindow.center(); + await desktopWindow.setTitle('Call'); + await desktopWindow.show(); + + _popupCalls[call.chatId.value] = desktopWindow.windowId; + + return desktopWindow.windowId; + } + + return null; + } + + /// Indicates that a call with the provide [chatId] is opened in popup. + bool inPopup(ChatId chatId) => + WebUtils.containsCall(chatId) || _popupCalls.containsKey(chatId); + + /// Moves a popup call [from] the old [to] the new [ChatId]. + void moveCall(ChatId from, ChatId to, {StoredCall? newState}) { + if (isWeb) { + WebUtils.moveCall(from, to, newState: newState); + } else { + int? id = _popupCalls.remove(from); + if (id != null) { + _popupCalls[to] = id; + } + } + } + + /// Removes the provided popup [call]. + void removeCall(OngoingCall call) { + if (PlatformUtils.isWeb) { + if (call.callChatItemId == null || call.connected) { + WebUtils.removeCall(call.chatId.value); + } + } else { + int? id = _popupCalls.remove(call.chatId.value); + if (id != null) { + WindowController.fromWindowId(id).close(); + } + } + } } /// Determining whether a [BuildContext] is mobile or not. @@ -382,14 +499,15 @@ extension MobileExtensionOnContext on BuildContext { } /// Listener interface for receiving window events. -class _WindowListener extends WindowListener { - _WindowListener({ +class DesktopWindowListener extends WindowListener { + DesktopWindowListener({ this.onLeaveFullscreen, this.onEnterFullscreen, this.onFocus, this.onBlur, this.onResized, this.onMoved, + this.onClose, }); /// Callback, called when the window exits fullscreen. @@ -410,6 +528,9 @@ class _WindowListener extends WindowListener { /// Callback, called when the window moves. final void Function(Offset offset)? onMoved; + /// Callback, called when the window closes. + final VoidCallback? onClose; + @override void onWindowEnterFullScreen() => onEnterFullscreen?.call(); @@ -433,4 +554,9 @@ class _WindowListener extends WindowListener { @override void onWindowMoved() async => onMoved?.call(await windowManager.getPosition()); + + @override + void onWindowClose() { + onClose?.call(); + } } diff --git a/lib/util/web/non_web.dart b/lib/util/web/non_web.dart index 2ef68b9b56b..52de7f2692c 100644 --- a/lib/util/web/non_web.dart +++ b/lib/util/web/non_web.dart @@ -129,10 +129,10 @@ class WebUtils { /// Returns a call identified by the provided [chatId] from the browser's /// storage. - static WebStoredCall? getCall(ChatId chatId) => null; + static StoredCall? getCall(ChatId chatId) => null; /// Stores the provided [call] in the browser's storage. - static void setCall(WebStoredCall call) { + static void setCall(StoredCall call) { // No-op. } @@ -147,7 +147,7 @@ class WebUtils { static void moveCall( ChatId chatId, ChatId newChatId, { - WebStoredCall? newState, + StoredCall? newState, }) { // No-op. } diff --git a/lib/util/web/web.dart b/lib/util/web/web.dart index ce4769e9fa9..51d8cca84c4 100644 --- a/lib/util/web/web.dart +++ b/lib/util/web/web.dart @@ -409,17 +409,17 @@ class WebUtils { /// Returns a call identified by the provided [chatId] from the browser's /// storage. - static WebStoredCall? getCall(ChatId chatId) { + static StoredCall? getCall(ChatId chatId) { var data = html.window.localStorage['call_$chatId']; if (data != null) { - return WebStoredCall.fromJson(json.decode(data)); + return StoredCall.fromJson(json.decode(data)); } return null; } /// Stores the provided [call] in the browser's storage. - static void setCall(WebStoredCall call) => + static void setCall(StoredCall call) => html.window.localStorage['call_${call.chatId}'] = json.encode(call.toJson()); @@ -433,7 +433,7 @@ class WebUtils { static void moveCall( ChatId chatId, ChatId newChatId, { - WebStoredCall? newState, + StoredCall? newState, }) { newState ??= WebUtils.getCall(chatId); WebUtils.removeCall(chatId); diff --git a/lib/util/web/web_utils.dart b/lib/util/web/web_utils.dart index eb4f24cae2f..b55bf8d8f48 100644 --- a/lib/util/web/web_utils.dart +++ b/lib/util/web/web_utils.dart @@ -49,19 +49,22 @@ class WebStorageEvent { } /// Model of an [OngoingCall] stored in the browser's storage. -class WebStoredCall { - const WebStoredCall({ +class StoredCall { + const StoredCall({ required this.chatId, this.call, this.creds, this.deviceId, this.state = OngoingCallState.local, + this.withAudio = true, + this.withVideo = false, + this.withScreen = false, }); - /// [ChatCall] of this [WebStoredCall]. + /// [ChatCall] of this [StoredCall]. final ChatCall? call; - /// [ChatId] of this [WebStoredCall]. + /// [ChatId] of this [StoredCall]. final ChatId chatId; /// Stored [OngoingCall.creds]. @@ -73,9 +76,18 @@ class WebStoredCall { /// Stored [OngoingCall.state]. final OngoingCallState state; - /// Constructs a [WebStoredCall] from the provided [data]. - factory WebStoredCall.fromJson(Map data) { - return WebStoredCall( + /// Indicator whether microphone is enabled. + final bool withAudio; + + /// Indicator whether camera is enabled. + final bool withVideo; + + /// Indicator whether screen share is enabled. + final bool withScreen; + + /// Constructs a [StoredCall] from the provided [data]. + factory StoredCall.fromJson(Map data) { + return StoredCall( chatId: ChatId(data['chatId']), call: data['call'] == null ? null @@ -109,10 +121,13 @@ class WebStoredCall { state: data['state'] == null ? OngoingCallState.local : OngoingCallState.values[data['state']], + withAudio: data['withAudio'], + withVideo: data['withVideo'], + withScreen: data['withScreen'], ); } - /// Returns a [Map] containing this [WebStoredCall] data. + /// Returns a [Map] containing this [StoredCall] data. Map toJson() { return { 'chatId': chatId.val, @@ -145,6 +160,9 @@ class WebStoredCall { 'creds': creds?.val, 'deviceId': deviceId?.val, 'state': state.index, + 'withAudio': withAudio, + 'withVideo': withVideo, + 'withScreen': withScreen, }; } } @@ -181,17 +199,23 @@ class WebCallPreferences { } } -/// Extension adding a conversion from an [OngoingCall] to a [WebStoredCall]. +/// Extension adding a conversion from an [OngoingCall] to a [StoredCall]. extension WebStoredOngoingCallConversion on OngoingCall { - /// Constructs a [WebStoredCall] containing all necessary information of this + /// Constructs a [StoredCall] containing all necessary information of this /// [OngoingCall] to be stored in the browser's storage. - WebStoredCall toStored() { - return WebStoredCall( + StoredCall toStored() { + return StoredCall( chatId: chatId.value, call: call.value, creds: creds, state: state.value, deviceId: deviceId, + withAudio: audioState.value == LocalTrackState.enabling || + audioState.value == LocalTrackState.enabled, + withVideo: videoState.value == LocalTrackState.enabling || + videoState.value == LocalTrackState.enabled, + withScreen: screenShareState.value == LocalTrackState.enabling || + screenShareState.value == LocalTrackState.enabled, ); } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index fee87a15f00..8e55cb5bcde 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -22,6 +23,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) desktop_drop_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin"); desktop_drop_plugin_register_with_registrar(desktop_drop_registrar); + g_autoptr(FlPluginRegistrar) desktop_multi_window_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopMultiWindowPlugin"); + desktop_multi_window_plugin_register_with_registrar(desktop_multi_window_registrar); g_autoptr(FlPluginRegistrar) medea_flutter_webrtc_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MedeaFlutterWebrtcPlugin"); medea_flutter_webrtc_plugin_register_with_registrar(medea_flutter_webrtc_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 4a4665ae5c4..dd73fc91373 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST audioplayers_linux desktop_drop + desktop_multi_window medea_flutter_webrtc medea_jason screen_retriever diff --git a/linux/my_application.cc b/linux/my_application.cc index 4d94029a43c..2b04d2dcfd0 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -7,6 +7,10 @@ #include "flutter/generated_plugin_registrant.h" +#include "desktop_multi_window/desktop_multi_window_plugin.h" +#include "medea_flutter_webrtc/medea_flutter_webrtc_plugin.h" +#include "medea_jason/medea_jason_plugin.h" + struct _MyApplication { GtkApplication parent_instance; char** dart_entrypoint_arguments; @@ -59,6 +63,15 @@ static void my_application_activate(GApplication* application) { fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + desktop_multi_window_plugin_set_window_created_callback([](FlPluginRegistry* registry){ + g_autoptr(FlPluginRegistrar) medea_flutter_webrtc_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "MedeaFlutterWebrtcPlugin"); + medea_flutter_webrtc_plugin_register_with_registrar(medea_flutter_webrtc_registrar); + g_autoptr(FlPluginRegistrar) medea_jason_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "MedeaJasonPlugin"); + medea_jason_plugin_register_with_registrar(medea_jason_registrar); + }); + gtk_widget_grab_focus(GTK_WIDGET(view)); } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 72f841b1d99..e2c8d9b8d74 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import audioplayers_darwin import connectivity_plus import desktop_drop +import desktop_multi_window import flutter_app_badger import flutter_local_notifications import medea_flutter_webrtc @@ -25,6 +26,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) + FlutterMultiWindowPlugin.register(with: registry.registrar(forPlugin: "FlutterMultiWindowPlugin")) FlutterAppBadgerPlugin.register(with: registry.registrar(forPlugin: "FlutterAppBadgerPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MedeaFlutterWebrtcPlugin.register(with: registry.registrar(forPlugin: "MedeaFlutterWebrtcPlugin")) diff --git a/macos/Podfile b/macos/Podfile index ec363298d94..9eb9f1d2fd8 100644 --- a/macos/Podfile +++ b/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.11' +platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build # latency. diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 340e11fac52..8775deed5a5 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,11 +1,13 @@ PODS: - audioplayers_darwin (0.0.1): - FlutterMacOS - - connectivity_plus_macos (0.0.1): + - connectivity_plus (0.0.1): - FlutterMacOS - ReachabilitySwift - desktop_drop (0.0.1): - FlutterMacOS + - desktop_multi_window (0.0.1): + - FlutterMacOS - flutter_app_badger (1.3.0): - FlutterMacOS - flutter_local_notifications (0.0.1): @@ -15,23 +17,22 @@ PODS: - FlutterMacOS - medea_jason (0.3.0-dev): - FlutterMacOS - - package_info_plus_macos (0.0.1): + - package_info_plus (0.0.1): - FlutterMacOS - - path_provider_macos (0.0.1): + - path_provider_foundation (0.0.1): + - Flutter - FlutterMacOS - ReachabilitySwift (5.0.0) + - rive_common (0.0.1): + - FlutterMacOS - screen_retriever (0.0.1): - FlutterMacOS - - Sentry (7.23.0): - - Sentry/Core (= 7.23.0) - - Sentry/Core (7.23.0) + - Sentry/HybridSDK (7.31.5) - sentry_flutter (0.0.1): - Flutter - FlutterMacOS - - Sentry (~> 7.23.0) - - share_plus_macos (0.0.1): - - FlutterMacOS - - url_launcher_macos (0.0.1): + - Sentry/HybridSDK (= 7.31.5) + - share_plus (0.0.1): - FlutterMacOS - wakelock_macos (0.0.1): - FlutterMacOS @@ -40,19 +41,20 @@ PODS: DEPENDENCIES: - audioplayers_darwin (from `Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos`) - - connectivity_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus_macos/macos`) + - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`) + - desktop_multi_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_multi_window/macos`) - flutter_app_badger (from `Flutter/ephemeral/.symlinks/plugins/flutter_app_badger/macos`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - medea_flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/medea_flutter_webrtc/macos`) - medea_jason (from `Flutter/ephemeral/.symlinks/plugins/medea_jason/macos`) - - package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`) - - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) + - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos`) + - rive_common (from `Flutter/ephemeral/.symlinks/plugins/rive_common/macos`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - sentry_flutter (from `Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos`) - - share_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos`) - - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`) - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) @@ -64,10 +66,12 @@ SPEC REPOS: EXTERNAL SOURCES: audioplayers_darwin: :path: Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos - connectivity_plus_macos: - :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus_macos/macos + connectivity_plus: + :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos desktop_drop: :path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos + desktop_multi_window: + :path: Flutter/ephemeral/.symlinks/plugins/desktop_multi_window/macos flutter_app_badger: :path: Flutter/ephemeral/.symlinks/plugins/flutter_app_badger/macos flutter_local_notifications: @@ -78,18 +82,18 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/medea_flutter_webrtc/macos medea_jason: :path: Flutter/ephemeral/.symlinks/plugins/medea_jason/macos - package_info_plus_macos: - :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos - path_provider_macos: - :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos + package_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos + rive_common: + :path: Flutter/ephemeral/.symlinks/plugins/rive_common/macos screen_retriever: :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos sentry_flutter: :path: Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos - share_plus_macos: - :path: Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos - url_launcher_macos: - :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + share_plus: + :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos wakelock_macos: :path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos window_manager: @@ -97,24 +101,25 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: audioplayers_darwin: dcad41de4fbd0099cb3749f7ab3b0cb8f70b810c - connectivity_plus_macos: f6e86fd000e971d361e54b5afcadc8c8fa773308 + connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 + desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486 flutter_app_badger: 55a64b179f8438e89d574320c77b306e327a1730 flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4 - FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811 + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 medea_flutter_webrtc: 028a81d72434f27a0004d2847055dfbb3be11d19 medea_jason: 2ce5ea82a8352f8de793306d7e2df15f9c06588c - package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c - path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 + package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce + path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + rive_common: 643f5c20ccd60c53136ab5c970eccf0d17e010fe screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 - Sentry: a0d4563fa4ddacba31fdcc35daaa8573d87224d6 - sentry_flutter: 8bde7d0e57a721727fe573f13bb292c497b5a249 - share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4 - url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 + Sentry: 4c9babff9034785067c896fd580b1f7de44da020 + sentry_flutter: b10ae7a5ddcbc7f04648eeb2672b5747230172f1 + share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 -PODFILE CHECKSUM: 59da05cf0bd74d77c27867ff4b93b4bf9a656cc6 +PODFILE CHECKSUM: 36d17fab14a2fedf13ab7502b4c049f7629115db COCOAPODS: 1.11.3 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 041033ada04..f76c3da07cc 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -202,7 +202,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { @@ -272,6 +272,7 @@ }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 2a9abb817a6..55649793ed3 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Future.sync(() => CustomWorld()); /// Application's initialization function. -Future appInitializationFn(World world) { +Future appInitializationFn(World world) async { FlutterError.onError = ignoreOverflowErrors; PlatformUtils = PlatformUtilsMock(); Get.put(MockGraphQlProvider()); - return Future.sync(app.main); + await app.main([]); } /// Creates a new [Session] for an [User] identified by the provided [name]. diff --git a/test/e2e/steps/restart_app.dart b/test/e2e/steps/restart_app.dart index 8bce5376d55..9c61e7d7f19 100644 --- a/test/e2e/steps/restart_app.dart +++ b/test/e2e/steps/restart_app.dart @@ -41,7 +41,7 @@ final StepDefinitionGeneric restartApp = then( await Future.delayed(Duration.zero); await Hive.close(); - await main(); + await main([]); await context.world.appDriver.waitForAppToSettle(); }, ); diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index c080f0bc378..0481efb4b15 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -26,6 +27,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); DesktopDropPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("DesktopDropPlugin")); + DesktopMultiWindowPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktopMultiWindowPlugin")); MedeaFlutterWebrtcPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MedeaFlutterWebrtcPluginCApi")); MedeaJasonPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 903f09647d9..f311ec2dede 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST audioplayers_windows connectivity_plus desktop_drop + desktop_multi_window medea_flutter_webrtc medea_jason permission_handler_windows diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index b43b9095ea3..6d75bfd18b1 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -4,6 +4,10 @@ #include "flutter/generated_plugin_registrant.h" +#include "desktop_multi_window/desktop_multi_window_plugin.h" +#include "medea_flutter_webrtc/medea_flutter_webrtc_plugin_c_api.h" +#include "medea_jason/medea_jason_plugin.h" + FlutterWindow::FlutterWindow(const flutter::DartProject& project) : project_(project) {} @@ -25,6 +29,15 @@ bool FlutterWindow::OnCreate() { return false; } RegisterPlugins(flutter_controller_->engine()); + DesktopMultiWindowSetWindowCreatedCallback([](void *controller) { + auto *flutter_view_controller = + reinterpret_cast(controller); + auto *registry = flutter_view_controller->engine(); + MedeaFlutterWebrtcPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("MedeaFlutterWebrtcPluginCApi")); + MedeaJasonPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("MedeaJasonPlugin")); + }); SetChildContent(flutter_controller_->view()->GetNativeWindow()); return true; }