diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..f64575ac --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,45 @@ +# bump 1777865733 +# bump 1777865823 +# bump 1777867304 +# bump 1777910634 +# bump 1777953705 +# bump 1777996905 +# bump 1778040107 +# bump 1778083315 +# bump 1778126507 +# bump 1778169706 +# bump 1778212900 +# bump 1778256094 +# bump 1778299290 +# bump 1778342489 +# bump 1778385689 +# bump 1778428890 +# bump 1778472091 +# bump 1778515299 +# bump 1778558477 +# bump 1778601684 +# bump 1778644876 +# bump 1778688083 +# bump 1778731285 +# bump 1778774484 +# bump 1778817681 +# bump 1778860885 +# bump 1778904079 +# bump 1778947280 +# bump 1778990484 +# bump 1779033674 +# bump 1779076878 +# bump 1779120084 +# bump 1779163281 +# bump 1779206483 +# bump 1779249683 +# bump 1779292886 +# bump 1779336077 +# bump 1779379288 +# bump 1779422483 +# bump 1779465682 +# bump 1779508877 +# bump 1779552077 +# bump 1779595276 +# bump 1779638476 +# bump 1779681681 diff --git a/webui-src/app/mail/mail_compose.js b/webui-src/app/mail/mail_compose.js index faff4f95..e86af7b8 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,27 @@ 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); + } + } + } + }); + } + } await peopleUtil.ownIds(async (data) => { Data.ownId = await data; for (let i = 0; i < Data.ownId.length; i++) { @@ -41,27 +58,87 @@ 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]; + } else if (msgType === 'forward') { + Data.identity = Data.ownId[0]; } }); } 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); + } + }); + } + } 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) => { + 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 +150,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 +182,7 @@ const Layout = () => {
On ${timeStamp.toLocaleDateString()} ${time}, - ${rs.userList.userMap[senderId]} + ${rs.userList.userMap[ctx.from._addr_string]} wrote: `; @@ -109,6 +197,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_inbox.js b/webui-src/app/mail/mail_inbox.js index 4cfa3901..a33ba164 100644 --- a/webui-src/app/mail/mail_inbox.js +++ b/webui-src/app/mail/mail_inbox.js @@ -2,25 +2,35 @@ const m = require('mithril'); const util = require('mail/mail_util'); const Layout = () => { + let visibleCount = 50; return { view: (v) => [ - m('.widget__heading', m('h3', 'Inbox')), + m('.widget__heading', m('h3', v.attrs.heading || 'Messages')), m('.widget__body', [ m( util.Table, m( 'tbody', - v.attrs.list.map((msg) => + v.attrs.list.slice(0, visibleCount).map((msg) => m(util.MessageSummary, { details: msg, - category: 'inbox', + category: v.attrs.category || 'inbox', }) ) ) ), + visibleCount < v.attrs.list.length && + m( + 'button', + { + onclick: () => (visibleCount += 50), + style: 'margin-top:0.5rem;display:block;', + }, + `Load More (${v.attrs.list.length - visibleCount} remaining)` + ), ]), ], }; }; -module.exports = Layout; +module.exports = Layout; \ No newline at end of file diff --git a/webui-src/app/mail/mail_resolver.js b/webui-src/app/mail/mail_resolver.js index 7568d847..23a9cea6 100644 --- a/webui-src/app/mail/mail_resolver.js +++ b/webui-src/app/mail/mail_resolver.js @@ -70,12 +70,21 @@ const tagselect = { }; const Layout = () => { let showCompose = false; - // setFunction like react to show/hide popup + let viewMode = localStorage.getItem('mailViewMode') || 'side'; function setShowCompose(bool) { showCompose = bool; } + function cycleViewMode() { + if (viewMode === 'side') viewMode = 'below'; + else if (viewMode === 'below') viewMode = 'side'; + else viewMode = 'side'; + localStorage.setItem('mailViewMode', viewMode); + document.getElementById('mailMainContent').dataset.viewMode = viewMode; + } return { - oninit: () => Messages.load(), + oninit: () => { + document.getElementById('mailMainContent').dataset.viewMode = viewMode; + }, view: (vnode) => { const sectionsSize = { inbox: (Messages.inbox || []).length, @@ -111,7 +120,7 @@ const Layout = () => { }), ]), m( - '.node-panel', + '.node-panel#mailMainContent', m('.widget', [ m.route.get().split('/').length < 4 && m('.top-heading', [ @@ -124,6 +133,14 @@ const Layout = () => { [tagselect.opts.map((opt) => m('option', { value: opt }, opt.toLocaleString()))] ), m(util.SearchBar, { list: {} }), + m( + 'button.view-mode-btn', + { + onclick: cycleViewMode, + title: 'Toggle view mode', + }, + viewMode === 'side' ? '⇢ Side' : '⇣ Below' + ), ]), vnode.children, ]) @@ -133,7 +150,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')) ) ), @@ -148,6 +169,7 @@ module.exports = { if (Object.prototype.hasOwnProperty.call(attrs, 'msgId')) { return m(Layout, m(util.MessageView, { msgId })); } + const tabLabel = tab ? tab.charAt(0).toUpperCase() + tab.slice(1) : 'Messages'; return m( Layout, m(sections[tab] || sectionsquickview[tab], { @@ -156,6 +178,8 @@ module.exports = { const msgBDate = new Date((msgB.ts.xint64 || 0) * 1000); return msgADate < msgBDate; }), + heading: tabLabel, + category: tab, }) ); }, diff --git a/webui-src/app/mail/mail_util.js b/webui-src/app/mail/mail_util.js index ca5511f2..dab0b3bd 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; @@ -91,6 +100,10 @@ const MessageSummary = () => { m.route.set('/mail/:tab/:msgId', { tab: v.attrs.category, msgId: details.msgId }), }, [ + m('td', peopleUtil.UserAvatar, { + avatar: fromUserInfo && fromUserInfo.mAvatar, + firstLetter: fromUserInfo && fromUserInfo.mNickname ? fromUserInfo.mNickname[0].toUpperCase() : '?', + }), m( 'td', m(`input.star-check[type=checkbox][id=msg-${details.msgId}]`, { checked: isStarred }), @@ -159,8 +172,21 @@ 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/forward context for compose to use + setPendingReplyContext({ + msgType, + toList: MailData.toList, + ccList: MailData.ccList, + from: MailData.sender, + subject: MailData.subject, + replyMessage: document.querySelector('#msgView')?.innerHTML || '', + mailBody: MailData.message, + timeStamp: MailData.timeStamp, + }); + } } const MailData = { msgId: '', @@ -186,12 +212,29 @@ const MessageView = () => { m.route.set('/mail/:tab', { tab: m.route.param().tab }); }); } + function markAsSpam() { + rs.rsJsonApiRequest('/rsMail/setMessageSpam', { msgId: MailData.msgId, bSpam: true }).then((res) => { + widget.popupMessage( + m('.widget', [ + m('.widget__heading', m('h3', res.body.retval ? 'Marked as Spam' : 'Error')), + m('.widget__body', m('p', res.body.retval ? 'Mail moved to Spam.' : 'Error marking as spam.')) + ]) + ); + m.route.set('/mail/:tab', { tab: m.route.param().tab }); + }); + } function confirmMailDelete() { widget.popupMessage([ m('p', 'Are you sure you want to delete this mail?'), m('button', { onclick: deleteMail }, 'Delete'), ]); } + function confirmMarkSpam() { + widget.popupMessage([ + m('p', 'Mark this mail as spam?'), + m('button', { onclick: markAsSpam }, 'Mark as Spam'), + ]); + } return { oninit: async (v) => { @@ -241,9 +284,10 @@ 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', 'Forward'), + m('button', { onclick: () => setShowCompose(true, 'reply') }, 'Reply'), + m('button', { onclick: () => setShowCompose(true, 'replyall') }, 'Reply All'), + m('button', { onclick: () => setShowCompose(true, 'forward') }, 'Forward'), + m('button', { onclick: confirmMarkSpam }, 'Spam'), m('button', { onclick: confirmMailDelete }, 'Delete'), ]), ]), @@ -343,6 +387,7 @@ const Table = () => { view: (v) => m('table.mails', [ m('tr', [ + m('th[title=avatar]', m('i.fas.fa-user')), m('th[title=starred]', m('i.fas.fa-star')), m('th[title=attachments]', m('i.fas.fa-paperclip')), m('th', 'Subject'), @@ -376,24 +421,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') ) - ) + ] ), }; }; @@ -449,4 +508,5 @@ module.exports = { RS_MSGTAGTYPE_TODO, RS_MSGTAGTYPE_WORK, BOX_ALL, + MSG_ADDRESS_MODE_CC, }; 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!

- +