webui: the card view paginates and dedups its body fetches

Cards fetch a message body each, for a 110-character snippet -- and the
card view (the default) rendered the WHOLE folder: one getMessage per
mail plus an identity lookup per sender, in one burst, on a mailbox of
any size. The cards now page at 50 like the table, with a small pager,
and a body already being fetched is not requested again on remount.

Also from the same audit: the Bcc row of own sent mail is displayed
again; forwarding a message whose subject starts with "Re:" says
"Fwd:" (the prefix follows the action, not the existing prefix); and
the list rows follow the reading pane's star/spam toggles instead of
keeping the stale icon until a remount.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jolavillette 2026-09-11 22:11:31 +02:00
parent d0f34871be
commit 851d301731
3 changed files with 60 additions and 3 deletions

View File

@ -190,6 +190,8 @@ const MessageSummary = () => {
isSpam = Boolean(details.msgflags & RS_MSG_SPAM);
if (v.attrs.details && v.attrs.details.msgflags !== undefined) {
details.msgflags = v.attrs.details.msgflags;
isStarred = (details.msgflags & 0xf00) === RS_MSG_STAR;
isSpam = Boolean(details.msgflags & RS_MSG_SPAM);
}
MessageCache[v.attrs.details.msgId] = details;
}
@ -210,6 +212,15 @@ const MessageSummary = () => {
}
});
},
// The reading pane's star/spam toggles refresh the summaries; the row's
// closure flags must follow the refreshed attrs or the icon stays stale
// until a remount.
onupdate: (v) => {
if (v.attrs.details && v.attrs.details.msgflags !== undefined) {
isStarred = (v.attrs.details.msgflags & 0xf00) === RS_MSG_STAR;
isSpam = Boolean(v.attrs.details.msgflags & RS_MSG_SPAM);
}
},
view: (v) => {
const spamActive = isSpam || Boolean((details.msgflags || v.attrs.details.msgflags) & RS_MSG_SPAM);
function spamMessage(e) {
@ -364,12 +375,18 @@ const MessageSummary = () => {
};
};
// Bodies already being fetched for a card: a remount during the round trip
// (filter toggle, page change) must not fire the same getMessage again.
const CardFetchesInFlight = new Set();
const MessageCard = () => {
return {
oninit: (v) => {
const msgId = v.attrs.msg.msgId;
if (!MessageCache[msgId]) {
if (!MessageCache[msgId] && !CardFetchesInFlight.has(msgId)) {
CardFetchesInFlight.add(msgId);
rs.rsJsonApiRequest('/rsMail/getMessage', { msgId }).then((res) => {
CardFetchesInFlight.delete(msgId);
if (res && res.body && res.body.retval) {
MessageCache[msgId] = res.body.msg;
MessageCache[msgId].msgtags = v.attrs.msg.msgtags;
@ -781,6 +798,7 @@ const MessageView = () => {
const senderName = (senderAddr && UserNicknamesCache[senderAddr]) || (senderAddr && rs.userList.username(senderAddr)) || '[Unknown]';
const toKeys = Object.keys(MailData.toList || {});
const ccKeys = Object.keys(MailData.ccList || {});
const bccKeys = Object.keys(MailData.bccList || {});
return m(
'.msg-view.mail-reading-card',
@ -888,6 +906,15 @@ const MessageView = () => {
return m('span.recipient-chip', { title: addr }, name);
}),
]),
// Own sent mail carries its Bcc list; the old view showed it.
bccKeys.length > 0 &&
m('.msg-recipients-row', [
m('span.recipient-label', 'Bcc:'),
bccKeys.map((addr) => {
const name = UserNicknamesCache[addr] || rs.userList.username(addr) || addr.slice(0, 8);
return m('span.recipient-chip', { title: addr }, name);
}),
]),
]),
]),
]),
@ -913,7 +940,12 @@ const MessageView = () => {
senderId: senderAddr,
recipientList: MailData.toList,
ccList: MailData.ccList,
subject: MailData.subject.startsWith('Re:') || MailData.subject.startsWith('Fwd:') ? MailData.subject : `${composeType === 'forward' ? 'Fwd' : 'Re'}: ${MailData.subject}`,
// The prefix depends on the ACTION, not on whatever
// prefix the subject already has: forwarding "Re: X"
// must send "Fwd: Re: X", not "Re: X".
subject: composeType === 'forward'
? (MailData.subject.startsWith('Fwd:') ? MailData.subject : `Fwd: ${MailData.subject}`)
: (MailData.subject.startsWith('Re:') ? MailData.subject : `Re: ${MailData.subject}`),
replyMessage: MailData.message,
timeStamp: new Date(MailData.timeStamp * 1000),
setShowCompose,

View File

@ -2150,3 +2150,28 @@ input.star-check {
padding: 1rem;
}
}
.mail-cards-pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 0.75rem;
border-top: 1px solid #e2e8f0;
color: #475569;
font-size: 0.9rem;
button {
padding: 0.3rem 0.7rem;
border: 1px solid #cbd5e1;
background: #f8fafc;
color: #334155;
border-radius: 0.375rem;
box-shadow: none;
&:disabled {
opacity: 0.45;
cursor: default;
}
}
}

File diff suppressed because one or more lines are too long