feat: Phase 1.3 - Nav helper badges (Videos/Alerts) and long-press tooltips

This commit is contained in:
Patrick Britton 2026-02-17 03:32:51 -06:00
parent 0c183c3491
commit bf4ac02d4b

View file

@ -13,6 +13,7 @@ import '../discover/discover_screen.dart';
import '../beacon/beacon_screen.dart'; import '../beacon/beacon_screen.dart';
import '../quips/create/quip_creation_flow.dart'; import '../quips/create/quip_creation_flow.dart';
import '../secure_chat/secure_chat_full_screen.dart'; import '../secure_chat/secure_chat_full_screen.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../widgets/radial_menu_overlay.dart'; import '../../widgets/radial_menu_overlay.dart';
import '../../widgets/onboarding_modal.dart'; import '../../widgets/onboarding_modal.dart';
import '../../providers/quip_upload_provider.dart'; import '../../providers/quip_upload_provider.dart';
@ -35,17 +36,56 @@ class _HomeShellState extends ConsumerState<HomeShell> with WidgetsBindingObserv
final SecureChatService _chatService = SecureChatService(); final SecureChatService _chatService = SecureChatService();
StreamSubscription<RemoteMessage>? _notifSub; StreamSubscription<RemoteMessage>? _notifSub;
// Nav helper badges show descriptive subtitle for first N taps
static const _maxHelperShows = 3;
Map<int, int> _navTapCounts = {};
static const _helperBadges = {
1: 'Videos', // Quips tab
2: 'Alerts', // Beacons tab
};
static const _longPressTooltips = {
0: 'Your main feed with posts from people you follow',
1: 'Quips are short-form videos — your stories',
2: 'Beacons are local alerts and real-time updates',
3: 'Your profile, settings, and saved posts',
};
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
_chatService.startBackgroundSync(); _chatService.startBackgroundSync();
_initNotificationListener(); _initNotificationListener();
_loadNavTapCounts();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) OnboardingModal.showIfNeeded(context); if (mounted) OnboardingModal.showIfNeeded(context);
}); });
} }
Future<void> _loadNavTapCounts() async {
final prefs = await SharedPreferences.getInstance();
if (mounted) {
setState(() {
_navTapCounts = {
for (final i in [1, 2])
i: prefs.getInt('nav_tap_$i') ?? 0,
};
});
}
}
Future<void> _incrementNavTap(int index) async {
if (!_helperBadges.containsKey(index)) return;
final prefs = await SharedPreferences.getInstance();
final current = prefs.getInt('nav_tap_$index') ?? 0;
await prefs.setInt('nav_tap_$index', current + 1);
if (mounted) {
setState(() => _navTapCounts[index] = current + 1);
}
}
void _initNotificationListener() { void _initNotificationListener() {
_notifSub = NotificationService.instance.foregroundMessages.listen((message) { _notifSub = NotificationService.instance.foregroundMessages.listen((message) {
if (mounted) { if (mounted) {
@ -369,9 +409,30 @@ class _HomeShellState extends ConsumerState<HomeShell> with WidgetsBindingObserv
String? assetPath, String? assetPath,
}) { }) {
final isActive = widget.navigationShell.currentIndex == index; final isActive = widget.navigationShell.currentIndex == index;
final helperBadge = _helperBadges[index];
final tapCount = _navTapCounts[index] ?? 0;
final showHelper = helperBadge != null && tapCount < _maxHelperShows;
final tooltip = _longPressTooltips[index];
return Expanded( return Expanded(
child: GestureDetector(
onLongPress: tooltip != null ? () {
final overlay = Overlay.of(context);
late OverlayEntry entry;
entry = OverlayEntry(
builder: (ctx) => _NavTooltipOverlay(
message: tooltip,
onDismiss: () => entry.remove(),
),
);
overlay.insert(entry);
Future.delayed(const Duration(seconds: 2), () {
if (entry.mounted) entry.remove();
});
} : null,
child: InkWell( child: InkWell(
onTap: () { onTap: () {
_incrementNavTap(index);
widget.navigationShell.goBranch( widget.navigationShell.goBranch(
index, index,
initialLocation: index == widget.navigationShell.currentIndex, initialLocation: index == widget.navigationShell.currentIndex,
@ -383,6 +444,9 @@ class _HomeShellState extends ConsumerState<HomeShell> with WidgetsBindingObserv
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [
Stack(
clipBehavior: Clip.none,
children: [ children: [
assetPath != null assetPath != null
? Image.asset( ? Image.asset(
@ -396,6 +460,23 @@ class _HomeShellState extends ConsumerState<HomeShell> with WidgetsBindingObserv
color: isActive ? AppTheme.navyBlue : SojornColors.bottomNavUnselected, color: isActive ? AppTheme.navyBlue : SojornColors.bottomNavUnselected,
size: SojornNav.bottomBarIconSize, size: SojornNav.bottomBarIconSize,
), ),
if (showHelper)
Positioned(
right: -18,
top: -4,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
decoration: BoxDecoration(
color: AppTheme.brightNavy,
borderRadius: BorderRadius.circular(6),
),
child: Text(helperBadge, style: const TextStyle(
fontSize: 8, fontWeight: FontWeight.w700, color: Colors.white,
)),
),
),
],
),
SizedBox(height: SojornNav.bottomBarLabelTopGap), SizedBox(height: SojornNav.bottomBarLabelTopGap),
Text( Text(
label, label,
@ -411,6 +492,46 @@ class _HomeShellState extends ConsumerState<HomeShell> with WidgetsBindingObserv
), ),
), ),
), ),
),
);
}
}
// Nav Tooltip Overlay (long-press on nav items)
class _NavTooltipOverlay extends StatelessWidget {
final String message;
final VoidCallback onDismiss;
const _NavTooltipOverlay({required this.message, required this.onDismiss});
@override
Widget build(BuildContext context) {
return Positioned.fill(
child: GestureDetector(
onTap: onDismiss,
behavior: HitTestBehavior.translucent,
child: Align(
alignment: const Alignment(0, 0.85),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 32),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: AppTheme.navyBlue,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Text(message, style: const TextStyle(
color: Colors.white, fontSize: 13, fontWeight: FontWeight.w500,
), textAlign: TextAlign.center),
),
),
),
); );
} }
} }