diff --git a/lib/widgets/message_list.dart b/lib/widgets/message_list.dart index 37238e0d08..1a9560b903 100644 --- a/lib/widgets/message_list.dart +++ b/lib/widgets/message_list.dart @@ -1,4 +1,5 @@ import 'dart:math'; +import 'dart:ui'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -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. @@ -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)), ]); @@ -327,11 +328,13 @@ class _MessageListState extends State 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); @@ -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), }; } @@ -450,10 +453,12 @@ 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 @@ -461,7 +466,7 @@ class MessageItem extends StatelessWidget { 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( @@ -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}); @@ -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(