Skip to content

Add topic-list page #1500

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 3 commits into from
May 28, 2025
Merged

Add topic-list page #1500

merged 3 commits into from
May 28, 2025

Conversation

PIG208
Copy link
Member

@PIG208 PIG208 commented May 6, 2025

Stacked on top of #1491.

Some non-goals of this change are deferred to #1499. In this implementation, we fetch the topics but do not handle all events to receive live-updates. It's expected that when topics are resolved/unresolved or moved, or when new messages arrived, the changes to the topic-list will not be seen until the next fetch.

We also skip thinning down the app bar, since that will require work on app bars on message-list page too.

The PR is structured to encourage side-by-side comparison with similar existing code. Namely _TopicItem from lib/widgets/inbox.dart and MessageListAppBarTitle.

Fixes: #1158

screenshots (taken on my Android device, hence the left-aligned app bar!)
light dark
regular image image
unsubscribed image image
unknown channel image image
small regular large
image image image
message-list channel action sheet
image image
non-muted topic muted topic
image image

debugDefaultTargetPlatformOverride = TargetPlatform.iOS:

@PIG208 PIG208 requested a review from chrisbobbe May 6, 2025 21:39
@PIG208 PIG208 added the maintainer review PR ready for review by Zulip maintainers label May 6, 2025
@PIG208 PIG208 force-pushed the pr-topics branch 2 times, most recently from a2d1174 to e8fb3c3 Compare May 8, 2025 17:26
@PIG208 PIG208 force-pushed the pr-topics branch 2 times, most recently from 9464d23 to 1a3d8e9 Compare May 13, 2025 20:13
@PIG208
Copy link
Member Author

PIG208 commented May 13, 2025

Updated the PR to implement a intentional deviation from the Figma design on aligning items, discussed in #mobile-team > topic list item alignment @ 💬.

@alya
Copy link
Collaborator

alya commented May 13, 2025

For the "small" and "large" screenshots, I think it would look much better if the checkmarks and indicators on the right scaled too.

@alya
Copy link
Collaborator

alya commented May 13, 2025

Let's name the option "List of topics". I think that's a bit easier to parse, and it's what we're currently testing for the web app UI.

@gnprice
Copy link
Member

gnprice commented May 13, 2025

For the "small" and "large" screenshots, I think it would look much better if the checkmarks and indicators on the right scaled too.

Seems reasonable. I'd want to clamp the scale factor, for the reasons described at #1023.

(For existing examples of implementing that, search for clamp in the code.)

@PIG208
Copy link
Member Author

PIG208 commented May 14, 2025

Thanks both! Updated the PR and the screenshots. I picked 1.5 to be the maxScaleFactor for icons, since that appears to be what we are using in most other places.

I clamped only the icons, though, not the topic name text. Because I think #1023 applies to scaling relatively unimportant information.

@alya
Copy link
Collaborator

alya commented May 14, 2025

Works for me, thanks!

@chrisbobbe
Copy link
Collaborator

chrisbobbe commented May 17, 2025

The app bar's back button and "Channel feed" button are broken; sometimes when I tap it doesn't respond and this gets logged:

======== Exception caught by foundation library ====================================================
The following assertion was thrown while dispatching notifications for WidgetStatesController:
setState() or markNeedsBuild() called when widget tree was locked.

This _IconButtonM3 widget cannot be marked as needing to build because the framework is locked.
The widget on which setState() or markNeedsBuild() was called was: _IconButtonM3
  style: ButtonStyle#2426d(mouseCursor: WidgetStateMapper<MouseCursor?>({WidgetState.disabled: null, WidgetState.any: null}))
  dependencies: [IconButtonTheme, IconTheme, InheritedCupertinoTheme, _InheritedTheme, _LocalizationsScope-[GlobalKey#2fb94]]
  state: _ButtonStyleState#9e3dc
When the exception was thrown, this was the stack: 
#0      Element.markNeedsBuild.<anonymous closure> (package:flutter/src/widgets/framework.dart:5273:9)
#1      Element.markNeedsBuild (package:flutter/src/widgets/framework.dart:5283:6)
#2      State.setState (package:flutter/src/widgets/framework.dart:1219:15)
#3      _ButtonStyleState.handleStatesControllerChange (package:flutter/src/material/button_style_button.dart:326:5)
#4      ChangeNotifier.notifyListeners (package:flutter/src/foundation/change_notifier.dart:435:24)
#5      WidgetStatesController.update (package:flutter/src/widgets/widget_state.dart:1152:7)
#6      _InkResponseState.updateHighlight (package:flutter/src/material/ink_well.dart:995:26)
#7      _InkResponseState.handleTapCancel (package:flutter/src/material/ink_well.dart:1211:5)
#8      GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:345:24)
#9      TapGestureRecognizer.handleTapCancel (package:flutter/src/gestures/tap.dart:800:11)
#10     BaseTapGestureRecognizer._checkCancel (package:flutter/src/gestures/tap.dart:388:5)
#11     BaseTapGestureRecognizer.resolve (package:flutter/src/gestures/tap.dart:336:7)
#12     OneSequenceGestureRecognizer.dispose (package:flutter/src/gestures/recognizer.dart:470:5)
#13     PrimaryPointerGestureRecognizer.dispose (package:flutter/src/gestures/recognizer.dart:765:11)
#14     RawGestureDetectorState.dispose (package:flutter/src/widgets/gesture_detector.dart:1529:18)
#15     StatefulElement.unmount (package:flutter/src/widgets/framework.dart:5922:11)
#16     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2075:13)
#17     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2073:7)
#18     ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5763:14)
#19     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2071:13)
#20     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2073:7)
#21     SingleChildRenderObjectElement.visitChildren (package:flutter/src/widgets/framework.dart:6994:14)
#22     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2071:13)
#23     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2073:7)
#24     ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5763:14)
#25     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2071:13)
#26     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2073:7)
#27     ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5763:14)
#28     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2071:13)
#29     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2073:7)
#30     SingleChildRenderObjectElement.visitChildren (package:flutter/src/widgets/framework.dart:6994:14)
#31     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2071:13)
#32     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2073:7)
#33     SingleChildRenderObjectElement.visitChildren (package:flutter/src/widgets/framework.dart:6994:14)
#34     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2071:13)
#35     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2073:7)
#36     ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5763:14)
#37     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2071:13)
#38     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2073:7)
#39     ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5763:14)
#40     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2071:13)
#41     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2073:7)
#42     ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5763:14)
#43     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2071:13)
#44     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2073:7)
#45     ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5763:14)
#46     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2071:13)
#47     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2073:7)
#48     ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5763:14)
#49     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2071:13)
#50     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2073:7)
#51     ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5763:14)
#52     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2071:13)
#53     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2073:7)
#54     ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5763:14)
#55     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2071:13)
#56     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2073:7)
#57     ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5763:14)
#58     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2071:13)
#59     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2073:7)
#60     ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5763:14)
#61     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2071:13)
#62     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2073:7)
#63     ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5763:14)
#64     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2071:13)
#65     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2073:7)
#66     ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5763:14)
#67     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2071:13)
#68     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2073:7)
#69     ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5763:14)
#70     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2071:13)
#71     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2073:7)
#72     ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5763:14)
#73     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2071:13)
#74     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2073:7)
#75     ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5763:14)
#76     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2071:13)
#77     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2073:7)
#78     ComponentElement.visitChildren (package:flutter/src/widgets/framework.dart:5763:14)
#79     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2071:13)
#80     _InactiveElements._unmount.<anonymous closure> (package:flutter/src/widgets/framework.dart:2073:7)
#81     SingleChildRenderObjectElement.visitChildren (package:flutter/src/widgets/framework.dart:6994:14)
#82     _InactiveElements._unmount (package:flutter/src/widgets/framework.dart:2071:13)
#83     ListIterable.forEach (dart:_internal/iterable.dart:49:13)
#84     _InactiveElements._unmountAll (package:flutter/src/widgets/framework.dart:2084:25)
#85     BuildOwner.lockState (package:flutter/src/widgets/framework.dart:2965:15)
#86     BuildOwner.finalizeTree (package:flutter/src/widgets/framework.dart:3288:7)
#87     WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:1266:19)
#88     RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:495:5)
#89     SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1438:15)
#90     SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1351:9)
#91     SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:1204:5)
#92     _invoke (dart:ui/hooks.dart:331:13)
#93     PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:444:5)
#94     _drawFrame (dart:ui/hooks.dart:303:31)
The WidgetStatesController sending notification was: WidgetStatesController#d045d({})
====================================================================================================

Looks like an upstream regression, flutter/flutter#168445, that's being worked on.

Edit: Oh, I guess that issue's description says "app crash"—the app isn't crashing for me, but I am seeing "setState() or markNeedsBuild() called when widget tree was locked." like in a stack trace posted there: flutter/flutter#168445 (comment)

@chrisbobbe
Copy link
Collaborator

We should use less than 28px padding at the start of each topic row. From the issue description:

For this issue, differences from the design there:

  • The design shows a "pinned topics" feature, which is a possible future Zulip feature but doesn't currently exist. Pending that feature, we'll leave out the "pinned" icon, and avoid using up horizontal space for it.

@chrisbobbe
Copy link
Collaborator

chrisbobbe commented May 17, 2025

For the layout, if we use an empty box as a placeholder for an icon when it's absent, it should scale exactly the same way as the icon does. See the start of the topic text being misaligned in the "large" screenshot:

screenshot image

@chrisbobbe
Copy link
Collaborator

I started a discussion about the chevron-down icon in the app bar: #mobile-team > topic list: chevron-down icon in app bar? @ 💬

@PIG208
Copy link
Member Author

PIG208 commented May 20, 2025

Thanks for the review! I adjusted the padding at the start of each row to 6px (matching the start padding the "pinned" icon would have), and addressed the other issues. The screenshots have been updated.

Looks like an upstream regression, flutter/flutter#168445, that's being worked on.

Thanks for looking into this! Yeah, for this one, we might need flutter/flutter#168546 to land first.

@gnprice
Copy link
Member

gnprice commented May 20, 2025

The app bar's back button and "Channel feed" button are broken; sometimes when I tap it doesn't respond

AFAICT the symptoms of that issue don't reproduce in a release build; the buttons work fine the first time. (I spent a few minutes just now trying to reproduce, in order to evaluate whether the issue will affect the upcoming beta v0.0.29.)

In a debug build, I also see it on other screens even without this PR.

So definitely an annoyance in dev — plus might be a sign of further bugginess that's just harder to spot the symptoms of — and will be good to see fixed upstream. But I don't think it needs to interact with merging this PR.

@chrisbobbe
Copy link
Collaborator

When I rebase this onto current main, the second commit has some tools/check errors:

$ tools/check analyze l10n
Running analyze...
Analyzing zulip-flutter...                                              

  error • Missing concrete implementation of 'getter abstract class
         ZulipLocalizations.topicsButtonLabel' •
         lib/generated/l10n/zulip_localizations_de.dart:8:7 •
         non_abstract_class_inherits_abstract_member

1 issue found. (ran in 2.8s)
Running l10n...
Error: there were updates to l10n:
 M lib/generated/l10n/zulip_localizations_de.dart

FAILED: analyze l10n

To rerun the suites that failed, run:
  $ tools/check analyze l10n

Could you rebase and fix those please?

@PIG208
Copy link
Member Author

PIG208 commented May 21, 2025

Thanks! Just updated this. I found that it also happens to the last commit. All commits that contain changes adding new strings to translate are probably affected, since we just recently added de (#1522).

@chrisbobbe
Copy link
Collaborator

chrisbobbe commented May 21, 2025

I like that the channel icon in the app bar is colorized! We should do that in the app bar of the message-list page, following Figma; I'll send a PR. -> #1524

Copy link
Collaborator

@chrisbobbe chrisbobbe 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 is exciting!! Small comments below, and it looks like there's a (nearly?) resolved discussion about making this page more accessible from a message-list page for a topic narrow: #mobile > Flutter beta: missing topic list @ 💬

final appBarBackgroundColor = colorSwatchFor(
context, store.subscriptions[streamId]).barBackground;
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: I would put context, on the previous line

Comment on lines 53 to 54
shape: Border(bottom: BorderSide(
width: 1, color: designVariables.borderBar))),
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: omit shape; the same shape is set by zulipThemeData

for (final GetStreamTopicsEntry(:maxId, name: topic) in lastFetchedTopics!) {
final unreadMessageIds =
unreadsModel!.streams[widget.streamId]?[topic] ?? <int>[];
final countInTopic = unreadMessageIds.length;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's centralize on Unreads.countInTopicNarrow for this; I could imagine wanting to refine it for something like muted senders etc.

Copy link
Member Author

@PIG208 PIG208 May 22, 2025

Choose a reason for hiding this comment

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

We might still need to keep unreadMessageIds (or inline it) to compute the value of hasMention.
Looks like inbox page does something like that with DMs:

      final countInNarrow = unreadsModel!.countInDmNarrow(dmNarrow);
      if (countInNarrow == 0) {
        continue;
      }
      final hasMention = unreadsModel!.dms[dmNarrow]!.any(
        (messageId) => unreadsModel!.mentions.contains(messageId));

Copy link
Collaborator

Choose a reason for hiding this comment

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

Makes sense; yeah, my comment was specifically about using .length; it's fine to do other things besides that.

// A null [Icon.icon] makes a blank space.
_IconMarker(icon: topic.isResolved ? ZulipIcons.check : null),
Expanded(child: Opacity(
opacity: isTopicVisibleInStream ? 1 : 0.5,
Copy link
Collaborator

Choose a reason for hiding this comment

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

This looks a little odd—the value (from the relevant dartdoc) is about "whether" this topic should appear, not how it should appear. Is this logic correct, and is that dartdoc on store.isTopicVisibleInStream correct?

Copy link
Member Author

@PIG208 PIG208 May 22, 2025

Choose a reason for hiding this comment

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

The topics with "isTopicVisibleInStream=false" will be hidden on web, in the list that appears when you just click on the channel.

This topic-list however should probably match the topic-list from "show all topics" button on web. Because in the Figma design, muted topics in the topic-list appear semi-transparent. I think similar to web, this offers a way for the user to access the muted topics.

In conclusion, both are correct, but we should probably name the bool differently and comment on why we use the helper this way.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, that value means whether it should appear in the channel feed, the message list. That'd be good to clarify in its doc.

Here in the list of topics for the channel, all topics are shown.

Copy link
Member Author

Choose a reason for hiding this comment

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

Or maybe it is more appropriate to use topicVisibilityPolicy directly, and compare the value to UserTopicVisibilityPolicy.muted.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, that's probably a good way to write this code.

Comment on lines 285 to 286
if (trailingWidgets.isEmpty)
const SizedBox(width: 53),
Copy link
Collaborator

Choose a reason for hiding this comment

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

I see this is what the Figma is doing, but it feels odd: it doesn't create a consistent end margin for the topic text, because the gap will be less than 53px when there's just one or two trailingWidgets, right? How about just removing this and allowing a longer line for the topic text.

Comment on lines 309 to 325
// This is adapted from [UnreadCountBadge].
class _UnreadCountBadge extends StatelessWidget {
const _UnreadCountBadge({required this.count});
Copy link
Collaborator

Choose a reason for hiding this comment

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

It really seems like we should be able to use UnreadCountBadge directly here—if the Figma has inconsistencies among unread-count badges, I'd suspect they're accidental; could you investigate?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, it would be ideal to share it.

The UnreadCountBadge we know today comes from Nov 2023 (d9eb0d3), when it was extracted from RecentDmConversationsPage. For DM, the current Figma design's unread count badge looks different.

I assume that the design for inbox page and subscription-list page got outdated this way too. Since we haven't got to implementing those redesigns yet, just updating UnreadCountBadge to use the new design might make it out-of-place in those pages.

Relatedly, if we can, I would like to extract _TopicItem from lib/widgets/inbox.dart for reuse too. When I tried that while drafting this, it turned out that we would need to implement more of the inbox-page redesign to make the shared widget fit.

I think a reasonable strategy will be implementing these widgets in topic-list first, while referring to the code structure of the existing ones (to compare what's changed). We can consider extracting these widgets once we get to implementing other redesigned pages, and phase out UnreadCountBadge in this process.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sure, that sounds reasonable. Could you leave a TODO for this plan?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I plan to file some issues later.

Copy link
Member Author

Choose a reason for hiding this comment

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

Opened #1527, and I found that #1406 already exists, nice.

await tester.pumpWidget(TestZulipApp(
accountId: eg.selfAccount.id,
child: TopicListPage(streamId: channel.streamId)));
await tester.pumpAndSettle();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can this be done with individual tester.pumps rather than a pumpAndSettle?

Comment on lines 212 to 213
assert(resolvedTopic.displayName == '✔ resolved', resolvedTopic.displayName);
check(findInTopicItemAt(0, find.text('✔ resovled'))).findsNothing();
Copy link
Collaborator

Choose a reason for hiding this comment

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

oops: should be find.text('✔ resolved')

@PIG208
Copy link
Member Author

PIG208 commented May 22, 2025

Thanks! Updated the PR.

@chrisbobbe
Copy link
Collaborator

Thanks, looks good! Marking for Greg's review.

@chrisbobbe chrisbobbe added integration review Added by maintainers when PR may be ready for integration and removed maintainer review PR ready for review by Zulip maintainers labels May 22, 2025
@chrisbobbe chrisbobbe requested a review from gnprice May 22, 2025 22:00
@chrisbobbe chrisbobbe assigned gnprice and unassigned chrisbobbe May 22, 2025
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 to you both! Generally this looks good; comments below.

I'm about to go AFK, but I've gotten up through the non-test changes in the second commit:
39b6724 topics: Add topic list page

Still ahead for me to read are the tests, and the third commit:
33fc01d topics: Add TopicListButton to channel action sheet

context, store.subscriptions[streamId]).barBackground;

return PageRoot(child: Scaffold(
appBar: ZulipAppBar(
Copy link
Member

Choose a reason for hiding this comment

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

topics: Add topic list page

For the topic-list page app bar, we leave out the icon "chveron_down.svg"

nit: spelling of icon name

import 'text.dart';
import 'theme.dart';

class TopicListPage 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.

The topic-list implementation is quite similar to parts of inbox page
and message-list page.  Therefore, we structure the code to make them
look similar to compare for changes side-by-side to help with reviewing
what has changed.

That's true, but FTR there's another reason this is useful, which in my mind is actually a bigger reason in this case: it helps with comparing them in the future, for maintaining them.

Specifically it helps us (a) when we're changing one, apply the same change to the other, where appropriate, and (b) later reconcile the differences they do have and then refactor to unify them.

},
behavior: HitTestBehavior.opaque,
child: Padding(
padding: EdgeInsetsDirectional.fromSTEB(4, 8, 12, 8),
Copy link
Member

Choose a reason for hiding this comment

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

Is there an example in Figma with both the feed icon and this action? They look crowded together in the UI:
image

So this should probably get more padding at the start. I'd be inclined to make it symmetric — that's how the icons are.

(I see it shows 4px padding in Figma… but that's in a situation where the amount of start padding has basically no visible effect, because the channel name is way off to the left anyway. If the channel name were so long that it bumped into this padding, I suspect 4px would look too crowded then too.)

Copy link
Member Author

Choose a reason for hiding this comment

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

Went with 12px, and posted a screenshot in #mobile > Flutter beta: missing topic list @ 💬:

final int streamId;
final bool willCenterTitle;

Widget _buildAppBarRow(BuildContext context) {
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
Widget _buildAppBarRow(BuildContext context) {
Widget _buildTitleRow(BuildContext context) {

This whole widget is only part of the app bar, namely the title. So the widget this helper returns can be at most the title.

Copy link
Member

Choose a reason for hiding this comment

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

In fact better:

Suggested change
Widget _buildAppBarRow(BuildContext context) {
Widget _buildStreamRow(BuildContext context) {

This is really doing the exact same job as the _buildStreamRow on MessageListAppBarTitle — just for the updated design. So matching the name helps draw that parallel.

MessageListPage.buildRoute(context: context,
narrow: ChannelNarrow(streamId)))),
],
backgroundColor: appBarBackgroundColor),
Copy link
Member

Choose a reason for hiding this comment

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

nit: put this above title and actions; the latter are more the content of the app bar (similar to a child argument, which always goes last), and this is more like metadata


return ListView.builder(
itemCount: topicItems.length,
itemBuilder: (BuildContext context, int index) =>
Copy link
Member

Choose a reason for hiding this comment

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

nit: omit boring types

Suggested change
itemBuilder: (BuildContext context, int index) =>
itemBuilder: (context, index) =>

Comment on lines 232 to 233
final opacity =
visibilityPolicy == UserTopicVisibilityPolicy.muted ? 0.5 : 1.0;
Copy link
Member

Choose a reason for hiding this comment

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

Let's use a switch-expression for this — helps give confidence we've considered all the cases, particularly if we ever add another value to this enum.

Comment on lines 256 to 276
// To account for scaled text, we align everything on the row
// to [CrossAxisAlignment.center] instead ([Row]'s default),
// like we do for the topic items on the inbox page.
// CZO discussion:
Copy link
Member

Choose a reason for hiding this comment

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

There's one quite visible thing we're giving up from the design in this version, so let's note it in a TODO:

Suggested change
// To account for scaled text, we align everything on the row
// to [CrossAxisAlignment.center] instead ([Row]'s default),
// like we do for the topic items on the inbox page.
// CZO discussion:
// To account for scaled text, we align everything on the row
// to [CrossAxisAlignment.center] instead ([Row]'s default),
// like we do for the topic items on the inbox page.
// TODO(design): align to baseline (and therefore to first line of
// topic name), but with adjustment for icons
// CZO discussion:

// https://chat.zulip.org/#narrow/channel/243-mobile-team/topic/topic.20list.20item.20alignment/near/2173252
children: [
// A null [Icon.icon] makes a blank space.
_IconMarker(icon: topic.isResolved ? ZulipIcons.check : null),
Copy link
Member

Choose a reason for hiding this comment

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

Should this get the opacity applied too?

Copy link
Member Author

@PIG208 PIG208 May 23, 2025

Choose a reason for hiding this comment

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

No, because it's not extra-faded in the Figma design. (This is also why we couldn't wrap the entire row in a single Opacity widget.)

fontStyle: topic.displayName == null ? FontStyle.italic : null,
color: designVariables.textMessage,
),
topic.unresolve().displayName ?? store.realmEmptyTopicDisplayName))),
Copy link
Member

Choose a reason for hiding this comment

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

maxLines: 3? I don't see it come up in the Figma design but I believe that's the intent.

(And overflow ellipsis.)

Copy link
Member Author

@PIG208 PIG208 May 23, 2025

Choose a reason for hiding this comment

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

Hm, I thought the example text "[…] which could be more than 2 lines if we want" meant that we can have >2 lines with no upperbound. But I guess without baseline alignment, more than 3 lines can look odd. Let's go with maxLines: 3 now and revisit this if we fix the alignment.

Copy link
Member Author

Choose a reason for hiding this comment

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

Opened #1528 for implementing baseline alignment.

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 for the revision! Those changes all look good.

I've just read through the rest of the branch, and generally it all looks good; a few comments below.

checkButton('Mark channel as read');
checkButton('List of topics');
Copy link
Member

Choose a reason for hiding this comment

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

nit: match the order these appear in the action sheet (and in the code, and the tests below)

Suggested change
checkButton('Mark channel as read');
checkButton('List of topics');
checkButton('List of topics');
checkButton('Mark channel as read');

import '../stdlib_checks.dart';
import 'test_app.dart';

void main() {
Copy link
Member

Choose a reason for hiding this comment

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

Filename should end in _test.dart so that flutter test picks it up 🙂

+++ test/widgets/topic_list_tests.dart

Comment on lines 210 to 215
eg.getStreamTopicsEntry(name: resolvedTopic.apiName),
eg.getStreamTopicsEntry(name: unresolvedTopic.apiName),
]);

assert(resolvedTopic.displayName == '✔ resolved', resolvedTopic.displayName);
check(findInTopicItemAt(0, find.text('✔ resolved'))).findsNothing();
Copy link
Member

Choose a reason for hiding this comment

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

Several of these test cases are a bit fragile because they let maxId be the same for several items in the list (which is also unrealistic — the message with that ID can belong to at most one conversation), and then their checks expect a particular order for the items to appear in.

Can fix by just giving explicit maxId: 2, maxId: 1, etc.

check(find.text('topic B')).findsOne();
});

group('_TopicItem', () {
Copy link
Member

Choose a reason for hiding this comment

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

nit: this group isn't really specific to _TopicItem — a lot of the logic it's testing is in _TopicListState. (In fact I saw this group and that there weren't any other tests left in the file, and then thought that meant there wouldn't be tests for what's in _TopicListState.build and wondered if I should ask for some.)

I think the group can just be unwrapped, and its contents de-indented to the top level of main. One accurate name for the group would be "page body"… but the whole file is about this particular page, so it seems reasonable for the page body to take the top level.

PIG208 and others added 3 commits May 27, 2025 17:21
For the topic-list page app bar, we leave out the icon "chevron_down.svg"
since it's related to a new design (zulip#1039) we haven't implemented yet.
This also why "TOPICS" is not aligned to the middle part of the app bar
on the message-list page.

We also leave out the new topic button and topic filtering, which are
out-of-scope for zulip#1158.

The topic-list implementation is quite similar to parts of inbox page
and message-list page.  Therefore, we structure the code to make
it easy to maintain in the future. Especially, this helps us
(a) when we're changing one, apply the same change to the other,
where appropriate, and (b) later reconcile the differences they
do have and then refactor to unify them.

Figma design:
  https://www.figma.com/design/1JTNtYo9memgW7vV6d0ygq/Zulip-Mobile?node-id=6819-35869&m=dev

The "TOPICS" icon on message-list page in a topic narrow is a UX
change from the design.  See CZO discussion:
  https://chat.zulip.org/#narrow/channel/48-mobile/topic/Flutter.20beta.3A.20missing.20topic.20list/near/2177505
@gnprice gnprice merged commit d81b812 into zulip:main May 28, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
integration review Added by maintainers when PR may be ready for integration
Projects
None yet
Development

Successfully merging this pull request may close these issues.

List of topics in channel
5 participants