[READ ONLY MIRROR] Open Source TikTok alternative built on AT Protocol github.com/sprksocial/client
flutter atproto video dart
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: notification read-state

+148 -8
+44 -8
lib/src/features/notifications/ui/widgets/notification_item.dart
··· 1 + import 'dart:async'; 2 + 1 3 import 'package:atproto_core/atproto_core.dart'; 2 4 import 'package:auto_route/auto_route.dart'; 3 5 import 'package:flutter/material.dart'; ··· 18 20 class NotificationItem extends ConsumerStatefulWidget { 19 21 const NotificationItem({ 20 22 required this.groupedNotification, 23 + required this.isVisibleInViewport, 21 24 this.onViewed, 22 25 super.key, 23 26 }); 24 27 25 28 final GroupedNotification groupedNotification; 29 + final bool isVisibleInViewport; 26 30 final VoidCallback? onViewed; 27 31 28 32 @override ··· 31 35 32 36 class _NotificationItemState extends ConsumerState<NotificationItem> { 33 37 bool _hasBeenViewed = false; 38 + Timer? _viewTimer; 34 39 35 40 SprkRepository get _sprkRepository => GetIt.instance<SprkRepository>(); 36 41 ··· 41 46 @override 42 47 void initState() { 43 48 super.initState(); 44 - // Mark as viewed after a short delay to ensure it's actually visible 45 - WidgetsBinding.instance.addPostFrameCallback((_) { 46 - Future.delayed(const Duration(milliseconds: 500), () { 47 - if (mounted && !_hasBeenViewed && !widget.groupedNotification.isRead) { 48 - _hasBeenViewed = true; 49 - _markAsViewed(); 50 - } 51 - }); 49 + _updateViewTimer(); 50 + } 51 + 52 + @override 53 + void didUpdateWidget(covariant NotificationItem oldWidget) { 54 + super.didUpdateWidget(oldWidget); 55 + 56 + if (oldWidget.isVisibleInViewport != widget.isVisibleInViewport || 57 + oldWidget.groupedNotification.isRead != 58 + widget.groupedNotification.isRead) { 59 + _updateViewTimer(); 60 + } 61 + } 62 + 63 + @override 64 + void dispose() { 65 + _viewTimer?.cancel(); 66 + super.dispose(); 67 + } 68 + 69 + void _updateViewTimer() { 70 + _viewTimer?.cancel(); 71 + 72 + if (_hasBeenViewed || 73 + widget.groupedNotification.isRead || 74 + !widget.isVisibleInViewport) { 75 + return; 76 + } 77 + 78 + _viewTimer = Timer(const Duration(milliseconds: 500), () { 79 + if (!mounted || 80 + _hasBeenViewed || 81 + widget.groupedNotification.isRead || 82 + !widget.isVisibleInViewport) { 83 + return; 84 + } 85 + 86 + _hasBeenViewed = true; 87 + _markAsViewed(); 52 88 }); 53 89 } 54 90
+104
lib/src/features/notifications/ui/widgets/notifications_list.dart
··· 16 16 17 17 class _NotificationsListState extends ConsumerState<NotificationsList> { 18 18 final ScrollController _scrollController = ScrollController(); 19 + final GlobalKey _listViewportKey = GlobalKey(); 20 + final Map<String, GlobalKey> _itemKeys = {}; 21 + Set<String> _visibleGroupIds = const <String>{}; 22 + bool _visibilityCheckScheduled = false; 19 23 20 24 @override 21 25 void initState() { 22 26 super.initState(); 23 27 _scrollController.addListener(_onScroll); 28 + WidgetsBinding.instance.addPostFrameCallback((_) { 29 + _scheduleVisibilityCheck(); 30 + }); 24 31 } 25 32 26 33 @override ··· 44 51 ) 45 52 .loadMore(priority: widget.priority, reasons: widget.reasons); 46 53 } 54 + 55 + _scheduleVisibilityCheck(); 56 + } 57 + 58 + void _scheduleVisibilityCheck() { 59 + if (_visibilityCheckScheduled || !mounted) { 60 + return; 61 + } 62 + 63 + _visibilityCheckScheduled = true; 64 + WidgetsBinding.instance.addPostFrameCallback((_) { 65 + _visibilityCheckScheduled = false; 66 + _updateVisibleItems(); 67 + }); 68 + } 69 + 70 + void _updateVisibleItems() { 71 + if (!mounted) { 72 + return; 73 + } 74 + 75 + final listContext = _listViewportKey.currentContext; 76 + if (listContext == null) { 77 + return; 78 + } 79 + 80 + final listRenderBox = listContext.findRenderObject() as RenderBox?; 81 + if (listRenderBox == null || !listRenderBox.hasSize) { 82 + return; 83 + } 84 + 85 + final viewportTop = listRenderBox.localToGlobal(Offset.zero).dy; 86 + final viewportBottom = viewportTop + listRenderBox.size.height; 87 + 88 + final visibleIds = <String>{}; 89 + 90 + for (final entry in _itemKeys.entries) { 91 + final itemContext = entry.value.currentContext; 92 + if (itemContext == null) { 93 + continue; 94 + } 95 + 96 + final renderBox = itemContext.findRenderObject() as RenderBox?; 97 + if (renderBox == null || !renderBox.hasSize) { 98 + continue; 99 + } 100 + 101 + final itemTop = renderBox.localToGlobal(Offset.zero).dy; 102 + final itemBottom = itemTop + renderBox.size.height; 103 + final visibleHeight = 104 + (itemBottom < viewportBottom ? itemBottom : viewportBottom) - 105 + (itemTop > viewportTop ? itemTop : viewportTop); 106 + 107 + if (visibleHeight <= 0) { 108 + continue; 109 + } 110 + 111 + final visibleFraction = visibleHeight / renderBox.size.height; 112 + if (visibleFraction >= 0.5) { 113 + visibleIds.add(entry.key); 114 + } 115 + } 116 + 117 + if (_setEquals(_visibleGroupIds, visibleIds)) { 118 + return; 119 + } 120 + 121 + setState(() { 122 + _visibleGroupIds = visibleIds; 123 + }); 124 + } 125 + 126 + bool _setEquals(Set<String> a, Set<String> b) { 127 + if (identical(a, b)) { 128 + return true; 129 + } 130 + 131 + if (a.length != b.length) { 132 + return false; 133 + } 134 + 135 + for (final value in a) { 136 + if (!b.contains(value)) { 137 + return false; 138 + } 139 + } 140 + 141 + return true; 47 142 } 48 143 49 144 @override ··· 186 281 } 187 282 188 283 final groupedNotifications = notificationState.groupedNotifications; 284 + WidgetsBinding.instance.addPostFrameCallback((_) { 285 + _scheduleVisibilityCheck(); 286 + }); 189 287 190 288 return RefreshIndicator( 191 289 onRefresh: () async { ··· 199 297 .refresh(priority: widget.priority, reasons: widget.reasons); 200 298 }, 201 299 child: ListView.builder( 300 + key: _listViewportKey, 202 301 controller: _scrollController, 203 302 physics: const AlwaysScrollableScrollPhysics(), 204 303 itemCount: ··· 214 313 } 215 314 216 315 final groupedNotification = groupedNotifications[index]; 316 + final groupId = groupedNotification.primaryNotification.uri 317 + .toString(); 318 + final itemKey = _itemKeys.putIfAbsent(groupId, () => GlobalKey()); 217 319 final notifier = ref.read( 218 320 notificationProvider( 219 321 priority: widget.priority, ··· 221 323 ).notifier, 222 324 ); 223 325 return NotificationItem( 326 + key: itemKey, 224 327 groupedNotification: groupedNotification, 328 + isVisibleInViewport: _visibleGroupIds.contains(groupId), 225 329 onViewed: () { 226 330 // Mark all notifications in the group as viewed 227 331 groupedNotification.notifications.forEach(