From c0a051595ee4955ce411f507837e998e34dbae64 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 25 May 2023 14:53:55 -0700 Subject: [PATCH 1/8] compose [nfc]: Parameterize StreamComposeBox on stream ID --- lib/widgets/app.dart | 2 +- lib/widgets/compose_box.dart | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/widgets/app.dart b/lib/widgets/app.dart index 748c43ed96..f062f2a957 100644 --- a/lib/widgets/app.dart +++ b/lib/widgets/app.dart @@ -179,7 +179,7 @@ class MessageListPage extends StatelessWidget { child: Expanded( child: MessageList(narrow: narrow))), - const StreamComposeBox(), + const StreamComposeBox(streamId: 7), // TODO vary by narrow; this is `#test here` ])))); } } diff --git a/lib/widgets/compose_box.dart b/lib/widgets/compose_box.dart index f4f5bcb060..05af64754d 100644 --- a/lib/widgets/compose_box.dart +++ b/lib/widgets/compose_box.dart @@ -443,8 +443,13 @@ class _AttachFromCameraButton extends _AttachUploadsButton { /// The send button for StreamComposeBox. class _StreamSendButton extends StatefulWidget { - const _StreamSendButton({required this.topicController, required this.contentController}); + const _StreamSendButton({ + required this.streamId, + required this.topicController, + required this.contentController, + }); + final int streamId; final TopicTextEditingController topicController; final ContentTextEditingController contentController; @@ -520,7 +525,7 @@ class _StreamSendButtonState extends State<_StreamSendButton> { if (store.connection.realmUrl.origin != 'https://chat.zulip.org') { throw Exception('This method can currently only be used on https://chat.zulip.org.'); } - final destination = StreamDestination(7, widget.topicController.textNormalized()); // TODO parametrize; this is `#test here` + final destination = StreamDestination(widget.streamId, widget.topicController.textNormalized()); final content = widget.contentController.textNormalized(); store.sendMessage(destination: destination, content: content); @@ -564,7 +569,9 @@ class _StreamSendButtonState extends State<_StreamSendButton> { /// The compose box for writing a stream message. class StreamComposeBox extends StatefulWidget { - const StreamComposeBox({super.key}); + const StreamComposeBox({super.key, required this.streamId}); + + final int streamId; @override State createState() => _StreamComposeBoxState(); @@ -628,7 +635,11 @@ class _StreamComposeBoxState extends State { focusNode: _contentFocusNode), ]))), const SizedBox(width: 8), - _StreamSendButton(topicController: _topicController, contentController: _contentController), + _StreamSendButton( + streamId: widget.streamId, + topicController: _topicController, + contentController: _contentController, + ), ]), Theme( data: themeData.copyWith( From b0bba6c5bf163039b02cc8bbacfdc257db6e92f7 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Tue, 30 May 2023 15:08:22 -0700 Subject: [PATCH 2/8] compose: Show actual stream name in content placeholder --- lib/widgets/compose_box.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/widgets/compose_box.dart b/lib/widgets/compose_box.dart index 05af64754d..6788afb54e 100644 --- a/lib/widgets/compose_box.dart +++ b/lib/widgets/compose_box.dart @@ -151,11 +151,13 @@ class ContentTextEditingController extends TextEditingController { /// The content input for StreamComposeBox. class _StreamContentInput extends StatefulWidget { const _StreamContentInput({ + required this.streamId, required this.controller, required this.topicController, required this.focusNode, }); + final int streamId; final ContentTextEditingController controller; final TopicTextEditingController topicController; final FocusNode focusNode; @@ -188,6 +190,9 @@ class _StreamContentInputState extends State<_StreamContentInput> { @override Widget build(BuildContext context) { + final store = PerAccountStoreWidget.of(context); + final streamName = store.streams[widget.streamId]?.name ?? '(unknown stream)'; + ColorScheme colorScheme = Theme.of(context).colorScheme; return InputDecorator( @@ -204,7 +209,7 @@ class _StreamContentInputState extends State<_StreamContentInput> { focusNode: widget.focusNode, style: TextStyle(color: colorScheme.onSurface), decoration: InputDecoration.collapsed( - hintText: "Message #test here > $_topicTextNormalized", + hintText: "Message #$streamName > $_topicTextNormalized", ), maxLines: null, ))); @@ -630,6 +635,7 @@ class _StreamComposeBoxState extends State { topicInput, const SizedBox(height: 8), _StreamContentInput( + streamId: widget.streamId, topicController: _topicController, controller: _contentController, focusNode: _contentFocusNode), From 6414034365a69e935d4bf7cd4531a1b65fd0325f Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 25 May 2023 14:58:32 -0700 Subject: [PATCH 3/8] compose [nfc]: Make a per-narrow ComposeBox --- lib/widgets/app.dart | 3 ++- lib/widgets/compose_box.dart | 23 ++++++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/widgets/app.dart b/lib/widgets/app.dart index f062f2a957..050377f8dd 100644 --- a/lib/widgets/app.dart +++ b/lib/widgets/app.dart @@ -179,7 +179,8 @@ class MessageListPage extends StatelessWidget { child: Expanded( child: MessageList(narrow: narrow))), - const StreamComposeBox(streamId: 7), // TODO vary by narrow; this is `#test here` + + ComposeBox(narrow: narrow), ])))); } } diff --git a/lib/widgets/compose_box.dart b/lib/widgets/compose_box.dart index 6788afb54e..520f1339d7 100644 --- a/lib/widgets/compose_box.dart +++ b/lib/widgets/compose_box.dart @@ -3,9 +3,10 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:image_picker/image_picker.dart'; -import 'dialog.dart'; import '../api/route/messages.dart'; +import '../model/narrow.dart'; +import 'dialog.dart'; import 'store.dart'; const double _inputVerticalPadding = 8; @@ -658,3 +659,23 @@ class _StreamComposeBoxState extends State { ])))); } } + +class ComposeBox extends StatelessWidget { + const ComposeBox({super.key, required this.narrow}); + + final Narrow narrow; + + @override + Widget build(BuildContext context) { + final narrow = this.narrow; + if (narrow is StreamNarrow) { + return StreamComposeBox(streamId: narrow.streamId); + } else if (narrow is TopicNarrow) { + return const SizedBox.shrink(); // TODO(#144): add a single-topic compose box + } else if (narrow is AllMessagesNarrow) { + return const StreamComposeBox(streamId: 7); // TODO drop compose on all-messages; this is `#test here` + } else { + throw Exception("impossible narrow"); // TODO(dart-3): show this statically + } + } +} From 2d8e80f0916fed482826e0d226e7f6068e105faa Mon Sep 17 00:00:00 2001 From: Greg Price Date: Tue, 30 May 2023 12:44:30 -0700 Subject: [PATCH 4/8] msglist [nfc]: Move MessageListPage to widgets/message_list from widgets/app --- lib/widgets/app.dart | 36 ----------------------------------- lib/widgets/message_list.dart | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/lib/widgets/app.dart b/lib/widgets/app.dart index 050377f8dd..8f75a3ddab 100644 --- a/lib/widgets/app.dart +++ b/lib/widgets/app.dart @@ -2,10 +2,8 @@ import 'package:flutter/material.dart'; import '../model/narrow.dart'; import 'about_zulip.dart'; -import 'compose_box.dart'; import 'login.dart'; import 'message_list.dart'; -import 'page.dart'; import 'store.dart'; class ZulipApp extends StatelessWidget { @@ -150,37 +148,3 @@ class HomePage extends StatelessWidget { ]))); } } - -class MessageListPage extends StatelessWidget { - const MessageListPage({super.key, required this.narrow}); - - static Route buildRoute({required BuildContext context, required Narrow narrow}) { - return MaterialAccountPageRoute(context: context, - builder: (context) => MessageListPage(narrow: narrow)); - } - - final Narrow narrow; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text("All messages")), - body: Builder( - builder: (BuildContext context) => Center( - child: Column(children: [ - MediaQuery.removePadding( - // Scaffold knows about the app bar, and so has run this - // BuildContext, which is under `body`, through - // MediaQuery.removePadding with `removeTop: true`. - context: context, - - // The compose box pads the bottom inset. - removeBottom: true, - - child: Expanded( - child: MessageList(narrow: narrow))), - - ComposeBox(narrow: narrow), - ])))); - } -} diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 2f1fb81f82..89a12905c3 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -8,10 +8,46 @@ import '../model/message_list.dart'; import '../model/narrow.dart'; import '../model/store.dart'; import 'action_sheet.dart'; +import 'compose_box.dart'; import 'content.dart'; +import 'page.dart'; import 'sticky_header.dart'; import 'store.dart'; +class MessageListPage extends StatelessWidget { + const MessageListPage({super.key, required this.narrow}); + + static Route buildRoute({required BuildContext context, required Narrow narrow}) { + return MaterialAccountPageRoute(context: context, + builder: (context) => MessageListPage(narrow: narrow)); + } + + final Narrow narrow; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("All messages")), + body: Builder( + builder: (BuildContext context) => Center( + child: Column(children: [ + MediaQuery.removePadding( + // Scaffold knows about the app bar, and so has run this + // BuildContext, which is under `body`, through + // MediaQuery.removePadding with `removeTop: true`. + context: context, + + // The compose box pads the bottom inset. + removeBottom: true, + + child: Expanded( + child: MessageList(narrow: narrow))), + + ComposeBox(narrow: narrow), + ])))); + } +} + class MessageList extends StatefulWidget { const MessageList({super.key, required this.narrow}); From 541ecd890175cd36a27a9ab3358c205e1bb0535f Mon Sep 17 00:00:00 2001 From: Greg Price Date: Tue, 30 May 2023 12:59:35 -0700 Subject: [PATCH 5/8] msglist: Vary AppBar title by narrow --- lib/widgets/message_list.dart | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 89a12905c3..6164dba759 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -27,7 +27,7 @@ class MessageListPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text("All messages")), + appBar: AppBar(title: MessageListAppBarTitle(narrow: narrow)), body: Builder( builder: (BuildContext context) => Center( child: Column(children: [ @@ -48,6 +48,31 @@ class MessageListPage extends StatelessWidget { } } +class MessageListAppBarTitle extends StatelessWidget { + const MessageListAppBarTitle({super.key, required this.narrow}); + + final Narrow narrow; + + @override + Widget build(BuildContext context) { + switch (narrow) { + case AllMessagesNarrow(): + return const Text("All messages"); + + case StreamNarrow(:var streamId): + final store = PerAccountStoreWidget.of(context); + final streamName = store.streams[streamId]?.name ?? '(unknown stream)'; + return Text("#$streamName"); // TODO show stream privacy icon + + case TopicNarrow(:var streamId, :var topic): + final store = PerAccountStoreWidget.of(context); + final streamName = store.streams[streamId]?.name ?? '(unknown stream)'; + return Text("#$streamName > $topic"); // TODO show stream privacy icon; format on two lines + } + } +} + + class MessageList extends StatefulWidget { const MessageList({super.key, required this.narrow}); From 1b918d9007ea941e0480d4b0f7ac58e5d3c2e1a6 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Tue, 30 May 2023 12:38:22 -0700 Subject: [PATCH 6/8] nav: Add button for test-here narrow Up to now we've provided navigation only to the all-messages narrow, and we've given that screen a compose box with a hack that has it send only to `#test here`. We're going to remove that hack and put no compose box on that screen. To prepare for that, we'll want some other way to quickly get to a screen with a compose box, for testing. Do that with the actual stream narrow for `#test here`. --- lib/widgets/app.dart | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/widgets/app.dart b/lib/widgets/app.dart index 8f75a3ddab..3e3067ff5a 100644 --- a/lib/widgets/app.dart +++ b/lib/widgets/app.dart @@ -119,6 +119,11 @@ class HomePage extends StatelessWidget { InlineSpan bold(String text) => TextSpan( text: text, style: const TextStyle(fontWeight: FontWeight.bold)); + int? testStreamId; + if (store.connection.realmUrl.origin == 'https://chat.zulip.org') { + testStreamId = 7; // i.e. `#test here`; TODO cut this scaffolding hack + } + return Scaffold( appBar: AppBar(title: const Text("Home")), body: Center( @@ -145,6 +150,14 @@ class HomePage extends StatelessWidget { MessageListPage.buildRoute(context: context, narrow: const AllMessagesNarrow())), child: const Text("All messages")), + if (testStreamId != null) ...[ + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => Navigator.push(context, + MessageListPage.buildRoute(context: context, + narrow: StreamNarrow(testStreamId!))), + child: const Text("#test here")), // scaffolding hack, see above + ], ]))); } } From 73719bdbc107835a932bd1b3cf1d5d65afdcab60 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Tue, 30 May 2023 12:39:43 -0700 Subject: [PATCH 7/8] compose: Drop chat.zulip.org-specific scaffolding hack Now that one can navigate to the actual stream narrow for the `#test here` stream on chat.zulip.org, there's no longer a need for the hack that offered that stream's compose box on the all-messages narrow's screen. And then with that hack gone, the send button works correctly on any realm; so we can drop the condition that made it refuse to operate outside of chat.zulip.org. --- lib/widgets/compose_box.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/widgets/compose_box.dart b/lib/widgets/compose_box.dart index 520f1339d7..2eca896cc4 100644 --- a/lib/widgets/compose_box.dart +++ b/lib/widgets/compose_box.dart @@ -528,9 +528,6 @@ class _StreamSendButtonState extends State<_StreamSendButton> { } final store = PerAccountStoreWidget.of(context); - if (store.connection.realmUrl.origin != 'https://chat.zulip.org') { - throw Exception('This method can currently only be used on https://chat.zulip.org.'); - } final destination = StreamDestination(widget.streamId, widget.topicController.textNormalized()); final content = widget.contentController.textNormalized(); store.sendMessage(destination: destination, content: content); @@ -673,7 +670,7 @@ class ComposeBox extends StatelessWidget { } else if (narrow is TopicNarrow) { return const SizedBox.shrink(); // TODO(#144): add a single-topic compose box } else if (narrow is AllMessagesNarrow) { - return const StreamComposeBox(streamId: 7); // TODO drop compose on all-messages; this is `#test here` + return const SizedBox.shrink(); } else { throw Exception("impossible narrow"); // TODO(dart-3): show this statically } From a7576202a2cea761a95e45f9fffc69a4be1848d7 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Thu, 25 May 2023 16:39:59 -0700 Subject: [PATCH 8/8] msglist: Navigate to stream and topic narrows from recipient headers Fixes: #72 --- lib/widgets/message_list.dart | 48 ++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 6164dba759..765f04adbd 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -266,26 +266,34 @@ class StreamTopicRecipientHeader extends StatelessWidget { ThemeData.estimateBrightnessForColor(streamColor) == Brightness.dark ? Colors.white : Colors.black; - return ColoredBox( - color: _kStreamMessageBorderColor, - child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [ - // TODO: Long stream name will break layout; find a fix. - RecipientHeaderChevronContainer( - color: streamColor, - // TODO globe/lock icons for web-public and private streams - child: Text(streamName, style: TextStyle(color: contrastingColor))), - Expanded( - child: Padding( - // Web has padding 9, 3, 3, 2 here; but 5px is the chevron. - padding: const EdgeInsets.fromLTRB(4, 3, 3, 2), - child: Text(topic, - // TODO: Give a way to see the whole topic (maybe a - // long-press interaction?) - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontWeight: FontWeight.w600)))), - // TODO topic links? - // Then web also has edit/resolve/mute buttons. Skip those for mobile. - ])); + return GestureDetector( + onTap: () => Navigator.push(context, + MessageListPage.buildRoute(context: context, + narrow: TopicNarrow(message.streamId, message.subject))), + child: ColoredBox( + color: _kStreamMessageBorderColor, + child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [ + // TODO: Long stream name will break layout; find a fix. + GestureDetector( + onTap: () => Navigator.push(context, + MessageListPage.buildRoute(context: context, + narrow: StreamNarrow(message.streamId))), + child: RecipientHeaderChevronContainer( + color: streamColor, + // TODO globe/lock icons for web-public and private streams + child: Text(streamName, style: TextStyle(color: contrastingColor)))), + Expanded( + child: Padding( + // Web has padding 9, 3, 3, 2 here; but 5px is the chevron. + padding: const EdgeInsets.fromLTRB(4, 3, 3, 2), + child: Text(topic, + // TODO: Give a way to see the whole topic (maybe a + // long-press interaction?) + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontWeight: FontWeight.w600)))), + // TODO topic links? + // Then web also has edit/resolve/mute buttons. Skip those for mobile. + ]))); } }