Skip to content

Commit

Permalink
widgets: Implement stream-colored UnreadCountBadge
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbobbe authored and gnprice committed Nov 15, 2023
1 parent d990df5 commit 0e33d57
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 3 deletions.
3 changes: 2 additions & 1 deletion lib/widgets/recent_dm_conversations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ class RecentDmConversationsItem extends StatelessWidget {
const SizedBox(width: 12),
unreadCount > 0
? Padding(padding: const EdgeInsetsDirectional.only(end: 16),
child: UnreadCountBadge(count: unreadCount))
child: UnreadCountBadge(baseStreamColor: null,
count: unreadCount))
: const SizedBox(),
])));
}
Expand Down
42 changes: 41 additions & 1 deletion lib/widgets/unread_count_badge.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:ui';

import 'package:flutter_color_models/flutter_color_models.dart';
import 'package:flutter/material.dart';

import 'text.dart';
Expand All @@ -12,18 +13,57 @@ class UnreadCountBadge extends StatelessWidget {
const UnreadCountBadge({
super.key,
required this.count,
required this.baseStreamColor,
this.bold = false,
});

final int count;
final bool bold;

/// A base stream color, from a stream subscription in user data, or null.
///
/// If not null, the background will be colored with an appropriate
/// transformation of this.
///
/// If null, the default neutral background will be used.
final Color? baseStreamColor;

@visibleForTesting
Color getBackgroundColor() {
if (baseStreamColor == null) {
return const Color.fromRGBO(102, 102, 153, 0.15);
}

// Follows `.unread-count` in Vlad's replit:
// <https://replit.com/@VladKorobov/zulip-sidebar#script.js>
// <https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/design.3A.20.23F117.20.22Inbox.22.20screen/near/1624484>

// The design uses "LCH", not "LAB", but we haven't found a Dart libary
// that can work with LCH:
// <https://replit.com/@VladKorobov/zulip-sidebar#script.js>
// <https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/design.3A.20.23F117.20.22Inbox.22.20screen/near/1677537>
// We use LAB because some quick reading suggests that the "L" axis
// is the same in both representations:
// <https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/lch>
// and because the design doesn't use the LCH representation except to
// adjust an "L" value.
//
// TODO try LCH; see linked discussion
// TODO fix bug where our results differ from the replit's (see unit tests)
// TODO profiling for expensive computation
final asLab = LabColor.fromColor(baseStreamColor!);
return asLab
.copyWith(lightness: asLab.lightness.clamp(30, 70))
.toColor()
.withOpacity(0.3);
}

@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(3),
color: const Color.fromRGBO(102, 102, 153, 0.15),
color: getBackgroundColor(),
),
child: Padding(
padding: const EdgeInsetsDirectional.fromSTEB(4, 0, 4, 1),
Expand Down
10 changes: 10 additions & 0 deletions test/widgets/unread_count_badge_checks.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'dart:ui';

import 'package:checks/checks.dart';
import 'package:zulip/widgets/unread_count_badge.dart';

extension UnreadCountBadgeChecks on Subject<UnreadCountBadge> {
Subject<int> get count => has((b) => b.count, 'count');
Subject<bool> get bold => has((b) => b.bold, 'bold');
Subject<Color> get backgroundColor => has((b) => b.getBackgroundColor(), 'background color');
}
70 changes: 69 additions & 1 deletion test/widgets/unread_count_badge_test.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,83 @@
import 'package:checks/checks.dart';
import 'package:flutter/widgets.dart';

import 'package:flutter_test/flutter_test.dart';
import 'package:zulip/widgets/unread_count_badge.dart';

import 'unread_count_badge_checks.dart';

void main() {
group('UnreadCountBadge', () {
testWidgets('smoke test; no crash', (tester) async {
await tester.pumpWidget(
const Directionality(textDirection: TextDirection.ltr,
child: UnreadCountBadge(count: 1)));
child: UnreadCountBadge(count: 1, baseStreamColor: null)));
tester.widget(find.text("1"));
});

test('colors', () {
void runCheck(Color? baseStreamColor, Color expectedBackgroundColor) {
check(UnreadCountBadge(count: 1, baseStreamColor: baseStreamColor))
.backgroundColor.equals(expectedBackgroundColor);
}

runCheck(null, const Color(0x26666699));

// Check against everything in ZULIP_ASSIGNMENT_COLORS and EXTREME_COLORS
// in <https://replit.com/@VladKorobov/zulip-sidebar#script.js>.
// On how to extract expected results from the replit, see:
// https://github.com/zulip/zulip-flutter/pull/371#discussion_r1393643523

// TODO Fix bug causing our implementation's results to differ from the
// replit's. Where they differ, see comment with what the replit gives.

// ZULIP_ASSIGNMENT_COLORS
runCheck(const Color(0xff76ce90), const Color(0x4d65bd80));
runCheck(const Color(0xfffae589), const Color(0x4dbdab53)); // 0x4dbdaa52
runCheck(const Color(0xffa6c7e5), const Color(0x4d8eafcc)); // 0x4d8fb0cd
runCheck(const Color(0xffe79ab5), const Color(0x4de295b0)); // 0x4de194af
runCheck(const Color(0xffbfd56f), const Color(0x4d9eb551)); // 0x4d9eb450
runCheck(const Color(0xfff4ae55), const Color(0x4de19d45)); // 0x4de09c44
runCheck(const Color(0xffb0a5fd), const Color(0x4daba0f8)); // 0x4daca2f9
runCheck(const Color(0xffaddfe5), const Color(0x4d83b4b9)); // 0x4d83b4ba
runCheck(const Color(0xfff5ce6e), const Color(0x4dcba749)); // 0x4dcaa648
runCheck(const Color(0xffc2726a), const Color(0x4dc2726a));
runCheck(const Color(0xff94c849), const Color(0x4d86ba3c)); // 0x4d86ba3b
runCheck(const Color(0xffbd86e5), const Color(0x4dbd86e5));
runCheck(const Color(0xffee7e4a), const Color(0x4dee7e4a));
runCheck(const Color(0xffa6dcbf), const Color(0x4d82b69b)); // 0x4d82b79b
runCheck(const Color(0xff95a5fd), const Color(0x4d95a5fd));
runCheck(const Color(0xff53a063), const Color(0x4d53a063));
runCheck(const Color(0xff9987e1), const Color(0x4d9987e1));
runCheck(const Color(0xffe4523d), const Color(0x4de4523d));
runCheck(const Color(0xffc2c2c2), const Color(0x4dababab));
runCheck(const Color(0xff4f8de4), const Color(0x4d4f8de4));
runCheck(const Color(0xffc6a8ad), const Color(0x4dc2a4a9)); // 0x4dc1a4a9
runCheck(const Color(0xffe7cc4d), const Color(0x4dc3ab2a)); // 0x4dc2aa28
runCheck(const Color(0xffc8bebf), const Color(0x4db3a9aa));
runCheck(const Color(0xffa47462), const Color(0x4da47462));

// EXTREME_COLORS
runCheck(const Color(0xFFFFFFFF), const Color(0x4dababab));
runCheck(const Color(0xFF000000), const Color(0x4d474747));
runCheck(const Color(0xFFD3D3D3), const Color(0x4dababab));
runCheck(const Color(0xFFA9A9A9), const Color(0x4da9a9a9));
runCheck(const Color(0xFF808080), const Color(0x4d808080));
runCheck(const Color(0xFFFFFF00), const Color(0x4dacb300)); // 0x4dacb200
runCheck(const Color(0xFFFF0000), const Color(0x4dff0000));
runCheck(const Color(0xFF008000), const Color(0x4d008000));
runCheck(const Color(0xFF0000FF), const Color(0x4d0000ff)); // 0x4d0902ff
runCheck(const Color(0xFFEE82EE), const Color(0x4dee82ee));
runCheck(const Color(0xFFFFA500), const Color(0x4def9800)); // 0x4ded9600
runCheck(const Color(0xFF800080), const Color(0x4d810181)); // 0x4d810281
runCheck(const Color(0xFF00FFFF), const Color(0x4d00c2c3)); // 0x4d00c3c5
runCheck(const Color(0xFFFF00FF), const Color(0x4dff00ff));
runCheck(const Color(0xFF00FF00), const Color(0x4d00cb00));
runCheck(const Color(0xFF800000), const Color(0x4d8d140c)); // 0x4d8b130b
runCheck(const Color(0xFF008080), const Color(0x4d008080));
runCheck(const Color(0xFF000080), const Color(0x4d492bae)); // 0x4d4b2eb3
runCheck(const Color(0xFFFFFFE0), const Color(0x4dadad90)); // 0x4dacad90
runCheck(const Color(0xFFFF69B4), const Color(0x4dff69b4));
});
});
}

0 comments on commit 0e33d57

Please sign in to comment.