Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
44 changes: 26 additions & 18 deletions frontend/src/components/MessageList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useSwipeRow } from '../hooks/useSwipeRow.js';
import ContextMenu from './ContextMenu.jsx';
import RowHoverActions from './RowHoverActions.jsx';
import GtdTabList from './GtdTabList.jsx';
import GtdZeroPet from './GtdZeroPet.jsx';
import { useUiScale, descale } from '../hooks/useUiScale.js';
import {
gtdActiveForContext, buildGtdDisplaySections, GTD_COLORS, GTD_CHIP_BG, sectionBadge, isSelectedRow,
Expand All @@ -20,6 +21,7 @@ import { openReplyFromMessage, openForwardFromMessage } from '../utils/composeFr
import SenderAvatarImage from './SenderAvatarImage.jsx';
import { shortcutBus } from '../utils/shortcutBus.js';
import { createLatestRequest } from '../utils/latestRequest.js';
import { resolveMessageListEmptyVisual } from '../utils/messageListEmptyState.js';
import { pendingMarkReadMap, completedMarkReadMap, setPending } from '../utils/pendingReads.js';
import { applyDeleteGuard, clearDeleteGuard, clearPendingDelete, setCompletedDelete, setPendingDelete } from '../utils/pendingDeletes.js';

Expand Down Expand Up @@ -3798,8 +3800,15 @@ function UndoBar({ notification, onDismiss, showTopBorder }) {

function EmptyState({ folderSyncing, searchQuery, unreadOnly, selectedFolder, accounts, onClearSearch, onShowAll, onCompose }) {
const { t } = useTranslation();
const visual = resolveMessageListEmptyVisual({
folderSyncing,
searchQuery,
unreadOnly,
hasAccounts: accounts.length > 0,
selectedFolder,
});

if (folderSyncing) {
if (visual === 'syncing') {
return (
<div style={{ padding: '60px 40px', textAlign: 'center', color: 'var(--text-tertiary)' }}>
<div style={{
Expand All @@ -3812,7 +3821,7 @@ function EmptyState({ folderSyncing, searchQuery, unreadOnly, selectedFolder, ac
);
}

if (searchQuery) {
if (visual === 'search') {
return (
<div style={{ padding: '60px 24px', textAlign: 'center' }}>
<div style={{
Expand All @@ -3836,7 +3845,7 @@ function EmptyState({ folderSyncing, searchQuery, unreadOnly, selectedFolder, ac
);
}

if (unreadOnly) {
if (visual === 'unread') {
return (
<div style={{ padding: '60px 24px', textAlign: 'center' }}>
<div style={{
Expand All @@ -3858,7 +3867,7 @@ function EmptyState({ folderSyncing, searchQuery, unreadOnly, selectedFolder, ac
);
}

if (!accounts.length) {
if (visual === 'no-accounts') {
return (
<div style={{ padding: '60px 24px', textAlign: 'center' }}>
<div style={{
Expand All @@ -3877,25 +3886,24 @@ function EmptyState({ folderSyncing, searchQuery, unreadOnly, selectedFolder, ac
);
}

const isInbox = !selectedFolder || selectedFolder === 'INBOX';
const isInbox = visual === 'inbox-zero';
return (
<div style={{ padding: '60px 24px', textAlign: 'center' }}>
<div style={{
width: 48, height: 48, borderRadius: 14, margin: '0 auto 16px',
background: 'var(--bg-secondary)', display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'var(--text-tertiary)',
}}>
{isInbox ? (
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/>
<path d="M5.45 5.11L2 12v6a2 2 0 002 2h16a2 2 0 002-2v-6l-3.45-6.89A2 2 0 0016.76 4H7.24a2 2 0 00-1.79 1.11z"/>
</svg>
) : (
{isInbox ? (
<div style={{ display: 'flex', justifyContent: 'center', margin: '0 auto 16px' }}>
<GtdZeroPet />
</div>
) : (
<div style={{
width: 48, height: 48, borderRadius: 14, margin: '0 auto 16px',
background: 'var(--bg-secondary)', display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'var(--text-tertiary)',
}}>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z"/>
</svg>
)}
</div>
</div>
)}
<div style={{ fontSize: 15, fontWeight: 500, color: 'var(--text-primary)', marginBottom: 6 }}>
{isInbox ? 'Inbox is empty' : 'Nothing here'}
</div>
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/utils/messageListEmptyState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export function resolveMessageListEmptyVisual({
folderSyncing,
searchQuery,
unreadOnly,
hasAccounts,
selectedFolder,
}) {
if (folderSyncing) return 'syncing';
if (searchQuery) return 'search';
if (unreadOnly) return 'unread';
if (!hasAccounts) return 'no-accounts';
return !selectedFolder || selectedFolder === 'INBOX' ? 'inbox-zero' : 'folder';
}
21 changes: 21 additions & 0 deletions frontend/src/utils/messageListEmptyState.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { resolveMessageListEmptyVisual } from './messageListEmptyState.js';

test('uses the pet visual only for an unfiltered empty inbox', () => {
const base = {
folderSyncing: false,
searchQuery: '',
unreadOnly: false,
hasAccounts: true,
selectedFolder: 'INBOX',
};

assert.equal(resolveMessageListEmptyVisual(base), 'inbox-zero');
assert.equal(resolveMessageListEmptyVisual({ ...base, selectedFolder: null }), 'inbox-zero');
assert.equal(resolveMessageListEmptyVisual({ ...base, selectedFolder: 'Archive' }), 'folder');
assert.equal(resolveMessageListEmptyVisual({ ...base, searchQuery: 'invoice' }), 'search');
assert.equal(resolveMessageListEmptyVisual({ ...base, unreadOnly: true }), 'unread');
assert.equal(resolveMessageListEmptyVisual({ ...base, folderSyncing: true }), 'syncing');
assert.equal(resolveMessageListEmptyVisual({ ...base, hasAccounts: false }), 'no-accounts');
});