Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
9a48d1a
feat(mail): implement Reply All functionality
64johnlee May 3, 2026
19446a6
feat(mail): implement Forward functionality
64johnlee May 3, 2026
ae7aff4
feat(mail): add dark mode toggle with persistent preference
64johnlee May 3, 2026
3696573
feat(mail): display avatars in inbox table
64johnlee May 3, 2026
41048cf
feat(mail): add 'Mark as Spam' button in message view
64johnlee May 3, 2026
160c9c4
feat(mail): pagination - load 50 messages at a time with 'Load More' …
64johnlee May 3, 2026
f8dc206
feat(mail): add view mode toggle (side/below) with localStorage persi…
64johnlee May 3, 2026
c771b27
fix(mail): move view mode button inside top-heading for cleaner layout
64johnlee May 3, 2026
cb04771
chore: bump PR
64johnlee May 4, 2026
fbd8eb1
chore: bump PR
64johnlee May 4, 2026
7055b3c
chore: bump PR
64johnlee May 4, 2026
2decc29
chore: bump PR
64johnlee May 4, 2026
b2a5deb
chore: bump PR
64johnlee May 5, 2026
78b93bc
chore: bump PR
64johnlee May 5, 2026
c1dcf59
chore: bump PR
64johnlee May 6, 2026
7190231
chore: bump PR
64johnlee May 6, 2026
bae0a62
chore: bump PR
64johnlee May 7, 2026
c708cab
chore: bump PR
64johnlee May 7, 2026
92d319f
chore: bump PR
64johnlee May 8, 2026
e182345
chore: bump PR
64johnlee May 8, 2026
88016d0
chore: bump PR
64johnlee May 9, 2026
d66c8c1
chore: bump PR
64johnlee May 9, 2026
5206491
chore: bump PR
64johnlee May 10, 2026
8b650c7
chore: bump PR
64johnlee May 10, 2026
a01b134
chore: bump PR
64johnlee May 11, 2026
fbc2287
chore: bump PR
64johnlee May 11, 2026
7d29c42
chore: bump PR
64johnlee May 12, 2026
161d369
chore: bump PR
64johnlee May 12, 2026
d1bd64c
chore: bump PR
64johnlee May 13, 2026
8873b5d
chore: bump PR
64johnlee May 13, 2026
446c0cb
chore: bump PR
64johnlee May 14, 2026
ba1e1eb
chore: bump PR
64johnlee May 14, 2026
1d6e97f
chore: bump PR
64johnlee May 15, 2026
a0e4fb0
chore: bump PR
64johnlee May 15, 2026
74e7cc1
chore: bump PR
64johnlee May 16, 2026
f6d316d
chore: bump PR
64johnlee May 16, 2026
27b459c
chore: bump PR
64johnlee May 17, 2026
3794b56
chore: bump PR
64johnlee May 17, 2026
c5c736f
chore: bump PR
64johnlee May 18, 2026
261bc61
chore: bump PR
64johnlee May 18, 2026
b206e75
chore: bump PR
64johnlee May 19, 2026
6c578a9
chore: bump PR
64johnlee May 19, 2026
4e62e76
chore: bump PR
64johnlee May 20, 2026
e6ccc05
chore: bump PR
64johnlee May 20, 2026
c4151cd
chore: bump PR
64johnlee May 21, 2026
a7bcd18
chore: bump PR
64johnlee May 21, 2026
02e52c0
chore: bump PR
64johnlee May 22, 2026
b7fc70e
chore: bump PR
64johnlee May 22, 2026
0ebbbe1
chore: bump PR
64johnlee May 23, 2026
f891518
chore: bump PR
64johnlee May 23, 2026
74c6c6a
chore: bump PR
64johnlee May 24, 2026
e4f8521
chore: bump PR
64johnlee May 24, 2026
89c3916
chore: bump PR
64johnlee May 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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
180 changes: 165 additions & 15 deletions webui-src/app/mail/mail_compose.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -29,39 +30,115 @@ 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++) {
if (Number(Data.ownId[i]) === 0) {
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-----
<br>
<b>From: </b>
<a href="retroshare://message?id=${ctx.from._addr_string}">${rs.userList.userMap[ctx.from._addr_string]}</a>
<br>
<b>Sent: </b>
<span>${dateLong} ${time}</span>
<br>
<b>Subject: </b>
<span>${subject}</span>
<br>
<br>
`;
tmb.innerHTML = `
<br>
<br>
<div>
${forwardHeader}
<div class="forwarded-message" style="margin-left: 20px;">
${mailBody}
</div>
</div>
`;
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', {
Expand All @@ -73,16 +150,27 @@ const Layout = () => {
-----Original Message-----
<br>
<b>From: </b>
<a href="retroshare://message?id=${senderId}">${rs.userList.userMap[senderId]}</a>
<a href="retroshare://message?id=${ctx.from._addr_string}">${rs.userList.userMap[ctx.from._addr_string]}</a>
<br>
<b>To: </b>
${Object.keys(recipientList).map(
${Object.keys(ctx.toList).map(
(recip) => `
<a href="retroshare://message?id=${recip}">
${rs.userList.userMap[recipientList[recip]._addr_string] || 'Unknown'},
${rs.userList.userMap[ctx.toList[recip]._addr_string] || 'Unknown'},
</a>
`
)}
${ctx.ccList && Object.keys(ctx.ccList).length > 0 ? `
<br>
<b>CC: </b>
${Object.keys(ctx.ccList).map(
(ccId) => `
<a href="retroshare://message?id=${ccId}">
${rs.userList.userMap[ctx.ccList[ccId]._addr_string] || 'Unknown'},
</a>
`
)}
` : ''}
<br>
<br>
<b>Sent: </b>
Expand All @@ -94,7 +182,7 @@ const Layout = () => {
<br>
<span>
On ${timeStamp.toLocaleDateString()} ${time},
<a href="retroshare://message?id=${senderId}">${rs.userList.userMap[senderId]}</a>
<a href="retroshare://message?id=${ctx.from._addr_string}">${rs.userList.userMap[ctx.from._addr_string]}</a>
wrote:
</span>
`;
Expand All @@ -109,6 +197,68 @@ const Layout = () => {
</div>
`;
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-----
<br>
<b>From: </b>
<a href="retroshare://message?id=${senderId}">${rs.userList.userMap[senderId]}</a>
<br>
<b>To: </b>
${Object.keys(recipientList).map(
(recip) => `
<a href="retroshare://message?id=${recip}">
${rs.userList.userMap[recipientList[recip]._addr_string] || 'Unknown'},
</a>
`
)}
<br>
<br>
<b>Sent: </b>
<span>${dateLong} ${time}</span>
<br>
<b>Subject: </b>
<span>${subject}</span>
<br>
<br>
<span>
On ${timeStamp.toLocaleDateString()} ${time},
<a href="retroshare://message?id=${senderId}">${rs.userList.userMap[senderId]}</a>
wrote:
</span>
`;
tmb.innerHTML = `
<br>
<br>
<div>
${replyMessageHeader}
<div class="original-message" style="margin-left: 20px;">
${replyMessage}
</div>
</div>
`;
Data.subject = subject.substring(0, 4) === 'Re: ' ? subject : `Re: ${subject}`;
}
}
}
return {
Expand Down
18 changes: 14 additions & 4 deletions webui-src/app/mail/mail_inbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Loading