Skip to content

Commit f552ae0

Browse files
Merge pull request #1664 from nextcloud/fix/neon_framework/user-status-dnd-over-emoji
2 parents 8b47f70 + 8754189 commit f552ae0

File tree

3 files changed

+133
-25
lines changed

3 files changed

+133
-25
lines changed

packages/neon_framework/lib/src/widgets/user_avatar.dart

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:flutter/material.dart';
2+
import 'package:meta/meta.dart';
23
import 'package:neon_framework/blocs.dart';
34
import 'package:neon_framework/models.dart';
45
import 'package:neon_framework/src/models/account.dart';
@@ -16,8 +17,6 @@ class NeonUserAvatar extends StatefulWidget {
1617
this.username,
1718
this.showStatus = true,
1819
this.size,
19-
this.backgroundColor,
20-
this.foregroundColor,
2120
super.key,
2221
}) : account = null;
2322

@@ -27,8 +26,6 @@ class NeonUserAvatar extends StatefulWidget {
2726
this.username,
2827
this.showStatus = true,
2928
this.size,
30-
this.backgroundColor,
31-
this.foregroundColor,
3229
super.key,
3330
});
3431

@@ -48,13 +45,6 @@ class NeonUserAvatar extends StatefulWidget {
4845
/// The size of the avatar.
4946
final double? size;
5047

51-
/// The color with which to fill the circle. Changing the background
52-
/// color will cause the avatar to animate to the new color.
53-
final Color? backgroundColor;
54-
55-
/// The color used to render the loading animation.
56-
final Color? foregroundColor;
57-
5848
@override
5949
State<NeonUserAvatar> createState() => _UserAvatarState();
6050
}
@@ -87,7 +77,6 @@ class _UserAvatarState extends State<NeonUserAvatar> {
8777

8878
final avatar = CircleAvatar(
8979
radius: size / 2,
90-
backgroundColor: widget.backgroundColor,
9180
child: ClipOval(
9281
child: NeonApiImage.withAccount(
9382
account: account,
@@ -119,40 +108,57 @@ class _UserAvatarState extends State<NeonUserAvatar> {
119108
stream: userStatusBloc!.statuses.map(
120109
(statuses) => statuses[username] ?? Result<user_status.$PublicInterface>.loading(),
121110
),
122-
builder: _userStatusIconBuilder,
111+
builder: (context, result) => NeonUserStatusIndicator(
112+
result: result,
113+
size: size,
114+
),
123115
),
124116
],
125117
);
126118
},
127119
);
120+
}
121+
122+
@internal
123+
class NeonUserStatusIndicator extends StatelessWidget {
124+
const NeonUserStatusIndicator({
125+
required this.result,
126+
required this.size,
127+
super.key,
128+
});
129+
130+
final Result<user_status.$PublicInterface> result;
131+
132+
final double size;
128133

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

133139
Widget? child;
134140
if (result.isLoading) {
135141
child = CircularProgressIndicator(
136142
strokeWidth: 1.5,
137-
color: widget.foregroundColor ?? Theme.of(context).colorScheme.onPrimary,
143+
color: Theme.of(context).colorScheme.onPrimary,
138144
);
139145
} else if (result.hasError) {
140146
child = Icon(
141147
Icons.error_outline,
142148
size: scaledSize,
143149
color: Theme.of(context).colorScheme.error,
144150
);
145-
} else if (hasEmoji) {
146-
child = Text(
147-
result.data!.icon!,
148-
style: const TextStyle(
149-
fontSize: 16,
150-
),
151-
);
152151
} else if (result.hasData) {
153-
final type = result.data!.status;
154-
if (type != user_status.$Type.offline) {
152+
final type = result.requireData.status;
153+
if (type == user_status.$Type.dnd || (!hasEmoji && type != user_status.$Type.offline)) {
155154
child = NeonServerIcon(icon: 'user-status-$type');
155+
} else if (hasEmoji) {
156+
child = Text(
157+
result.data!.icon!,
158+
style: const TextStyle(
159+
fontSize: 16,
160+
),
161+
);
156162
}
157163
}
158164

packages/neon_framework/lib/widgets.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ export 'package:neon_framework/src/widgets/linear_progress_indicator.dart';
66
export 'package:neon_framework/src/widgets/list_view.dart';
77
export 'package:neon_framework/src/widgets/relative_time.dart';
88
export 'package:neon_framework/src/widgets/server_icon.dart';
9-
export 'package:neon_framework/src/widgets/user_avatar.dart';
9+
export 'package:neon_framework/src/widgets/user_avatar.dart' hide NeonUserStatusIndicator;
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import 'package:built_collection/built_collection.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter_test/flutter_test.dart';
4+
import 'package:mocktail/mocktail.dart';
5+
import 'package:neon_framework/blocs.dart';
6+
import 'package:neon_framework/l10n/localizations.dart';
7+
import 'package:neon_framework/src/testing/mocks.dart';
8+
import 'package:neon_framework/src/utils/provider.dart';
9+
import 'package:neon_framework/src/widgets/user_avatar.dart';
10+
import 'package:neon_framework/widgets.dart';
11+
import 'package:nextcloud/nextcloud.dart';
12+
import 'package:nextcloud/user_status.dart' as user_status;
13+
import 'package:rxdart/rxdart.dart';
14+
15+
void main() {
16+
setUp(() {
17+
final storage = MockNeonStorage();
18+
when(() => storage.requestCache).thenReturn(null);
19+
});
20+
21+
for (final (withStatus, matcher) in [(false, findsNothing), (true, findsOne)]) {
22+
testWidgets('${withStatus ? 'With' : 'Without'} status', (tester) async {
23+
final account = MockAccount();
24+
when(() => account.id).thenReturn('test');
25+
when(() => account.username).thenReturn('test');
26+
when(() => account.client).thenReturn(NextcloudClient(Uri.parse('')));
27+
28+
final userStatusBloc = MockUserStatusBloc();
29+
when(() => userStatusBloc.statuses).thenAnswer(
30+
(_) => BehaviorSubject.seeded(
31+
BuiltMap({
32+
'test': Result<user_status.$PublicInterface>(
33+
user_status.Public(
34+
(b) => b
35+
..userId = 'test'
36+
..status = user_status.$Type.online,
37+
),
38+
null,
39+
isLoading: false,
40+
isCached: false,
41+
),
42+
}),
43+
),
44+
);
45+
46+
final accountsBloc = MockAccountsBloc();
47+
when(() => accountsBloc.activeAccount).thenAnswer((_) => BehaviorSubject.seeded(account));
48+
when(() => accountsBloc.getUserStatusBlocFor(account)).thenReturn(userStatusBloc);
49+
50+
await tester.pumpWidget(
51+
MaterialApp(
52+
localizationsDelegates: NeonLocalizations.localizationsDelegates,
53+
home: NeonProvider<AccountsBloc>(
54+
create: (_) => accountsBloc,
55+
child: NeonUserAvatar(
56+
showStatus: withStatus,
57+
),
58+
),
59+
),
60+
);
61+
await tester.pumpAndSettle();
62+
63+
expect(find.byType(NeonUserStatusIndicator), matcher);
64+
});
65+
}
66+
67+
group('Status indicator', () {
68+
for (final (status, icon, textMatcher, iconMatcher) in [
69+
(user_status.$Type.offline, '😀', findsOne, findsNothing),
70+
(user_status.$Type.offline, null, findsNothing, findsNothing),
71+
(user_status.$Type.online, '😀', findsOne, findsNothing),
72+
(user_status.$Type.online, null, findsNothing, findsOne),
73+
(user_status.$Type.dnd, '😀', findsNothing, findsOne),
74+
(user_status.$Type.dnd, null, findsNothing, findsOne),
75+
]) {
76+
testWidgets('${status.value} ${icon != null ? 'with' : 'without'} emoji', (tester) async {
77+
await tester.pumpWidget(
78+
MaterialApp(
79+
home: NeonUserStatusIndicator(
80+
result: Result<user_status.$PublicInterface>(
81+
user_status.Public(
82+
(b) => b
83+
..userId = 'test'
84+
..status = status
85+
..icon = icon,
86+
),
87+
null,
88+
isLoading: false,
89+
isCached: false,
90+
),
91+
size: 1,
92+
),
93+
),
94+
);
95+
await tester.pumpAndSettle();
96+
97+
expect(find.byType(Text), textMatcher);
98+
expect(find.byType(NeonServerIcon), iconMatcher);
99+
});
100+
}
101+
});
102+
}

0 commit comments

Comments
 (0)