Sponsor to downline messaging: lineage view, daily broadcast (email+inbox), login modal

- Downline resolver (3 levels); email visible for directs only, username+ID deeper
- Broadcast composer (rich editor) to directs or full downline, 1/day cap, sends email + on-site inbox
- Upline messages inbox in My line; unmissable sign-in modal for unread, ack marks read
- messages.js dual-mode store; sponsor_messages table; sanitizeRich reused

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-06 08:39:06 -05:00
parent 289538dbeb
commit 7701a29169
14 changed files with 344 additions and 27 deletions
+25 -1
View File
@@ -261,6 +261,30 @@ async function setUsername(email, username) {
async function setMemberId(email, id) { return impl().setMemberId(normEmail(email), Number(id) || 0); }
async function namesForMembers(ids) { return impl().namesForMembers([...new Set(ids)].filter(n => n > 0)); }
async function listByReferrer(refs) { return impl().listByReferrer(refs || []); }
// the tokens a member could have been joined under (code, username, or numeric id)
function refTokens(a) {
return [a.code, a.username, a.memberId ? String(a.memberId) : null].filter(Boolean).map(String);
}
// walk the downline breadth-first to `depth` levels. Returns
// [{ level, members:[pub...] }]; emails are included on pub but the CALLER
// decides who may see them (directs only, per product rule).
async function downline(email, depth = 3) {
const root = await byEmail(email);
if (!root) return [];
const seen = new Set([String(root.email)]);
const levels = [];
let frontier = [root];
for (let lvl = 1; lvl <= depth; lvl++) {
const toks = [...new Set(frontier.flatMap(refTokens))];
if (!toks.length) break;
const kids = (await listByReferrer(toks)).filter(k => !seen.has(String(k.email)));
if (!kids.length) break;
kids.forEach(k => seen.add(String(k.email)));
levels.push({ level: lvl, members: kids });
frontier = kids;
}
return levels;
}
async function linkWallet(email, address) {
const a = normAddr(address);
if (!/^0x[0-9a-f]{40}$/.test(a)) return { error: 'Bad wallet address.' };
@@ -269,6 +293,6 @@ async function linkWallet(email, address) {
async function count() { return impl().count(); }
module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, byUsername,
setUsername, setMemberId, namesForMembers, listByReferrer, linkWallet, count,
setUsername, setMemberId, namesForMembers, listByReferrer, downline, linkWallet, count,
setLineBanner: (e, b, t) => impl().setLineBanner(String(e || '').toLowerCase(), b, t),
byMemberId: id => impl().byMemberId(id) };