Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open calls in separate windows on desktop (#87) #93

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
72323e7
Initial
krida2000 Aug 23, 2022
a3a65b1
Merge remote-tracking branch 'origin/main' into 87-separate-window-ca…
krida2000 Aug 24, 2022
6968eb8
Add ability to open calls in popup
krida2000 Aug 24, 2022
3b8d96c
Corrections
krida2000 Aug 25, 2022
3fd5804
Corrections
krida2000 Aug 26, 2022
a246352
Correcions [skip ci]
krida2000 Aug 29, 2022
98d91ff
Add on window close callback
krida2000 Aug 31, 2022
4fdfcff
Corrections
krida2000 Aug 31, 2022
fcd45c8
Corrections
krida2000 Sep 1, 2022
e37b9d6
Corrections
krida2000 Sep 2, 2022
c424f50
Merge remote-tracking branch 'origin/main' into 87-separate-window-ca…
krida2000 Sep 2, 2022
9ae7bfd
Minor correction
krida2000 Sep 2, 2022
79dd186
Minor changes
krida2000 Sep 2, 2022
96bb329
Merge remote-tracking branch 'origin/main' into 87-separate-window-ca…
krida2000 Jan 27, 2023
efebf20
Corrections
krida2000 Jan 27, 2023
fe4f2be
Corrections
krida2000 Jan 30, 2023
0b114ae
Corrections
krida2000 Jan 31, 2023
f526efd
Merge remote-tracking branch 'origin/main' into 87-separate-window-ca…
krida2000 Jan 31, 2023
9031b88
Final correction
krida2000 Jan 31, 2023
b81c9bb
Add changelog
krida2000 Jan 31, 2023
a6c0893
Fix macos build
krida2000 Feb 1, 2023
dfba62c
Final corrections
krida2000 Feb 1, 2023
1f97292
Allow macOS to run `Call`s in windows
SleepySquash Feb 1, 2023
3f4badb
Corrections
krida2000 Feb 2, 2023
9c8eb55
Corrections
krida2000 Feb 2, 2023
ee8c5ce
Merge remote-tracking branch 'origin/main' into 87-separate-window-ca…
krida2000 Feb 2, 2023
ccc2f4a
Fix analyzer
krida2000 Feb 3, 2023
7c0428a
Corrections
krida2000 Feb 3, 2023
3878f73
Merge remote-tracking branch 'origin/main' into 87-separate-window-ca…
krida2000 Feb 3, 2023
c204236
Revert pubspec.lock
krida2000 Feb 3, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -83,6 +84,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
Expand Down
1 change: 0 additions & 1 deletion lib/domain/model/ongoing_call.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lib/domain/repository/call.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ abstract class AbstractCallRepository {
/// Adds the provided [ChatCall] to the [calls], if not already.
Rx<OngoingCall>? 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<OngoingCall> addStored(
WebStoredCall stored, {
StoredCall stored, {
bool withAudio = true,
bool withVideo = true,
bool withScreen = false,
Expand Down
88 changes: 66 additions & 22 deletions lib/domain/service/auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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.
Expand Down Expand Up @@ -118,31 +120,73 @@ 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) {
if (PlatformUtils.isWeb) {
_storageSubscription = WebUtils.onStorageChange.listen((e) {
if (e.key == 'credentials') {
Comment on lines +142 to +144
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вот тут есть предположение, что WebUtils.onStorageChange и DesktopMultiWindow.addMethodHandler можно объединить в PlatformUtils стриме, как Вам идея? Будет стрим из MapEntry<String, String>.

Хотя это может оказаться лишним рефактором. Меня напрягает дублирование кода тут. Можем ли мы вынести код в отдельную функцию тут внутри, которую и будем вызывать в WebUtils.onStorageChange и DesktopMultiWindow.addMethodHandler? Аргументиком будет тот же MapEntry.

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 (_tokenGuard.isLocked) {
_tokenGuard.release();
}
}

if (_tokenGuard.isLocked) {
_tokenGuard.release();
});
} else {
DesktopMultiWindow.addMethodHandler((methodCall, fromWindowId) async {
if (methodCall.method == 'credentials') {
if (methodCall.arguments == null) {
_authRepository.token = null;
credentials.value = null;
_status.value = RxStatus.empty();
} else {
Credentials creds =
Credentials.fromJson(json.decode(methodCall.arguments!));
_authRepository.token = creds.session.token;
_authRepository.applyToken();
credentials.value = creds;
_status.value = RxStatus.success();
}

if (_tokenGuard.isLocked) {
_tokenGuard.release();
}
}
}
});
});
}
} 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<int> 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();
}

Expand Down Expand Up @@ -324,9 +368,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;
}
Expand Down
18 changes: 14 additions & 4 deletions lib/domain/service/call.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -56,6 +57,9 @@ class CallService extends DisposableService {
/// Repository of [OngoingCall]s collection.
final AbstractCallRepository _callsRepo;

/// Map of window IDs in that opened a call.
Map<ChatId, int> popupCalls = {};

/// Returns ID of the authenticated [MyUser].
UserId get me => _authService.credentials.value!.userId;

Expand All @@ -68,7 +72,7 @@ class CallService extends DisposableService {
}) async {
final Rx<OngoingCall>? stored = _callsRepo[chatId];

if (WebUtils.containsCall(chatId)) {
if (WebUtils.containsCall(chatId) || popupCalls.containsKey(chatId)) {
throw CallIsInPopupException();
} else if (stored != null &&
stored.value.state.value != OngoingCallState.ended) {
Expand Down Expand Up @@ -98,7 +102,7 @@ class CallService extends DisposableService {
bool withVideo = false,
bool withScreen = false,
}) async {
if (WebUtils.containsCall(chatId) && !WebUtils.isPopup) {
if (WebUtils.containsCall(chatId) || popupCalls.containsKey(chatId)) {
krida2000 marked this conversation as resolved.
Show resolved Hide resolved
throw CallIsInPopupException();
}

Expand Down Expand Up @@ -163,7 +167,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();
Expand All @@ -177,7 +181,7 @@ class CallService extends DisposableService {

/// Constructs an [OngoingCall] from the provided [stored] call.
Rx<OngoingCall> addStored(
WebStoredCall stored, {
StoredCall stored, {
bool withAudio = true,
bool withVideo = true,
bool withScreen = false,
Expand Down Expand Up @@ -249,6 +253,12 @@ class CallService extends DisposableService {
Rx<OngoingCall>? call = _callsRepo[chatId];
if (call != null) {
_callsRepo.move(chatId, newChatId);

int? id = popupCalls.remove(chatId);
if (id != null) {
popupCalls[newChatId] = id;
}

_callsRepo.moveCredentials(callId, newCallId);
if (WebUtils.isPopup) {
WebUtils.moveCall(chatId, newChatId, newState: call.value.toStored());
Expand Down
104 changes: 81 additions & 23 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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';
Expand All @@ -54,18 +58,58 @@ import 'util/platform_utils.dart';
import 'util/web/web_utils.dart';

/// Entry point of this application.
Future<void> main() async {
Future<void> main(List<String> 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 = jsonDecode(args[2]) as Map<String, dynamic>;
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<void> appRunner() async {
WebUtils.setPathUrlStrategy();

await _initHive();
await _initHive(windowId: PlatformUtils.windowId, 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();

Expand All @@ -87,12 +131,17 @@ Future<void> main() async {
Get.put<AbstractAuthRepository>(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()));
Expand Down Expand Up @@ -176,26 +225,35 @@ class App extends StatelessWidget {

/// Initializes a [Hive] storage and registers a [SessionDataHiveProvider] in
/// the [Get]'s context.
Future<void> _initHive() async {
await Hive.initFlutter('hive');

// Load and compare application version.
Box box = await Hive.openBox('version');
String version = Pubspec.version;
String? stored = box.get(0);

// If mismatch is detected, then clean the existing [Hive] cache.
if (stored != version) {
await Hive.close();
await Hive.clean('hive');
Future<void> _initHive({int? windowId, Credentials? credentials}) async {
if (windowId == null) {
await Hive.initFlutter('hive');
Hive.openBox('version').then((box) async {
await box.put(0, version);
await box.close();
});

// Load and compare application version.
Box box = await Hive.openBox('version');
String version = Pubspec.version;
String? stored = box.get(0);

// If mismatch is detected, then clean the existing [Hive] cache.
if (stored != version) {
await Hive.close();
await Hive.clean('hive');
await Hive.initFlutter('hive');
Hive.openBox('version').then((box) async {
await box.put(0, version);
await box.close();
});
}
} else {
await Hive.clean('hive/$windowId');
krida2000 marked this conversation as resolved.
Show resolved Hide resolved
await Hive.initFlutter('hive/$windowId');
}

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();
}

Expand Down
Loading