From 67c1be4dab6e0dd6631a3536b8b807d1e604e9d8 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin Date: Fri, 27 Mar 2026 00:34:53 +0530 Subject: [PATCH 1/8] Add ActiveCallBanner and ActiveCallTopBar widgets for minimized call UI New widgets that show call status, transcript snippet, and inline controls when the user minimizes an active phone call. ActiveCallTopBar provides a slim green bar on non-home tabs to return to the full call screen. --- .../pages/phone_calls/active_call_banner.dart | 356 ++++++++++++++++++ 1 file changed, 356 insertions(+) create mode 100644 app/lib/pages/phone_calls/active_call_banner.dart diff --git a/app/lib/pages/phone_calls/active_call_banner.dart b/app/lib/pages/phone_calls/active_call_banner.dart new file mode 100644 index 00000000000..3bf44a16f56 --- /dev/null +++ b/app/lib/pages/phone_calls/active_call_banner.dart @@ -0,0 +1,356 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:provider/provider.dart'; + +import 'package:omi/backend/schema/phone_call.dart'; +import 'package:omi/pages/phone_calls/active_call_page.dart'; +import 'package:omi/providers/phone_call_provider.dart'; +import 'package:omi/utils/analytics/mixpanel.dart'; +import 'package:omi/utils/l10n_extensions.dart'; + +/// Compact call banner shown on the home screen when a phone call is active. +/// Displays contact info, live transcript snippet, and inline call controls. +/// Tapping the banner navigates back to the full [ActiveCallPage]. +class ActiveCallBanner extends StatelessWidget { + const ActiveCallBanner({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, provider, _) { + bool isCallInProgress = provider.callState == PhoneCallState.active || + provider.callState == PhoneCallState.connecting || + provider.callState == PhoneCallState.ringing; + + if (!isCallInProgress) return const SizedBox.shrink(); + + return GestureDetector( + onTap: () { + MixpanelManager().track('Phone Call Banner Tapped'); + Navigator.of(context).push(MaterialPageRoute(builder: (_) => const ActiveCallPage())); + }, + child: Container( + margin: const EdgeInsets.fromLTRB(16, 12, 16, 4), + decoration: BoxDecoration( + color: const Color(0xFF1F1F25), + borderRadius: BorderRadius.circular(24), + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 14, 12, 14), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Row 1: Call info + duration + expand icon + _CallInfoRow( + contactName: provider.contactName, + phoneNumber: provider.remoteNumber ?? '', + duration: provider.callDuration, + state: provider.callState, + ), + // Row 2: Transcript snippet (if any) + if (provider.transcriptSegments.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 10), + child: _TranscriptSnippet( + text: provider.transcriptSegments.last.text, + isUser: provider.transcriptSegments.last.isUser, + speakerLabel: provider.getSpeakerLabel(provider.transcriptSegments.last), + ), + ), + // Row 3: Compact call controls + Padding( + padding: const EdgeInsets.only(top: 12), + child: _CompactCallControls( + state: provider.callState, + isMuted: provider.isMuted, + isSpeakerOn: provider.isSpeakerOn, + onMuteToggle: provider.toggleMute, + onSpeakerToggle: provider.toggleSpeaker, + onEndCall: () => provider.endCall(), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} + +class _CallInfoRow extends StatelessWidget { + final String? contactName; + final String phoneNumber; + final Duration duration; + final PhoneCallState state; + + const _CallInfoRow({ + required this.contactName, + required this.phoneNumber, + required this.duration, + required this.state, + }); + + String _formatDuration(Duration d) { + String twoDigits(int n) => n.toString().padLeft(2, '0'); + if (d.inHours > 0) { + return '${twoDigits(d.inHours)}:${twoDigits(d.inMinutes.remainder(60))}:${twoDigits(d.inSeconds.remainder(60))}'; + } + return '${twoDigits(d.inMinutes)}:${twoDigits(d.inSeconds.remainder(60))}'; + } + + @override + Widget build(BuildContext context) { + String statusText; + switch (state) { + case PhoneCallState.connecting: + statusText = context.l10n.callStateConnecting; + break; + case PhoneCallState.ringing: + statusText = context.l10n.callStateRinging; + break; + case PhoneCallState.active: + statusText = _formatDuration(duration); + break; + default: + statusText = ''; + } + + return Row( + children: [ + // Green phone icon + Container( + width: 32, + height: 32, + decoration: const BoxDecoration(color: Color(0xFF34C759), shape: BoxShape.circle), + child: const Icon(Icons.phone_in_talk, color: Colors.white, size: 16), + ), + const SizedBox(width: 10), + // Contact name / number + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + contactName ?? phoneNumber, + style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w600), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (contactName != null && phoneNumber.isNotEmpty) + Text( + phoneNumber, + style: TextStyle(color: Colors.grey[500], fontSize: 12), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + // Duration / status + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration(color: const Color(0xFF35343B), borderRadius: BorderRadius.circular(12)), + child: Text( + statusText, + style: const TextStyle(color: Color(0xFF34C759), fontSize: 13, fontWeight: FontWeight.w500), + ), + ), + const SizedBox(width: 6), + // Expand icon + const Icon(Icons.keyboard_arrow_up, color: Colors.grey, size: 22), + ], + ); + } +} + +class _TranscriptSnippet extends StatelessWidget { + final String text; + final bool isUser; + final String speakerLabel; + + const _TranscriptSnippet({required this.text, required this.isUser, required this.speakerLabel}); + + @override + Widget build(BuildContext context) { + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration(color: const Color(0xFF2A2A30), borderRadius: BorderRadius.circular(12)), + child: Text( + '$speakerLabel: $text', + style: TextStyle(color: Colors.grey[400], fontSize: 13, height: 1.3), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ); + } +} + +class _CompactCallControls extends StatelessWidget { + final PhoneCallState state; + final bool isMuted; + final bool isSpeakerOn; + final VoidCallback onMuteToggle; + final VoidCallback onSpeakerToggle; + final VoidCallback onEndCall; + + const _CompactCallControls({ + required this.state, + required this.isMuted, + required this.isSpeakerOn, + required this.onMuteToggle, + required this.onSpeakerToggle, + required this.onEndCall, + }); + + @override + Widget build(BuildContext context) { + bool isActive = state == PhoneCallState.active || state == PhoneCallState.ringing; + + return Row( + children: [ + // Mute button + _CompactControlButton( + icon: isMuted ? Icons.mic_off : Icons.mic, + label: isMuted ? context.l10n.phoneUnmute : context.l10n.phoneMute, + isActive: isMuted, + onTap: isActive ? onMuteToggle : null, + ), + const SizedBox(width: 8), + // Speaker button + _CompactControlButton( + icon: isSpeakerOn ? Icons.volume_up : Icons.volume_down, + label: context.l10n.phoneSpeaker, + isActive: isSpeakerOn, + onTap: isActive ? onSpeakerToggle : null, + ), + const Spacer(), + // End call button + GestureDetector( + onTap: () { + HapticFeedback.heavyImpact(); + onEndCall(); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + decoration: BoxDecoration(color: Colors.red, borderRadius: BorderRadius.circular(20)), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.call_end, color: Colors.white, size: 18), + const SizedBox(width: 6), + Text( + context.l10n.phoneEndCall, + style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600), + ), + ], + ), + ), + ), + ], + ); + } +} + +class _CompactControlButton extends StatelessWidget { + final IconData icon; + final String label; + final bool isActive; + final VoidCallback? onTap; + + const _CompactControlButton({required this.icon, required this.label, this.isActive = false, this.onTap}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + HapticFeedback.mediumImpact(); + onTap?.call(); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: isActive ? Colors.white : const Color(0xFF35343B), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: isActive ? Colors.black : (onTap != null ? Colors.white : Colors.grey[600]), size: 18), + const SizedBox(width: 6), + Text( + label, + style: TextStyle( + color: isActive ? Colors.black : (onTap != null ? Colors.white : Colors.grey[600]), + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + } +} + +/// Slim bar shown at the top of non-home tabs when a call is active. +/// Tapping it navigates to the full [ActiveCallPage]. +class ActiveCallTopBar extends StatelessWidget { + const ActiveCallTopBar({super.key}); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, provider, _) { + bool isCallInProgress = provider.callState == PhoneCallState.active || + provider.callState == PhoneCallState.connecting || + provider.callState == PhoneCallState.ringing; + + if (!isCallInProgress) return const SizedBox.shrink(); + + String twoDigits(int n) => n.toString().padLeft(2, '0'); + Duration d = provider.callDuration; + String timeStr = d.inHours > 0 + ? '${twoDigits(d.inHours)}:${twoDigits(d.inMinutes.remainder(60))}:${twoDigits(d.inSeconds.remainder(60))}' + : '${twoDigits(d.inMinutes)}:${twoDigits(d.inSeconds.remainder(60))}'; + + String displayName = provider.contactName ?? provider.remoteNumber ?? ''; + + return GestureDetector( + onTap: () { + MixpanelManager().track('Phone Call Top Bar Tapped'); + Navigator.of(context).push(MaterialPageRoute(builder: (_) => const ActiveCallPage())); + }, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + color: const Color(0xFF34C759), + child: Row( + children: [ + const Icon(Icons.phone_in_talk, color: Colors.white, size: 16), + const SizedBox(width: 8), + Expanded( + child: Text( + displayName, + style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Text( + timeStr, + style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), + ), + const SizedBox(width: 6), + const Icon(Icons.keyboard_arrow_up, color: Colors.white, size: 18), + ], + ), + ), + ); + }, + ); + } +} From 34a30d1b462786ef640eb3af68bd0d0d3e0f5c39 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin Date: Fri, 27 Mar 2026 00:35:08 +0530 Subject: [PATCH 2/8] Add minimize button to ActiveCallPage and pop directly to home Replace PopScope lock with a back button that lets users minimize the call and return to the home screen. Uses popUntil to skip the intermediate PhoneCallsPage. --- .../pages/phone_calls/active_call_page.dart | 84 ++++++++++++------- 1 file changed, 53 insertions(+), 31 deletions(-) diff --git a/app/lib/pages/phone_calls/active_call_page.dart b/app/lib/pages/phone_calls/active_call_page.dart index 56176fc15b1..ec94933f55e 100644 --- a/app/lib/pages/phone_calls/active_call_page.dart +++ b/app/lib/pages/phone_calls/active_call_page.dart @@ -62,39 +62,61 @@ class _ActiveCallPageState extends State { Widget build(BuildContext context) { return Consumer( builder: (context, provider, _) { - return PopScope( - canPop: provider.callState == PhoneCallState.idle || provider.callState == PhoneCallState.ended, - child: Scaffold( - backgroundColor: Colors.black, - body: SafeArea( - child: Column( - children: [ - const SizedBox(height: 32), - _CallInfoHeader( - contactName: provider.contactName, - phoneNumber: provider.remoteNumber ?? '', - duration: provider.callDuration, - state: provider.callState, - ), - const SizedBox(height: 16), - Expanded( - child: _LiveTranscriptView( - segments: provider.transcriptSegments, - getSpeakerLabel: provider.getSpeakerLabel, - ), + bool isCallInProgress = provider.callState == PhoneCallState.active || + provider.callState == PhoneCallState.connecting || + provider.callState == PhoneCallState.ringing; + + return Scaffold( + backgroundColor: Colors.black, + body: SafeArea( + child: Column( + children: [ + // Top bar with minimize button — always visible so user can return to app + Padding( + padding: const EdgeInsets.only(left: 8, right: 8, top: 4), + child: Row( + children: [ + if (isCallInProgress) + IconButton( + onPressed: () { + MixpanelManager().track('Phone Call Minimized'); + Navigator.of(context).popUntil((route) => route.isFirst); + }, + icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white, size: 22), + padding: const EdgeInsets.all(12), + constraints: const BoxConstraints(), + ) + else + const SizedBox(width: 46), + const Spacer(), + ], ), - _CallControls( - state: provider.callState, - isMuted: provider.isMuted, - isSpeakerOn: provider.isSpeakerOn, - onMuteToggle: provider.toggleMute, - onSpeakerToggle: provider.toggleSpeaker, - onEndCall: () => provider.endCall(), - onKeypad: () => _showDtmfDialpad(context, provider), + ), + const SizedBox(height: 8), + _CallInfoHeader( + contactName: provider.contactName, + phoneNumber: provider.remoteNumber ?? '', + duration: provider.callDuration, + state: provider.callState, + ), + const SizedBox(height: 16), + Expanded( + child: _LiveTranscriptView( + segments: provider.transcriptSegments, + getSpeakerLabel: provider.getSpeakerLabel, ), - const SizedBox(height: 32), - ], - ), + ), + _CallControls( + state: provider.callState, + isMuted: provider.isMuted, + isSpeakerOn: provider.isSpeakerOn, + onMuteToggle: provider.toggleMute, + onSpeakerToggle: provider.toggleSpeaker, + onEndCall: () => provider.endCall(), + onKeypad: () => _showDtmfDialpad(context, provider), + ), + const SizedBox(height: 32), + ], ), ), ); From dee0c710a07ed1286e3a4e328722512e01230f5c Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin Date: Fri, 27 Mar 2026 00:35:24 +0530 Subject: [PATCH 3/8] Show ActiveCallBanner on conversations page above capture widget --- app/lib/pages/conversations/conversations_page.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/lib/pages/conversations/conversations_page.dart b/app/lib/pages/conversations/conversations_page.dart index c5d94de6901..f5fe6409db7 100644 --- a/app/lib/pages/conversations/conversations_page.dart +++ b/app/lib/pages/conversations/conversations_page.dart @@ -11,6 +11,7 @@ import 'package:omi/pages/conversations/widgets/daily_summaries_list.dart'; import 'package:omi/pages/conversations/widgets/folder_tabs.dart'; import 'package:omi/pages/conversations/widgets/goals_widget.dart'; import 'package:omi/pages/conversations/widgets/processing_capture.dart'; +import 'package:omi/pages/phone_calls/active_call_banner.dart'; import 'package:omi/pages/conversations/widgets/search_result_header_widget.dart'; import 'package:omi/pages/conversations/widgets/search_widget.dart'; import 'package:omi/pages/conversations/widgets/today_tasks_widget.dart'; @@ -180,6 +181,7 @@ class _ConversationsPageState extends State with AutomaticKee // Header widgets (unchanged) const SliverToBoxAdapter(child: SpeechProfileCardWidget()), const SliverToBoxAdapter(child: UpdateFirmwareCardWidget()), + const SliverToBoxAdapter(child: ActiveCallBanner()), const SliverToBoxAdapter(child: ConversationCaptureWidget()), // Search bar From d78bdbe4ad14d10f08edc707bfd7d0ff72433688 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin Date: Fri, 27 Mar 2026 00:35:54 +0530 Subject: [PATCH 4/8] Hide recording capture widget when a phone call is active The ActiveCallBanner replaces this widget during calls so both don't show simultaneously. --- .../conversations/widgets/processing_capture.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/lib/pages/conversations/widgets/processing_capture.dart b/app/lib/pages/conversations/widgets/processing_capture.dart index a9b4f2a861b..bc85ca24d50 100644 --- a/app/lib/pages/conversations/widgets/processing_capture.dart +++ b/app/lib/pages/conversations/widgets/processing_capture.dart @@ -21,6 +21,8 @@ import 'package:omi/utils/enums.dart'; import 'package:omi/utils/logger.dart'; import 'package:omi/utils/other/temp.dart'; import 'package:omi/utils/platform/platform_service.dart'; +import 'package:omi/backend/schema/phone_call.dart'; +import 'package:omi/providers/phone_call_provider.dart'; class ConversationCaptureWidget extends StatefulWidget { const ConversationCaptureWidget({super.key}); @@ -34,6 +36,14 @@ class _ConversationCaptureWidgetState extends State { @override Widget build(BuildContext context) { + // Hide capture widget when a phone call is in progress (banner replaces it) + var phoneCallState = context.watch().callState; + if (phoneCallState == PhoneCallState.active || + phoneCallState == PhoneCallState.connecting || + phoneCallState == PhoneCallState.ringing) { + return const SizedBox.shrink(); + } + return Consumer( builder: (context, provider, child) { var topConvoId = (provider.conversationProvider?.conversations ?? []).isNotEmpty From 3f6a0ba0bfa38b33dd1c3f66a2d46cac52839fcf Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin Date: Fri, 27 Mar 2026 00:36:27 +0530 Subject: [PATCH 5/8] Show ActiveCallTopBar on non-home tabs when a call is active Green bar at the top of Tasks, Memories, and Apps tabs lets users tap to return to the full call screen from any tab. --- app/lib/pages/home/page.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/lib/pages/home/page.dart b/app/lib/pages/home/page.dart index 8df4575361a..c7faec8fd27 100644 --- a/app/lib/pages/home/page.dart +++ b/app/lib/pages/home/page.dart @@ -31,6 +31,7 @@ import 'package:omi/pages/conversations/auto_sync_page.dart'; import 'package:omi/pages/conversations/sync_page.dart'; import 'package:omi/pages/conversations/widgets/merge_action_bar.dart'; import 'package:omi/pages/memories/page.dart'; +import 'package:omi/pages/phone_calls/active_call_banner.dart'; import 'package:omi/pages/phone_calls/phone_calls_page.dart'; import 'package:omi/pages/phone_calls/phone_calls_upsell_sheet.dart'; import 'package:omi/models/subscription.dart'; @@ -630,6 +631,8 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker children: [ Column( children: [ + // Show slim green call bar on non-home tabs when a call is active + if (homeProvider.selectedIndex != 0) const ActiveCallTopBar(), Expanded( child: IndexedStack(index: context.watch().selectedIndex, children: _pages), ), From f147271d4af94f4c5a6df3c8d7efa70d79c31077 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin Date: Fri, 27 Mar 2026 00:36:38 +0530 Subject: [PATCH 6/8] Hide mic record button in bottom nav during active phone call --- app/lib/widgets/bottom_nav_bar.dart | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/lib/widgets/bottom_nav_bar.dart b/app/lib/widgets/bottom_nav_bar.dart index 670a667fb42..1f35050ad6f 100644 --- a/app/lib/widgets/bottom_nav_bar.dart +++ b/app/lib/widgets/bottom_nav_bar.dart @@ -6,8 +6,10 @@ import 'package:provider/provider.dart'; import 'package:omi/pages/conversation_capturing/page.dart'; import 'package:omi/providers/capture_provider.dart'; +import 'package:omi/backend/schema/phone_call.dart'; import 'package:omi/providers/device_provider.dart'; import 'package:omi/providers/home_provider.dart'; +import 'package:omi/providers/phone_call_provider.dart'; import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/enums.dart'; import 'package:omi/utils/logger.dart'; @@ -23,6 +25,10 @@ class BottomNavBar extends StatelessWidget { return Consumer2( builder: (context, home, deviceProvider, child) { final isOmiDeviceConnected = deviceProvider.isConnected && deviceProvider.connectedDevice != null; + final phoneCallState = context.watch().callState; + final isOnCall = phoneCallState == PhoneCallState.active || + phoneCallState == PhoneCallState.connecting || + phoneCallState == PhoneCallState.ringing; return Stack( children: [ @@ -84,8 +90,8 @@ class BottomNavBar extends StatelessWidget { ), ), ), - // Center space for record button - only when no OMI device is connected - if (showCenterButton && !isOmiDeviceConnected) const SizedBox(width: 80), + // Center space for record button - only when no OMI device is connected and not on a call + if (showCenterButton && !isOmiDeviceConnected && !isOnCall) const SizedBox(width: 80), // Memories tab Expanded( child: InkWell( @@ -132,8 +138,8 @@ class BottomNavBar extends StatelessWidget { ), ), ), - // Central Record Button - Only show when no OMI device is connected - if (!isOmiDeviceConnected) + // Central Record Button - Only show when no OMI device is connected and not on a call + if (!isOmiDeviceConnected && !isOnCall) Positioned( left: MediaQuery.of(context).size.width / 2 - 40, bottom: 40, From acca6b09bdb16e6c909c95a8c616521f6be94936 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin Date: Fri, 27 Mar 2026 00:37:04 +0530 Subject: [PATCH 7/8] Block placing a second call while one is already active Shows a snackbar with 'A call is already in progress' if the user tries to dial while a call is in connecting/ringing/active state. --- app/lib/pages/phone_calls/phone_calls_page.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/lib/pages/phone_calls/phone_calls_page.dart b/app/lib/pages/phone_calls/phone_calls_page.dart index af0b32ee4c4..47b1e9be57f 100644 --- a/app/lib/pages/phone_calls/phone_calls_page.dart +++ b/app/lib/pages/phone_calls/phone_calls_page.dart @@ -5,6 +5,7 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:intl_country_data/intl_country_data.dart'; import 'package:provider/provider.dart'; +import 'package:omi/backend/schema/phone_call.dart'; import 'package:omi/pages/phone_calls/active_call_page.dart'; import 'package:omi/pages/phone_calls/phone_setup_intro_page.dart'; import 'package:omi/providers/phone_call_provider.dart'; @@ -93,6 +94,15 @@ class _PhoneCallsPageState extends State with SingleTickerProvid var provider = context.read(); + // Block if already on a call + if (provider.callState != PhoneCallState.idle && provider.callState != PhoneCallState.ended) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.callAlreadyInProgress)), + ); + return; + } + if (provider.verifiedNumbers.isEmpty) { Navigator.of(context).push(MaterialPageRoute(builder: (_) => const PhoneSetupIntroPage())); return; From 6477ea647807469ec2faafaa3c9f82db849821fa Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin Date: Fri, 27 Mar 2026 00:58:06 +0530 Subject: [PATCH 8/8] Fix disabled control buttons absorbing taps and remove unused isUser param Address Greptile review: pass null onTap to GestureDetector when button is disabled so taps propagate to the banner, and remove dead isUser field from _TranscriptSnippet. --- app/lib/pages/phone_calls/active_call_banner.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/lib/pages/phone_calls/active_call_banner.dart b/app/lib/pages/phone_calls/active_call_banner.dart index 3bf44a16f56..662ab6b74ea 100644 --- a/app/lib/pages/phone_calls/active_call_banner.dart +++ b/app/lib/pages/phone_calls/active_call_banner.dart @@ -54,7 +54,6 @@ class ActiveCallBanner extends StatelessWidget { padding: const EdgeInsets.only(top: 10), child: _TranscriptSnippet( text: provider.transcriptSegments.last.text, - isUser: provider.transcriptSegments.last.isUser, speakerLabel: provider.getSpeakerLabel(provider.transcriptSegments.last), ), ), @@ -168,10 +167,9 @@ class _CallInfoRow extends StatelessWidget { class _TranscriptSnippet extends StatelessWidget { final String text; - final bool isUser; final String speakerLabel; - const _TranscriptSnippet({required this.text, required this.isUser, required this.speakerLabel}); + const _TranscriptSnippet({required this.text, required this.speakerLabel}); @override Widget build(BuildContext context) { @@ -266,10 +264,12 @@ class _CompactControlButton extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onTap: () { - HapticFeedback.mediumImpact(); - onTap?.call(); - }, + onTap: onTap == null + ? null + : () { + HapticFeedback.mediumImpact(); + onTap!(); + }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration(