Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(neon_framework): User status DND takes precedence over emoji #1664

Merged
merged 1 commit into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
}
});
}