Files
280f58b7b5 feat(mail): switching category tabs replaces the history entry, as in Gmail (#161)
* feat(mail): replace the history entry when switching category tabs on a phone

Below md, a category tab navigates the inbox list frame with Turbo's
replace action, so the browser's Back leaves the inbox instead of
stepping back through every tab. At md and up the link keeps advance,
so desktop behaviour is unchanged.

The choice is made on click by the ui--tab-history controller, not on
connect: the tab strip is morphed on every list refresh, and a morph
removes attributes the server did not render.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>

* feat(mail): category tabs replace the history entry at every width

#150 asked for this on a phone only; the maintainer wants it everywhere,
as Gmail does. With no breakpoint to ask about, the tab links render
data-turbo-action="replace" from the server, so the click-time controller
goes: a server-rendered attribute survives the strip's morph on its own.
The account page's tab strip, added on main since, gets the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(changelog): credit the tab history entry to #161, which took over #113

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Flouss <15911342+Flouss@users.noreply.github.com>
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-15 14:44:47 +02:00

90 lines
5.0 KiB
Twig

{#
mail/account.html.twig — one account's INBOX.
Reached by clicking the account itself in the sidebar. It used to be every
thread in the account whatever it was labelled, which meant your own sent mail
interleaved through what had arrived, with drafts and spam among it — an
archive, where a person clicking their address means "show me what came in".
Everything else in the account is a folder row one click below it.
The heading names the account AND says Inbox, because those two facts together
are the only thing that distinguishes this screen from the unified inbox above
it and from the account's other folders beside it.
#}
{% extends '_layout/_mailbox.html.twig' %}
{% block title %}{{ account.displayAddress }} — {{ 'app.name'|trans }}{% endblock %}
{% block inbox_tabs %}
{% import '_partials/_category_tab_glyphs.html.twig' as glyphs %}
<div class="px-4 py-2.5 border-b border-line flex items-center gap-2">
<h2 class="text-sm font-semibold text-ink-soft">{{ 'sidebar.nav.inbox'|trans }}</h2>
<span class="text-xs text-ink-faint truncate">{{ account.displayAddress }}</span>
</div>
{# The category tabs, for this account's mail.
The same tabs as the unified inbox — the five and the reader's own — and
chosen by the same rule (MailController::visibleTabs()): one exists while
it holds mail in THIS account, the one you are on stays. But deliberately
plainer: an icon and a name. What the unified strip adds is a set of
counters ("3 new", the sender hint, the unread tint) that are summed
across EVERY account and rewritten by the sidebar controller from that
same payload. Drawn here they would describe the wrong mailbox, and be
overwritten with the wrong numbers after the next sync, so they are not
drawn; the keys they are patched by are not emitted either. Dragging a
conversation onto a tab is the unified inbox's, for the same reason: its
targets are built from that page's counts.
Links go to THIS page with `?tab=`, carrying whatever else narrows the
list and dropping `page`, because page four of Primary is not page four
of Social. The strip scrolls inside itself rather than squeezing, since
five tabs are wider than a phone, and ui--scroll-active brings the open
tab into view the way it does on the unified strip (#53). #}
{% set tabQuery = app.request.query.all|filter((v, k) => k not in ['tab', 'page']) %}
{% if tabs|length > 1 %}
<div class="border-b border-line px-1">
<nav class="relative flex text-sm overflow-x-auto overflow-y-hidden pb-px -mb-px [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
data-controller="ui--scroll-active">
{% for tabCase in tabs %}
{% set isActive = tab is not null and tabCase.value == tab.value %}
<a
href="{{ path('app_mail_account', {account: account.id}|merge(tabQuery)|merge({tab: tabCase.value})) }}"
data-turbo-frame="inbox-list-frame"
{# `replace`, as on the Inbox's strip: see the note there. #}
data-turbo-action="replace"
{% if isActive %}aria-current="page" data-ui--scroll-active-target="current"{% endif %}
class="flex items-center gap-2.5 px-3 @xl:px-5 py-1.5 min-h-14 font-medium border-b-2 -mb-px transition-colors shrink-0 whitespace-nowrap {{ isActive ? 'text-accent border-accent' : 'text-ink-muted hover:text-gray-700 border-transparent hover:border-gray-300' }}"
>
{% if isActive %}
<svg class="w-5 h-5 shrink-0" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" clip-rule="evenodd" d="{{ glyphs.solid(tabCase.isCustom ? 'custom' : tabCase.value) }}"/>
</svg>
{% else %}
<svg class="w-5 h-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="{{ glyphs.outline(tabCase.isCustom ? 'custom' : tabCase.value) }}"/>
</svg>
{% endif %}
{{ tabCase.isCustom ? tabCase.name : ('tabs.' ~ tabCase.value)|trans }}
</a>
{% endfor %}
</nav>
</div>
{% endif %}
{% endblock %}
{% block message_list %}
{% if threads is empty %}
<li class="px-4 py-8 text-center text-sm text-ink-faint">
{{ 'inbox.empty_account'|trans }}
</li>
{% else %}
{% for thread in threads %}
{# Every row here is the same account, so the dot would say nothing. #}
{% include '_partials/_thread_row.html.twig' with { thread: thread, unified: false } %}
{% endfor %}
{% endif %}
{% endblock %}