From 8905c6ae5ffde4b833e9bb5a5096f462ae2dc1c1 Mon Sep 17 00:00:00 2001 From: provokateurin Date: Sun, 25 Feb 2024 13:58:32 +0100 Subject: [PATCH] feat(neon_talk): Add room list Signed-off-by: provokateurin --- packages/neon/neon_talk/lib/src/app.dart | 4 + .../neon/neon_talk/lib/src/blocs/talk.dart | 51 ++++++- .../neon/neon_talk/lib/src/pages/main.dart | 59 +++++++- .../lib/src/widgets/unread_indicator.dart | 54 +++++++ packages/neon/neon_talk/pubspec.yaml | 5 + packages/neon/neon_talk/test/bloc_test.dart | 134 ++++++++++++++++++ .../unread_indicator_no_unread_messages.png | Bin 0 -> 16929 bytes .../unread_indicator_unread_mention.png | Bin 0 -> 4763 bytes ...unread_indicator_unread_mention_direct.png | Bin 0 -> 4790 bytes .../unread_indicator_unread_messages.png | Bin 0 -> 4671 bytes ..._indicator_unread_single_user_messages.png | Bin 0 -> 4790 bytes .../neon_talk/test/unread_indicator_test.dart | 111 +++++++++++++++ 12 files changed, 414 insertions(+), 4 deletions(-) create mode 100644 packages/neon/neon_talk/lib/src/widgets/unread_indicator.dart create mode 100644 packages/neon/neon_talk/test/bloc_test.dart create mode 100644 packages/neon/neon_talk/test/goldens/unread_indicator_no_unread_messages.png create mode 100644 packages/neon/neon_talk/test/goldens/unread_indicator_unread_mention.png create mode 100644 packages/neon/neon_talk/test/goldens/unread_indicator_unread_mention_direct.png create mode 100644 packages/neon/neon_talk/test/goldens/unread_indicator_unread_messages.png create mode 100644 packages/neon/neon_talk/test/goldens/unread_indicator_unread_single_user_messages.png create mode 100644 packages/neon/neon_talk/test/unread_indicator_test.dart diff --git a/packages/neon/neon_talk/lib/src/app.dart b/packages/neon/neon_talk/lib/src/app.dart index 49fe390618c..80cd6f45ca9 100644 --- a/packages/neon/neon_talk/lib/src/app.dart +++ b/packages/neon/neon_talk/lib/src/app.dart @@ -9,6 +9,7 @@ import 'package:neon_talk/src/options.dart'; import 'package:neon_talk/src/pages/main.dart'; import 'package:neon_talk/src/routes.dart'; import 'package:nextcloud/nextcloud.dart'; +import 'package:rxdart/rxdart.dart'; /// Implementation of the server `talk` app. @experimental @@ -39,4 +40,7 @@ class TalkApp extends AppImplementation { @override final RouteBase route = $talkAppRoute; + + @override + BehaviorSubject getUnreadCounter(TalkBloc bloc) => bloc.unreadCounter; } diff --git a/packages/neon/neon_talk/lib/src/blocs/talk.dart b/packages/neon/neon_talk/lib/src/blocs/talk.dart index 410f0b877ae..eede3d39aa5 100644 --- a/packages/neon/neon_talk/lib/src/blocs/talk.dart +++ b/packages/neon/neon_talk/lib/src/blocs/talk.dart @@ -1,19 +1,66 @@ +import 'dart:async'; + +import 'package:built_collection/built_collection.dart'; import 'package:meta/meta.dart'; import 'package:neon_framework/blocs.dart'; import 'package:neon_framework/models.dart'; +import 'package:neon_framework/utils.dart'; +import 'package:nextcloud/spreed.dart' as spreed; +import 'package:rxdart/rxdart.dart'; /// Bloc for fetching Talk rooms sealed class TalkBloc implements InteractiveBloc { /// Creates a new Talk Bloc instance. @internal factory TalkBloc(Account account) => _TalkBloc(account); + + /// The list of rooms. + BehaviorSubject>> get rooms; + + /// The total number of unread messages. + BehaviorSubject get unreadCounter; } class _TalkBloc extends InteractiveBloc implements TalkBloc { - _TalkBloc(this.account); + _TalkBloc(this.account) { + rooms.listen((result) { + if (!result.hasData) { + return; + } + + var unread = 0; + for (final room in result.requireData) { + unread += room.unreadMessages; + } + unreadCounter.add(unread); + }); + + unawaited(refresh()); + } final Account account; @override - Future refresh() async {} + final rooms = BehaviorSubject(); + + @override + final unreadCounter = BehaviorSubject(); + + @override + void dispose() { + unawaited(rooms.close()); + unawaited(unreadCounter.close()); + super.dispose(); + } + + @override + Future refresh() async { + await RequestManager.instance.wrapNextcloud( + account: account, + cacheKey: 'talk-rooms', + subject: rooms, + rawResponse: account.client.spreed.room.getRoomsRaw(), + unwrap: (response) => response.body.ocs.data, + ); + } } diff --git a/packages/neon/neon_talk/lib/src/pages/main.dart b/packages/neon/neon_talk/lib/src/pages/main.dart index da60c8ea868..08d8df2f4ed 100644 --- a/packages/neon/neon_talk/lib/src/pages/main.dart +++ b/packages/neon/neon_talk/lib/src/pages/main.dart @@ -1,10 +1,65 @@ import 'package:flutter/material.dart'; +import 'package:neon_framework/blocs.dart'; +import 'package:neon_framework/utils.dart'; +import 'package:neon_framework/widgets.dart'; +import 'package:neon_talk/src/blocs/talk.dart'; +import 'package:neon_talk/src/widgets/unread_indicator.dart'; +import 'package:nextcloud/spreed.dart' as spreed; /// The main page displaying the chat list. -class TalkMainPage extends StatelessWidget { +class TalkMainPage extends StatefulWidget { /// Creates a new Talk main page. const TalkMainPage({super.key}); @override - Widget build(BuildContext context) => const Placeholder(); + State createState() => _TalkMainPageState(); +} + +class _TalkMainPageState extends State { + late TalkBloc bloc; + + @override + void initState() { + super.initState(); + + bloc = NeonProvider.of(context); + + bloc.errors.listen((error) { + NeonError.showSnackbar(context, error); + }); + } + + @override + Widget build(BuildContext context) => ResultBuilder.behaviorSubject( + subject: bloc.rooms, + builder: (context, rooms) => NeonListView( + scrollKey: 'talk-rooms', + isLoading: rooms.isLoading, + error: rooms.error, + onRefresh: bloc.refresh, + itemCount: rooms.data?.length ?? 0, + itemBuilder: (context, index) => buildRoom(rooms.requireData[index]), + ), + ); + + Widget buildRoom(spreed.Room room) => ListTile( + title: Text(room.displayName), + subtitle: buildMessagePreview(room.lastMessage.chatMessage), + trailing: TalkUnreadIndicator( + room: room, + ), + ); + + Widget buildMessagePreview(spreed.ChatMessage? chatMessage) { + final buffer = StringBuffer(); + + if (chatMessage != null) { + buffer.write('${chatMessage.actorDisplayName}: ${chatMessage.message}'); + } + + return Text( + buffer.toString(), + maxLines: 1, + ); + } } diff --git a/packages/neon/neon_talk/lib/src/widgets/unread_indicator.dart b/packages/neon/neon_talk/lib/src/widgets/unread_indicator.dart new file mode 100644 index 00000000000..ba83096bd41 --- /dev/null +++ b/packages/neon/neon_talk/lib/src/widgets/unread_indicator.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:nextcloud/spreed.dart' as spreed; + +/// Displays the number of unread messages and whether the user was mentioned directly or indirectly for a given [room]. +class TalkUnreadIndicator extends StatelessWidget { + /// Creates a new Talk unread indicator. + const TalkUnreadIndicator({ + required this.room, + super.key, + }); + + /// The room that the indicator will display unread messages and mentions for. + final spreed.Room room; + + @override + Widget build(BuildContext context) { + if (room.unreadMessages == 0) { + return const SizedBox.shrink(); + } + + final Color textColor; + final Color? backgroundColor; + final Color borderColor; + + if (room.unreadMentionDirect || spreed.RoomType.fromValue(room.type).isSingleUser) { + textColor = Theme.of(context).colorScheme.onPrimary; + backgroundColor = Theme.of(context).colorScheme.primary; + borderColor = Theme.of(context).colorScheme.onBackground; + } else { + textColor = Theme.of(context).colorScheme.onBackground; + backgroundColor = null; + borderColor = room.unreadMention ? Theme.of(context).colorScheme.primary : Colors.grey; + } + + return Chip( + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(50)), + side: BorderSide( + color: borderColor, + ), + ), + padding: const EdgeInsets.all(2), + backgroundColor: backgroundColor, + label: Text( + room.unreadMessages.toString(), + style: TextStyle( + fontWeight: FontWeight.bold, + fontFamily: 'monospace', + color: textColor, + ), + ), + ); + } +} diff --git a/packages/neon/neon_talk/pubspec.yaml b/packages/neon/neon_talk/pubspec.yaml index 3205f0a4da9..65428bcd91c 100644 --- a/packages/neon/neon_talk/pubspec.yaml +++ b/packages/neon/neon_talk/pubspec.yaml @@ -19,10 +19,15 @@ dependencies: url: https://github.com/nextcloud/neon path: packages/neon_framework nextcloud: ^5.0.2 + rxdart: ^0.27.0 dev_dependencies: build_runner: ^2.4.8 + flutter_test: + sdk: flutter go_router_builder: ^2.4.1 + http: ^1.2.1 + mocktail: ^1.0.3 neon_lints: git: url: https://github.com/nextcloud/neon diff --git a/packages/neon/neon_talk/test/bloc_test.dart b/packages/neon/neon_talk/test/bloc_test.dart new file mode 100644 index 00000000000..27fc4f2659c --- /dev/null +++ b/packages/neon/neon_talk/test/bloc_test.dart @@ -0,0 +1,134 @@ +import 'dart:convert'; + +import 'package:built_collection/built_collection.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:neon_framework/blocs.dart'; +import 'package:neon_framework/models.dart'; +import 'package:neon_framework/testing.dart'; +import 'package:neon_talk/src/blocs/talk.dart'; + +Map getRoom({ + required int id, + required int unreadMessages, +}) => + { + 'actorId': '', + 'actorType': '', + 'attendeeId': 0, + 'attendeePermissions': 0, + 'avatarVersion': '', + 'breakoutRoomMode': 0, + 'breakoutRoomStatus': 0, + 'callFlag': 0, + 'callPermissions': 0, + 'callRecording': 0, + 'callStartTime': 0, + 'canDeleteConversation': false, + 'canEnableSIP': false, + 'canLeaveConversation': false, + 'canStartCall': false, + 'defaultPermissions': 0, + 'description': '', + 'displayName': '', + 'hasCall': false, + 'hasPassword': false, + 'id': id, + 'isFavorite': false, + 'lastActivity': 0, + 'lastCommonReadMessage': 0, + 'lastMessage': [], + 'lastPing': 0, + 'lastReadMessage': 0, + 'listable': 0, + 'lobbyState': 0, + 'lobbyTimer': 0, + 'messageExpiration': 0, + 'name': '', + 'notificationCalls': 0, + 'notificationLevel': 0, + 'objectId': '', + 'objectType': '', + 'participantFlags': 0, + 'participantType': 0, + 'permissions': 0, + 'readOnly': 0, + 'sessionId': '', + 'sipEnabled': 0, + 'token': '', + 'type': 0, + 'unreadMention': false, + 'unreadMentionDirect': false, + 'unreadMessages': unreadMessages, + }; + +Account mockTalkAccount() => mockServer({ + RegExp(r'/ocs/v2\.php/apps/spreed/api/v4/room'): { + 'get': (match, queryParameters) => Response( + json.encode({ + 'ocs': { + 'meta': {'status': '', 'statuscode': 0}, + 'data': [ + getRoom( + id: 0, + unreadMessages: 0, + ), + getRoom( + id: 1, + unreadMessages: 1, + ), + getRoom( + id: 2, + unreadMessages: 2, + ), + ], + }, + }), + 200, + ), + }, + }); + +void main() { + late Account account; + late TalkBloc bloc; + + setUpAll(() { + final storage = MockNeonStorage(); + when(() => storage.requestCache).thenReturn(null); + }); + + setUp(() { + account = mockTalkAccount(); + bloc = TalkBloc(account); + }); + + tearDown(() { + bloc.dispose(); + }); + + test('refresh', () async { + expect( + bloc.rooms.transformResult((e) => BuiltList(e.map((r) => r.id))), + emitsInOrder([ + Result>.loading(), + Result.success(BuiltList([0, 1, 2])), + Result.success(BuiltList([0, 1, 2])).asLoading(), + Result.success(BuiltList([0, 1, 2])), + ]), + ); + expect( + bloc.unreadCounter, + emitsInOrder([ + 3, + 3, + 3, + ]), + ); + + // The delay is necessary to avoid a race condition with loading twice at the same time + await Future.delayed(const Duration(milliseconds: 1)); + await bloc.refresh(); + }); +} diff --git a/packages/neon/neon_talk/test/goldens/unread_indicator_no_unread_messages.png b/packages/neon/neon_talk/test/goldens/unread_indicator_no_unread_messages.png new file mode 100644 index 0000000000000000000000000000000000000000..128801a15687a6f8e08de2ff5315e913973ea3d3 GIT binary patch literal 16929 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 MH1=FyWP16(0D{x%9{>OV literal 0 HcmV?d00001 diff --git a/packages/neon/neon_talk/test/goldens/unread_indicator_unread_messages.png b/packages/neon/neon_talk/test/goldens/unread_indicator_unread_messages.png new file mode 100644 index 0000000000000000000000000000000000000000..cf82ba2abf55f76525a91d573391acc3c34ed73e GIT binary patch literal 4671 zcmeI0{Zms{9>z}+DokC3E?aF?DAl#ht`>A+!X)GpmZFrlyvb`5VyL3jyb#7f2;nVC zl{#u!?ON18sIALOLI_|(BpAXpRkq7p*pLK4pa_+`5Lg2t2qf%fXKeq5c5;6?_nv#s zoacVN&vWL?y_66a?fvS`R{;RtG4Dqm0{|5W05aWc3*2*?b3PYt5bR^ok)U~Cw+Vjm zU`NIzd%;(+SJvkMyk?4t`dc!$+B$Aw|HJP8Xo_{>`t?0~4g^Wg_*Rq71qPZw@_+rV zbyz*3YO67cIo}14=*%}|AN=X>=m36$WOrNQpLb^NGhL3WM@vTD{_=^>;|#-CtZ>r# zSHa-i{5=R=W4eU|z{|+w!Hc(Wy?g-(Jj#df4}j^p`m7V^cTa~v*F^n`XsGZi$SB&L56vgsc~Xr z!o08`4Ks`-$stev~27Z}Lv#wd}yl2c=_^=JTtbQ zlx--T`PNaHB6}LLYfwJ=?(T@0x!$y3gE8z|`;aLqzxQ4F@PL1qrK_lHH*ObSP!163 zQOC#Ds_LF zLx&P8T{A+1ZGFyLzLm{VtTc#d5{53MwzNI?3bjEV z?Z7~u{K9u4fMTSgK3uf7=Zj$>rHJ-|*((sTDX>y0)G2J34yrUXqU!}hR5#&|fq*i1 zRi#WUkg7T_&MU;Gzib#Zl=tlFJ9l`_euF@W>?YWbd(yuG#dHNK{2FzJq0rE=elwcDwG`8dlfp*W=+8QDxhHfFC@ZBe#so z=v!^ID>Y#peduKICV{$oaL@-Hvjd-N%pC7pU#`pU8?}w)h#lKhhNVA%XXgOI7&#kK zEw!sH0^uRLnwUYI9i)EB+X5CN_#p3&I8Cb8XcDyO(@djhf$oMl?Y0onP4I<@e0utH z9ch0m7CNaXNmBd!`VK=Vej!{WMcB&Vd5m6fHW#Ky;mKFC_e*OV!3Lxx$42q>;@DpM+zx*YkYFzY2wn({iT zyKKd>O>^O^VpdFv%@M!(rnY$XjCb&xj9(Tw3|Q4>Xx*LW0L`HN_cC{?_gV-lBFY{No7U$hdrU$3sN(jHpnq0lS*!k?D+z~#04 zOONLoGnr!wtAdCo5HdnkE8qWGW7ax*r&_HxlrVTa#kRMgj4=7agDDHLyrI_WI-^YjH=|*@TpsV3AK3Yra4bY+tGHcV&EO5>Q`T)Kls0qi zrmti?yBW^T!OBgijI}b0Bs7U=GJKZ#`0Rp!WI3{bTqx+ ztdnIMgrv5tn7v_L!d1ob66+vlB0$O{FG#Hp7VBl|aYgeCr`@i^U@$4WLK#gJ1>wmk z6zbZ&5Nb!p+EIrN4j(-?w}nG&7wemj`@P&=?#5LU5*K!J<@lPocb%^PjOIGVb)?6l zQ@vm5M0ybpH~sy}1;t_9$9{fReEw=)iQS%t(GU~D3J3`fmvI7zL-8*h;e!KbVN3ak z^X?7^olVUgJO+X&Xf%6qSfkY@!M@v^Q_w_CBe!2kra$~Qt^*6Ue)<0c_mhh|+$V51 l0XG3R0XKo)Ie|VRVs%iadHa))Ao$-4h&d7$)f}05{(tnHuciP1 literal 0 HcmV?d00001 diff --git a/packages/neon/neon_talk/test/goldens/unread_indicator_unread_single_user_messages.png b/packages/neon/neon_talk/test/goldens/unread_indicator_unread_single_user_messages.png new file mode 100644 index 0000000000000000000000000000000000000000..331cdb4f7899345f845e4b38357234f58494e1d9 GIT binary patch literal 4790 zcmeI0`%@ZL7RN6>RvJUJJ6mnUSL?2h(>2QX8nizEzgH7ycHW6hPQG*2+ z6QI^n3cdOj0KZO;KKDsNaf?<)u96kBPHt~D#9|3wtoKgFxF935JJQO7DFMeMNZSCLu7LWw z_;hKbGJiUJfu2z}^0;wle<+K2%f3)?8z!8BX0!IY??+ECSD8Rgi zGH&_kXXX_N>N!jI`N{L7jEqsK)^I=!=!fzPvVXlyP`vEnd5f=q#?ts9D%FV3NuLySETw)p*mgK_T(z9 zwx$Jz6!7QJRBVoH`?}s>kSNOXa3<3L_rok(%5`UNaOwG{uJqBe0mIWXtIzEVCZ5G` zct!1ZJu;Wa2MU)x^#9~CdTQ5NhzkBP`D+dE#xlX@kOKo|_RsIR&ei2$=N{R+&Koo3 za@~CnM`~Q+sUNa8IQF^82~J^Kouq*#f?Wjr9cQM0`pSzU$!Y+jhl+jDTm62+ysicP zh`c-ZcBO*B4Q~6k-LsT}5mUi>Q0};+Xr(WyAO~gAOr#87m3-jpg~RnbRP)}8ImKfm z(HGl>HLC74Ai82>*M5htpWB>mEwYrf7QBfff6KTjrW(kXEp3uyKgRtH!j6|-$W#17 z=MK(14@d%dl0s0KLV1x&mS2n^?LTuupw?i?{A~kmyLOT+UOtP+X|mjfjL-EAKdJpDUi(~HmR_f^B$_(9c2t4kTZ2D7vOF9EN~Mx( zKiQgEWZpWkii){i25)ynQ7mT>g4|vgA$m;d@1V)?6YwV{Csl$rV~5foJM+YX^;R zb@N!nPkPoeawe~a6dF3*PReAuhrvG&YvU+1RUk#bS*IcYtF0;r=N%_g)UC z(jcg)cOTuR``~a0>grTQJqUVr7{}ou_;H@)3ggQ!@cbbhcINTVt{h><-ID_~ZX|2` zC9hZRH6eod%9H;Q9^)FwT_dC@tilb(mvEI%To;V8WuCA;YWzAikw6&lTx@3D5>-SM z&LZa1Ui~cY9(5YUrLi$Qzd`xD?yt7U$19VPVEXyiTdOxPSV+4qt=SVqWR?E(O^lj9 z#ORC-F^^g=Uo^6ILM8p*i$7pbxAauf@)oh`OZ9}ZI6gS7JIwEqW#JNJ5ADLrBY7}% zZ&Kc@?gpb)V9C2Wynd+0wMBMU^c#9n{zPWWXf7`?x+W+R(6T?VVk<`H3Z?kQ9AOI5jJA`<1m zJdx+<^4)K?rqK*6s{i(-2T8_PO^BU^?lt4xdBbQNI+Z~V2vg3E?8+_Kv=hL?Hv)7s z3;C+i9eH9iOB$E;dm}~I-2XRaG!8eQ?}}0nVa@Ha;Wuu{N;YK@65q z3=2wcE+VFpgXILYVvC1BhX1g@y8AFuou$s@Dze{};q$irRDy%=9llF=ebQP3t_f>A zd$@f94gp>^ygGv?#Ke{otyL*%+7qt8tJ{Y8gPLM%aR}oG^2EXd`Tj@$EmO0wdBx=+ zx^~rVF*M#pkD4=f)=M-`_Ypyvu|m(VEAi6{A;xU&N!dl-@my{|AS%Ttlb1kLOr_|R zAE~H`M5KQfpcGr1@k>9P9ZO5)CNjD_4@^FYGlOhj-wCda)q;jSxQx~7KIfVRjRV=r zM*TjQ*V8{!c5pup&K&>CzdQI*sK-%De?;Jjz!8BX0>5wqQw&@Ccg2;M>X=gNXAX$| MH1=FyWP16(0D{x%9{>OV literal 0 HcmV?d00001 diff --git a/packages/neon/neon_talk/test/unread_indicator_test.dart b/packages/neon/neon_talk/test/unread_indicator_test.dart new file mode 100644 index 00000000000..586f6cb3069 --- /dev/null +++ b/packages/neon/neon_talk/test/unread_indicator_test.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:neon_talk/src/widgets/unread_indicator.dart'; +import 'package:nextcloud/spreed.dart' as spreed; + +class MockRoom extends Mock implements spreed.Room {} + +void main() { + testWidgets('No unread messages', (tester) async { + final room = MockRoom(); + when(() => room.unreadMessages).thenReturn(0); + + await tester.pumpWidget( + TalkUnreadIndicator( + room: room, + ), + ); + await expectLater( + find.byType(TalkUnreadIndicator), + matchesGoldenFile('goldens/unread_indicator_no_unread_messages.png'), + ); + }); + + testWidgets('Unread messages', (tester) async { + final room = MockRoom(); + when(() => room.unreadMessages).thenReturn(42); + when(() => room.unreadMention).thenReturn(false); + when(() => room.unreadMentionDirect).thenReturn(false); + when(() => room.type).thenReturn(spreed.RoomType.group.value); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: TalkUnreadIndicator( + room: room, + ), + ), + ), + ); + await expectLater( + find.byType(TalkUnreadIndicator), + matchesGoldenFile('goldens/unread_indicator_unread_messages.png'), + ); + }); + + testWidgets('Unread single user messages', (tester) async { + final room = MockRoom(); + when(() => room.unreadMessages).thenReturn(42); + when(() => room.unreadMention).thenReturn(false); + when(() => room.unreadMentionDirect).thenReturn(false); + when(() => room.type).thenReturn(spreed.RoomType.oneToOne.value); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: TalkUnreadIndicator( + room: room, + ), + ), + ), + ); + await expectLater( + find.byType(TalkUnreadIndicator), + matchesGoldenFile('goldens/unread_indicator_unread_single_user_messages.png'), + ); + }); + + testWidgets('Unread mention', (tester) async { + final room = MockRoom(); + when(() => room.unreadMessages).thenReturn(42); + when(() => room.unreadMention).thenReturn(true); + when(() => room.unreadMentionDirect).thenReturn(false); + when(() => room.type).thenReturn(spreed.RoomType.group.value); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: TalkUnreadIndicator( + room: room, + ), + ), + ), + ); + await expectLater( + find.byType(TalkUnreadIndicator), + matchesGoldenFile('goldens/unread_indicator_unread_mention.png'), + ); + }); + + testWidgets('Unread mention direct', (tester) async { + final room = MockRoom(); + when(() => room.unreadMessages).thenReturn(42); + when(() => room.unreadMention).thenReturn(true); + when(() => room.unreadMentionDirect).thenReturn(true); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: TalkUnreadIndicator( + room: room, + ), + ), + ), + ); + await expectLater( + find.byType(TalkUnreadIndicator), + matchesGoldenFile('goldens/unread_indicator_unread_mention_direct.png'), + ); + }); +}