Skip to content

Add stream and topic narrows #147

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 8 commits into from
Jun 2, 2023
48 changes: 13 additions & 35 deletions lib/widgets/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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(
Expand All @@ -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<void> 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(),
]))));
}
}
53 changes: 44 additions & 9 deletions lib/widgets/compose_box.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand All @@ -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,
)));
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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<StreamComposeBox> createState() => _StreamComposeBoxState();
Expand Down Expand Up @@ -623,12 +633,17 @@ class _StreamComposeBoxState extends State<StreamComposeBox> {
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(
Expand All @@ -641,3 +656,23 @@ class _StreamComposeBoxState extends State<StreamComposeBox> {
]))));
}
}

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
}
}
}
109 changes: 89 additions & 20 deletions lib/widgets/message_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> 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});

Expand Down Expand Up @@ -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.
])));
}
}

Expand Down