Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
18 changes: 9 additions & 9 deletions mobile/lib/features/channels/message_content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:gpt_markdown/gpt_markdown.dart';
import 'package:gpt_markdown/custom_widgets/markdown_config.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:open_filex/open_filex.dart';
import 'package:path_provider/path_provider.dart';
Expand Down Expand Up @@ -265,6 +264,9 @@ class MessageContent extends HookConsumerWidget {
finalContent,
style: style,
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),
linkBuilder: (context, linkText, url, linkStyle) => _buildLink(
Expand All @@ -277,7 +279,7 @@ class MessageContent extends HookConsumerWidget {
resolvedChannelTap,
resolvedChannelNames,
),
imageBuilder: (context, imageUrl) =>
imageBuilder: (context, imageUrl, _, _) =>
_buildMedia(context, imageUrl, imetaByUrl[imageUrl]),
textAlign: textAlign,
maxLines: maxLines,
Expand Down Expand Up @@ -699,9 +701,9 @@ class _MessageCodeBlock extends HookWidget {
}

final codeBaseStyle = TextStyle(
fontFamily: 'GeistMono',
fontSize: 13,
height: 1.5,
fontFamily: CodeStyle.fontFamily,
fontSize: CodeStyle.fontSize,
height: CodeStyle.lineHeight,
color: context.colors.onSurface,
);
final isDark = context.theme.brightness == Brightness.dark;
Expand All @@ -713,11 +715,9 @@ class _MessageCodeBlock extends HookWidget {
return Container(
margin: const EdgeInsets.only(top: Grid.half),
decoration: BoxDecoration(
color: context.colors.surfaceContainerHighest.withValues(alpha: 0.6),
color: CodeStyle.background(context.colors),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: context.colors.outline.withValues(alpha: 0.7),
),
border: Border.all(color: CodeStyle.border(context.colors)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
Expand Down
1 change: 0 additions & 1 deletion mobile/lib/shared/custom_emoji/custom_emoji_render.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:gpt_markdown/gpt_markdown.dart';
import 'package:gpt_markdown/custom_widgets/markdown_config.dart';

import '../relay/relay.dart';

Expand Down
23 changes: 22 additions & 1 deletion mobile/lib/shared/theme/app_theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import 'app_colors.dart';
import 'code_style.dart';
import 'color_scheme.dart';
import 'grid.dart';
import 'text_theme.dart';
import 'package:gpt_markdown/gpt_markdown.dart';

/// Border radius constants matching desktop shadcn "New York" style.
/// Desktop uses --radius: 0.625rem (10px) as base:
Expand Down Expand Up @@ -89,7 +91,26 @@ class AppTheme {
colorScheme: scheme,
splashFactory: NoSplash.splashFactory,
scaffoldBackgroundColor: scheme.surface,
extensions: [appColors],
extensions: [
appColors,
// Inline code is rendered by gpt_markdown, which otherwise draws it in
// its own bundled face, at its own size, tinted from `onSurface`.
// Stating the app's code style here gives every Markdown surface the
// face, size and chip colours a fenced code block already uses.
GptMarkdownThemeData(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve the ambient non-code Markdown theme when registering inline-code styling

GptMarkdownThemeData(brightness: ...) is not a partial override: its factory builds a fresh Material theme and fills every heading/divider field. Registering it makes GptMarkdownTheme.of(context) stop deriving those fields from the app theme. On this head, toggling only this extension changes H4 from 24 to 22sp, H5 from 20 to 16sp, H1 weight from 600 to 400, and horizontal rules from scheme.outline to Material gray, in both light and dark. Existing messages and forum content therefore change styling even without inline code; custom community outline colors are also lost.

Preserve the app’s h1–h6 styles and divider color when constructing the extension (or otherwise retain the ambient defaults while overriding inline code). Add a non-code heading/divider regression test so this code-only change does not restyle the rest of Markdown.

brightness: brightness,
inlineCode: InlineCodeStyle(
fontFamily: CodeStyle.fontFamily,
fontSizeFactor: CodeStyle.fontSizeFactor,
color: scheme.onSurface,
backgroundColor: CodeStyle.background(scheme),
borderColor: CodeStyle.border(scheme),
// A chip is small, so it takes the smallest step of the app's
// radius scale rather than the `Radii.card` a block uses.
borderRadius: const Radius.circular(Radii.xs),
),
),
],
fontFamily: 'Inter',
textTheme: textTheme,
appBarTheme: AppBarTheme(
Expand Down
34 changes: 34 additions & 0 deletions mobile/lib/shared/theme/code_style.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';

import 'message_typography.dart';

/// The app's own code style — one place stating the mono face, its size and
/// the colours a code surface is drawn with.
///
/// Two surfaces render code and they have to read as one family: the fenced
/// block in `MessageContent`, and the inline chip `gpt_markdown` paints from
/// the `InlineCodeStyle` declared in `AppTheme`. Both take their values from
/// here, so neither can drift from the other by editing one copy.
abstract final class CodeStyle {
/// The mono face, already used by the composer and by code blocks.
static const fontFamily = 'GeistMono';

/// Code size at message body scale.
static const fontSize = 13.0;

/// Line height of code text.
static const lineHeight = 1.5;

/// [fontSize] stated against the message body size, for APIs that scale code
/// relative to the text around it rather than fixing a point size. Keeping
/// it a ratio means code follows the body if that size ever changes.
static final fontSizeFactor = fontSize / messageBodyTextStyle.fontSize!;

/// Fill behind a code surface.
static Color background(ColorScheme scheme) =>
scheme.surfaceContainerHighest.withValues(alpha: 0.6);

/// Outline around a code surface.
static Color border(ColorScheme scheme) =>
scheme.outline.withValues(alpha: 0.7);
}
1 change: 1 addition & 0 deletions mobile/lib/shared/theme/theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export 'adaptive_theme.dart';
export 'app_colors.dart';
export 'app_theme.dart';
export 'buzz_theme.dart';
export 'code_style.dart';
export 'color_scheme.dart';
export 'community_theme_preference.dart';
export 'community_theme_provider.dart';
Expand Down
4 changes: 2 additions & 2 deletions mobile/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -588,10 +588,10 @@ packages:
dependency: "direct main"
description:
name: gpt_markdown
sha256: "5c565a438e569b3b546023d0d4eccccf87e260a3289a17c4e241c41d5e7545df"
sha256: "42887afaf5c7a71348221048a6c1e34c9836703ee0e6b4865e789253fcaa204a"
url: "https://pub.dev"
source: hosted
version: "1.1.6"
version: "1.2.1"
gtk:
dependency: transitive
description:
Expand Down
2 changes: 1 addition & 1 deletion mobile/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ dependencies:
nostr: ^2.0.0
pointycastle: ^4.0.0
url_launcher: ^6.3.2
gpt_markdown: ^1.1.6
gpt_markdown: ^1.2.1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Handle the new autolinks in both agent-transcript renderers

This bump enables autolink: true in the two unchanged GptMarkdown calls at mobile/lib/features/channels/agent_activity/transcript_item_widget.dart:70 and :141, but neither provides onLinkTap. A bare URL such as https://example.com/report now renders as a blue underlined LinkButton; its press closure only calls the null config.onLinkTap, so tapping does nothing. I reproduced the generated link and absent handler in both MessageItem and ThoughtItem widgets. In 1.1.6 these bare URLs stayed ordinary text.

The PR body explicitly accepts autolinking outside messages, but that needs an actual URL-opening handler. Either wire the handler for both surfaces and cover activation, or set autolink: false there to preserve existing behavior. The current opt-out in MessageContent does not cover these two consumers.

highlight: ^0.7.0
intl: ^0.20.2
uuid: ^4.5.1
Expand Down
44 changes: 44 additions & 0 deletions mobile/test/features/channels/message_content_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2105,6 +2105,50 @@ Photos
expect(_allRichText(tester), isNot(contains('**')));
});

testWidgets('renders inline code in the app code style', (tester) async {
// The message surfaces pass this style in; the widget's own fallback
// is the smaller `bodyMedium`, which would move the expected size.
await tester.pumpWidget(
_testable(
const MessageContent(
content: 'Run `just test` now',
baseStyle: messageBodyTextStyle,
),
),
);

// gpt_markdown tags inline code with a CodeTextSpan carrying both the
// resolved text style and the colours the chip behind it is painted
// with. It renders through BidiRichText, a RichText subclass, so
// find.byType(RichText) would miss it.
final codeSpans = <CodeTextSpan>[];
for (final rich in tester.widgetList<RichText>(
find.byWidgetPredicate((widget) => widget is RichText),
)) {
rich.text.visitChildren((span) {
if (span is CodeTextSpan && span.text == 'just test') {
codeSpans.add(span);
}
return true;
});
}

expect(codeSpans, hasLength(1));
final code = codeSpans.single;
const scheme = lightColorScheme;

// Face, size and ink — the values a fenced code block is drawn with,
// not the package's bundled mono at its own 0.94 of the body size.
expect(code.style?.fontFamily, CodeStyle.fontFamily);
expect(code.style?.fontSize, closeTo(CodeStyle.fontSize, 0.001));
expect(code.style?.color, scheme.onSurface);

// Chip fill and outline come from the app's code surface rather than
// the package's `onSurface` tints.
expect(code.codeStyle.backgroundColor, CodeStyle.background(scheme));
expect(code.codeStyle.borderColor, CodeStyle.border(scheme));
});

testWidgets('renders code block between paragraphs', (tester) async {
await tester.pumpWidget(
_testable(
Expand Down
Loading