From 87541892d3d5dc2c19b0434db20f70ea507cb1e6 Mon Sep 17 00:00:00 2001 From: provokateurin Date: Mon, 26 Feb 2024 17:44:08 +0100 Subject: [PATCH] fix(neon_framework): User status DND takes precedence over emoji Signed-off-by: provokateurin --- .../lib/src/widgets/user_avatar.dart | 54 +++++----- packages/neon_framework/lib/widgets.dart | 2 +- .../neon_framework/test/user_avatar_test.dart | 102 ++++++++++++++++++ 3 files changed, 133 insertions(+), 25 deletions(-) create mode 100644 packages/neon_framework/test/user_avatar_test.dart diff --git a/packages/neon_framework/lib/src/widgets/user_avatar.dart b/packages/neon_framework/lib/src/widgets/user_avatar.dart index 10773268284..3af6e03d65c 100644 --- a/packages/neon_framework/lib/src/widgets/user_avatar.dart +++ b/packages/neon_framework/lib/src/widgets/user_avatar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:meta/meta.dart'; import 'package:neon_framework/blocs.dart'; import 'package:neon_framework/models.dart'; import 'package:neon_framework/src/models/account.dart'; @@ -16,8 +17,6 @@ class NeonUserAvatar extends StatefulWidget { this.username, this.showStatus = true, this.size, - this.backgroundColor, - this.foregroundColor, super.key, }) : account = null; @@ -27,8 +26,6 @@ class NeonUserAvatar extends StatefulWidget { this.username, this.showStatus = true, this.size, - this.backgroundColor, - this.foregroundColor, super.key, }); @@ -48,13 +45,6 @@ class NeonUserAvatar extends StatefulWidget { /// The size of the avatar. final double? size; - /// The color with which to fill the circle. Changing the background - /// color will cause the avatar to animate to the new color. - final Color? backgroundColor; - - /// The color used to render the loading animation. - final Color? foregroundColor; - @override State createState() => _UserAvatarState(); } @@ -87,7 +77,6 @@ class _UserAvatarState extends State { final avatar = CircleAvatar( radius: size / 2, - backgroundColor: widget.backgroundColor, child: ClipOval( child: NeonApiImage.withAccount( account: account, @@ -119,14 +108,31 @@ class _UserAvatarState extends State { stream: userStatusBloc!.statuses.map( (statuses) => statuses[username] ?? Result.loading(), ), - builder: _userStatusIconBuilder, + builder: (context, result) => NeonUserStatusIndicator( + result: result, + size: size, + ), ), ], ); }, ); +} + +@internal +class NeonUserStatusIndicator extends StatelessWidget { + const NeonUserStatusIndicator({ + required this.result, + required this.size, + super.key, + }); + + final Result result; + + final double size; - Widget _userStatusIconBuilder(BuildContext context, Result result) { + @override + Widget build(BuildContext context) { final hasEmoji = result.data?.icon != null; final scaledSize = size / (hasEmoji ? 2 : 2.5); @@ -134,7 +140,7 @@ class _UserAvatarState extends State { if (result.isLoading) { child = CircularProgressIndicator( strokeWidth: 1.5, - color: widget.foregroundColor ?? Theme.of(context).colorScheme.onPrimary, + color: Theme.of(context).colorScheme.onPrimary, ); } else if (result.hasError) { child = Icon( @@ -142,17 +148,17 @@ class _UserAvatarState extends State { size: scaledSize, color: Theme.of(context).colorScheme.error, ); - } else if (hasEmoji) { - child = Text( - result.data!.icon!, - style: const TextStyle( - fontSize: 16, - ), - ); } else if (result.hasData) { - final type = result.data!.status; - if (type != user_status.$Type.offline) { + final type = result.requireData.status; + if (type == user_status.$Type.dnd || (!hasEmoji && type != user_status.$Type.offline)) { child = NeonServerIcon(icon: 'user-status-$type'); + } else if (hasEmoji) { + child = Text( + result.data!.icon!, + style: const TextStyle( + fontSize: 16, + ), + ); } } diff --git a/packages/neon_framework/lib/widgets.dart b/packages/neon_framework/lib/widgets.dart index ee3a4b90591..87ce666fd69 100644 --- a/packages/neon_framework/lib/widgets.dart +++ b/packages/neon_framework/lib/widgets.dart @@ -6,4 +6,4 @@ export 'package:neon_framework/src/widgets/linear_progress_indicator.dart'; export 'package:neon_framework/src/widgets/list_view.dart'; export 'package:neon_framework/src/widgets/relative_time.dart'; export 'package:neon_framework/src/widgets/server_icon.dart'; -export 'package:neon_framework/src/widgets/user_avatar.dart'; +export 'package:neon_framework/src/widgets/user_avatar.dart' hide NeonUserStatusIndicator; diff --git a/packages/neon_framework/test/user_avatar_test.dart b/packages/neon_framework/test/user_avatar_test.dart new file mode 100644 index 00000000000..5fe35fd841c --- /dev/null +++ b/packages/neon_framework/test/user_avatar_test.dart @@ -0,0 +1,102 @@ +import 'package:built_collection/built_collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:neon_framework/blocs.dart'; +import 'package:neon_framework/l10n/localizations.dart'; +import 'package:neon_framework/src/testing/mocks.dart'; +import 'package:neon_framework/src/utils/provider.dart'; +import 'package:neon_framework/src/widgets/user_avatar.dart'; +import 'package:neon_framework/widgets.dart'; +import 'package:nextcloud/nextcloud.dart'; +import 'package:nextcloud/user_status.dart' as user_status; +import 'package:rxdart/rxdart.dart'; + +void main() { + setUp(() { + final storage = MockNeonStorage(); + when(() => storage.requestCache).thenReturn(null); + }); + + for (final (withStatus, matcher) in [(false, findsNothing), (true, findsOne)]) { + testWidgets('${withStatus ? 'With' : 'Without'} status', (tester) async { + final account = MockAccount(); + when(() => account.id).thenReturn('test'); + when(() => account.username).thenReturn('test'); + when(() => account.client).thenReturn(NextcloudClient(Uri.parse(''))); + + final userStatusBloc = MockUserStatusBloc(); + when(() => userStatusBloc.statuses).thenAnswer( + (_) => BehaviorSubject.seeded( + BuiltMap({ + 'test': Result( + user_status.Public( + (b) => b + ..userId = 'test' + ..status = user_status.$Type.online, + ), + null, + isLoading: false, + isCached: false, + ), + }), + ), + ); + + final accountsBloc = MockAccountsBloc(); + when(() => accountsBloc.activeAccount).thenAnswer((_) => BehaviorSubject.seeded(account)); + when(() => accountsBloc.getUserStatusBlocFor(account)).thenReturn(userStatusBloc); + + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: NeonLocalizations.localizationsDelegates, + home: NeonProvider( + create: (_) => accountsBloc, + child: NeonUserAvatar( + showStatus: withStatus, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.byType(NeonUserStatusIndicator), matcher); + }); + } + + group('Status indicator', () { + for (final (status, icon, textMatcher, iconMatcher) in [ + (user_status.$Type.offline, '😀', findsOne, findsNothing), + (user_status.$Type.offline, null, findsNothing, findsNothing), + (user_status.$Type.online, '😀', findsOne, findsNothing), + (user_status.$Type.online, null, findsNothing, findsOne), + (user_status.$Type.dnd, '😀', findsNothing, findsOne), + (user_status.$Type.dnd, null, findsNothing, findsOne), + ]) { + testWidgets('${status.value} ${icon != null ? 'with' : 'without'} emoji', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: NeonUserStatusIndicator( + result: Result( + user_status.Public( + (b) => b + ..userId = 'test' + ..status = status + ..icon = icon, + ), + null, + isLoading: false, + isCached: false, + ), + size: 1, + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.byType(Text), textMatcher); + expect(find.byType(NeonServerIcon), iconMatcher); + }); + } + }); +}