Files
Inboxintel/frontend/src/components/EmailRow.jsx
T
cesnimda 7710d49c77 feat: keyboard shortcuts for email navigation
'/' focuses the search bar from anywhere (Layout-level listener).
j/k move focus up/down the email list in FolderView and SearchResults;
e archives the focused email, # trashes it. All shortcuts are ignored
while an input/textarea/select has focus, so typing is never hijacked.

A small hint footer in the bottom-right reminds users of the shortcuts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 21:55:36 +02:00

125 lines
4.2 KiB
React
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react';
import { EmailApi } from '../api/client.js';
const fmtDate = (iso) => {
const d = new Date(iso);
const now = new Date();
const diffDays = (now - d) / 86400000;
if (diffDays < 1) return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
if (diffDays < 7) return d.toLocaleDateString([], { weekday: 'short' });
return d.toLocaleDateString([], { month: 'short', day: 'numeric' });
};
const fmtSize = (b) => {
if (b < 1024) return `${b} B`;
if (b < 1048576) return `${(b / 1024).toFixed(0)} KB`;
return `${(b / 1048576).toFixed(1)} MB`;
};
export default function EmailRow({ email: initial, onRemove, selected, onToggleSelect, focused }) {
const [email, setEmail] = useState(initial);
const [acting, setActing] = useState(false);
const act = (fn, patch) => async (e) => {
e.stopPropagation();
if (acting) return;
setActing(true);
try {
await fn(email.id);
setEmail((prev) => ({ ...prev, ...patch }));
} finally {
setActing(false);
}
};
const handleUnsub = async (e) => {
e.stopPropagation();
if (acting) return;
setActing(true);
try {
const res = await EmailApi.unsubscribe(email.id);
if (res.method === 'mailto') {
window.location.href = res.target;
} else {
setEmail((prev) => ({ ...prev, _unsubDone: true }));
}
} finally {
setActing(false);
}
};
const handleTrash = async (e) => {
e.stopPropagation();
if (acting) return;
setActing(true);
try {
await EmailApi.trash(email.id);
onRemove?.(email.id);
} finally {
setActing(false);
}
};
const openInGmail = () => window.open(
`https://mail.google.com/mail/u/0/#all/${email.gmailMessageId}`,
'_blank',
'noopener,noreferrer'
);
return (
<tr
className={`email-row${email.isUnread ? ' email-row--unread' : ''}${acting ? ' email-row--acting' : ''}${focused ? ' email-row--focused' : ''}`}
onClick={openInGmail}
title="Open in Gmail"
>
{onToggleSelect && (
<td className="el-select" onClick={(e) => e.stopPropagation()}>
<input type="checkbox" checked={!!selected} onChange={() => onToggleSelect(email.id)} />
</td>
)}
<td className="el-unread">{email.isUnread && <span className="unread-dot" />}</td>
<td className="el-sender" title={email.senderAddress}>
{email.senderDisplayName || email.senderAddress}
</td>
<td className="el-subject">
<span className="el-subj-text">{email.subject || '(no subject)'}</span>
{email.snippet && <span className="el-snippet"> {email.snippet}</span>}
</td>
<td className="el-meta">
{email.hasAttachments && <span className="el-attach" title="Has attachment">📎</span>}
{email.sizeEstimateBytes > 1048576 && <span className="el-size">{fmtSize(email.sizeEstimateBytes)}</span>}
</td>
<td className="el-date">{fmtDate(email.sentAtUtc)}</td>
<td className="el-actions" onClick={(e) => e.stopPropagation()}>
<button
className="action-btn"
title={email.isUnread ? 'Mark as read' : 'Mark as unread'}
onClick={email.isUnread
? act(EmailApi.markRead, { isUnread: false })
: act(EmailApi.markUnread, { isUnread: true })}
>{email.isUnread ? '✓' : '●'}</button>
<button
className={`action-btn${email.isStarred ? ' action-btn--active' : ''}`}
title={email.isStarred ? 'Unstar' : 'Star'}
onClick={email.isStarred
? act(EmailApi.unstar, { isStarred: false })
: act(EmailApi.star, { isStarred: true })}
></button>
{email.hasListUnsubscribe && (
<button
className={`action-btn action-btn--unsub${email._unsubDone ? ' action-btn--done' : ''}`}
title={email._unsubDone ? 'Unsubscribed' : 'Unsubscribe'}
onClick={handleUnsub}
disabled={email._unsubDone}
>{email._unsubDone ? '✓' : '✉✕'}</button>
)}
<button
className="action-btn action-btn--danger"
title="Move to trash"
onClick={handleTrash}
>🗑</button>
</td>
</tr>
);
}