Make search result use same code #429

Closed
opened 2026-06-04 20:04:29 +00:00 by guettli · 2 comments
guettli commented 2026-06-04 20:04:29 +00:00 (Migrated from codeberg.org)

Explain why search result was excluded:

https://codeberg.org/guettli/sharedinbox/issues/424

I would like to have one way to show mails.

Explain why search result was excluded: https://codeberg.org/guettli/sharedinbox/issues/424 I would like to have one way to show mails.
guettlibot commented 2026-06-04 20:10:03 +00:00 (Migrated from codeberg.org)

Now I have everything I need. Here is the implementation plan:


Implementation Plan — Issue #429: Unify mail display with a single ThreadTile widget

Background

There are currently two parallel code paths for rendering a mail row:

  • EmailTile (lib/ui/widgets/email_tile.dart) — used in SearchScreen and EmailListScreen._buildEmailList(). Accepts an Email object (individual message).
  • Inline ListTile — duplicated in EmailListScreen._buildThreadList() (lines 695–773) and CombinedInboxScreen._buildThreadTile() (lines 224–301). Accepts an EmailThread object (thread summary).

The thread-level widget is richer: it shows message count, snippet preview, flagged star, and the Dismissible swipe actions. Unifying around EmailThread + a shared widget gives search results feature parity with the inbox without duplicating layout code.

Steps

1. Create lib/ui/widgets/thread_tile.dart

Extract the common ListTile from the two inline implementations into a new ThreadTile stateless widget. Constructor:

ThreadTile({
  required EmailThread thread,
  required VoidCallback onTap,
  Widget? leading,          // for selection-mode checkboxes
  bool selected = false,
  VoidCallback? onLongPress,
  bool showLocation = false, // shows "accountId • mailboxPath" subtitle line
})

The Dismissible wrappers stay in the screens — they are behaviour, not presentation.

The showLocation flag follows the same pattern as the existing EmailTile.showLocation and covers the cross-account combined inbox case (CombinedInboxScreen currently has a showAccount subtitle line).

2. Add EmailThread.fromEmail factory to lib/core/models/email.dart

Email has all the fields needed for a single-message thread:

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

No schema change; no build_runner run required.

3. Replace inline ListTile in EmailListScreen._buildThreadList()

Swap the inline ListTile block (lines 695–773) for ThreadTile(thread: t, leading: ..., selected: isSelected, onTap: ..., onLongPress: ...). Keep the surrounding Dismissible.

4. Replace inline ListTile in CombinedInboxScreen._buildThreadTile()

Swap the inline ListTile block (lines 224–301) for ThreadTile(thread: t, showLocation: showAccount, onTap: ...). Keep the surrounding Dismissible.

5. Replace EmailTile usage in search results with ThreadTile

  • SearchScreen (line 192): convert Email eEmailThread.fromEmail(e), render with ThreadTile(thread: ..., showLocation: true, onTap: ...).
  • EmailListScreen._buildEmailList() (line 860): convert each Email eEmailThread.fromEmail(e), render with ThreadTile. Selection-mode checkbox and long-press are passed as leading / onLongPress.

6. Delete lib/ui/widgets/email_tile.dart

EmailTile will have zero usages after step 5.

Scope / what does NOT change

  • Repository layer (searchEmails, searchEmailsGlobal) returns List<Email> — no change.
  • Swipe-to-archive/delete logic in both screens — no change.
  • Selection mode and bulk-action logic — no change.
  • No DB schema change, so no dart run build_runner build needed.
Now I have everything I need. Here is the implementation plan: --- ## Implementation Plan — Issue #429: Unify mail display with a single `ThreadTile` widget ### Background There are currently two parallel code paths for rendering a mail row: - **`EmailTile`** (`lib/ui/widgets/email_tile.dart`) — used in `SearchScreen` and `EmailListScreen._buildEmailList()`. Accepts an `Email` object (individual message). - **Inline `ListTile`** — duplicated in `EmailListScreen._buildThreadList()` (lines 695–773) and `CombinedInboxScreen._buildThreadTile()` (lines 224–301). Accepts an `EmailThread` object (thread summary). The thread-level widget is richer: it shows message count, snippet preview, flagged star, and the `Dismissible` swipe actions. Unifying around `EmailThread` + a shared widget gives search results feature parity with the inbox without duplicating layout code. ### Steps **1. Create `lib/ui/widgets/thread_tile.dart`** Extract the common `ListTile` from the two inline implementations into a new `ThreadTile` stateless widget. Constructor: ```dart ThreadTile({ required EmailThread thread, required VoidCallback onTap, Widget? leading, // for selection-mode checkboxes bool selected = false, VoidCallback? onLongPress, bool showLocation = false, // shows "accountId • mailboxPath" subtitle line }) ``` The `Dismissible` wrappers stay in the screens — they are behaviour, not presentation. The `showLocation` flag follows the same pattern as the existing `EmailTile.showLocation` and covers the cross-account combined inbox case (`CombinedInboxScreen` currently has a `showAccount` subtitle line). **2. Add `EmailThread.fromEmail` factory to `lib/core/models/email.dart`** `Email` has all the fields needed for a single-message thread: ``` 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 ``` No schema change; no `build_runner` run required. **3. Replace inline `ListTile` in `EmailListScreen._buildThreadList()`** Swap the inline `ListTile` block (lines 695–773) for `ThreadTile(thread: t, leading: ..., selected: isSelected, onTap: ..., onLongPress: ...)`. Keep the surrounding `Dismissible`. **4. Replace inline `ListTile` in `CombinedInboxScreen._buildThreadTile()`** Swap the inline `ListTile` block (lines 224–301) for `ThreadTile(thread: t, showLocation: showAccount, onTap: ...)`. Keep the surrounding `Dismissible`. **5. Replace `EmailTile` usage in search results with `ThreadTile`** - `SearchScreen` (line 192): convert `Email e` → `EmailThread.fromEmail(e)`, render with `ThreadTile(thread: ..., showLocation: true, onTap: ...)`. - `EmailListScreen._buildEmailList()` (line 860): convert each `Email e` → `EmailThread.fromEmail(e)`, render with `ThreadTile`. Selection-mode checkbox and long-press are passed as `leading` / `onLongPress`. **6. Delete `lib/ui/widgets/email_tile.dart`** `EmailTile` will have zero usages after step 5. ### Scope / what does NOT change - Repository layer (`searchEmails`, `searchEmailsGlobal`) returns `List<Email>` — no change. - Swipe-to-archive/delete logic in both screens — no change. - Selection mode and bulk-action logic — no change. - No DB schema change, so no `dart run build_runner build` needed.
guettlibot commented 2026-06-05 15:15:04 +00:00 (Migrated from codeberg.org)

Done. Here's a summary of what was implemented:


What was done (issue #429)

Problem: Three separate code paths rendered mail rows — EmailTile for search results (Email objects) and duplicated inline ListTile blocks in EmailListScreen and CombinedInboxScreen for thread lists (EmailThread objects). Search results were visually simpler (no message count, no preview, no flagged star).

Changes:

  1. New lib/ui/widgets/thread_tile.dart — single ThreadTile widget for all mail row rendering. Accepts EmailThread, supports selection mode (leading), swipe-able context (onTap/onLongPress), and an optional locationLabel for cross-account views. The _fmtDate date cache (previously duplicated in both screens) now lives here.

  2. EmailThread.fromEmail(Email) factory in lib/core/models/email.dart — wraps a single Email as a one-message thread so search results can use ThreadTile without touching the repository layer.

  3. EmailListScreen and CombinedInboxScreen inline ListTile blocks replaced with ThreadTile. All Dismissible / swipe / selection logic unchanged.

  4. SearchScreen and EmailListScreen._buildEmailList() converted to use ThreadTile via EmailThread.fromEmail().

  5. EmailTile deleted — zero usages remaining.

dart analyze reports no issues.

Pull request: https://codeberg.org/guettli/sharedinbox/pulls/445

Done. Here's a summary of what was implemented: --- ## What was done (issue #429) **Problem:** Three separate code paths rendered mail rows — `EmailTile` for search results (`Email` objects) and duplicated inline `ListTile` blocks in `EmailListScreen` and `CombinedInboxScreen` for thread lists (`EmailThread` objects). Search results were visually simpler (no message count, no preview, no flagged star). **Changes:** 1. **New `lib/ui/widgets/thread_tile.dart`** — single `ThreadTile` widget for all mail row rendering. Accepts `EmailThread`, supports selection mode (`leading`), swipe-able context (`onTap`/`onLongPress`), and an optional `locationLabel` for cross-account views. The `_fmtDate` date cache (previously duplicated in both screens) now lives here. 2. **`EmailThread.fromEmail(Email)` factory** in `lib/core/models/email.dart` — wraps a single `Email` as a one-message thread so search results can use `ThreadTile` without touching the repository layer. 3. **`EmailListScreen`** and **`CombinedInboxScreen`** inline `ListTile` blocks replaced with `ThreadTile`. All `Dismissible` / swipe / selection logic unchanged. 4. **`SearchScreen`** and **`EmailListScreen._buildEmailList()`** converted to use `ThreadTile` via `EmailThread.fromEmail()`. 5. **`EmailTile` deleted** — zero usages remaining. `dart analyze` reports no issues. **Pull request:** https://codeberg.org/guettli/sharedinbox/pulls/445
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: guettli/sharedinbox#429