feat(video_calls): fixed online indicator and in app notifications for video call

This commit is contained in:
misha
2026-06-24 08:26:16 +03:00
parent 8ff826fa3e
commit c5bf5acfe7
22 changed files with 569 additions and 51 deletions

View File

@@ -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;

View File

@@ -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;
}

View File

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

View File

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

View File

@@ -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(