forked from eatvio/eatvio_app
feat(video_calls): fixed online indicator and in app notifications for video call
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/features/video_calls/managers/video_call_manager.dart';
|
||||
import 'package:eatvio_app/features/video_calls/presentation/bloc/video_call_session/video_call_session_cubit.dart';
|
||||
import 'package:eatvio_app/features/video_calls/presentation/bloc/video_call_session/video_call_session_state.dart';
|
||||
import 'package:eatvio_app/features/video_calls/presentation/widgets/video_call_banner_overlay.dart';
|
||||
import 'package:eatvio_app/features/video_calls/presentation/widgets/video_call_global_pip_layer.dart';
|
||||
import 'package:eatvio_app/features/video_calls/presentation/widgets/video_call_system_pip_content.dart';
|
||||
import 'package:eatvio_app/features/video_calls/presentation/widgets/video_call_system_pip_listener.dart';
|
||||
@@ -22,6 +24,10 @@ class VideoCallAppShell extends StatelessWidget {
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
child ?? const SizedBox.shrink(),
|
||||
Overlay(
|
||||
key: VideoCallBannerOverlay.overlayKey,
|
||||
initialEntries: const <OverlayEntry>[],
|
||||
),
|
||||
const VideoCallGlobalPipLayer(),
|
||||
const VideoCallSystemPipListener(),
|
||||
],
|
||||
@@ -36,10 +42,7 @@ class VideoCallAppShell extends StatelessWidget {
|
||||
previous.connectPhase != current.connectPhase ||
|
||||
previous.linkIssue != current.linkIssue,
|
||||
builder: (context, state) {
|
||||
final allowSystemPipShell =
|
||||
state.callStageEntered &&
|
||||
state.connectPhase == VideoCallConnectPhase.connected &&
|
||||
state.linkIssue != VideoCallLinkIssue.lost;
|
||||
final allowSystemPipShell = sl<VideoCallManager>().isActiveCall;
|
||||
|
||||
if (!allowSystemPipShell) return shell;
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Dedicated overlay layer above the app navigator for in-call banners.
|
||||
abstract final class VideoCallBannerOverlay {
|
||||
static final GlobalKey<OverlayState> overlayKey = GlobalKey<OverlayState>(
|
||||
debugLabel: 'videoCallBannerOverlay',
|
||||
);
|
||||
|
||||
static OverlayState? get overlayState => overlayKey.currentState;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:eatvio_app/core/theme/custom_text_style.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/chat/presentation/widgets/chat_user_online_status_line.dart';
|
||||
import 'package:eatvio_app/features/video_calls/presentation/widgets/video_call_controls.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -10,6 +11,8 @@ class VideoCallConnectingEntranceScreen extends StatelessWidget {
|
||||
super.key,
|
||||
required this.counterpartDisplayName,
|
||||
this.counterpartAvatarUrl,
|
||||
required this.counterpartIsOnline,
|
||||
required this.counterpartLastSeenText,
|
||||
required this.micEnabled,
|
||||
required this.videoEnabled,
|
||||
required this.canEnterCall,
|
||||
@@ -24,6 +27,8 @@ class VideoCallConnectingEntranceScreen extends StatelessWidget {
|
||||
|
||||
final String counterpartDisplayName;
|
||||
final String? counterpartAvatarUrl;
|
||||
final bool counterpartIsOnline;
|
||||
final String counterpartLastSeenText;
|
||||
final bool micEnabled;
|
||||
final bool videoEnabled;
|
||||
final bool canEnterCall;
|
||||
@@ -140,33 +145,7 @@ class VideoCallConnectingEntranceScreen extends StatelessWidget {
|
||||
const SizedBox(height: _topContentSpacing),
|
||||
_avatar(context, counterpartDisplayName, counterpartAvatarUrl),
|
||||
const SizedBox(height: 16),
|
||||
if (canEnterCall)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: appTheme.greenA700,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
s.video_call_status_online,
|
||||
style: CustomTextStyles.bodySmallMulishBlack900
|
||||
.copyWith(height: 14 / 12),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Text(
|
||||
s.video_call_screen_connecting_subtitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: CustomTextStyles.bodySmallMulishBlack900
|
||||
.copyWith(height: 14 / 12),
|
||||
),
|
||||
_buildStatusLine(context, s),
|
||||
SizedBox(height: canEnterCall ? 20 : 16),
|
||||
Text(
|
||||
s.video_call_screen_connecting_title,
|
||||
@@ -345,4 +324,29 @@ class VideoCallConnectingEntranceScreen extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatusLine(BuildContext context, S s) {
|
||||
final statusStyle = CustomTextStyles.bodySmallMulishBlack900.copyWith(
|
||||
height: 14 / 12,
|
||||
);
|
||||
final onlineStatus = ChatUserOnlineStatusLine(
|
||||
isOnline: counterpartIsOnline,
|
||||
lastSeenText: counterpartLastSeenText,
|
||||
style: statusStyle,
|
||||
);
|
||||
|
||||
if (counterpartIsOnline || counterpartLastSeenText.trim().isNotEmpty) {
|
||||
return Center(child: onlineStatus);
|
||||
}
|
||||
|
||||
if (!canEnterCall) {
|
||||
return Text(
|
||||
s.video_call_screen_connecting_subtitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: statusStyle,
|
||||
);
|
||||
}
|
||||
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/features/video_calls/managers/video_call_manager.dart';
|
||||
import 'package:eatvio_app/features/video_calls/presentation/bloc/video_call_session/video_call_session_cubit.dart';
|
||||
import 'package:eatvio_app/features/video_calls/presentation/bloc/video_call_session/video_call_session_state.dart';
|
||||
import 'package:eatvio_app/features/video_calls/presentation/pages/video_call_screen.dart';
|
||||
@@ -21,7 +22,8 @@ class VideoCallSystemPipListener extends StatefulWidget {
|
||||
_VideoCallSystemPipListenerState();
|
||||
}
|
||||
|
||||
class _VideoCallSystemPipListenerState extends State<VideoCallSystemPipListener> {
|
||||
class _VideoCallSystemPipListenerState extends State<VideoCallSystemPipListener>
|
||||
with WidgetsBindingObserver {
|
||||
static const MethodChannel _userLeaveHintChannel = MethodChannel(
|
||||
'com.eatvioapp/video_call_pip',
|
||||
);
|
||||
@@ -34,9 +36,24 @@ class _VideoCallSystemPipListenerState extends State<VideoCallSystemPipListener>
|
||||
super.initState();
|
||||
if (!Platform.isAndroid) return;
|
||||
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_userLeaveHintChannel.setMethodCallHandler(_onNativePipMethodCall);
|
||||
final coordinator = sl<VideoCallSystemPipCoordinator>();
|
||||
_subscription = coordinator.statusStream.listen(_onPipStatus);
|
||||
unawaited(_disarmSystemPipIfNoActiveCall());
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
unawaited(_disarmSystemPipIfNoActiveCall());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _disarmSystemPipIfNoActiveCall() async {
|
||||
if (!sl<VideoCallManager>().isActiveCall) {
|
||||
await sl<VideoCallSystemPipCoordinator>().disarm();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onNativePipMethodCall(MethodCall call) async {
|
||||
@@ -88,6 +105,7 @@ class _VideoCallSystemPipListenerState extends State<VideoCallSystemPipListener>
|
||||
@override
|
||||
void dispose() {
|
||||
if (Platform.isAndroid) {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_userLeaveHintChannel.setMethodCallHandler(null);
|
||||
}
|
||||
unawaited(_subscription?.cancel());
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'package:eatvio_app/core/app_router.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/features/video_calls/presentation/widgets/video_call_banner_overlay.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
@@ -7,6 +9,13 @@ abstract final class VideoCallTopOverlay {
|
||||
'assets/icons/video_call/poor_connection.svg';
|
||||
static const String warningSvg = 'assets/icons/video_call/warning.svg';
|
||||
|
||||
static OverlayState? _resolveOverlayState(BuildContext context) {
|
||||
return VideoCallBannerOverlay.overlayState ??
|
||||
AppRouter.navigatorKey.currentState?.overlay ??
|
||||
Overlay.maybeOf(context, rootOverlay: true);
|
||||
}
|
||||
|
||||
|
||||
static OverlayEntry insertBanner(
|
||||
BuildContext context, {
|
||||
required String message,
|
||||
@@ -14,7 +23,7 @@ abstract final class VideoCallTopOverlay {
|
||||
required String svgAsset,
|
||||
Duration? autoRemoveAfter,
|
||||
}) {
|
||||
final overlayState = Overlay.maybeOf(context, rootOverlay: true);
|
||||
final overlayState = _resolveOverlayState(context);
|
||||
late OverlayEntry entry;
|
||||
entry = OverlayEntry(
|
||||
builder: (ctx) => Positioned(
|
||||
|
||||
Reference in New Issue
Block a user