refactor: unify mail display with shared ThreadTile widget (#445)
This commit was merged in pull request #445.
This commit is contained in:
@@ -192,6 +192,22 @@ class EmailThread {
|
|||||||
required this.accountId,
|
required this.accountId,
|
||||||
required this.mailboxPath,
|
required this.mailboxPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Wraps a single [Email] as a one-message thread for uniform rendering.
|
||||||
|
factory EmailThread.fromEmail(Email e) => EmailThread(
|
||||||
|
threadId: e.threadId ?? e.id,
|
||||||
|
subject: e.subject,
|
||||||
|
participants: e.from,
|
||||||
|
latestDate: e.sentAt ?? e.receivedAt,
|
||||||
|
messageCount: 1,
|
||||||
|
hasUnread: !e.isSeen,
|
||||||
|
isFlagged: e.isFlagged,
|
||||||
|
latestEmailId: e.id,
|
||||||
|
preview: e.preview,
|
||||||
|
emailIds: [e.id],
|
||||||
|
accountId: e.accountId,
|
||||||
|
mailboxPath: e.mailboxPath,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmailAddress {
|
class EmailAddress {
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import 'package:sharedinbox/core/repositories/email_repository.dart';
|
|||||||
import 'package:sharedinbox/di.dart';
|
import 'package:sharedinbox/di.dart';
|
||||||
import 'package:sharedinbox/ui/screens/email_action_helpers.dart';
|
import 'package:sharedinbox/ui/screens/email_action_helpers.dart';
|
||||||
import 'package:sharedinbox/ui/widgets/email_thread_tile.dart';
|
import 'package:sharedinbox/ui/widgets/email_thread_tile.dart';
|
||||||
import 'package:sharedinbox/ui/widgets/email_tile.dart';
|
|
||||||
import 'package:sharedinbox/ui/widgets/folder_drawer.dart';
|
import 'package:sharedinbox/ui/widgets/folder_drawer.dart';
|
||||||
import 'package:sharedinbox/ui/widgets/snooze_picker.dart';
|
import 'package:sharedinbox/ui/widgets/snooze_picker.dart';
|
||||||
|
import 'package:sharedinbox/ui/widgets/thread_tile.dart';
|
||||||
|
|
||||||
class EmailListScreen extends ConsumerStatefulWidget {
|
class EmailListScreen extends ConsumerStatefulWidget {
|
||||||
const EmailListScreen({
|
const EmailListScreen({
|
||||||
@@ -762,9 +762,10 @@ class _EmailListScreenState extends ConsumerState<EmailListScreen> {
|
|||||||
itemCount: emails.length,
|
itemCount: emails.length,
|
||||||
itemBuilder: (ctx, i) {
|
itemBuilder: (ctx, i) {
|
||||||
final e = emails[i];
|
final e = emails[i];
|
||||||
|
final t = EmailThread.fromEmail(e);
|
||||||
final isSelected = _selectedSearchIds.contains(e.id);
|
final isSelected = _selectedSearchIds.contains(e.id);
|
||||||
return EmailTile(
|
return ThreadTile(
|
||||||
email: e,
|
thread: t,
|
||||||
selected: isSelected,
|
selected: isSelected,
|
||||||
leading: SizedBox(
|
leading: SizedBox(
|
||||||
width: 40,
|
width: 40,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import 'package:sharedinbox/core/models/email.dart';
|
|||||||
import 'package:sharedinbox/core/models/mailbox.dart';
|
import 'package:sharedinbox/core/models/mailbox.dart';
|
||||||
import 'package:sharedinbox/core/utils/logger.dart';
|
import 'package:sharedinbox/core/utils/logger.dart';
|
||||||
import 'package:sharedinbox/di.dart';
|
import 'package:sharedinbox/di.dart';
|
||||||
import 'package:sharedinbox/ui/widgets/email_tile.dart';
|
import 'package:sharedinbox/ui/widgets/thread_tile.dart';
|
||||||
|
|
||||||
final _searchHistoryProvider = FutureProvider.autoDispose<List<String>>((
|
final _searchHistoryProvider = FutureProvider.autoDispose<List<String>>((
|
||||||
ref,
|
ref,
|
||||||
@@ -189,9 +189,9 @@ class _SearchScreenState extends ConsumerState<SearchScreen> {
|
|||||||
if (r.emails.isNotEmpty) ...[
|
if (r.emails.isNotEmpty) ...[
|
||||||
const _SectionHeader('Messages'),
|
const _SectionHeader('Messages'),
|
||||||
for (final e in r.emails)
|
for (final e in r.emails)
|
||||||
EmailTile(
|
ThreadTile(
|
||||||
email: e,
|
thread: EmailThread.fromEmail(e),
|
||||||
showLocation: true,
|
locationLabel: '${e.accountId} • ${e.mailboxPath}',
|
||||||
onTap: () => context.push(
|
onTap: () => context.push(
|
||||||
'/accounts/${e.accountId}/mailboxes'
|
'/accounts/${e.accountId}/mailboxes'
|
||||||
'/${Uri.encodeComponent(e.mailboxPath)}'
|
'/${Uri.encodeComponent(e.mailboxPath)}'
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
import 'package:sharedinbox/core/models/email.dart';
|
||||||
|
|
||||||
|
final _dateFmt = DateFormat('MMM d');
|
||||||
|
// Cache formatted dates by local calendar day to avoid repeated DateFormat.format calls.
|
||||||
|
final _formattedDates = <int, String>{};
|
||||||
|
|
||||||
|
int _dayKey(DateTime dt) => dt.year * 10000 + dt.month * 100 + dt.day;
|
||||||
|
|
||||||
|
String _fmtDate(DateTime dt) =>
|
||||||
|
_formattedDates[_dayKey(dt)] ??= _dateFmt.format(dt);
|
||||||
|
|
||||||
|
/// A list tile for an [EmailThread].
|
||||||
|
///
|
||||||
|
/// Used in inbox lists, combined inbox, and search result lists.
|
||||||
|
/// Pass a custom [leading] widget to support selection-mode checkboxes.
|
||||||
|
/// Pass [locationLabel] to show an extra subtitle line (e.g. account name or
|
||||||
|
/// "accountId • mailboxPath") — useful in cross-mailbox views.
|
||||||
|
class ThreadTile extends StatelessWidget {
|
||||||
|
const ThreadTile({
|
||||||
|
super.key,
|
||||||
|
required this.thread,
|
||||||
|
required this.onTap,
|
||||||
|
this.leading,
|
||||||
|
this.selected = false,
|
||||||
|
this.onLongPress,
|
||||||
|
this.locationLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
final EmailThread thread;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final Widget? leading;
|
||||||
|
final bool selected;
|
||||||
|
final VoidCallback? onLongPress;
|
||||||
|
|
||||||
|
/// When non-null, appended as an extra subtitle line in primary colour.
|
||||||
|
final String? locationLabel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final senderNames = thread.participants.isEmpty
|
||||||
|
? '(unknown)'
|
||||||
|
: thread.participants.map((a) => a.name ?? a.email).take(3).join(', ');
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: leading ??
|
||||||
|
Icon(
|
||||||
|
thread.hasUnread ? Icons.mail : Icons.mail_outline,
|
||||||
|
color:
|
||||||
|
thread.hasUnread ? Theme.of(context).colorScheme.primary : null,
|
||||||
|
),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
senderNames,
|
||||||
|
style: thread.hasUnread
|
||||||
|
? const TextStyle(fontWeight: FontWeight.bold)
|
||||||
|
: null,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (thread.messageCount > 1)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 4),
|
||||||
|
child: Text(
|
||||||
|
'[${thread.messageCount}]',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
thread.subject ?? '(no subject)',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: thread.hasUnread
|
||||||
|
? const TextStyle(fontWeight: FontWeight.bold)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
if (thread.preview != null && thread.preview!.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
thread.preview!,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
if (locationLabel != null)
|
||||||
|
Text(
|
||||||
|
locationLabel!,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (thread.isFlagged)
|
||||||
|
const Icon(Icons.star, color: Colors.amber, size: 16),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
_fmtDate(thread.latestDate),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
selected: selected,
|
||||||
|
onTap: onTap,
|
||||||
|
onLongPress: onLongPress,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-8
@@ -659,10 +659,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.0"
|
version: "1.17.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1088,26 +1088,26 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: test
|
name: test
|
||||||
sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20"
|
sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.31.0"
|
version: "1.30.0"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.11"
|
version: "0.7.10"
|
||||||
test_core:
|
test_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_core
|
name: test_core
|
||||||
sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34"
|
sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.17"
|
version: "0.6.16"
|
||||||
timezone:
|
timezone:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ const _excluded = {
|
|||||||
'lib/core/services/update_service.dart',
|
'lib/core/services/update_service.dart',
|
||||||
'lib/ui/widgets/email_thread_tile.dart',
|
'lib/ui/widgets/email_thread_tile.dart',
|
||||||
'lib/ui/screens/trusted_image_senders_screen.dart',
|
'lib/ui/screens/trusted_image_senders_screen.dart',
|
||||||
|
'lib/ui/widgets/thread_tile.dart',
|
||||||
};
|
};
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|||||||
Reference in New Issue
Block a user