fix(mobile): restore channel icons and align chat colors with desktop (#8478)

## Summary
- Restores `#` and lock icons for public and private channels on Home,
with matching loading placeholders.
- Updates the dark-mode navigation colors to the desktop design-system
tokens (read and unread text, section headings).
- Replaces the chat color picker with the seven desktop accents (Gray,
Chartreuse, Flame, Magenta, Iris, Cobalt, Jade). Saved legacy colors map
to the closest one.
- Uses white text in your own bubbles, and black where white falls below
4.5:1 (Chartreuse, dark-mode Gray). Links (underlined), quotes, tables
and code use the same text color.
- Insets code blocks in bubbles evenly, with corners concentric to the
bubble and even inner padding.
- The swipe-to-reply indicator uses the chosen chat color.

## Testing
- `flutter analyze` and `dart format` are clean. The full mobile suite
passes (3,705 tests).
- Installed and checked on a Pixel 10 and an iPhone Air against a real
community.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
Co-authored-by: Queue <874bf2d815c2c4b459b90438c669c57f2702f3a76bb6e16fa97184259aac3e9e@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
klopez4212
2026-10-20 13:48:12 -04:00
committed by GitHub
parent 0b53fc699d
commit 5fefec604f
26 changed files with 869 additions and 212 deletions

View File

@@ -27,8 +27,8 @@ class _ChannelTile extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final contentColor = isMuted
? navigationSecondaryForeground(context)
: navigationPrimaryForeground(context);
? navigationMetadataForeground(context)
: channelNavigationForeground(context);
return InkWell(
borderRadius: BorderRadius.circular(Radii.md),
@@ -55,6 +55,22 @@ class _ChannelTile extends ConsumerWidget {
),
),
const SizedBox(width: _kChannelLabelGap),
] else ...[
SizedBox(
width: _kChannelLeadingWidth,
child: Align(
alignment: Alignment.centerLeft,
child: Icon(
channel.visibility == 'private'
? BuzzIcons.lock
: BuzzIcons.hash,
key: ValueKey('channel-icon-${channel.id}'),
size: _kChannelIconSize,
color: contentColor,
),
),
),
const SizedBox(width: _kChannelLabelGap),
],
Expanded(
child: Column(
@@ -73,7 +89,12 @@ class _ChannelTile extends ConsumerWidget {
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: contentListTitleTextStyle.copyWith(
color: contentColor,
color: isMuted
? contentColor
: channelNavigationForeground(
context,
unread: isUnread,
),
fontWeight: isUnread ? FontWeight.w700 : FontWeight.w400,
),
),
@@ -92,7 +113,7 @@ class _ChannelTile extends ConsumerWidget {
child: Icon(
BuzzIcons.bellOff,
size: 12,
color: context.colors.onSurface.withValues(alpha: 0.4),
color: navigationMetadataForeground(context),
),
),
),

View File

@@ -115,9 +115,7 @@ class _CustomSectionHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
final sectionColor = context.colors.brightness == Brightness.dark
? const Color(0xFF595959)
: const Color(0xFF959595);
final sectionColor = navigationMetadataForeground(context);
return GestureDetector(
onTap: onToggle,
behavior: HitTestBehavior.opaque,
@@ -144,7 +142,7 @@ class _CustomSectionHeader extends StatelessWidget {
icon: Icon(
BuzzIcons.ellipsisVertical,
size: _kChannelIconSize,
color: sectionColor,
color: navigationSecondaryForeground(context),
),
onPressed: () async {
final value = await showAnchoredPopover<String>(
@@ -468,9 +466,7 @@ class _SectionHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
final sectionColor = context.colors.brightness == Brightness.dark
? const Color(0xFF595959)
: const Color(0xFF959595);
final sectionColor = navigationMetadataForeground(context);
return GestureDetector(
onTap: onToggle,
@@ -499,7 +495,7 @@ class _SectionHeader extends StatelessWidget {
icon: Icon(
BuzzIcons.ellipsisVertical,
size: _kChannelIconSize,
color: sectionColor,
color: navigationSecondaryForeground(context),
),
onPressed: () async {
final value = await showAnchoredPopover<String>(
@@ -551,7 +547,10 @@ class _SectionLabel extends StatelessWidget {
),
),
const SizedBox(width: Grid.quarter),
_SectionChevron(expanded: expanded, color: color),
_SectionChevron(
expanded: expanded,
color: navigationSecondaryForeground(context),
),
],
);
}

View File

@@ -83,6 +83,17 @@ class _ChannelSkeletonSection extends StatelessWidget {
),
child: Row(
children: [
const SizedBox(
width: _kChannelLeadingWidth,
child: Align(
alignment: Alignment.centerLeft,
child: SkeletonBar(
width: _kChannelIconSize,
height: _kChannelIconSize,
),
),
),
const SizedBox(width: _kChannelLabelGap),
SkeletonBar(
key: Key('channels-skeleton-row-label-$index'),
width: widths[index],

View File

@@ -200,12 +200,9 @@ class MessageContent extends HookConsumerWidget {
final markdownTheme = GptMarkdownTheme.of(context);
final chatColors = ChatColors.of(context);
final ownTextColor = chatColors.foreground;
// Desktop uses blue links on neutral bubbles and the bubble ink on colors.
final linkColor = ownMessage && ownTextColor == Colors.white
? ownTextColor
: context.colors.brightness == Brightness.dark
? const Color(0xFF83C4FF)
: const Color(0xFF0B5FA8);
// Own-bubble links share the bubble ink; markdown marks them by underline.
final linkColor = ownMessage ? ownTextColor : context.colors.tertiary;
final bubbleInk = ownMessage ? ownTextColor : context.colors.onSurface;
final baseTextStyle =
(baseStyle ??
context.textTheme.bodyMedium?.copyWith(
@@ -361,12 +358,27 @@ class MessageContent extends HookConsumerWidget {
child: GptMarkdown(
text,
style: style,
styleSheet: bubble
? GptMarkdownStyleSheet(
blockQuote: BlockQuoteStyle(barColor: bubbleInk),
hr: HrStyle(color: bubbleInk.withValues(alpha: 0.3)),
table: TableStyle(
borderColor: bubbleInk.withValues(alpha: 0.3),
headerBackground: Colors.transparent,
headerTextStyle: TextStyle(color: bubbleInk),
),
)
: null,
followLinkColor: false,
// normalizeBareLinks() already turns bare URLs into Markdown links;
// gpt_markdown 1.2.0 autolinks by default, so both would run.
autolink: false,
codeBuilder: (context, name, code, closed) =>
_MessageCodeBlock(name: name, code: code),
codeBuilder: (context, name, code, closed) => _MessageCodeBlock(
name: name,
code: code,
bubble: bubble,
ink: bubble && ownMessage ? ownTextColor : null,
),
linkBuilder: (context, linkText, url, linkStyle) => _buildLink(
context,
ref,
@@ -426,7 +438,7 @@ class MessageContent extends HookConsumerWidget {
child: LocalSendBubbleSurface(
color: ownMessage
? chatColors.outgoing
: context.colors.secondaryContainer,
: receivedMessageSurface(context.colors.brightness),
shape: const _MessageBubbleShape(),
child: Padding(
padding: EdgeInsets.fromLTRB(
@@ -445,6 +457,21 @@ class MessageContent extends HookConsumerWidget {
),
child: GptMarkdownTheme(
gptThemeData: markdownTheme.copyWith(
linkColor: linkColor,
// A neutral chip would put white ink on a light
// fill; tint the accent instead.
inlineCode: ownMessage
? markdownTheme.inlineCode.copyWith(
color: ownTextColor,
backgroundColor: chatCodeTint(
ownTextColor,
0.18,
),
borderColor: ownTextColor.withValues(
alpha: 0.3,
),
)
: null,
h1: markdownTheme.h1?.copyWith(
color: ownMessage
? ownTextColor
@@ -869,7 +896,18 @@ class _MessageCodeBlock extends HookWidget {
final String name;
final String code;
const _MessageCodeBlock({required this.name, required this.code});
/// Whether the block sits inside a message bubble.
final bool bubble;
/// Text color on an accent bubble; null keeps the neutral highlighted card.
final Color? ink;
const _MessageCodeBlock({
required this.name,
required this.code,
this.bubble = false,
this.ink,
});
@override
Widget build(BuildContext context) {
@@ -884,83 +922,101 @@ class _MessageCodeBlock extends HookWidget {
});
}
final ink = this.ink;
// A fence's closing newline would otherwise render as a blank last line.
final shownCode = code.trimRight();
final codeBaseStyle = TextStyle(
fontFamily: CodeStyle.fontFamily,
fontSize: CodeStyle.fontSize,
height: CodeStyle.lineHeight,
color: context.colors.onSurface,
color: ink ?? context.colors.onSurface,
);
final isDark = context.theme.brightness == Brightness.dark;
final codeTheme = isDark ? highlightDarkTheme : highlightLightTheme;
// Syntax colors are tuned for neutral cards and lose contrast on accents.
final codeSpans = useMemoized(
() => highlightCode(code, name, codeTheme, codeBaseStyle),
[code, name, isDark],
() => ink == null
? highlightCode(shownCode, name, codeTheme, codeBaseStyle)
: [TextSpan(text: shownCode)],
[code, name, isDark, ink],
);
return Container(
margin: const EdgeInsets.only(top: Grid.half),
// In a bubble, the vertical margin brings the bubble's 8px vertical
// padding up to its 16px side padding, so the block is inset evenly and
// its corners stay concentric with the bubble's.
margin: bubble
? const EdgeInsets.symmetric(vertical: Grid.xs - Grid.xxs)
: const EdgeInsets.only(top: Grid.half),
decoration: BoxDecoration(
color: CodeStyle.background(context.colors),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: CodeStyle.border(context.colors)),
color: ink == null
? CodeStyle.background(context.colors)
: chatCodeTint(ink, 0.14),
borderRadius: BorderRadius.circular(
bubble ? Radii.container - Grid.xs : 12,
),
border: Border.all(
color:
ink?.withValues(alpha: 0.24) ?? CodeStyle.border(context.colors),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
child: Stack(
children: [
if (name.isNotEmpty)
Padding(
padding: const EdgeInsets.only(
left: Grid.twelve,
top: Grid.half + Grid.quarter,
),
child: Text(
name,
style: context.textTheme.labelSmall?.copyWith(
color: context.colors.onSurfaceVariant,
),
),
// Even 12px insets; the right side also reserves the copy button.
Padding(
padding: const EdgeInsets.fromLTRB(
Grid.twelve,
Grid.twelve,
Grid.twelve + 28,
Grid.twelve,
),
Stack(
children: [
Padding(
padding: EdgeInsets.fromLTRB(
Grid.twelve,
name.isEmpty ? Grid.half + Grid.quarter : Grid.quarter,
44,
Grid.half + Grid.quarter,
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
if (name.isNotEmpty)
Padding(
padding: const EdgeInsets.only(bottom: Grid.half),
child: Text(
name,
style: context.textTheme.labelSmall?.copyWith(
color: ink ?? context.colors.onSurfaceVariant,
),
),
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: RichText(
softWrap: false,
text: TextSpan(style: codeBaseStyle, children: codeSpans),
),
),
),
Positioned(
top: 0,
right: Grid.quarter,
child: SizedBox(
width: 28,
height: 28,
child: IconButton(
onPressed: handleCopy,
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
style: IconButton.styleFrom(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
icon: Icon(
isCopied.value ? BuzzIcons.check : BuzzIcons.copy,
size: 14,
color: isCopied.value
],
),
),
Positioned(
top: Grid.half + Grid.quarter,
right: Grid.half + Grid.quarter,
child: SizedBox(
width: 28,
height: 28,
child: IconButton(
onPressed: handleCopy,
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
style: IconButton.styleFrom(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
icon: Icon(
isCopied.value ? BuzzIcons.check : BuzzIcons.copy,
size: 14,
color:
ink ??
(isCopied.value
? context.colors.primary
: context.colors.onSurfaceVariant,
),
),
: context.colors.onSurfaceVariant),
),
),
],
),
),
],
),

View File

@@ -139,11 +139,10 @@ class MessageSwipeReply extends HookConsumerWidget {
child: child,
builder: (context, child) {
final progress = (offset.value / 72).clamp(0.0, 1.0);
final accent = context.appColors.accent;
final filledForeground =
ThemeData.estimateBrightnessForColor(accent) == Brightness.dark
? Colors.white
: Colors.black;
// Follows the chosen message color, like the user's own bubbles.
final chatColors = ChatColors.of(context);
final accent = chatColors.outgoing;
final filledForeground = chatColors.foreground;
return Stack(
key: layoutKey,
clipBehavior: Clip.none,

View File

@@ -79,6 +79,12 @@ const accentColors = [
dark: Color(0xFF818CF8),
wireValue: '#6366f1',
),
AccentColor(
name: 'Chartreuse',
light: Color(0xFFD7D72E),
dark: Color(0xFFD7D72E),
wireValue: '#d7d72e',
),
];
Color accentColorForScheme(ColorScheme scheme, int accentIndex) {

View File

@@ -100,3 +100,17 @@ LinearGradient? buzzTopSectionGradient(
],
);
}
/// Desktop navigation ink distinguishes read destinations from unread labels.
Color channelNavigationForeground(BuildContext context, {bool unread = false}) {
if (unread || Theme.of(context).brightness != Brightness.dark) {
return navigationPrimaryForeground(context);
}
return Theme.of(context).colorScheme.onSurfaceVariant;
}
/// Metadata ink used by Home section headings and muted destinations.
Color navigationMetadataForeground(BuildContext context) =>
Theme.of(context).brightness == Brightness.dark
? const Color(0xFFA4A4A4)
: const Color(0xFF5F5F5F);

View File

@@ -2,59 +2,82 @@ import 'package:flutter/material.dart';
import 'accent_colors.dart';
/// Desktop message palette, in picker order. Wire values remain compatible
/// with existing community preferences; fills match buzz-app bubble tokens.
/// Desktop accent families in picker order. Fills are accent-9 from buzz-app's
/// design-system tokens (4ad00a8a).
///
/// Wire values are desktop's v1 `community-theme` accents for the same color
/// family, so a synced preference means the same color on every client. The
/// cross-client mapping lives in `test-fixtures/community-theme-accents.json`.
const chatColorChoices = [
AccentColor(
name: 'Neutral',
light: Color(0xFFE9E9EA),
dark: Color(0xFF2E2E2E),
name: 'Gray',
light: Color(0xFF525252),
dark: Color(0xFFC7C7C7),
wireValue: 'neutral',
),
AccentColor(
name: 'Blue',
light: Color(0xFF0077DB),
dark: Color(0xFF0077DB),
wireValue: '#3b82f6',
name: 'Chartreuse',
light: Color(0xFFD7D72E),
dark: Color(0xFFD7D72E),
wireValue: '#d7d72e',
),
AccentColor(
name: 'Green',
light: Color(0xFF008934),
dark: Color(0xFF008934),
wireValue: '#22c55e',
),
AccentColor(
name: 'Orange',
light: Color(0xFFC95100),
dark: Color(0xFFC95100),
name: 'Flame',
light: Color(0xFFD93A0C),
dark: Color(0xFFD93A0C),
wireValue: '#f97316',
),
AccentColor(
name: 'Pink',
light: Color(0xFFD33785),
dark: Color(0xFFD33785),
name: 'Magenta',
light: Color(0xFFCC1F7A),
dark: Color(0xFFCC1F7A),
wireValue: '#ec4899',
),
AccentColor(
name: 'Lilac',
light: Color(0xFFA94AD8),
dark: Color(0xFFA94AD8),
name: 'Iris',
light: Color(0xFF6C4EF0),
dark: Color(0xFF6C4EF0),
wireValue: '#c0a2f1',
),
AccentColor(
name: 'Cobalt',
light: Color(0xFF2B4FE0),
dark: Color(0xFF2B4FE0),
wireValue: '#3b82f6',
),
AccentColor(
name: 'Jade',
light: Color(0xFF0B8467),
dark: Color(0xFF0B8467),
wireValue: '#22c55e',
),
];
/// Resolves old community colors using desktop's retired-color aliases.
/// Resolves a v1 accent: each choice's own value, desktop's remaining legacy
/// values via their closest family, and family names written by earlier
/// preview builds. Anything else falls back to Chartreuse.
int chatColorIndex(String wireValue) {
final normalized = switch (wireValue) {
'#06b6d4' || '#6366f1' => '#3b82f6',
'#06b6d4' => '#22c55e',
'#6366f1' => '#3b82f6',
'#ef4444' => '#ec4899',
'#a855f7' => '#c0a2f1',
_ => wireValue,
};
final index = chatColorChoices.indexWhere(
(color) => color.wireValue == normalized,
(color) =>
color.wireValue == normalized || color.name.toLowerCase() == normalized,
);
return index < 0 ? 0 : index;
return index < 0 ? 1 : index;
}
/// Desktop-compatible value for a family name written by earlier preview
/// builds, so those preferences sync again once republished.
String? previewFamilyWireValue(String value) {
for (final color in chatColorChoices) {
if (color.name.toLowerCase() == value) return color.wireValue;
}
return null;
}
/// Resolves a picker swatch for the active appearance mode.
@@ -62,3 +85,23 @@ Color chatColorForScheme(ColorScheme scheme, int index) {
final color = chatColorChoices[index];
return scheme.brightness == Brightness.dark ? color.dark : color.light;
}
/// Bubble text is white unless white falls below 4.5:1 on the fill, as on
/// Chartreuse and dark-mode Gray; those light fills take black instead.
Color chatForegroundForScheme(ColorScheme scheme, int index) {
final fill = chatColorForScheme(scheme, index).computeLuminance();
return 1.05 / (fill + .05) >= 4.5 ? Colors.white : Colors.black;
}
/// Translucent code surface behind [ink] on an accent bubble. It shades toward
/// the opposite of the ink, so it can only raise contrast over the fill.
Color chatCodeTint(Color ink, double opacity) =>
(ink == Colors.white ? Colors.black : Colors.white).withValues(
alpha: opacity,
);
/// Received messages use their own surface rather than a control fill.
Color receivedMessageSurface(Brightness brightness) =>
brightness == Brightness.dark
? const Color(0xFF292929)
: const Color(0xFFF3F3F4);

View File

@@ -25,8 +25,7 @@ abstract final class CodeStyle {
static final fontSizeFactor = fontSize / messageBodyTextStyle.fontSize!;
/// Fill behind a code surface.
static Color background(ColorScheme scheme) =>
scheme.surfaceContainerHighest.withValues(alpha: 0.6);
static Color background(ColorScheme scheme) => scheme.surfaceContainerHighest;
/// Outline around a code surface.
static Color border(ColorScheme scheme) =>

View File

@@ -14,7 +14,7 @@ const lightColorScheme = ColorScheme(
onSecondary: Color(0xFFFFFFFF),
secondaryContainer: Color(0xFFF0F0F0),
onSecondaryContainer: Color(0xFF0A0A0A),
tertiary: Color(0xFF0D74CE),
tertiary: Color(0xFF0B5FA8),
onTertiary: Color(0xFFFFFFFF),
tertiaryContainer: Color(0xFFE6F4FE),
onTertiaryContainer: Color(0xFF113264),
@@ -42,37 +42,37 @@ const lightColorScheme = ColorScheme(
const darkColorScheme = ColorScheme(
brightness: Brightness.dark,
primary: Color(0xFFE5E5E5),
onPrimary: Color(0xFF0F0F0F),
primaryContainer: Color(0xFF2A2A2A),
onPrimaryContainer: Color(0xFFF5F5F5),
secondary: Color(0xFFC1C1C1),
onSecondary: Color(0xFF0F0F0F),
secondaryContainer: Color(0xFF2A2A2A),
onSecondaryContainer: Color(0xFFF5F5F5),
tertiary: Color(0xFF70B8FF),
onTertiary: Color(0xFF0F0F0F),
primary: Color(0xFFFFFFFF),
onPrimary: Color(0xFF000000),
primaryContainer: Color(0xFF282828),
onPrimaryContainer: Color(0xFFFFFFFF),
secondary: Color(0xFFC7C7C7),
onSecondary: Color(0xFF000000),
secondaryContainer: Color(0xFF282828),
onSecondaryContainer: Color(0xFFFFFFFF),
tertiary: Color(0xFF83C4FF),
onTertiary: Color(0xFF000000),
tertiaryContainer: Color(0xFF0D2847),
onTertiaryContainer: Color(0xFFC2E6FF),
error: Color(0xFFFF9592),
onError: Color(0xFF191111),
errorContainer: Color(0xFF3B1219),
onErrorContainer: Color(0xFFFFD1D9),
surface: Color(0xFF1C1C1C),
onSurface: Color(0xFFF5F5F5),
onSurfaceVariant: Color(0xFFC1C1C1),
outline: Color(0xFF333333),
outlineVariant: Color(0xFF333333),
inverseSurface: Color(0xFFE5E5E5),
onInverseSurface: Color(0xFF0F0F0F),
surface: Color(0xFF000000),
onSurface: Color(0xFFFFFFFF),
onSurfaceVariant: Color(0xFFC7C7C7),
outline: Color(0xFF1A1A1A),
outlineVariant: Color(0xFF1A1A1A),
inverseSurface: Color(0xFFC7C7C7),
onInverseSurface: Color(0xFF000000),
inversePrimary: Color(0xFF262626),
shadow: Color(0xFF000000),
scrim: Color(0xFF000000),
surfaceContainerLowest: Color(0xFF0F0F0F),
surfaceContainerLow: Color(0xFF161616),
surfaceContainer: Color(0xFF232323),
surfaceContainerHigh: Color(0xFF2A2A2A),
surfaceContainerHighest: Color(0xFF2A2A2A),
surfaceContainerLowest: Color(0xFF000000),
surfaceContainerLow: Color(0xFF101010),
surfaceContainer: Color(0xFF1A1A1A),
surfaceContainerHigh: Color(0xFF282828),
surfaceContainerHighest: Color(0xFF282828),
surfaceTint: Colors.transparent,
);

View File

@@ -4,13 +4,15 @@ import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'accent_colors.dart';
import 'chat_colors.dart';
import 'theme_catalog.dart';
import 'theme_provider.dart' show effectiveTheme, schemeForAppearanceMode;
const communityThemeDTag = 'community-theme';
const defaultCommunityTheme = CommunityThemePreference(
theme: 'buzz',
accent: '#3b82f6',
// Seeded without a picker action, so it must stay readable by older clients.
accent: '#3b82f6', // Cobalt
followSystem: true,
);
@@ -32,13 +34,16 @@ class CommunityThemePreference {
json['theme'] is! String ||
findTheme(json['theme'] as String) == null ||
json['accent'] is! String ||
accentIndexForWireValue(json['accent'] as String) == null ||
(accentIndexForWireValue(json['accent'] as String) == null &&
previewFamilyWireValue(json['accent'] as String) == null) ||
json['followSystem'] is! bool) {
throw const FormatException('Invalid community theme preference');
}
return CommunityThemePreference(
theme: json['theme'] as String,
accent: json['accent'] as String,
accent:
previewFamilyWireValue(json['accent'] as String) ??
json['accent'] as String,
followSystem: json['followSystem'] as bool,
);
}
@@ -157,7 +162,9 @@ class CommunityThemeStorage {
};
return CommunityThemePreference(
theme: resolvedTheme,
accent: legacyAccentWireValue(legacyAccent),
accent: legacyAccent == null
? defaultCommunityTheme.accent
: legacyAccentWireValue(legacyAccent),
followSystem: mode == ThemeMode.system,
);
}

View File

@@ -51,7 +51,7 @@ ThemeData mobileAppearanceTheme(Brightness brightness, String chatColor) {
...theme.extensions.values,
ChatColors(
outgoing: chatColorForScheme(theme.colorScheme, index),
foreground: index == 0 ? theme.colorScheme.onSurface : Colors.white,
foreground: chatForegroundForScheme(theme.colorScheme, index),
),
],
);

View File

@@ -5,7 +5,7 @@ import 'app_colors.dart';
/// Shared neutral background for mobile controls and selected tabs.
Color neutralControlSurface(Brightness brightness) =>
brightness == Brightness.dark
? const Color(0xFF595959)
? const Color(0xFF101010)
: const Color(0xFFF0F0F0);
extension AppThemeExtension on BuildContext {