diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5fdf45e..8e3c79c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3,12 +3,19 @@ PODS: - Flutter - Firebase/CoreOnly (12.12.0): - FirebaseCore (~> 12.12.0) + - Firebase/Crashlytics (12.12.0): + - Firebase/CoreOnly + - FirebaseCrashlytics (~> 12.12.0) - Firebase/Messaging (12.12.0): - Firebase/CoreOnly - FirebaseMessaging (~> 12.12.0) - firebase_core (4.7.0): - Firebase/CoreOnly (= 12.12.0) - Flutter + - firebase_crashlytics (5.2.0): + - Firebase/Crashlytics (= 12.12.0) + - firebase_core + - Flutter - firebase_messaging (16.2.0): - Firebase/Messaging (= 12.12.0) - firebase_core @@ -17,8 +24,19 @@ PODS: - FirebaseCoreInternal (~> 12.12.0) - GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Logger (~> 8.1) + - FirebaseCoreExtension (12.12.0): + - FirebaseCore (~> 12.12.0) - FirebaseCoreInternal (12.12.0): - "GoogleUtilities/NSData+zlib (~> 8.1)" + - FirebaseCrashlytics (12.12.1): + - FirebaseCore (~> 12.12.0) + - FirebaseInstallations (~> 12.12.0) + - FirebaseRemoteConfigInterop (~> 12.12.0) + - FirebaseSessions (~> 12.12.0) + - GoogleDataTransport (~> 10.1) + - GoogleUtilities/Environment (~> 8.1) + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) - FirebaseInstallations (12.12.0): - FirebaseCore (~> 12.12.0) - GoogleUtilities/Environment (~> 8.1) @@ -33,11 +51,19 @@ PODS: - GoogleUtilities/Reachability (~> 8.1) - GoogleUtilities/UserDefaults (~> 8.1) - nanopb (~> 3.30910.0) + - FirebaseRemoteConfigInterop (12.12.0) + - FirebaseSessions (12.12.0): + - FirebaseCore (~> 12.12.0) + - FirebaseCoreExtension (~> 12.12.0) + - FirebaseInstallations (~> 12.12.0) + - GoogleDataTransport (~> 10.1) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - nanopb (~> 3.30910.0) + - PromisesSwift (~> 2.1) - Flutter (1.0.0) - flutter_local_notifications (0.0.1): - Flutter - - flutter_native_splash (2.4.3): - - Flutter - gal (1.0.0): - Flutter - FlutterMacOS @@ -84,6 +110,8 @@ PODS: - permission_handler_apple (9.3.0): - Flutter - PromisesObjC (2.4.0) + - PromisesSwift (2.4.0): + - PromisesObjC (= 2.4.0) - share_plus (0.0.1): - Flutter - sqflite_darwin (0.0.4): @@ -149,10 +177,10 @@ PODS: DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - Flutter (from `Flutter`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - gal (from `.symlinks/plugins/gal/darwin`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - objectbox_flutter_libs (from `.symlinks/plugins/objectbox_flutter_libs/ios`) @@ -171,14 +199,19 @@ SPEC REPOS: trunk: - Firebase - FirebaseCore + - FirebaseCoreExtension - FirebaseCoreInternal + - FirebaseCrashlytics - FirebaseInstallations - FirebaseMessaging + - FirebaseRemoteConfigInterop + - FirebaseSessions - GoogleDataTransport - GoogleUtilities - nanopb - ObjectBox - PromisesObjC + - PromisesSwift - sqlite3 - TensorFlowLiteC - TensorFlowLiteSwift @@ -188,14 +221,14 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/connectivity_plus/ios" firebase_core: :path: ".symlinks/plugins/firebase_core/ios" + firebase_crashlytics: + :path: ".symlinks/plugins/firebase_crashlytics/ios" firebase_messaging: :path: ".symlinks/plugins/firebase_messaging/ios" Flutter: :path: Flutter flutter_local_notifications: :path: ".symlinks/plugins/flutter_local_notifications/ios" - flutter_native_splash: - :path: ".symlinks/plugins/flutter_native_splash/ios" gal: :path: ".symlinks/plugins/gal/darwin" image_picker_ios: @@ -227,14 +260,18 @@ SPEC CHECKSUMS: connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd Firebase: aa154fee4e9b8eac17aa42344988865b3e857d33 firebase_core: 9156a152117c843440b0b990c785aa0259bc5447 + firebase_crashlytics: e24acd48861c5edf6e0f6c134d6a0b28593c76d7 firebase_messaging: 0d962ab44ff24ed36deb8fa2ee043c4671858269 FirebaseCore: 86241206e656f5c80c995e370e6c975913b9b284 + FirebaseCoreExtension: ff6fd42eb5287e71d3e160450de6509733d9ead7 FirebaseCoreInternal: 7c12fc3011d889085e765e317d7b9fd1cef97af9 + FirebaseCrashlytics: 03f4e20d0c9b7fd6338cb9066f4bfb69d3f42fd0 FirebaseInstallations: 4e6e162aa4abaaeeeb01dd00179dfc5ad9c2194e FirebaseMessaging: 341004946fa7ffc741344b20f1b667514fc93e31 + FirebaseRemoteConfigInterop: 23996ab7397494722df4fdd1fd398024389d5da8 + FirebaseSessions: 804bd321f2d2f2ddafe74ef7856062aa19f179c2 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb - flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf gal: baecd024ebfd13c441269ca7404792a7152fde89 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 @@ -245,6 +282,7 @@ SPEC CHECKSUMS: package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqlite3: a51c07cf16e023d6c48abd5e5791a61a47354921 diff --git a/lib/features/feed/presentation/widgets/post_card_footer.dart b/lib/features/feed/presentation/widgets/post_card_footer.dart index 7fdea85..2624284 100644 --- a/lib/features/feed/presentation/widgets/post_card_footer.dart +++ b/lib/features/feed/presentation/widgets/post_card_footer.dart @@ -69,15 +69,17 @@ class PostCardFooter extends StatelessWidget { const horizontalPadding = 8.0; const topPadding = 6.0; const bottomPadding = 4.0; - const iconSize = 18.0; + const iconSize = 20.0; return LayoutBuilder( builder: (context, constraints) { - final compactLayout = constraints.maxWidth < 220; + final compactLayout = constraints.maxWidth < 240; final actionSpacing = compactLayout ? 4.0 : 8.0; - final actionPadding = compactLayout ? 1.5 : 3.0; + final actionHorizontalPadding = compactLayout ? 6.0 : 8.0; + final actionVerticalPadding = compactLayout ? 6.0 : 8.0; + final minimumTapTarget = compactLayout ? 40.0 : 44.0; final canShowCounts = showCounts && constraints.maxWidth >= 240; - final actions = [ + final actions = [ _FooterAction( icon: Icons.chat_bubble_outline, activeIcon: Icons.chat_bubble, @@ -87,7 +89,9 @@ class PostCardFooter extends StatelessWidget { onTap: isOffline ? null : onReply, color: colorScheme.onSurfaceVariant, iconSize: iconSize, - padding: actionPadding, + horizontalPadding: actionHorizontalPadding, + verticalPadding: actionVerticalPadding, + minTapTarget: minimumTapTarget, showCount: canShowCounts, tooltip: isOffline ? offlineActionMessage('reply to this post') : null, ), @@ -102,7 +106,9 @@ class PostCardFooter extends StatelessWidget { color: colorScheme.onSurfaceVariant, activeColor: Colors.green, iconSize: iconSize, - padding: actionPadding, + horizontalPadding: actionHorizontalPadding, + verticalPadding: actionVerticalPadding, + minTapTarget: minimumTapTarget, showCount: canShowCounts, tooltip: isOffline ? offlineActionMessage('repost this post') : null, ), @@ -116,7 +122,9 @@ class PostCardFooter extends StatelessWidget { color: colorScheme.onSurfaceVariant, activeColor: Colors.pink, iconSize: iconSize, - padding: actionPadding, + horizontalPadding: actionHorizontalPadding, + verticalPadding: actionVerticalPadding, + minTapTarget: minimumTapTarget, showCount: canShowCounts, tooltip: isOffline ? offlineActionMessage('like this post') : null, ), @@ -131,23 +139,20 @@ class PostCardFooter extends StatelessWidget { color: colorScheme.onSurfaceVariant, activeColor: saveActiveColor, iconSize: iconSize, - padding: actionPadding, + horizontalPadding: actionHorizontalPadding, + verticalPadding: actionVerticalPadding, + minTapTarget: minimumTapTarget, showCount: canShowCounts, ), - if (onMore != null) - _FooterAction( - icon: Icons.more_vert, - activeIcon: Icons.more_vert, - isActive: false, - isLoading: false, - count: 0, - onTap: onMore, - color: colorScheme.onSurfaceVariant, - iconSize: iconSize, - padding: actionPadding, - showCount: false, - ), ]; + final trailingMeta = _buildTrailingMeta( + context: context, + colorScheme: colorScheme, + iconSize: iconSize, + actionHorizontalPadding: actionHorizontalPadding, + actionVerticalPadding: actionVerticalPadding, + minimumTapTarget: minimumTapTarget, + ); return Container( decoration: BoxDecoration( @@ -165,7 +170,7 @@ class PostCardFooter extends StatelessWidget { children: actions, ), const SizedBox(height: 4), - Align(alignment: Alignment.centerRight, child: _buildTimestamp(context, colorScheme)), + Align(alignment: Alignment.centerRight, child: trailingMeta), ], ) : Row( @@ -173,7 +178,7 @@ class PostCardFooter extends StatelessWidget { for (int i = 0; i < actions.length; i++) ...[if (i > 0) SizedBox(width: actionSpacing), actions[i]], SizedBox(width: actionSpacing), Expanded( - child: Align(alignment: Alignment.centerRight, child: _buildTimestamp(context, colorScheme)), + child: Align(alignment: Alignment.centerRight, child: trailingMeta), ), ], ), @@ -218,6 +223,40 @@ class PostCardFooter extends StatelessWidget { ); } + Widget _buildTrailingMeta({ + required BuildContext context, + required ColorScheme colorScheme, + required double iconSize, + required double actionHorizontalPadding, + required double actionVerticalPadding, + required double minimumTapTarget, + }) { + return Row( + key: const ValueKey('post_footer_trailing_meta'), + mainAxisSize: MainAxisSize.min, + children: [ + _buildTimestamp(context, colorScheme), + if (onMore != null) ...[ + const SizedBox(width: 2), + _FooterAction( + icon: Icons.more_vert, + activeIcon: Icons.more_vert, + isActive: false, + isLoading: false, + count: 0, + onTap: onMore, + color: colorScheme.onSurfaceVariant, + iconSize: iconSize, + horizontalPadding: actionHorizontalPadding, + verticalPadding: actionVerticalPadding, + minTapTarget: minimumTapTarget, + showCount: false, + ), + ], + ], + ); + } + void _showSaveOptions(BuildContext context) { HapticHelper.mediumImpact(); final isLocalSaved = isSaved && (saveType == 'local' || saveType == 'both'); @@ -254,7 +293,9 @@ class _FooterAction extends StatelessWidget { required this.isActive, required this.isLoading, required this.iconSize, - required this.padding, + required this.horizontalPadding, + required this.verticalPadding, + required this.minTapTarget, required this.count, required this.showCount, this.onTap, @@ -269,7 +310,9 @@ class _FooterAction extends StatelessWidget { final bool isActive; final bool isLoading; final double iconSize; - final double padding; + final double horizontalPadding; + final double verticalPadding; + final double minTapTarget; final int count; final bool showCount; final VoidCallback? onTap; @@ -286,25 +329,28 @@ class _FooterAction extends StatelessWidget { Widget button = InkWell( onTap: isLoading ? null : onTap, onLongPress: onLongPress, - borderRadius: BorderRadius.zero, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: padding, vertical: padding), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (isLoading) - SizedBox( - width: iconSize, - height: iconSize, - child: CircularProgressIndicator(strokeWidth: 2, color: iconColor), - ) - else - Icon(isActive ? activeIcon : icon, size: iconSize, color: iconColor), - if (showCount && count > 0) ...[ - const SizedBox(width: 4), - Text(formatCount(count), style: context.textTheme.bodySmall?.copyWith(color: iconColor)), + borderRadius: BorderRadius.circular(8), + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: minTapTarget, minHeight: minTapTarget), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: horizontalPadding, vertical: verticalPadding), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (isLoading) + SizedBox( + width: iconSize, + height: iconSize, + child: CircularProgressIndicator(strokeWidth: 2, color: iconColor), + ) + else + Icon(isActive ? activeIcon : icon, size: iconSize, color: iconColor), + if (showCount && count > 0) ...[ + const SizedBox(width: 4), + Text(formatCount(count), style: context.textTheme.bodySmall?.copyWith(color: iconColor)), + ], ], - ], + ), ), ), ); diff --git a/test/features/feed/presentation/post_card_footer_test.dart b/test/features/feed/presentation/post_card_footer_test.dart index c2eb00f..9174df8 100644 --- a/test/features/feed/presentation/post_card_footer_test.dart +++ b/test/features/feed/presentation/post_card_footer_test.dart @@ -133,9 +133,14 @@ void main() { testWidgets('has a top border via BoxDecoration', (tester) async { await tester.pumpWidget(_buildSubject(const PostCardFooter(timestamp: '1H'))); - final container = tester.widget( - find.ancestor(of: find.byType(Row), matching: find.byType(Container)).first, + final containerFinder = find.byWidgetPredicate( + (widget) => + widget is Container && + widget.decoration is BoxDecoration && + ((widget.decoration as BoxDecoration).border?.top.width ?? 0) > 0, ); + expect(containerFinder, findsOneWidget); + final container = tester.widget(containerFinder); final decoration = container.decoration as BoxDecoration?; expect(decoration?.border, isNotNull); }); @@ -222,5 +227,50 @@ void main() { await tester.pumpWidget(_buildSubject(PostCardFooter(timestamp: '1H', onMore: () {}))); expect(find.byIcon(Icons.more_vert), findsOneWidget); }); + + testWidgets('keeps kebab to the right of timestamp in trailing meta area', (tester) async { + await tester.pumpWidget(_buildSubject(PostCardFooter(timestamp: '1H', onMore: () {}))); + + final timestampRight = tester.getTopRight(find.text('1H')).dx; + final kebabLeft = tester.getTopLeft(find.byIcon(Icons.more_vert)).dx; + expect(kebabLeft, greaterThan(timestampRight)); + }); + + testWidgets('uses larger tap target for post actions', (tester) async { + await tester.pumpWidget(_buildSubject(const PostCardFooter(timestamp: '1H'))); + + final replyTapTargetFinder = find.ancestor( + of: find.byIcon(Icons.chat_bubble_outline), + matching: find.byType(InkWell), + ); + final tapTargetSize = tester.getSize(replyTapTargetFinder.first); + expect(tapTargetSize.width, greaterThanOrEqualTo(40)); + expect(tapTargetSize.height, greaterThanOrEqualTo(40)); + }); + + testWidgets('keeps action tap callbacks isolated', (tester) async { + var replyTaps = 0; + var repostTaps = 0; + var likeTaps = 0; + + await tester.pumpWidget( + _buildSubject( + PostCardFooter( + timestamp: '1H', + onReply: () => replyTaps++, + onRepost: () => repostTaps++, + onLike: () => likeTaps++, + ), + ), + ); + + await tester.tap(find.byIcon(Icons.chat_bubble_outline)); + await tester.tap(find.byIcon(Icons.repeat)); + await tester.tap(find.byIcon(Icons.favorite_outline)); + + expect(replyTaps, 1); + expect(repostTaps, 1); + expect(likeTaps, 1); + }); }); }