Skip to content

Tweak dark theme #1213

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 5 commits into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/widgets/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,7 @@ class WebsitePreview extends StatelessWidget {
// TODO(#488) use different color for non-message contexts
// TODO(#647) use different color for highlighted messages
// TODO(#681) use different color for DM messages
color: MessageListTheme.of(context).streamMessageBgDefault,
color: MessageListTheme.of(context).bgMessageRegular,
child: ClipRect(
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: 80),
Expand Down
83 changes: 28 additions & 55 deletions lib/widgets/message_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import '../model/typing_status.dart';
import 'action_sheet.dart';
import 'actions.dart';
import 'app_bar.dart';
import 'color.dart';
import 'compose_box.dart';
import 'content.dart';
import 'emoji_reaction.dart';
Expand All @@ -28,14 +29,10 @@ import 'theme.dart';
/// Message-list styles that differ between light and dark themes.
class MessageListTheme extends ThemeExtension<MessageListTheme> {
static final light = MessageListTheme._(
dateSeparator: Colors.black,
dateSeparatorText: const HSLColor.fromAHSL(0.75, 0, 0, 0.15).toColor(),
bgMessageRegular: const HSLColor.fromAHSL(1, 0, 0, 1).toColor(),
dmRecipientHeaderBg: const HSLColor.fromAHSL(1, 46, 0.35, 0.93).toColor(),
messageTimestamp: const HSLColor.fromAHSL(0.8, 0, 0, 0.2).toColor(),
recipientHeaderText: const HSLColor.fromAHSL(1, 0, 0, 0.15).toColor(),
labelTime: const HSLColor.fromAHSL(0.49, 0, 0, 0).toColor(),
senderBotIcon: const HSLColor.fromAHSL(1, 180, 0.08, 0.65).toColor(),
senderName: const HSLColor.fromAHSL(1, 0, 0, 0.2).toColor(),
streamMessageBgDefault: Colors.white,
streamRecipientHeaderChevronRight: Colors.black.withValues(alpha: 0.3),

// From the Figma mockup at:
Expand All @@ -53,14 +50,10 @@ class MessageListTheme extends ThemeExtension<MessageListTheme> {
);

static final dark = MessageListTheme._(
dateSeparator: Colors.white,
dateSeparatorText: const HSLColor.fromAHSL(0.75, 0, 0, 1).toColor(),
bgMessageRegular: const HSLColor.fromAHSL(1, 0, 0, 0.11).toColor(),
dmRecipientHeaderBg: const HSLColor.fromAHSL(1, 46, 0.15, 0.2).toColor(),
messageTimestamp: const HSLColor.fromAHSL(0.8, 0, 0, 0.85).toColor(),
recipientHeaderText: const HSLColor.fromAHSL(0.8, 0, 0, 1).toColor(),
labelTime: const HSLColor.fromAHSL(0.5, 0, 0, 1).toColor(),
senderBotIcon: const HSLColor.fromAHSL(1, 180, 0.05, 0.5).toColor(),
senderName: const HSLColor.fromAHSL(0.85, 0, 0, 1).toColor(),
streamMessageBgDefault: const HSLColor.fromAHSL(1, 0, 0, 0.15).toColor(),
streamRecipientHeaderChevronRight: Colors.white.withValues(alpha: 0.3),

// 0.75 opacity from here:
Expand All @@ -77,14 +70,10 @@ class MessageListTheme extends ThemeExtension<MessageListTheme> {
);

MessageListTheme._({
required this.dateSeparator,
required this.dateSeparatorText,
required this.bgMessageRegular,
required this.dmRecipientHeaderBg,
required this.messageTimestamp,
required this.recipientHeaderText,
required this.labelTime,
required this.senderBotIcon,
required this.senderName,
required this.streamMessageBgDefault,
required this.streamRecipientHeaderChevronRight,
required this.unreadMarker,
required this.unreadMarkerGap,
Expand All @@ -101,43 +90,31 @@ class MessageListTheme extends ThemeExtension<MessageListTheme> {
return extension!;
}

final Color dateSeparator;
final Color dateSeparatorText;
final Color bgMessageRegular;
final Color dmRecipientHeaderBg;
final Color messageTimestamp;
final Color recipientHeaderText;
final Color labelTime;
final Color senderBotIcon;
final Color senderName;
final Color streamMessageBgDefault;
final Color streamRecipientHeaderChevronRight;
final Color unreadMarker;
final Color unreadMarkerGap;
final Color unsubscribedStreamRecipientHeaderBg;

@override
MessageListTheme copyWith({
Color? dateSeparator,
Color? dateSeparatorText,
Color? bgMessageRegular,
Color? dmRecipientHeaderBg,
Color? messageTimestamp,
Color? recipientHeaderText,
Color? labelTime,
Color? senderBotIcon,
Color? senderName,
Color? streamMessageBgDefault,
Color? streamRecipientHeaderChevronRight,
Color? unreadMarker,
Color? unreadMarkerGap,
Color? unsubscribedStreamRecipientHeaderBg,
}) {
return MessageListTheme._(
dateSeparator: dateSeparator ?? this.dateSeparator,
dateSeparatorText: dateSeparatorText ?? this.dateSeparatorText,
bgMessageRegular: bgMessageRegular ?? this.bgMessageRegular,
dmRecipientHeaderBg: dmRecipientHeaderBg ?? this.dmRecipientHeaderBg,
messageTimestamp: messageTimestamp ?? this.messageTimestamp,
recipientHeaderText: recipientHeaderText ?? this.recipientHeaderText,
labelTime: labelTime ?? this.labelTime,
senderBotIcon: senderBotIcon ?? this.senderBotIcon,
senderName: senderName ?? this.senderName,
streamMessageBgDefault: streamMessageBgDefault ?? this.streamMessageBgDefault,
streamRecipientHeaderChevronRight: streamRecipientHeaderChevronRight ?? this.streamRecipientHeaderChevronRight,
unreadMarker: unreadMarker ?? this.unreadMarker,
unreadMarkerGap: unreadMarkerGap ?? this.unreadMarkerGap,
Expand All @@ -151,14 +128,10 @@ class MessageListTheme extends ThemeExtension<MessageListTheme> {
return this;
}
return MessageListTheme._(
dateSeparator: Color.lerp(dateSeparator, other.dateSeparator, t)!,
dateSeparatorText: Color.lerp(dateSeparatorText, other.dateSeparatorText, t)!,
bgMessageRegular: Color.lerp(bgMessageRegular, other.bgMessageRegular, t)!,
dmRecipientHeaderBg: Color.lerp(dmRecipientHeaderBg, other.dmRecipientHeaderBg, t)!,
messageTimestamp: Color.lerp(messageTimestamp, other.messageTimestamp, t)!,
recipientHeaderText: Color.lerp(recipientHeaderText, other.recipientHeaderText, t)!,
labelTime: Color.lerp(labelTime, other.labelTime, t)!,
senderBotIcon: Color.lerp(senderBotIcon, other.senderBotIcon, t)!,
senderName: Color.lerp(senderName, other.senderName, t)!,
streamMessageBgDefault: Color.lerp(streamMessageBgDefault, other.streamMessageBgDefault, t)!,
streamRecipientHeaderChevronRight: Color.lerp(streamRecipientHeaderChevronRight, other.streamRecipientHeaderChevronRight, t)!,
unreadMarker: Color.lerp(unreadMarker, other.unreadMarker, t)!,
unreadMarkerGap: Color.lerp(unreadMarkerGap, other.unreadMarkerGap, t)!,
Expand Down Expand Up @@ -373,8 +346,7 @@ class MessageListAppBarTitle extends StatelessWidget {
Padding(
padding: const EdgeInsetsDirectional.only(start: 4),
child: Icon(icon,
// TODO(design) copies the recipient header in web; is there a better color?
color: designVariables.colorMessageHeaderIconInteractive, size: 14)),
color: designVariables.title.withFadedAlpha(0.5), size: 14)),
]);
}

Expand Down Expand Up @@ -930,11 +902,12 @@ class DateSeparator extends StatelessWidget {
const textBottomPadding = 2.0;

final messageListTheme = MessageListTheme.of(context);
final designVariables = DesignVariables.of(context);

final line = BorderSide(width: 0, color: messageListTheme.dateSeparator);
final line = BorderSide(width: 0, color: designVariables.foreground);

// TODO(#681) use different color for DM messages
return ColoredBox(color: messageListTheme.streamMessageBgDefault,
return ColoredBox(color: messageListTheme.bgMessageRegular,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 2),
child: Row(children: [
Expand Down Expand Up @@ -981,7 +954,7 @@ class MessageItem extends StatelessWidget {
child: _UnreadMarker(
isRead: message.flags.contains(MessageFlag.read),
child: ColoredBox(
color: messageListTheme.streamMessageBgDefault,
color: messageListTheme.bgMessageRegular,
child: Column(children: [
MessageWithPossibleSender(item: item),
if (trailingWhitespace != null && item.isLastInBlock) SizedBox(height: trailingWhitespace!),
Expand Down Expand Up @@ -1069,7 +1042,7 @@ class StreamMessageRecipientHeader extends StatelessWidget {
iconColor = swatch.iconOnBarBackground;
} else {
backgroundColor = messageListTheme.unsubscribedStreamRecipientHeaderBg;
iconColor = messageListTheme.recipientHeaderText;
iconColor = designVariables.title;
}

final Widget streamWidget;
Expand Down Expand Up @@ -1125,8 +1098,7 @@ class StreamMessageRecipientHeader extends StatelessWidget {
overflow: TextOverflow.ellipsis,
style: recipientHeaderTextStyle(context))),
const SizedBox(width: 4),
// TODO(design) copies the recipient header in web; is there a better color?
Icon(size: 14, color: designVariables.colorMessageHeaderIconInteractive,
Icon(size: 14, color: designVariables.title.withFadedAlpha(0.5),
// A null [Icon.icon] makes a blank space.
iconDataForTopicVisibilityPolicy(
store.topicVisibilityPolicy(message.streamId, topic))),
Expand Down Expand Up @@ -1186,6 +1158,7 @@ class DmRecipientHeader extends StatelessWidget {
}

final messageListTheme = MessageListTheme.of(context);
final designVariables = DesignVariables.of(context);

return GestureDetector(
// When already in a DM narrow, disable tap interaction that would just
Expand All @@ -1206,7 +1179,7 @@ class DmRecipientHeader extends StatelessWidget {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: Icon(
color: messageListTheme.recipientHeaderText,
color: designVariables.title,
size: 16,
ZulipIcons.user)),
Expanded(
Expand All @@ -1220,7 +1193,7 @@ class DmRecipientHeader extends StatelessWidget {

TextStyle recipientHeaderTextStyle(BuildContext context) {
return TextStyle(
color: MessageListTheme.of(context).recipientHeaderText,
color: DesignVariables.of(context).title,
fontSize: 16,
letterSpacing: proportionalLetterSpacing(context, 0.02, baseFontSize: 16),
height: (18 / 16),
Expand Down Expand Up @@ -1264,7 +1237,7 @@ class DateText extends StatelessWidget {
final zulipLocalizations = ZulipLocalizations.of(context);
return Text(
style: TextStyle(
color: messageListTheme.dateSeparatorText,
color: messageListTheme.labelTime,
fontSize: fontSize,
height: height,
// This is equivalent to css `all-small-caps`, see:
Expand Down Expand Up @@ -1356,7 +1329,7 @@ class MessageWithPossibleSender extends StatelessWidget {
style: TextStyle(
fontSize: 18,
height: (22 / 18),
color: messageListTheme.senderName,
color: designVariables.title,
).merge(weightVariableTextStyle(context, wght: 600)),
overflow: TextOverflow.ellipsis)),
if (sender?.isBot ?? false) ...[
Expand All @@ -1371,7 +1344,7 @@ class MessageWithPossibleSender extends StatelessWidget {
const SizedBox(width: 4),
Text(time,
style: TextStyle(
color: messageListTheme.messageTimestamp,
color: messageListTheme.labelTime,
fontSize: 16,
height: (18 / 16),
fontFeatures: const [FontFeature.enable('c2sc'), FontFeature.enable('smcp')],
Expand Down
9 changes: 1 addition & 8 deletions lib/widgets/theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
bgSearchInput: const Color(0xffe3e3e3),
textMessage: const Color(0xff262626),
channelColorSwatches: ChannelColorSwatches.light,
colorMessageHeaderIconInteractive: Colors.black.withValues(alpha: 0.2),
contextMenuCancelBg: const Color(0xff797986).withValues(alpha: 0.15),
contextMenuCancelPressedBg: const Color(0xff797986).withValues(alpha: 0.20),
dmHeaderBg: const HSLColor.fromAHSL(1, 46, 0.35, 0.93).toColor(),
Expand Down Expand Up @@ -195,14 +194,13 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
labelMenuButton: const Color(0xffffffff).withValues(alpha: 0.85),
mainBackground: const Color(0xff1d1d1d),
textInput: const Color(0xffffffff).withValues(alpha: 0.9),
title: const Color(0xffffffff),
title: const Color(0xffffffff).withValues(alpha: 0.9),
bgSearchInput: const Color(0xff313131),
textMessage: const Color(0xffffffff).withValues(alpha: 0.8),
channelColorSwatches: ChannelColorSwatches.dark,
contextMenuCancelBg: const Color(0xff797986).withValues(alpha: 0.15), // the same as the light mode in Figma
contextMenuCancelPressedBg: const Color(0xff797986).withValues(alpha: 0.20), // the same as the light mode in Figma
// TODO(design-dark) need proper dark-theme color (this is ad hoc)
colorMessageHeaderIconInteractive: Colors.white.withValues(alpha: 0.2),
dmHeaderBg: const HSLColor.fromAHSL(1, 46, 0.15, 0.2).toColor(),
// TODO(design-dark) need proper dark-theme color (this is ad hoc)
groupDmConversationIcon: Colors.white.withValues(alpha: 0.5),
Expand Down Expand Up @@ -256,7 +254,6 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
required this.bgSearchInput,
required this.textMessage,
required this.channelColorSwatches,
required this.colorMessageHeaderIconInteractive,
required this.contextMenuCancelBg,
required this.contextMenuCancelPressedBg,
required this.dmHeaderBg,
Expand Down Expand Up @@ -318,7 +315,6 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
final ChannelColorSwatches channelColorSwatches;

// Not named variables in Figma; taken from older Figma drafts, or elsewhere.
final Color colorMessageHeaderIconInteractive;
final Color contextMenuCancelBg; // In Figma, but unnamed.
final Color contextMenuCancelPressedBg; // In Figma, but unnamed.
final Color dmHeaderBg;
Expand Down Expand Up @@ -367,7 +363,6 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
Color? bgSearchInput,
Color? textMessage,
ChannelColorSwatches? channelColorSwatches,
Color? colorMessageHeaderIconInteractive,
Color? contextMenuCancelBg,
Color? contextMenuCancelPressedBg,
Color? dmHeaderBg,
Expand Down Expand Up @@ -415,7 +410,6 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
bgSearchInput: bgSearchInput ?? this.bgSearchInput,
textMessage: textMessage ?? this.textMessage,
channelColorSwatches: channelColorSwatches ?? this.channelColorSwatches,
colorMessageHeaderIconInteractive: colorMessageHeaderIconInteractive ?? this.colorMessageHeaderIconInteractive,
contextMenuCancelBg: contextMenuCancelBg ?? this.contextMenuCancelBg,
contextMenuCancelPressedBg: contextMenuCancelPressedBg ?? this.contextMenuCancelPressedBg,
dmHeaderBg: dmHeaderBg ?? this.dmHeaderBg,
Expand Down Expand Up @@ -470,7 +464,6 @@ class DesignVariables extends ThemeExtension<DesignVariables> {
bgSearchInput: Color.lerp(bgSearchInput, other.bgSearchInput, t)!,
textMessage: Color.lerp(textMessage, other.textMessage, t)!,
channelColorSwatches: ChannelColorSwatches.lerp(channelColorSwatches, other.channelColorSwatches, t),
colorMessageHeaderIconInteractive: Color.lerp(colorMessageHeaderIconInteractive, other.colorMessageHeaderIconInteractive, t)!,
contextMenuCancelBg: Color.lerp(contextMenuCancelBg, other.contextMenuCancelBg, t)!,
contextMenuCancelPressedBg: Color.lerp(contextMenuCancelPressedBg, other.contextMenuCancelPressedBg, t)!,
dmHeaderBg: Color.lerp(dmHeaderBg, other.dmHeaderBg, t)!,
Expand Down
6 changes: 3 additions & 3 deletions test/widgets/message_list_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -268,17 +268,17 @@ void main() {
return widget.color;
}

check(backgroundColor()).isSameColorAs(MessageListTheme.light.streamMessageBgDefault);
check(backgroundColor()).isSameColorAs(MessageListTheme.light.bgMessageRegular);

tester.platformDispatcher.platformBrightnessTestValue = Brightness.dark;
await tester.pump();

await tester.pump(kThemeAnimationDuration * 0.4);
final expectedLerped = MessageListTheme.light.lerp(MessageListTheme.dark, 0.4);
check(backgroundColor()).isSameColorAs(expectedLerped.streamMessageBgDefault);
check(backgroundColor()).isSameColorAs(expectedLerped.bgMessageRegular);

await tester.pump(kThemeAnimationDuration * 0.6);
check(backgroundColor()).isSameColorAs(MessageListTheme.dark.streamMessageBgDefault);
check(backgroundColor()).isSameColorAs(MessageListTheme.dark.bgMessageRegular);
});

group('fetch initial batch of messages', () {
Expand Down