Skip to content

RecentDmConversationsPage: Show unread counts #334

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

Merged
merged 7 commits into from
Oct 31, 2023
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
2 changes: 2 additions & 0 deletions lib/model/unreads.dart
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ class Unreads extends ChangeNotifier {

final int selfUserId;

int countInDmNarrow(DmNarrow narrow) => dms[narrow]?.length ?? 0;

void handleMessageEvent(MessageEvent event) {
final message = event.message;
if (message.flags.contains(MessageFlag.read)) {
Expand Down
60 changes: 54 additions & 6 deletions lib/widgets/recent_dm_conversations.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import 'dart:ui';

import 'package:flutter/material.dart';

import '../model/narrow.dart';
import '../model/recent_dm_conversations.dart';
import '../model/unreads.dart';
import 'content.dart';
import 'icons.dart';
import 'message_list.dart';
Expand All @@ -23,18 +26,30 @@ class RecentDmConversationsPage extends StatefulWidget {

class _RecentDmConversationsPageState extends State<RecentDmConversationsPage> with PerAccountStoreAwareStateMixin<RecentDmConversationsPage> {
RecentDmConversationsView? model;
Unreads? unreadsModel;

@override
void onNewStore() {
model?.removeListener(_modelChanged);
model = PerAccountStoreWidget.of(context).recentDmConversationsView
..addListener(_modelChanged);

unreadsModel?.removeListener(_modelChanged);
unreadsModel = PerAccountStoreWidget.of(context).unreads
..addListener(_modelChanged);
}

@override
void dispose() {
model?.removeListener(_modelChanged);
unreadsModel?.removeListener(_modelChanged);
super.dispose();
}

void _modelChanged() {
setState(() {
// The actual state lives in [model].
// This method was called because that just changed.
// The actual state lives in [model] and [unreadsModel].
// This method was called because one of those just changed.
});
}

Expand All @@ -45,14 +60,25 @@ class _RecentDmConversationsPageState extends State<RecentDmConversationsPage> w
appBar: AppBar(title: const Text('Direct messages')),
body: ListView.builder(
itemCount: sorted.length,
itemBuilder: (context, index) => RecentDmConversationsItem(narrow: sorted[index])));
itemBuilder: (context, index) {
final narrow = sorted[index];
return RecentDmConversationsItem(
narrow: narrow,
unreadCount: unreadsModel!.countInDmNarrow(narrow),
);
}));
}
}

class RecentDmConversationsItem extends StatelessWidget {
const RecentDmConversationsItem({super.key, required this.narrow});
const RecentDmConversationsItem({
super.key,
required this.narrow,
required this.unreadCount,
});

final DmNarrow narrow;
final int unreadCount;

@override
Widget build(BuildContext context) {
Expand All @@ -66,6 +92,9 @@ class RecentDmConversationsItem extends StatelessWidget {
title = selfUser.fullName;
avatar = AvatarImage(userId: selfUser.userId);
case [var otherUserId]:
// TODO(#296) actually don't show this row if the user is muted?
// (should we offer a "spam folder" style summary screen of recent
// 1:1 DM conversations from muted users?)
final otherUser = store.users[otherUserId];
title = otherUser?.fullName ?? '(unknown user)';
avatar = AvatarImage(userId: otherUserId);
Expand Down Expand Up @@ -101,8 +130,27 @@ class RecentDmConversationsItem extends StatelessWidget {
maxLines: 2,
overflow: TextOverflow.ellipsis,
title))),
const SizedBox(width: 8),
// TODO(#253): Unread count
const SizedBox(width: 12),
unreadCount > 0
? Padding(
padding: const EdgeInsetsDirectional.only(end: 16),
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(3),
color: const Color.fromRGBO(102, 102, 153, 0.15),
),
child: Padding(
padding: const EdgeInsetsDirectional.fromSTEB(4, 0, 4, 1),
child: Text(
style: const TextStyle(
fontFamily: 'Source Sans 3',
fontSize: 16,
height: (18 / 16),
fontFeatures: [FontFeature.enable('smcp')], // small caps
color: Color(0xFF222222),
).merge(weightVariableTextStyle(context)),
unreadCount.toString()))))
: const SizedBox(),
])));
}
}
4 changes: 4 additions & 0 deletions test/flutter_checks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ extension ValueNotifierChecks<T> on Subject<ValueNotifier<T>> {
Subject<T> get value => has((c) => c.value, 'value');
}

extension TextChecks on Subject<Text> {
Subject<String?> get data => has((t) => t.data, 'data');
}

extension TextStyleChecks on Subject<TextStyle> {
Subject<bool> get inherit => has((t) => t.inherit, 'inherit');
Subject<List<FontVariation>?> get fontVariations => has((t) => t.fontVariations, 'fontVariations');
Expand Down
65 changes: 55 additions & 10 deletions test/widgets/recent_dm_conversations_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:zulip/widgets/recent_dm_conversations.dart';
import 'package:zulip/widgets/store.dart';

import '../example_data.dart' as eg;
import '../flutter_checks.dart';
import '../model/binding.dart';
import '../model/test_store.dart';
import '../test_navigation.dart';
Expand Down Expand Up @@ -61,12 +62,9 @@ void main() {
TestZulipBinding.ensureInitialized();

group('RecentDmConversationsPage', () {
Finder findConversationItem(Narrow narrow) {
return find.byWidgetPredicate(
(widget) =>
widget is RecentDmConversationsItem && widget.narrow == narrow,
);
}
Finder findConversationItem(Narrow narrow) => find.byWidgetPredicate(
(widget) => widget is RecentDmConversationsItem && widget.narrow == narrow,
);

testWidgets('page builds; conversations appear in order', (WidgetTester tester) async {
final user1 = eg.user(userId: 1);
Expand Down Expand Up @@ -109,7 +107,7 @@ void main() {
});

group('RecentDmConversationsItem', () {
group('appearance', () {
group('content/appearance', () {
void checkAvatar(WidgetTester tester, DmNarrow narrow) {
final shape = tester.widget<AvatarShape>(
find.descendant(
Expand Down Expand Up @@ -148,8 +146,28 @@ void main() {
}
}

Future<void> markMessageAsRead(WidgetTester tester, Message message) async {
final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
store.handleEvent(UpdateMessageFlagsAddEvent(
id: 1, flag: MessageFlag.read, all: false, messages: [message.id]));
await tester.pump();
}

void checkUnreadCount(WidgetTester tester, int expectedCount) {
final Text? textWidget = tester.widgetList<Text>(find.descendant(
of: find.byType(RecentDmConversationsItem),
matching: find.textContaining(RegExp(r'^\d+$'),
))).singleOrNull;

if (expectedCount == 0) {
check(textWidget).isNull();
} else {
check(textWidget).isNotNull().data.equals(expectedCount.toString());
}
}

group('self-1:1', () {
testWidgets('has right content', (WidgetTester tester) async {
testWidgets('has right title/avatar', (WidgetTester tester) async {
final message = eg.dmMessage(from: eg.selfUser, to: []);
await setupPage(tester, users: [], dmMessages: [message]);

Expand All @@ -172,10 +190,19 @@ void main() {
newNameForSelfUser: name);
checkTitle(tester, name, 2);
});

testWidgets('unread counts', (WidgetTester tester) async {
final message = eg.dmMessage(from: eg.selfUser, to: []);
await setupPage(tester, users: [], dmMessages: [message]);

checkUnreadCount(tester, 1);
await markMessageAsRead(tester, message);
checkUnreadCount(tester, 0);
});
});

group('1:1', () {
testWidgets('has right content', (WidgetTester tester) async {
testWidgets('has right title/avatar', (WidgetTester tester) async {
final user = eg.user(userId: 1);
final message = eg.dmMessage(from: eg.selfUser, to: [user]);
await setupPage(tester, users: [user], dmMessages: [message]);
Expand Down Expand Up @@ -209,6 +236,15 @@ void main() {
await setupPage(tester, users: [user], dmMessages: [message]);
checkTitle(tester, user.fullName, 2);
});

testWidgets('unread counts', (WidgetTester tester) async {
final message = eg.dmMessage(from: eg.otherUser, to: [eg.selfUser]);
await setupPage(tester, users: [], dmMessages: [message]);

checkUnreadCount(tester, 1);
await markMessageAsRead(tester, message);
checkUnreadCount(tester, 0);
});
});

group('group', () {
Expand All @@ -220,7 +256,7 @@ void main() {
return result;
}

testWidgets('has right content', (WidgetTester tester) async {
testWidgets('has right title/avatar', (WidgetTester tester) async {
final users = usersList(2);
final user0 = users[0];
final user1 = users[1];
Expand Down Expand Up @@ -258,6 +294,15 @@ void main() {
await setupPage(tester, users: users, dmMessages: [message]);
checkTitle(tester, users.map((u) => u.fullName).join(', '), 2);
});

testWidgets('unread counts', (WidgetTester tester) async {
final message = eg.dmMessage(from: eg.thirdUser, to: [eg.selfUser, eg.otherUser]);
await setupPage(tester, users: [], dmMessages: [message]);

checkUnreadCount(tester, 1);
await markMessageAsRead(tester, message);
checkUnreadCount(tester, 0);
});
});
});

Expand Down