diff --git a/lib/widgets/app.dart b/lib/widgets/app.dart index 748c43ed96..3e3067ff5a 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 { @@ -121,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( @@ -147,39 +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 + ], ]))); } } - -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))), - const StreamComposeBox(), - ])))); - } -} diff --git a/lib/widgets/compose_box.dart b/lib/widgets/compose_box.dart index f4f5bcb060..2eca896cc4 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; @@ -151,11 +152,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 +191,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 +210,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, ))); @@ -443,8 +449,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; @@ -517,10 +528,7 @@ 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(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 +572,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(); @@ -623,12 +633,17 @@ class _StreamComposeBoxState extends State { topicInput, const SizedBox(height: 8), _StreamContentInput( + streamId: widget.streamId, topicController: _topicController, controller: _contentController, focusNode: _contentFocusNode), ]))), const SizedBox(width: 8), - _StreamSendButton(topicController: _topicController, contentController: _contentController), + _StreamSendButton( + streamId: widget.streamId, + topicController: _topicController, + contentController: _contentController, + ), ]), Theme( data: themeData.copyWith( @@ -641,3 +656,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 SizedBox.shrink(); + } else { + throw Exception("impossible narrow"); // TODO(dart-3): show this statically + } + } +} diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 2f1fb81f82..765f04adbd 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -8,10 +8,71 @@ 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: MessageListAppBarTitle(narrow: narrow)), + 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 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}); @@ -205,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. + ]))); } }