Skip to content
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

inbox: Add "Inbox" page! #381

Merged
merged 12 commits into from
Nov 20, 2023
Merged

inbox: Add "Inbox" page! #381

merged 12 commits into from
Nov 20, 2023

Conversation

chrisbobbe
Copy link
Collaborator

Stacked atop #371.

Still a draft for now, but wanted to share what I have so far. 🙂

@chrisbobbe
Copy link
Collaborator Author

#380 in its current form would fix a small deviation from the Figma. No need to have it block progress on this though.

@gnprice
Copy link
Member

gnprice commented Nov 13, 2023

(Cross-linking: this fixes #117, right?)

Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

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

Thanks! Generally looks good; comments below.

A couple of comments are about efficiency or getting work out of the build methods. But I want to highlight this high-order bit about that aspect:

As long as this feels reasonably responsive in manual testing, though, then the right thing for now is to prioritize speed of writing the code over speed of running it, and just leave a TODO.

Thoughts on the TODO items in the main commit's message:

TODO (before merge?):

- tests

Yeah. For now let's keep the tests simple, just in the interest of prioritizing our other Alpha and Beta 1 work. But it'd be good to have (a) a simple smoke test where there's some unread DMs and some unread stream conversations; and (b) a quick test of the collapse/uncollapse functionality.

(Fancier tests would include that if you collapse one section, then scroll it far offscreen and scroll it back on, it's still collapsed. That's something that's significant to the UX but would fail in a plausible alternate implementation, where the collapsed state was local to the list item for the section.)

- sorting (of streams; and within a stream, of topics)

These are OK to leave as TODOs if adding them to the implementation seems complicated.

- icons:
    https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/design.3A.20.23F117.20.22Inbox.22.20screen/near/1680637
  (that message, and the next)

Will be good to fix but doesn't need to block merging; if we merge before fixing those, we can file follow-up issues for Beta 2.

- currently the whole stream row is tappable to collapse/uncollapse;
  should we limit that to just the area near the
  arrow_down/arrow_right, and have the rest of the row open a
  message list to the stream narrow?

Yeah, I'd be inclined to do the latter. If the Figma design expresses the former (or if this needs further discussion for another reason), then this would be a good question for that chat thread.

- @-mention indicator next to stream/topic unread badge count

Let's file a Beta 2 followup issue for this. Can sort it toward the top of Beta 2 on the priorities board.

- helper widgets; code structure etc.?

Comments on this below.

- commit msg. with "fixes" line

Comment on lines 162 to 167
streamPostPolicy: streamPostPolicy ?? 1,
messageRetentionDays: messageRetentionDays ?? 365,
historyPublicToSubscribers: historyPublicToSubscribers ?? true,
firstMessageId: firstMessageId ?? 123, // TODO generate example data
streamWeeklyTraffic: streamWeeklyTraffic ?? 100, // TODO generate example data
canRemoveSubscribersGroupId: canRemoveSubscribersGroupId ?? 123 // TODO generate example data
Copy link
Member

Choose a reason for hiding this comment

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

Some of these others should come from stream too.

Copy link
Member

Choose a reason for hiding this comment

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

(This may end up coming first from #382.)


import 'package:flutter/material.dart';

import 'colors.dart';
Copy link
Member

Choose a reason for hiding this comment

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

nit: I think I like color.dart a bit better. Partly it's that the word "color" in the singular is the name of a concept — so this is the file that "deals with color". And then partly it's that after experience in zulip-mobile where it was often hard to predict or remember whether a name was in the singular or plural, I've come to prefer resolving any ambiguity there in favor of singular.

State<InboxPage> createState() => InboxPageState();
}

class InboxPageState extends State<InboxPage> with PerAccountStoreAwareStateMixin<InboxPage> {
Copy link
Member

Choose a reason for hiding this comment

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

Can this be private? (Or @visibleForTesting?)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sure, it can be @visibleForTesting. If I make it private, by prefixing with _, I get analysis feedback saying "Invalid use of a private type in a public API" in several places. Such as:

class AllDmsHeaderItem extends StatelessWidget {
  const AllDmsHeaderItem({
    super.key,
    required this.count,
    required this.collapsed,
    required this.pageState, // <-- here
  });

  final int count;
  final bool collapsed;
  final _InboxPageState pageState; // <-- here

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 guess that points toward this other comment then 🙂 : #381 (comment)

Comment on lines 57 to 59
unreadsModel?.removeListener(_modelChanged);
unreadsModel = newStore.unreads..addListener(_modelChanged);
recentDmConversationsModel = newStore.recentDmConversationsView
..addListener(_modelChanged);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
unreadsModel?.removeListener(_modelChanged);
unreadsModel = newStore.unreads..addListener(_modelChanged);
recentDmConversationsModel = newStore.recentDmConversationsView
..addListener(_modelChanged);
unreadsModel?.removeListener(_modelChanged);
unreadsModel = newStore.unreads..addListener(_modelChanged);
recentDmConversationsModel?.removeListener(_modelChanged);
recentDmConversationsModel = newStore.recentDmConversationsView
..addListener(_modelChanged);

right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep, thanks for the catch!

// We also update some state that lives elsewhere: we reset a collapsible
// row's collapsed state when it's cleared 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.

Suggested change
// We also update some state that lives elsewhere: we reset a collapsible
// row's collapsed state when it's cleared of unreads.
// We also update some state that lives locally: we reset a collapsible
// row's collapsed state when it's cleared of unreads.

"elsewhere" is a bit confusing because it can sound like it means somewhere other than here (whereas you mean other than those two models mentioned above).

Widget build(BuildContext context) {
final store = PerAccountStoreWidget.of(context);

final sections = [];
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 see, I guess the bit in _modelChanged above is just the tip of an iceberg here. The ideal version of this would definitely involve a separate view-model class, updated incrementally on events rather than recomputed in full each time something changes.

As long as this feels reasonably responsive in manual testing, though, then the right thing for now is to prioritize speed of writing the code over speed of running it, and just leave a TODO. So:

Suggested change
final sections = [];
// TODO(perf) make an incrementally-updated view-model for InboxPage
final sections = [];

Comment on lines 142 to 148
final header = AllDmsHeaderItem(
count: count,
collapsed: allDmsCollapsed,
pageState: this,
);
return StickyHeaderItem(header: header,
child: AllDmsSection(
Copy link
Member

Choose a reason for hiding this comment

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

The StickyHeaderItem can be pushed down inside AllDmsSection. That'd be a bit cleaner because the latter is already constructing the same AllDmsHeaderItem widget, to include in its Column.

final store = PerAccountStoreWidget.of(context);
final selfUser = store.users[store.account.userId]!;

final title = switch (narrow.otherRecipientIds) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
final title = switch (narrow.otherRecipientIds) {
final title = switch (narrow.otherRecipientIds) { // TODO dedupe with [RecentDmConversationsItem]

and add a similar comment on that end — that way at least if we look at editing one side of this, we'll be aware of the other one.

Comment on lines 201 to 252
return Material(
color: collapsed ? Colors.white : const Color(0xFFF3F0E7),
child: InkWell(
onTap: () {
pageState.allDmsCollapsed = !collapsed;
},
// TODO min-height 48px, like other touch targets?
child: Row(crossAxisAlignment: CrossAxisAlignment.center, children: [
Padding(padding: const EdgeInsets.all(10),
child: Icon(size: 20, color: const Color(0x7F1D2E48),
collapsed ? ZulipIcons.arrow_right : ZulipIcons.arrow_down)),
const Icon(size: 18, color: Color(0xFF222222),
ZulipIcons.user),
const SizedBox(width: 5),
Expanded(child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text(
style: const TextStyle(
fontFamily: 'Source Sans 3',
fontSize: 17,
height: (20 / 17),
color: Color(0xFF222222),
).merge(weightVariableTextStyle(context, wght: 600, wghtIfPlatformRequestsBold: 900)),
maxLines: 1,
overflow: TextOverflow.ellipsis,
'Direct messages'))),
const SizedBox(width: 12),
Padding(padding: const EdgeInsetsDirectional.only(end: 16),
child: UnreadCountBadge(baseStreamColor: null, bold: true,
count: count)),
Copy link
Member

Choose a reason for hiding this comment

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

The bulk of this code would be good to share with StreamHeaderItem. Basically there's a bunch of code for the layout and visual design, and it takes several inputs: bar color, icon color, IconData icon, title, baseStreamColor. Computing those inputs will be separate logic for a stream vs. all-DMs.

Three reasonable ways to organize that sharing:

  • The widgets StreamHeaderItem and AllDmsHeaderItem remain, but they invoke a shared widget _HeaderItemLayout that takes those inputs. Similar to our _ComposeBoxLayout.
  • There's a single widget class _HeaderItem, taking… hmm I guess this aspect gets annoying for this option, as we want to take either a stream or "all DMs". Could be expressed like int? streamId, but that's unpleasantly implicit.
    • OTOH this option works well for TopicItem vs. DmItem — can take a SendableNarrow narrow.
    • Then the build method computes these inputs as locals, initialized by the two cases of a switch.
  • I guess here's a third way: an abstract base class _HeaderItem, and these two become subclasses. The inputs are abstract methods or getters on the base class, implemented by the subclasses.
    • This gets awkward in that the icon color and bar color want to share computation of the clamped color.

So probably the easiest way here is the shared helper _HeaderItemLayout.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Would the shared helper _HeaderItemLayout take an int param for the unread count?

Copy link
Member

Choose a reason for hiding this comment

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

Ah yeah, that too. And I guess the onTap callback as well.

Copy link
Collaborator Author

@chrisbobbe chrisbobbe Nov 16, 2023

Choose a reason for hiding this comment

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

I wasn't seeing a smooth path to the first way, so in my next revision I do something I found easy enough. It's basically option 3 (like the abstract MessageActionSheetMenuItemButton and its subclasses in lib/widgets/action_sheet.dart), except the awkward bit is solved. The icon color and bar color do share computation of the clamped color. As a bonus, all of a stream's unread-count badge instances share computation of their color, too. (There are a lot of those, when a stream has unreads across many topics.) A ColorSwatch of colors related to the stream color is memoized on the Subscription object.

const StreamSectionData(this.streamId, this.count, this.items);
}

class AllDmsHeaderItem extends StatelessWidget {
Copy link
Member

Choose a reason for hiding this comment

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

Can these widget classes be private?

If they remain public, they should get more specific names that make it clear they're for the inbox. E.g., InboxAllDmsHeaderItem. That will mitigate having them show up in IDE autocomplete when working in unrelated parts of the code.

@chrisbobbe
Copy link
Collaborator Author

Thanks for the review! Revision pushed, and please see my reply at #381 (comment).

Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

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

Thanks! A few comments below, but otherwise this all looks good — it just needs tests. (Which can be kept pretty simple, as described above at #381 (review) .)

We've talked about several TODO items here that you're going to file as issues for Beta 2.


As I mentioned in our call today, I played with this on my Pixel 5 to see the performance, using a test user on chat.zulip.org who has a ton of unreads. In debug mode the performance was pretty choppy — occasional pauses while scrolling that I believe reflected a new section scrolling into view, so that with the current version the whole section has to be built and laid out, which can be hundreds of items for different topics. Those pauses in debug mode were sometimes around a second long.

It's fine for debug mode to be choppy, though, so I tried it in profile mode. There there's still enough jank to be noticeable, but it's a lot milder — mostly a couple of dropped frames at a time, sometimes like 150ms. So this level of performance is fine for Beta 1, but it's something we'll want to revisit for Beta 2.

Probably that means I'll rework something in StickyHeaderListView to make it possible to have a separate list item for each conversation, just like you did in a previous draft, and without the glitchy interactions that that led to between one section's header and the next. (Effectively the current StickyHeaderListView assumes that each header is no taller than the item it belongs to; that's true in the message list, and it's true here by making each entire section into an item, but it's not true if each conversation is its own item.)

Comment on lines +338 to +352
// TODO I'm not sure this is the right home for this; it seems like we might
// instead have chosen to put it in more UI-centered code, like in a custom
// material [ColorScheme] class or something. But it works for now.
StreamColorSwatch colorSwatch() => _swatch ??= StreamColorSwatch(color);
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, agreed on both counts — this doesn't feel like the right home, but it's fine for now (for beta 1) and I can try refactoring it later.

Comment on lines 33 to 35
final effectiveBackgroundColor = switch (backgroundColor) {
// TODO why is the "as" casting needed?
StreamColorSwatch() => (backgroundColor as StreamColorSwatch).unreadCountBadgeBackground,
Copy link
Member

Choose a reason for hiding this comment

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

The cast is needed because backgroundColor is a field, which means as far as the language semantics are concerned it's a getter — and in principle this could be on some subclass which overrides the backgroundColor getter with something that doesn't return the same thing every time.

One way to handle this is to destructure in the pattern-matching:

Suggested change
final effectiveBackgroundColor = switch (backgroundColor) {
// TODO why is the "as" casting needed?
StreamColorSwatch() => (backgroundColor as StreamColorSwatch).unreadCountBadgeBackground,
final effectiveBackgroundColor = switch (backgroundColor) {
StreamColorSwatch(:var unreadCountBadgeBackground) => unreadCountBadgeBackground,

Or can avoid repeating the long name:

      StreamColorSwatch(unreadCountBadgeBackground: var color) => color,

The other solution is to make a local:

    final backgroundColor = this.backgroundColor;
    final effectiveBackgroundColor = switch (backgroundColor) {
      StreamColorSwatch() => backgroundColor.unreadCountBadgeBackground,

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Makes sense! Thanks.

// TODO why is the "as" casting needed?
StreamColorSwatch() => (backgroundColor as StreamColorSwatch).unreadCountBadgeBackground,
Color() => backgroundColor,
_ => const Color.fromRGBO(102, 102, 153, 0.15),
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
_ => const Color.fromRGBO(102, 102, 153, 0.15),
null => const Color.fromRGBO(102, 102, 153, 0.15),

The explicitness seems helpful. And the type-checker verifies that it's still exhaustive.

@chrisbobbe
Copy link
Collaborator Author

chrisbobbe commented Nov 18, 2023

Thanks for the review! Revision pushed (edit: er, now it's pushed, a moment after posting this). Oh and here's a screenshot; @alya and @terpimost FYI:

Screenshot 2023-11-17 at 10 51 30 PM

And for Greg:

@chrisbobbe chrisbobbe marked this pull request as ready for review November 18, 2023 04:18
@chrisbobbe chrisbobbe changed the title WIP inbox: Add "Inbox" page! inbox: Add "Inbox" page! Nov 18, 2023
chrisbobbe and others added 5 commits November 19, 2023 23:02
We'll use these soon, in the Inbox view.
The subtraction is fine if there isn't overflow, which should
be impossible for Zulip message IDs.  (They need to fit into
54 bits in order to not confuse the web client.)

Still, compareTo feels a bit more explicit about what we mean.
The trick of sorting first by the low-order key and then by
the high-order key works great if the sort algorithm leaves
elements in their existing relative order when they compare
equal, aka if the sort is "stable".

But Dart's List.sort disclaims any guarantee of being stable.
So this trick doesn't work; the pinned streams and unpinned
streams could each get scrambled out of alphabetical order
in the course of the second sort.

Instead do one sort, with a comparison function that looks
at both criteria.
Since we always have the subscription object around, we might as
well use it for the stream properties too, simplifying this code
a bit.
Copy link
Member

@gnprice gnprice left a comment

Choose a reason for hiding this comment

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

Thanks! This generally all looks good; a couple of comments below.

Since this is so close, in the interest of efficiency (and since you're focusing on the next feature, the reactions UI), I'll just make the tweaks to fix these and merge.

Comment on lines 205 to 206
// On how to extract expected results from the replit, see:
// TODO
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// On how to extract expected results from the replit, see:
// TODO

Or write a line or two sketching how you did it. But if not, I think the basic idea can be figured out from the example of the other group above.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oh! This time I did save my work. 🙂 I meant to come back to this before submitting my revision; oops.

Here's the code I added in the replit, just under ZULIP_ASSIGNMENT_COLORS. It duplicates some of the logic, where it wasn't exposed with a convenient helper function. But it can be checked against the implementation.

const dartColorIntFromHexString = (hexString) => {
  const lowercaseWithoutHash = hexString.substring(1).toLowerCase();
  switch (lowercaseWithoutHash.length) {
    case 6: // opaque (alpha omitted)
      return `0xff${lowercaseWithoutHash}`;
    case 8: // not opaque
      // convert rrggbbaa to aarrggbb
      return '0x'
        + lowercaseWithoutHash.substring(6, 8)
        + lowercaseWithoutHash.substring(0, 2);
    default:
      throw new Error();
  }
};

const dartCheckLine = (baseHex, expectedHex) => {
  return `runCheck(${dartColorIntFromHexString(baseHex)}, const Color(${dartColorIntFromHexString(expectedHex)}));\n`;
};

let iconOnPlainBackgroundResult = '';
let iconOnBarBackgroundResult = '';
let barBackgroundResult = '';
ZULIP_ASSIGNMENT_COLORS.forEach(color => {
  const correctedColor = correctStreamColor({ color });
  const barBackground = getRecipientBarColor({ color: correctStreamColor({ color })});
  const iconOnPlainBackground = correctedColor;
  const iconOnBarBackground = colord(correctedColor).darken(0.12).toHex();

  iconOnPlainBackgroundResult += dartCheckLine(color, iconOnPlainBackground);
  iconOnBarBackgroundResult += dartCheckLine(color, iconOnBarBackground);
  barBackgroundResult += dartCheckLine(color, barBackground);
});

console.log(iconOnPlainBackgroundResult);
console.log(iconOnBarBackgroundResult);
console.log(barBackgroundResult);

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Pushed 640caef linking to my message here with that code.

Copy link
Member

Choose a reason for hiding this comment

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

Great, thanks!

Comment on lines 125 to 132
..sort((a, b) {
final streamA = streams[a.key]!;
final streamB = streams[b.key]!;

// TODO(i18n) something like JS's String.prototype.localeCompare
return streamA.name.toLowerCase().compareTo(streamB.name.toLowerCase());
})
..sort((a, b) {
Copy link
Member

Choose a reason for hiding this comment

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

From the [List.sort] doc:

The sort function is not guaranteed to be stable, so distinct objects that compare as equal may occur in any order in the result

That means that in sorting twice, the first sort may not have a useful effect. Instead, we'll want a single sort that checks one condition and then tie-breaks with the other.

(It seems to work in manual testing ­— so possibly the current implementation in the Dart VM is indeed stable. But that might change.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah OK, makes sense.

topicItems.sort((a, b) {
final (_, _, aLastUnreadId) = a;
final (_, _, bLastUnreadId) = b;
return bLastUnreadId - aLastUnreadId;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return bLastUnreadId - aLastUnreadId;
return bLastUnreadId.compareTo(aLastUnreadId);

Using subtraction for comparison makes me a bit nervous with the possibility of overflow. Not super likely for Zulip message IDs, but doesn't seem impossible in principle, and we might as well make an explicit comparison anyway.

Copy link
Member

Choose a reason for hiding this comment

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

… I guess here's one good reason to think overflow will never happen: these message IDs are all over the web client, represented there as plain JS numbers. So they need to fit into 54 bits in order to not break web.

Still, compareTo feels like it says a bit more directly what we mean here.

@gnprice gnprice merged commit 98e3527 into zulip:main Nov 20, 2023
@chrisbobbe chrisbobbe deleted the pr-inbox-view branch November 20, 2023 14:32
chrisbobbe added a commit that referenced this pull request Nov 20, 2023
@chrisbobbe chrisbobbe restored the pr-inbox-view branch November 20, 2023 15:00
@chrisbobbe chrisbobbe deleted the pr-inbox-view branch November 20, 2023 15:01
@chrisbobbe
Copy link
Collaborator Author

Thanks for the review and those fixes!

@sirpengi sirpengi mentioned this pull request Nov 20, 2023
gnprice added a commit to gnprice/zulip-flutter that referenced this pull request Feb 8, 2024
…tions

These were never getting consulted, because these aren't entire items
of the enclosing list view; rather the items are the whole sections,
i.e. either a whole stream or all DMs.  Because they weren't doing
anything, they're confusing for understanding how this code works
and making changes to it.

I think this happened because in an early draft of zulip#381, the list-view
items were individual conversations.  That's the arrangement that
would be better for performance/smoothness reasons anyway, but it
won't work until we make some changes to the sticky_header library;
see zulip#389.  When we go to do zulip#389, we can always add these
StickyHeaderItem widgets back.
chrisbobbe pushed a commit that referenced this pull request Feb 12, 2024
…tions

These were never getting consulted, because these aren't entire items
of the enclosing list view; rather the items are the whole sections,
i.e. either a whole stream or all DMs.  Because they weren't doing
anything, they're confusing for understanding how this code works
and making changes to it.

I think this happened because in an early draft of #381, the list-view
items were individual conversations.  That's the arrangement that
would be better for performance/smoothness reasons anyway, but it
won't work until we make some changes to the sticky_header library;
see #389.  When we go to do #389, we can always add these
StickyHeaderItem widgets back.
chrisbobbe added a commit to chrisbobbe/zulip-flutter that referenced this pull request May 4, 2024
We added the light-mode color computations in zulip#381.

We're not ready to use this yet, but the color computations are
unlikely to change before that time comes, and finding the right
ones is a chore that's good to get done.

Related: zulip#95
chrisbobbe added a commit to chrisbobbe/zulip-flutter that referenced this pull request May 4, 2024
We added the light-mode color computations in zulip#381.

We're not ready to use this yet, but the color computations are
unlikely to change before that time comes, and finding the right
ones is a chore that's good to get done.

Related: zulip#95
chrisbobbe added a commit to chrisbobbe/zulip-flutter that referenced this pull request May 9, 2024
We added the light-mode color computations in zulip#381.

We're not ready to use this yet, but the color computations are
unlikely to change before that time comes, and finding the right
ones is a chore that's good to get done.

Related: zulip#95
gnprice pushed a commit to chrisbobbe/zulip-flutter that referenced this pull request May 10, 2024
We added the light-mode color computations in zulip#381.

We're not ready to use this yet, but the color computations are
unlikely to change before that time comes, and finding the right
ones is a chore that's good to get done.

Related: zulip#95
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants