Skip to content

Commit

Permalink
msglist: update stream recipient headers
Browse files Browse the repository at this point in the history
  • Loading branch information
sirpengi committed Nov 22, 2023
1 parent 2205e6e commit 5df0eac
Showing 1 changed file with 89 additions and 59 deletions.
148 changes: 89 additions & 59 deletions lib/widgets/message_list.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:math';
import 'dart:ui';

import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -91,8 +92,6 @@ class MessageListAppBarTitle extends StatelessWidget {
final Narrow narrow;

Widget _buildStreamRow(ZulipStream? stream, String text) {
// A null [Icon.icon] makes a blank space.
final icon = (stream != null) ? iconDataForStream(stream) : null;
return Row(
mainAxisSize: MainAxisSize.min,
// TODO(design): The vertical alignment of the stream privacy icon is a bit ad hoc.
Expand All @@ -101,7 +100,9 @@ class MessageListAppBarTitle extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Icon(size: 16, icon),
// A null [Icon.icon] makes a blank space.
Icon(size: 16,
(stream != null) ? iconDataForStream(stream) : null),
const SizedBox(width: 8),
Flexible(child: Text(text)),
]);
Expand Down Expand Up @@ -327,11 +328,13 @@ class _MessageListState extends State<MessageList> with PerAccountStoreAwareStat
padding: EdgeInsets.symmetric(vertical: 16.0),
child: CircularProgressIndicator())); // TODO perhaps a different indicator
case MessageListRecipientHeaderItem():
final header = RecipientHeader(message: data.message);
final header = RecipientHeader(message: data.message, narrow: widget.narrow);
return StickyHeaderItem(allowOverflow: true,
header: header, child: header);
case MessageListMessageItem():
final header = RecipientHeader(message: data.message, narrow: widget.narrow);
return MessageItem(
header: header,
key: ValueKey(data.message.id),
trailingWhitespace: i == 1 ? 8 : 11,
item: data);
Expand Down Expand Up @@ -431,16 +434,16 @@ class MarkAsReadWidget extends StatelessWidget {
}

class RecipientHeader extends StatelessWidget {
const RecipientHeader({super.key, required this.message});
const RecipientHeader({super.key, required this.message, required this.narrow});

final Message message;
final Narrow narrow;

@override
Widget build(BuildContext context) {
// TODO recipient headings depend on narrow
final message = this.message;
return switch (message) {
StreamMessage() => StreamTopicRecipientHeader(message: message),
StreamMessage() => StreamMessageRecipientHeader(message: message, showStream: narrow is AllMessagesNarrow),
DmMessage() => DmRecipientHeader(message: message),
};
}
Expand All @@ -450,18 +453,20 @@ class MessageItem extends StatelessWidget {
const MessageItem({
super.key,
required this.item,
required this.header,
this.trailingWhitespace,
});

final MessageListMessageItem item;
final Widget header;
final double? trailingWhitespace;

@override
Widget build(BuildContext context) {
final message = item.message;
return StickyHeaderItem(
allowOverflow: !item.isLastInBlock,
header: RecipientHeader(message: message),
header: header,
child: _UnreadMarker(
isRead: message.flags.contains(MessageFlag.read),
child: ColoredBox(
Expand Down Expand Up @@ -514,60 +519,82 @@ class _UnreadMarker extends StatelessWidget {
}
}

class StreamTopicRecipientHeader extends StatelessWidget {
const StreamTopicRecipientHeader({super.key, required this.message});
class StreamMessageRecipientHeader extends StatelessWidget {
const StreamMessageRecipientHeader({super.key, required this.message, required this.showStream});

final StreamMessage message;
final bool showStream;

@override
Widget build(BuildContext context) {
final store = PerAccountStoreWidget.of(context);

final stream = store.streams[message.streamId];
final streamName = stream?.name ?? message.displayRecipient; // TODO(log) if missing
final topic = message.subject;

final subscription = store.subscriptions[message.streamId];
final streamColor = Color(subscription?.color ?? 0x00c2c2c2);
final contrastingColor =
ThemeData.estimateBrightnessForColor(streamColor) == Brightness.dark
? Colors.white
: Colors.black;
final swatch = swatchForStream(store, message.streamId);

final textStyle = const TextStyle(
fontFamily: 'Source Sans 3',
fontSize: 16,
height: (18 / 16),
letterSpacing: 0.02 * 16,
color: Colors.black,
).merge(weightVariableTextStyle(context, wght: 600, wghtIfPlatformRequestsBold: 900));

final streamName = (showStream)
? GestureDetector(
onTap: () => Navigator.push(context,
MessageListPage.buildRoute(context: context,
narrow: StreamNarrow(message.streamId))),
child: Row(children: [
const SizedBox(width: 16),
// A null [Icon.icon] makes a blank space.
Icon(size: 18, color: swatch.iconOnBarBackground,
(stream != null) ? iconDataForStream(stream) : null),
const SizedBox(width: 5),
Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(
// TODO: figma specs has this in size:17 and height: 20/17
// but instead will match the topic name
style: textStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
stream?.name ?? message.displayRecipient, // TODO(log) if missing
)),
const SizedBox(width: 1),
Icon(size: 16, color: const HSLColor.fromAHSL(0.6, 0, 0, 0).toColor(),
ZulipIcons.chevron_right),
const SizedBox(width: 1),
]))
: const SizedBox(width: 16);

return GestureDetector(
onTap: () => Navigator.push(context,
MessageListPage.buildRoute(context: context,
narrow: TopicNarrow.ofMessage(message))),
child: ColoredBox(
color: _kStreamMessageBorderColor,
child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [
// TODO(#282): 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.
RecipientHeaderDate(message: message),
])));
color: swatch.barBackground,
child: Row(
textBaseline: TextBaseline.alphabetic,
crossAxisAlignment: CrossAxisAlignment.baseline,
children: [
// TODO(#282): Long stream name will break layout; find a fix.
streamName,
Expanded(
// TODO: Give a way to see the whole topic (maybe a
// long-press interaction?)
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: Text(
style: textStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
message.subject))),
RecipientHeaderDate(message: message),
const SizedBox(width: 16),
])));
}
}

final _kStreamMessageBorderColor = const HSLColor.fromAHSL(1, 0, 0, 0.88).toColor();

class DmRecipientHeader extends StatelessWidget {
const DmRecipientHeader({super.key, required this.message});

Expand Down Expand Up @@ -614,24 +641,27 @@ class RecipientHeaderDate extends StatelessWidget {

final Message message;

String _formatTimestamp() {
final dateTime = DateTime.fromMillisecondsSinceEpoch(
message.timestamp * Duration.millisecondsPerSecond);
// TODO(#278)
return DateFormat('MMM dd', 'en_US').format(dateTime);
}

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
child: Text(
style: _kRecipientHeaderDateStyle,
_kRecipientHeaderDateFormat.format(
DateTime.fromMillisecondsSinceEpoch(message.timestamp * 1000))));
return Text(_formatTimestamp(),
maxLines: 1,
style: TextStyle(
fontFamily: 'Source Sans 3',
fontSize: 16,
height: (19 / 16),
fontFeatures: const [FontFeature.enable('c2sc'), FontFeature.enable('smcp')], // small caps
color: const HSLColor.fromAHSL(0.4, 0, 0, 0).toColor(),
).merge(weightVariableTextStyle(context)));
}
}

final _kRecipientHeaderDateStyle = TextStyle(
fontWeight: FontWeight.w600,
color: const HSLColor.fromAHSL(0.75, 0, 0, 0.15).toColor(),
);

final _kRecipientHeaderDateFormat = DateFormat('y-MM-dd', 'en_US'); // TODO(#278)

/// A widget with the distinctive chevron-tailed shape in Zulip recipient headers.
class RecipientHeaderChevronContainer extends StatelessWidget {
const RecipientHeaderChevronContainer(
Expand Down

0 comments on commit 5df0eac

Please sign in to comment.