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!
-
+