zulip-flutter icon indicating copy to clipboard operation
zulip-flutter copied to clipboard

Add topic-list page

Open PIG208 opened this issue 11 months ago β€’ 12 comments

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 avatar May 06 '25 21:05 PIG208

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

PIG208 avatar May 13 '25 20:05 PIG208

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

alya avatar May 13 '25 22:05 alya

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.

alya avatar May 13 '25 22:05 alya

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.)

gnprice avatar May 13 '25 23:05 gnprice

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.

PIG208 avatar May 14 '25 00:05 PIG208

Works for me, thanks!

alya avatar May 14 '25 04:05 alya

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, https://github.com/flutter/flutter/issues/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: https://github.com/flutter/flutter/issues/168445#issuecomment-2861654745

chrisbobbe avatar May 17 '25 00:05 chrisbobbe

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 avatar May 17 '25 00:05 chrisbobbe

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 avatar May 17 '25 00:05 chrisbobbe

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

chrisbobbe avatar May 17 '25 01:05 chrisbobbe

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, https://github.com/flutter/flutter/issues/168445, that's being worked on.

Thanks for looking into this! Yeah, for this one, we might need https://github.com/flutter/flutter/pull/168546 to land first.

PIG208 avatar May 20 '25 00:05 PIG208

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.

gnprice avatar May 20 '25 03:05 gnprice

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?

chrisbobbe avatar May 21 '25 21:05 chrisbobbe

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).

PIG208 avatar May 21 '25 21:05 PIG208

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

chrisbobbe avatar May 21 '25 22:05 chrisbobbe

Thanks! Updated the PR.

PIG208 avatar May 22 '25 03:05 PIG208

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

chrisbobbe avatar May 22 '25 22:05 chrisbobbe