Skip to content

Commit

Permalink
fix(neon_framework): User status DND takes precedence over emoji
Browse files Browse the repository at this point in the history
Signed-off-by: provokateurin <kate@provokateurin.de>
  • Loading branch information
provokateurin committed Feb 28, 2024
1 parent bfe1bc7 commit 258cb11
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 25 deletions.
54 changes: 30 additions & 24 deletions packages/neon_framework/lib/src/widgets/user_avatar.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -16,8 +17,6 @@ class NeonUserAvatar extends StatefulWidget {
this.username,
this.showStatus = true,
this.size,
this.backgroundColor,
this.foregroundColor,
super.key,
}) : account = null;

Expand All @@ -27,8 +26,6 @@ class NeonUserAvatar extends StatefulWidget {
this.username,
this.showStatus = true,
this.size,
this.backgroundColor,
this.foregroundColor,
super.key,
});

Expand All @@ -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<NeonUserAvatar> createState() => _UserAvatarState();
}
Expand Down Expand Up @@ -87,7 +77,6 @@ class _UserAvatarState extends State<NeonUserAvatar> {

final avatar = CircleAvatar(
radius: size / 2,
backgroundColor: widget.backgroundColor,
child: ClipOval(
child: NeonApiImage.withAccount(
account: account,
Expand Down Expand Up @@ -119,40 +108,57 @@ class _UserAvatarState extends State<NeonUserAvatar> {
stream: userStatusBloc!.statuses.map(
(statuses) => statuses[username] ?? Result<user_status.$PublicInterface>.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<user_status.$PublicInterface> result;

final double size;

Widget _userStatusIconBuilder(BuildContext context, Result<user_status.$PublicInterface> result) {
@override
Widget build(BuildContext context) {
final hasEmoji = result.data?.icon != null;
final scaledSize = size / (hasEmoji ? 2 : 2.5);

Widget? child;
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(
Icons.error_outline,
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,
),
);
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/neon_framework/lib/widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
102 changes: 102 additions & 0 deletions packages/neon_framework/test/user_avatar_test.dart
Original file line number Diff line number Diff line change
@@ -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.$PublicInterface>(
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<AccountsBloc>(
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.$PublicInterface>(
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);
});
}
});
}

0 comments on commit 258cb11

Please sign in to comment.