Skip to content
Open
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
13 changes: 10 additions & 3 deletions lib/model/unreads.dart
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,7 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
// TODO(#370): maintain this count incrementally, rather than recomputing from scratch
int countInCombinedFeedNarrow() {
int c = 0;
for (final messageIds in dms.values) {
c = c + messageIds.length;
}
c += countInAllDms();
for (final MapEntry(key: streamId, value: topics) in streams.entries) {
for (final MapEntry(key: topic, value: messageIds) in topics.entries) {
if (channelStore.isTopicVisible(streamId, topic)) {
Expand Down Expand Up @@ -230,6 +228,15 @@ class Unreads extends PerAccountStoreBase with ChangeNotifier {
// TODO: Implement unreads handling?
int countInKeywordSearchNarrow() => 0;

int countInAllDms() {
int c = 0;
for (final MapEntry(key: narrow, value: messageIds) in dms.entries) {
if (channelStore.shouldMuteDmConversation(narrow)) continue;
c += messageIds.length;
Comment on lines +231 to +235
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name feels misleading, given that this isn't actually all DMs. 🙂 A bit like how we moved away from "All messages" to "Combined feed".

How about just countInDms? And maybe dartdoc mentioning it excludes muted DM conversations.

}
return c;
}

int countInNarrow(Narrow narrow) {
switch (narrow) {
case CombinedFeedNarrow():
Expand Down
42 changes: 42 additions & 0 deletions lib/widgets/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import 'store.dart';
import 'subscription_list.dart';
import 'text.dart';
import 'theme.dart';
import 'unread_count_badge.dart';
import 'user.dart';

enum _HomePageTab {
Expand Down Expand Up @@ -351,6 +352,8 @@ abstract class _MenuButton extends StatelessWidget {
color: selected ? designVariables.iconSelected : designVariables.icon);
}

Widget? buildTrailing(BuildContext context) => null;

void onPressed(BuildContext context);

void _handlePress(BuildContext context) {
Expand Down Expand Up @@ -391,6 +394,8 @@ abstract class _MenuButton extends StatelessWidget {
~WidgetState.pressed: selected ? borderSideSelected : null,
}));

final trailing = buildTrailing(context);

return AnimatedScaleOnTap(
duration: const Duration(milliseconds: 100),
scaleEnd: 0.95,
Expand All @@ -407,6 +412,7 @@ abstract class _MenuButton extends StatelessWidget {
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 19, height: 26 / 19)
.merge(weightVariableTextStyle(context, wght: selected ? 600 : 400)))),
?trailing,
]))));
}
}
Expand Down Expand Up @@ -457,6 +463,18 @@ class _InboxButton extends _NavigationBarMenuButton {
return zulipLocalizations.inboxPageTitle;
}

@override
Widget? buildTrailing(BuildContext context) {
final store = PerAccountStoreWidget.of(context);
final unreadCount = store.unreads.countInCombinedFeedNarrow();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I worry a bit about adding this call site given this TODO for #370:

  // TODO(#370): maintain this count incrementally, rather than recomputing from scratch
  int countInCombinedFeedNarrow() {

Do you have any timings on how long this takes with lots of unreads?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, this is only in the main-menu bottom sheet, not the bottom tabs. In that case I guess it doesn't make the impact any worse than it is now with the mark-as-read button in the message list.

if (unreadCount == 0) return null;
return UnreadCountBadge(
style: UnreadCountBadgeStyle.mainMenu,
count: unreadCount,
channelIdForBackground: null,
);
}

@override
_HomePageTab get navigationTarget => _HomePageTab.inbox;
}
Expand All @@ -472,6 +490,18 @@ class _MentionsButton extends _MenuButton {
return zulipLocalizations.mentionsPageTitle;
}

@override
Widget? buildTrailing(BuildContext context) {
final store = PerAccountStoreWidget.of(context);
final unreadCount = store.unreads.countInMentionsNarrow();
if (unreadCount == 0) return null;
return UnreadCountBadge(
style: UnreadCountBadgeStyle.mainMenu,
count: unreadCount,
channelIdForBackground: null,
);
}

@override
void onPressed(BuildContext context) {
Navigator.of(context).push(MessageListPage.buildRoute(
Expand Down Expand Up @@ -541,6 +571,18 @@ class _DirectMessagesButton extends _NavigationBarMenuButton {
return zulipLocalizations.recentDmConversationsPageTitle;
}

@override
Widget? buildTrailing(BuildContext context) {
final store = PerAccountStoreWidget.of(context);
final unreadCount = store.unreads.countInAllDms();
if (unreadCount == 0) return null;
return UnreadCountBadge(
style: UnreadCountBadgeStyle.mainMenu,
count: unreadCount,
channelIdForBackground: null,
);
}

@override
_HomePageTab get navigationTarget => _HomePageTab.directMessages;
}
Expand Down
46 changes: 36 additions & 10 deletions lib/widgets/unread_count_badge.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,20 @@ import 'theme.dart';
/// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=2037-186671&m=dev
/// It looks like that component was created for the main menu,
/// then adapted for various other contexts, like the Inbox page.
/// See [UnreadCountBadgeStyle].
///
/// Currently this widget supports only those other contexts (not the main menu)
/// and only the component's "kind=unread" variant (not "kind=quantity").
/// For example, the "Channels" page and the topic-list page:
/// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=6205-26001&m=dev
/// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=6823-37113&m=dev
/// (We use this for the topic-list page even though the Figma makes it a bit
/// more compact there…the inconsistency seems worse and might be accidental.)
// TODO support the main-menu context, update dartdoc
/// Currently this widget supports only the component's "kind=unread" variant,
/// not "kind=quantity".
// TODO support the "kind=quantity" variant, update dartdoc
class UnreadCountBadge extends StatelessWidget {
const UnreadCountBadge({
super.key,
this.style = UnreadCountBadgeStyle.other,
required this.count,
required this.channelIdForBackground,
});

final UnreadCountBadgeStyle style;
final int count;

/// An optional [Subscription.streamId], for a channel-colorized background.
Expand Down Expand Up @@ -55,23 +52,52 @@ class UnreadCountBadge extends StatelessWidget {
backgroundColor = designVariables.bgCounterUnread;
}

final padding = switch (style) {
UnreadCountBadgeStyle.mainMenu =>
const EdgeInsets.symmetric(horizontal: 5, vertical: 4),
UnreadCountBadgeStyle.other =>
const EdgeInsets.symmetric(horizontal: 5, vertical: 3),
};

final double wght = switch (style) {
UnreadCountBadgeStyle.mainMenu => 600,
UnreadCountBadgeStyle.other => 500,
};

return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5),
color: backgroundColor,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 3),
padding: padding,
child: Text(
style: TextStyle(
fontSize: 16,
height: (16 / 16),
color: textColor,
).merge(weightVariableTextStyle(context, wght: 500)),
).merge(weightVariableTextStyle(context, wght: wght)),
count.toString())));
}
}

enum UnreadCountBadgeStyle {
/// The style to use in the main menu.
///
/// Figma:
/// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=2037-185126&m=dev
mainMenu,

/// The style to use in other contexts besides the main menu.
///
/// Other contexts include the "Channels" page and the topic-list page:
/// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=6205-26001&m=dev
/// https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=6823-37113&m=dev
/// (We use this for the topic-list page even though the Figma makes it a bit
/// more compact there…the inconsistency seems worse and might be accidental.)
other,
}

class MutedUnreadBadge extends StatelessWidget {
const MutedUnreadBadge({super.key});

Expand Down
18 changes: 17 additions & 1 deletion test/model/unreads_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -191,16 +191,18 @@ void main() {
await store.addSubscription(eg.subscription(stream2));
await store.addSubscription(eg.subscription(stream3, isMuted: true));
await store.setUserTopic(stream1, 'a', UserTopicVisibilityPolicy.muted);
await store.setMutedUsers([eg.thirdUser.userId]);
fillWithMessages([
eg.streamMessage(stream: stream1, topic: 'a', flags: []),
eg.streamMessage(stream: stream1, topic: 'b', flags: []),
eg.streamMessage(stream: stream1, topic: 'b', flags: []),
eg.streamMessage(stream: stream2, topic: 'c', flags: []),
eg.streamMessage(stream: stream3, topic: 'd', flags: []),
eg.dmMessage(from: eg.otherUser, to: [eg.selfUser], flags: []),
// Exclude because user is muted
eg.dmMessage(from: eg.thirdUser, to: [eg.selfUser], flags: []),
Comment on lines +202 to 203
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the muted channel and topic just above don't have comments, so seems clearest not to add one only here

]);
check(model.countInCombinedFeedNarrow()).equals(5);
check(model.countInCombinedFeedNarrow()).equals(4);
});

test('countInChannel/Narrow', () async {
Expand Down Expand Up @@ -271,6 +273,20 @@ void main() {
]);
check(model.countInStarredMessagesNarrow()).equals(0);
});

test('countInAllDms', () async {
prepare();
await store.setMutedUsers([eg.thirdUser.userId]);
fillWithMessages([
// No one is muted: don't exclude
eg.dmMessage(from: eg.otherUser, to: [eg.selfUser], flags: []),
// Everyone is muted: exclude
eg.dmMessage(from: eg.thirdUser, to: [eg.selfUser], flags: []),
// One is muted, one isn't: don't exclude
eg.dmMessage(from: eg.thirdUser, to: [eg.selfUser, eg.otherUser], flags: []),
]);
check(model.countInCombinedFeedNarrow()).equals(2);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the name of the test case, this should be calling countInAllDms, right?

});
});

group('isUnread', () {
Expand Down