Compare commits

..
Author SHA1 Message Date
Thomas SharedInboxandClaude Sonnet 4.6 3d47af177a feat: show URL tooltip on long-press of unsubscribe chip (#294)
Wrap the ActionChip in a Tooltip whose message is the resolved
unsubscribe URI, so a long-press (mobile) or hover (desktop) reveals
the URL before the user taps.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 21:01:26 +02:00
Thomas SharedInboxandClaude Sonnet 4.6 f6a37eaa16 fix: prevent HTML email content from being cut off horizontally (#288)
HTML emails often use fixed-width tables (e.g. <table width="600">) that
exceed the WebView viewport, causing the right portion of the email to be
clipped with no way to scroll. Fix by injecting CSS that:

- Adds `overflow-x: hidden` to body so wide content does not escape the viewport
- Sets `max-width: 100%` on all elements (via `*`) to scale down wide containers
- Forces `table { width: 100%; }` so fixed-pixel-width email tables reflow to fit
- Adds `td/th { overflow-wrap/word-break }` for wrapping in table cells
- Adds `pre { white-space: pre-wrap; }` so pre-formatted text wraps instead of
  stretching the page

Adds a regression test that asserts all four CSS rules are present in the
generated HTML.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 19:50:30 +02:00
5 changed files with 66 additions and 6 deletions
+7 -4
View File
@@ -938,10 +938,13 @@ class _UnsubscribeChip extends StatelessWidget {
Widget build(BuildContext context) {
final uri = _parseUnsubscribeUri(header);
if (uri == null) return const SizedBox.shrink();
return ActionChip(
avatar: const Icon(Icons.unsubscribe_outlined, size: 16),
label: const Text('Unsubscribe'),
onPressed: () => launchUrl(uri, mode: LaunchMode.externalApplication),
return Tooltip(
message: uri.toString(),
child: ActionChip(
avatar: const Icon(Icons.unsubscribe_outlined, size: 16),
label: const Text('Unsubscribe'),
onPressed: () => launchUrl(uri, mode: LaunchMode.externalApplication),
),
);
}
}
+5 -2
View File
@@ -31,10 +31,13 @@ String buildEmailHtml(String htmlBody, {bool loadRemoteImages = false}) {
<meta name="color-scheme" content="light">
<meta http-equiv="Content-Security-Policy" content="$csp">
<style>
body { margin: 0; padding: 0; font-family: sans-serif; word-break: break-word; color-scheme: light; background-color: #ffffff; color: #000000; }
body { margin: 0; padding: 0; font-family: sans-serif; word-break: break-word; overflow-x: hidden; color-scheme: light; background-color: #ffffff; color: #000000; }
img { max-width: 100%; height: auto; }
a { color: #1976D2; }
* { box-sizing: border-box; }
* { box-sizing: border-box; max-width: 100%; }
table { width: 100%; border-collapse: collapse; }
td, th { overflow-wrap: break-word; word-break: break-word; }
pre { white-space: pre-wrap; word-break: break-word; overflow-x: auto; }
</style>
</head>
<body>
+38
View File
@@ -475,6 +475,44 @@ void main() {
expect(find.text('Share'), findsOneWidget);
});
testWidgets(
'long-press on unsubscribe chip shows URL tooltip',
(tester) async {
final email = testEmail(
listUnsubscribeHeader: '<https://example.com/unsubscribe>',
);
await tester.pumpWidget(
buildApp(
initialLocation:
'/accounts/acc-1/mailboxes/INBOX/emails/acc-1%3A42',
overrides: _overrides(
body: const EmailBody(emailId: 'acc-1:42', attachments: []),
email: email,
),
),
);
await tester.pumpAndSettle();
expect(find.text('Unsubscribe'), findsOneWidget);
expect(
find.byWidgetPredicate(
(w) =>
w is Tooltip && w.message == 'https://example.com/unsubscribe',
),
findsOneWidget,
);
await tester.longPress(find.text('Unsubscribe'));
await tester.pumpAndSettle();
expect(
find.text('https://example.com/unsubscribe'),
findsOneWidget,
);
},
);
testWidgets('Show Mail Structure opens dialog with MIME parts', (
tester,
) async {
+2
View File
@@ -588,6 +588,7 @@ Email testEmail({
bool isSeen = false,
bool isFlagged = false,
bool hasAttachment = false,
String? listUnsubscribeHeader,
}) =>
Email(
id: id,
@@ -603,6 +604,7 @@ Email testEmail({
isSeen: isSeen,
isFlagged: isFlagged,
hasAttachment: hasAttachment,
listUnsubscribeHeader: listUnsubscribeHeader,
);
class FakeSearchHistoryRepository implements SearchHistoryRepository {
@@ -41,6 +41,20 @@ void main() {
expect(html, contains('https: http: data: blob:'));
_expectLightMode(html);
});
test('prevents horizontal overflow so wide HTML emails are not cut off',
() {
final html =
buildEmailHtml('<table width="600"><tr><td>x</td></tr></table>');
// Body clips overflow so fixed-width email tables don't escape the viewport.
expect(html, contains('overflow-x: hidden'));
// Tables are forced to full viewport width so fixed pixel widths don't overflow.
expect(html, contains('table { width: 100%'));
// All elements are capped at viewport width via max-width.
expect(html, contains('max-width: 100%'));
// Pre-formatted text wraps instead of stretching the page.
expect(html, contains('white-space: pre-wrap'));
});
});
// On Linux (the test host) the widget falls back to plain text extracted via