From 9a48d1a16dbb222830379df11b9ff9051e51ac7b Mon Sep 17 00:00:00 2001 From: 64johnlee <64lamei@gmail.com> Date: Sun, 3 May 2026 20:34:32 +0800 Subject: [PATCH 01/53] feat(mail): implement Reply All functionality - Added Reply All button in MessageView - Pass reply context (to+cc lists) via pendingReplyContext global - Pre-fill CC field with original message's CC recipients - Include CC info in the quoted original message header - Handle both 'reply' and 'replyall' msgTypes in compose --- webui-src/app/mail/mail_compose.js | 137 +++++++++++++++++++++++++--- webui-src/app/mail/mail_resolver.js | 6 +- webui-src/app/mail/mail_util.js | 28 +++++- 3 files changed, 152 insertions(+), 19 deletions(-) diff --git a/webui-src/app/mail/mail_compose.js b/webui-src/app/mail/mail_compose.js index faff4f95..c0b420ff 100644 --- a/webui-src/app/mail/mail_compose.js +++ b/webui-src/app/mail/mail_compose.js @@ -2,6 +2,7 @@ const m = require('mithril'); const rs = require('rswebui'); const widget = require('widgets'); const peopleUtil = require('people/people_util'); +const { MSG_ADDRESS_MODE_CC } = require('mail/mail_util'); const Layout = () => { const Data = { @@ -29,11 +30,29 @@ const Layout = () => { }; async function loadMailUserDetails(msgType, senderId, recipientList) { Data.allUsers = await peopleUtil.sortUsers(rs.userList.users); - if (msgType === 'reply') { + if (msgType === 'reply' || msgType === 'replyall') { Data.allUsers.forEach(async (user) => { if (user.mGroupId === (await senderId)) Data.recipients.to.sendList.push(user); }); } + if (msgType === 'replyall') { + // Add all original recipients to appropriate fields + if (recipientList) { + Object.keys(recipientList).forEach((recipId) => { + const user = Data.allUsers.find((u) => u.mGroupId === recipId); + if (user) { + const dest = recipientList[recipId]; + if (dest._mode === MSG_ADDRESS_MODE_CC) { + if (!Data.recipients.cc.sendList.find((u) => u.mGroupId === recipId)) { + Data.recipients.cc.sendList.push(user); + } + } else if (dest._mode === MSG_ADDRESS_MODE_TO) { + // Don't add to 'to' since sender is already there + } + } + }); + } + } await peopleUtil.ownIds(async (data) => { Data.ownId = await data; for (let i = 0; i < Data.ownId.length; i++) { @@ -41,7 +60,7 @@ const Layout = () => { Data.ownId.splice(i, 1); // workaround for id '0' } } - if (msgType === 'reply') { + if (msgType === 'reply' || msgType === 'replyall') { Data.identity = Data.ownId.filter((id) => Object.prototype.hasOwnProperty.call(recipientList, id) )[0]; @@ -49,19 +68,34 @@ const Layout = () => { }); } async function loadDetails(attrs) { - const { msgType, senderId, recipientList } = await attrs; - await loadMailUserDetails(msgType, senderId, recipientList); + const { msgType, senderId, recipientList, pendingCtx } = await attrs; - Object.keys(Data.recipients).forEach((item) => { - Data.recipients[item].inputList = Data.allUsers; - }); + // Handle pending reply context from MessageView (reply/replyall) + if (pendingCtx) { + const ctx = pendingCtx; + if (ctx.msgType === 'reply') { + await loadMailUserDetails('reply', ctx.from._addr_string, ctx.toList); + } else if (ctx.msgType === 'replyall') { + await loadMailUserDetails('replyall', ctx.from._addr_string, ctx.toList); + // For reply all, pre-fill CC with the original CC list + if (ctx.ccList) { + Object.keys(ctx.ccList).forEach((ccId) => { + const ccUser = Data.allUsers.find((u) => u.mGroupId === ccId); + if (ccUser && !Data.recipients.cc.sendList.find((u) => u.mGroupId === ccId)) { + Data.recipients.cc.sendList.push(ccUser); + } + }); + } + } + + Object.keys(Data.recipients).forEach((item) => { + Data.recipients[item].inputList = Data.allUsers; + }); - if (msgType === 'compose') { Data.identity = Data.ownId[0]; - } - if (msgType === 'reply') { - const { subject, replyMessage, timeStamp } = await attrs; + // Set subject and original message quote + const { subject, replyMessage, timeStamp } = ctx; const tmb = document.querySelector('#composerMailBody'); const time = timeStamp.toLocaleTimeString('UTC', { hour: '2-digit', minute: '2-digit' }); const dateLong = timeStamp.toLocaleDateString('UTC', { @@ -73,16 +107,27 @@ const Layout = () => { -----Original Message-----
From: - ${rs.userList.userMap[senderId]} + ${rs.userList.userMap[ctx.from._addr_string]}
To: - ${Object.keys(recipientList).map( + ${Object.keys(ctx.toList).map( (recip) => ` - ${rs.userList.userMap[recipientList[recip]._addr_string] || 'Unknown'}, + ${rs.userList.userMap[ctx.toList[recip]._addr_string] || 'Unknown'}, + + ` + )} + ${ctx.ccList && Object.keys(ctx.ccList).length > 0 ? ` +
+ CC: + ${Object.keys(ctx.ccList).map( + (ccId) => ` + + ${rs.userList.userMap[ctx.ccList[ccId]._addr_string] || 'Unknown'}, ` )} + ` : ''}

Sent: @@ -94,7 +139,7 @@ const Layout = () => {
On ${timeStamp.toLocaleDateString()} ${time}, - ${rs.userList.userMap[senderId]} + ${rs.userList.userMap[ctx.from._addr_string]} wrote: `; @@ -109,6 +154,68 @@ const Layout = () => { `; Data.subject = subject.substring(0, 4) === 'Re: ' ? subject : `Re: ${subject}`; + } else { + // Original compose logic + await loadMailUserDetails(msgType, senderId, recipientList); + + Object.keys(Data.recipients).forEach((item) => { + Data.recipients[item].inputList = Data.allUsers; + }); + + if (msgType === 'compose') { + Data.identity = Data.ownId[0]; + } + + if (msgType === 'reply') { + const { subject, replyMessage, timeStamp } = await attrs; + const tmb = document.querySelector('#composerMailBody'); + const time = timeStamp.toLocaleTimeString('UTC', { hour: '2-digit', minute: '2-digit' }); + const dateLong = timeStamp.toLocaleDateString('UTC', { + year: 'numeric', + month: 'long', + day: 'numeric', + }); + const replyMessageHeader = ` + -----Original Message----- +
+ From: + ${rs.userList.userMap[senderId]} +
+ To: + ${Object.keys(recipientList).map( + (recip) => ` + + ${rs.userList.userMap[recipientList[recip]._addr_string] || 'Unknown'}, + + ` + )} +
+
+ Sent: + ${dateLong} ${time} +
+ Subject: + ${subject} +
+
+ + On ${timeStamp.toLocaleDateString()} ${time}, + ${rs.userList.userMap[senderId]} + wrote: + + `; + tmb.innerHTML = ` +
+
+
+ ${replyMessageHeader} +
+ ${replyMessage} +
+
+ `; + Data.subject = subject.substring(0, 4) === 'Re: ' ? subject : `Re: ${subject}`; + } } } return { diff --git a/webui-src/app/mail/mail_resolver.js b/webui-src/app/mail/mail_resolver.js index 7568d847..68db8ace 100644 --- a/webui-src/app/mail/mail_resolver.js +++ b/webui-src/app/mail/mail_resolver.js @@ -133,7 +133,11 @@ const Layout = () => { { style: { display: showCompose ? 'block' : 'none' } }, m( '.composePopup', - m(compose, { msgType: 'compose', setShowCompose }), + m(compose, { + msgType: 'compose', + setShowCompose, + pendingCtx: getPendingReplyContext(), + }), m('button.red.close-btn', { onclick: () => setShowCompose(false) }, m('i.fas.fa-times')) ) ), diff --git a/webui-src/app/mail/mail_util.js b/webui-src/app/mail/mail_util.js index ca5511f2..13090d3e 100644 --- a/webui-src/app/mail/mail_util.js +++ b/webui-src/app/mail/mail_util.js @@ -5,6 +5,15 @@ const widget = require('widgets'); const peopleUtil = require('people/people_util'); const compose = require('mail/mail_compose'); +// Global state to pass reply context from MessageView to compose +let pendingReplyContext = null; +const setPendingReplyContext = (ctx) => { pendingReplyContext = ctx; }; +const getPendingReplyContext = () => { + const ctx = pendingReplyContext; + pendingReplyContext = null; + return ctx; +}; + // rsmail.h const RS_MSG_BOXMASK = 0x000f; @@ -159,8 +168,20 @@ const AttachmentSection = () => { const MessageView = () => { let showCompose = false; // setFunction like react to show/hide popup - function setShowCompose(bool) { + function setShowCompose(bool, msgType = 'compose') { showCompose = bool; + if (bool && msgType !== 'compose') { + // Store the reply context for compose to use + setPendingReplyContext({ + msgType, + toList: MailData.toList, + ccList: MailData.ccList, + from: MailData.sender, + subject: MailData.subject, + replyMessage: document.querySelector('#msgView')?.innerHTML || '', + timeStamp: MailData.timeStamp, + }); + } } const MailData = { msgId: '', @@ -241,8 +262,8 @@ const MessageView = () => { m('i.fas.fa-arrow-left') ), m('.msg-view-nav__action', [ - m('button', { onclick: () => setShowCompose(true) }, 'Reply'), - m('button', 'Reply All'), + m('button', { onclick: () => setShowCompose(true, 'reply') }, 'Reply'), + m('button', { onclick: () => setShowCompose(true, 'replyall') }, 'Reply All'), m('button', 'Forward'), m('button', { onclick: confirmMailDelete }, 'Delete'), ]), @@ -449,4 +470,5 @@ module.exports = { RS_MSGTAGTYPE_TODO, RS_MSGTAGTYPE_WORK, BOX_ALL, + MSG_ADDRESS_MODE_CC, }; From 19446a69e5355f4b06a2f9d0f74ba1b401489d0b Mon Sep 17 00:00:00 2001 From: 64johnlee <64lamei@gmail.com> Date: Sun, 3 May 2026 20:40:02 +0800 Subject: [PATCH 02/53] feat(mail): implement Forward functionality - Wire up Forward button in MessageView to pass 'forward' msgType - Compose: clear recipients (To/CC/BCC) since user will choose new ones - Set subject with 'Fwd: ' prefix - Quote original message with '-----Forwarded Message-----' header - Store mailBody in pendingReplyContext for forward --- webui-src/app/mail/mail_compose.js | 47 ++++++++++++++++++++++++++++-- webui-src/app/mail/mail_util.js | 5 ++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/webui-src/app/mail/mail_compose.js b/webui-src/app/mail/mail_compose.js index c0b420ff..e86af7b8 100644 --- a/webui-src/app/mail/mail_compose.js +++ b/webui-src/app/mail/mail_compose.js @@ -46,8 +46,6 @@ const Layout = () => { if (!Data.recipients.cc.sendList.find((u) => u.mGroupId === recipId)) { Data.recipients.cc.sendList.push(user); } - } else if (dest._mode === MSG_ADDRESS_MODE_TO) { - // Don't add to 'to' since sender is already there } } }); @@ -64,6 +62,8 @@ const Layout = () => { Data.identity = Data.ownId.filter((id) => Object.prototype.hasOwnProperty.call(recipientList, id) )[0]; + } else if (msgType === 'forward') { + Data.identity = Data.ownId[0]; } }); } @@ -86,6 +86,49 @@ const Layout = () => { } }); } + } else if (ctx.msgType === 'forward') { + // Forward: clear recipients, set subject with Fwd: prefix, quote original + await loadMailUserDetails('forward', ctx.from._addr_string, {}); + Data.recipients.to.sendList = []; + Data.recipients.cc.sendList = []; + + Object.keys(Data.recipients).forEach((item) => { + Data.recipients[item].inputList = Data.allUsers; + }); + + const { subject, mailBody, timeStamp } = ctx; + const tmb = document.querySelector('#composerMailBody'); + const time = timeStamp.toLocaleTimeString('UTC', { hour: '2-digit', minute: '2-digit' }); + const dateLong = timeStamp.toLocaleDateString('UTC', { + year: 'numeric', + month: 'long', + day: 'numeric', + }); + const forwardHeader = ` + -----Forwarded Message----- +
+ From: + ${rs.userList.userMap[ctx.from._addr_string]} +
+ Sent: + ${dateLong} ${time} +
+ Subject: + ${subject} +
+
+ `; + tmb.innerHTML = ` +
+
+
+ ${forwardHeader} +
+ ${mailBody} +
+
+ `; + Data.subject = subject.substring(0, 4) === 'Fwd: ' ? subject : `Fwd: ${subject}`; } Object.keys(Data.recipients).forEach((item) => { diff --git a/webui-src/app/mail/mail_util.js b/webui-src/app/mail/mail_util.js index 13090d3e..893b659b 100644 --- a/webui-src/app/mail/mail_util.js +++ b/webui-src/app/mail/mail_util.js @@ -171,7 +171,7 @@ const MessageView = () => { function setShowCompose(bool, msgType = 'compose') { showCompose = bool; if (bool && msgType !== 'compose') { - // Store the reply context for compose to use + // Store the reply/forward context for compose to use setPendingReplyContext({ msgType, toList: MailData.toList, @@ -179,6 +179,7 @@ const MessageView = () => { from: MailData.sender, subject: MailData.subject, replyMessage: document.querySelector('#msgView')?.innerHTML || '', + mailBody: MailData.message, timeStamp: MailData.timeStamp, }); } @@ -264,7 +265,7 @@ const MessageView = () => { m('.msg-view-nav__action', [ m('button', { onclick: () => setShowCompose(true, 'reply') }, 'Reply'), m('button', { onclick: () => setShowCompose(true, 'replyall') }, 'Reply All'), - m('button', 'Forward'), + m('button', { onclick: () => setShowCompose(true, 'forward') }, 'Forward'), m('button', { onclick: confirmMailDelete }, 'Delete'), ]), ]), From ae7aff41525af51a08ec4a9ad67c09228f577272 Mon Sep 17 00:00:00 2001 From: 64johnlee <64lamei@gmail.com> Date: Sun, 3 May 2026 20:45:56 +0800 Subject: [PATCH 03/53] feat(mail): add dark mode toggle with persistent preference - Add dark mode CSS styles for mail UI (sidebar, tables, inputs, widgets) - Add toggle button at bottom of sidebar (persisted in localStorage) - Default to dark mode on first load (no preference saved) - Dark mode applies to tab-content, sidebar, tables, inputs, buttons, widgets --- webui-src/app/mail/mail_util.js | 40 ++++++++++++++++++++++----------- webui-src/index.html | 14 ++++++++++-- webui-src/styles.css | 2 +- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/webui-src/app/mail/mail_util.js b/webui-src/app/mail/mail_util.js index 893b659b..aacc2eda 100644 --- a/webui-src/app/mail/mail_util.js +++ b/webui-src/app/mail/mail_util.js @@ -398,24 +398,38 @@ const activeSideLink = { }; const Sidebar = () => { + let darkMode = localStorage.getItem('darkMode') === 'true'; + const toggleDarkMode = () => { + darkMode = !darkMode; + localStorage.setItem('darkMode', darkMode); + document.body.classList.toggle('dark-mode', darkMode); + }; return { view: ({ attrs: { tabs, baseRoute, size } }) => m( - '.sidebar', - tabs.map((panelName, index) => - m( - m.route.Link, - { - class: index === activeSideLink.sideactive ? 'selected-sidebar-link' : '', - onclick: () => { - activeSideLink.sideactive = index; - activeSideLink.quicksideactive = -1; + '.sidebar' + (darkMode ? '.dark-mode' : ''), + [ + tabs.map((panelName, index) => + m( + m.route.Link, + { + class: index === activeSideLink.sideactive ? 'selected-sidebar-link' : '', + onclick: () => { + activeSideLink.sideactive = index; + activeSideLink.quicksideactive = -1; + }, + href: baseRoute + panelName, }, - href: baseRoute + panelName, - }, - size[panelName] > 0 ? `${panelName} (${size[panelName]})` : panelName + size[panelName] > 0 ? `${panelName} (${size[panelName]})` : panelName + ) + ), + m('div', { style: 'margin-top:auto;padding:1rem;border-top:1px solid #333;' }, + m('button', { + onclick: toggleDarkMode, + style: 'width:100%;font-size:0.75rem;padding:0.375rem;' + }, darkMode ? '☀️ Light Mode' : '🌙 Dark Mode') ) - ) + ] ), }; }; diff --git a/webui-src/index.html b/webui-src/index.html index 8828c5e8..8ac8643c 100644 --- a/webui-src/index.html +++ b/webui-src/index.html @@ -9,10 +9,20 @@ - +

Javascript not found!

- +