Merge branch 'dev'
This commit is contained in:
commit
2c710bef68
|
|
@ -134,7 +134,8 @@ export class FolkCampaignWizard extends HTMLElement {
|
|||
}
|
||||
|
||||
private async apiFetch(path: string, opts: RequestInit = {}): Promise<Response> {
|
||||
const token = (window as any).__authToken || localStorage.getItem('encryptid-token') || '';
|
||||
let token = '';
|
||||
try { const s = JSON.parse(localStorage.getItem('encryptid_session') || ''); token = s.accessToken || ''; } catch {}
|
||||
return fetch(`${this.basePath}${path}`, {
|
||||
...opts,
|
||||
headers: {
|
||||
|
|
|
|||
|
|
@ -146,8 +146,9 @@ interface BalanceItem {
|
|||
export async function enrichWithPrices(
|
||||
balances: BalanceItem[],
|
||||
chainId: string,
|
||||
options?: { filterSpam?: boolean },
|
||||
): Promise<BalanceItem[]> {
|
||||
// Skip testnets and unsupported chains
|
||||
// Skip testnets and unsupported chains — no CoinGecko data to verify against
|
||||
if (!CHAIN_PLATFORM[chainId] && !NATIVE_COIN_ID[chainId]) return balances;
|
||||
|
||||
// Check if any balance actually needs pricing
|
||||
|
|
@ -165,7 +166,7 @@ export async function enrichWithPrices(
|
|||
try {
|
||||
const priceData = await fetchChainPrices(chainId, tokenAddresses);
|
||||
|
||||
return balances.map((b) => {
|
||||
const enriched = balances.map((b) => {
|
||||
// Skip if already has a real fiat value
|
||||
if (b.fiatBalance && b.fiatBalance !== "0" && parseFloat(b.fiatBalance) > 0) {
|
||||
return b;
|
||||
|
|
@ -194,6 +195,21 @@ export async function enrichWithPrices(
|
|||
fiatBalance: String(fiatValue),
|
||||
};
|
||||
});
|
||||
|
||||
if (options?.filterSpam) {
|
||||
return enriched.filter((b) => {
|
||||
// Native tokens always pass
|
||||
if (!b.tokenAddress || b.tokenAddress === "0x0000000000000000000000000000000000000000") return true;
|
||||
// CoinGecko recognized this token
|
||||
if (priceData.prices.has(b.tokenAddress.toLowerCase())) return true;
|
||||
// Safe API independently valued it at >= $1
|
||||
if (parseFloat(b.fiatBalance || "0") >= 1) return true;
|
||||
// Unknown ERC-20 with no verified value = spam
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
return enriched;
|
||||
} catch (e) {
|
||||
console.warn(`[price-feed] Failed to enrich prices for chain ${chainId}:`, e);
|
||||
return balances;
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ routes.get("/api/safe/:chainId/:address/balances", async (c) => {
|
|||
fiatBalance: item.fiatBalance || "0",
|
||||
fiatConversion: item.fiatConversion || "0",
|
||||
}));
|
||||
const enriched = (await enrichWithPrices(data, chainId))
|
||||
const enriched = (await enrichWithPrices(data, chainId, { filterSpam: true }))
|
||||
.filter(b => BigInt(b.balance || "0") > 0n);
|
||||
c.header("Cache-Control", "public, max-age=30");
|
||||
return c.json(enriched);
|
||||
|
|
@ -600,7 +600,7 @@ routes.get("/api/eoa/:chainId/:address/balances", async (c) => {
|
|||
|
||||
await Promise.allSettled(promises);
|
||||
|
||||
const enriched = await enrichWithPrices(balances, chainId);
|
||||
const enriched = await enrichWithPrices(balances, chainId, { filterSpam: true });
|
||||
c.header("Cache-Control", "public, max-age=30");
|
||||
return c.json(enriched);
|
||||
});
|
||||
|
|
@ -661,7 +661,7 @@ routes.get("/api/eoa/:address/all-balances", async (c) => {
|
|||
|
||||
await Promise.allSettled(tokenPromises);
|
||||
if (chainBalances.length > 0) {
|
||||
const enriched = await enrichWithPrices(chainBalances, chainId);
|
||||
const enriched = await enrichWithPrices(chainBalances, chainId, { filterSpam: true });
|
||||
results.push({ chainId, chainName: info.name, balances: enriched });
|
||||
}
|
||||
})
|
||||
|
|
@ -700,7 +700,7 @@ routes.get("/api/safe/:address/all-balances", async (c) => {
|
|||
})).filter((b: BalanceItem) => BigInt(b.balance || "0") > 0n);
|
||||
|
||||
if (chainBalances.length > 0) {
|
||||
const enriched = await enrichWithPrices(chainBalances, chainId);
|
||||
const enriched = await enrichWithPrices(chainBalances, chainId, { filterSpam: true });
|
||||
results.push({ chainId, chainName: info.name, balances: enriched });
|
||||
}
|
||||
} catch {}
|
||||
|
|
|
|||
|
|
@ -1594,7 +1594,7 @@ app.post("/api/blender-gen", async (c) => {
|
|||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
model: process.env.OLLAMA_MODEL || "qwen2.5:14b",
|
||||
model: process.env.OLLAMA_MODEL || "qwen2.5-coder:7b",
|
||||
prompt: `Generate a Blender Python script that creates: ${prompt}\n\nThe script should:\n- Import bpy\n- Clear the default scene\n- Create the described objects with materials\n- Set up basic lighting and camera\n- Render to /tmp/render.png at 1024x1024\n\nOnly output the Python code, no explanations.`,
|
||||
stream: false,
|
||||
}),
|
||||
|
|
@ -1606,6 +1606,9 @@ app.post("/api/blender-gen", async (c) => {
|
|||
// Extract code block if wrapped in markdown
|
||||
const codeMatch = script.match(/```(?:python)?\n([\s\S]*?)```/);
|
||||
if (codeMatch) script = codeMatch[1].trim();
|
||||
} else {
|
||||
const errText = await llmRes.text().catch(() => "");
|
||||
console.error(`[blender-gen] Ollama ${llmRes.status}: ${errText}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[blender-gen] LLM error:", e);
|
||||
|
|
|
|||
|
|
@ -413,12 +413,15 @@ export function renderShell(opts: ShellOptions): string {
|
|||
if (!raw) return;
|
||||
var session = JSON.parse(raw);
|
||||
if (!session || !session.accessToken) return;
|
||||
fetch('/' + '${escapeAttr(spaceSlug)}' + '/invite/accept', {
|
||||
fetch('/api/spaces/' + encodeURIComponent('${escapeAttr(spaceSlug)}') + '/invite/accept', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + session.accessToken },
|
||||
body: JSON.stringify({ inviteToken: inviteToken }),
|
||||
}).then(function(res) { return res.json(); }).then(function(data) {
|
||||
if (data.ok) { window.location.reload(); }
|
||||
if (data.ok) {
|
||||
var slug = data.spaceSlug || '${escapeAttr(spaceSlug)}';
|
||||
window.location.href = 'https://' + slug + '.rspace.online';
|
||||
}
|
||||
});
|
||||
} catch(e) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2194,7 +2194,7 @@ spaces.post("/:slug/invite", async (c) => {
|
|||
spaceSlug: slug,
|
||||
actorDid: claims.sub,
|
||||
actorUsername: claims.username,
|
||||
actionUrl: `/rspace`,
|
||||
actionUrl: `https://${slug}.rspace.online`,
|
||||
metadata: { role },
|
||||
}).catch(() => {});
|
||||
|
||||
|
|
@ -2209,15 +2209,21 @@ spaces.post("/:slug/invite", async (c) => {
|
|||
if (inviteTransport) {
|
||||
try {
|
||||
const spaceUrl = `https://${slug}.rspace.online`;
|
||||
const inviterName = claims.username || "an admin";
|
||||
await inviteTransport.sendMail({
|
||||
from: process.env.SMTP_FROM || "rSpace <noreply@rmail.online>",
|
||||
to: body.email,
|
||||
subject: `You've been added to "${slug}" on rSpace`,
|
||||
html: [
|
||||
`<p>You've been added to <strong>${slug}</strong> as a <strong>${role}</strong>.</p>`,
|
||||
`<p><a href="${spaceUrl}" style="display:inline-block;padding:12px 24px;background:#14b8a6;color:white;border-radius:8px;text-decoration:none;font-weight:600;">Open Space</a></p>`,
|
||||
`<p style="color:#64748b;font-size:12px;">rSpace — collaborative knowledge work</p>`,
|
||||
].join("\n"),
|
||||
subject: `${inviterName} added you to "${slug}" on rSpace`,
|
||||
html: `
|
||||
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:520px;margin:0 auto;padding:2rem;">
|
||||
<h2 style="color:#1a1a2e;margin-bottom:0.5rem;">You've been added to ${slug}</h2>
|
||||
<p style="color:#475569;line-height:1.6;"><strong>${inviterName}</strong> added you to the <strong>${slug}</strong> space as a <strong>${role}</strong>.</p>
|
||||
<p style="color:#475569;line-height:1.6;">You now have access to all the collaborative tools in this space — notes, maps, voting, calendar, and more.</p>
|
||||
<p style="text-align:center;margin:2rem 0;">
|
||||
<a href="${spaceUrl}" style="display:inline-block;padding:12px 28px;background:linear-gradient(135deg,#14b8a6,#0d9488);color:white;border-radius:8px;text-decoration:none;font-weight:600;font-size:1rem;">Open ${slug}</a>
|
||||
</p>
|
||||
<p style="color:#94a3b8;font-size:0.85rem;">rSpace — collaborative knowledge work</p>
|
||||
</div>`,
|
||||
});
|
||||
} catch (emailErr: any) {
|
||||
console.error("Direct-add email notification failed:", emailErr.message);
|
||||
|
|
@ -2309,7 +2315,7 @@ spaces.post("/:slug/members/add", async (c) => {
|
|||
spaceSlug: slug,
|
||||
actorDid: claims.sub,
|
||||
actorUsername: claims.username,
|
||||
actionUrl: `/rspace`,
|
||||
actionUrl: `https://${slug}.rspace.online`,
|
||||
metadata: { role },
|
||||
}).catch(() => {});
|
||||
|
||||
|
|
|
|||
|
|
@ -5407,22 +5407,28 @@ app.post('/api/invites/identity', async (c) => {
|
|||
const joinLink = `https://auth.rspace.online/join?token=${encodeURIComponent(token)}`;
|
||||
if (smtpTransport) {
|
||||
try {
|
||||
const spaceInfo = spaceSlug
|
||||
? `<p style="color:#475569;line-height:1.6;">You'll automatically join the <strong>${escapeHtml(spaceSlug)}</strong> space as a <strong>${escapeHtml(spaceRole || 'member')}</strong>, with access to collaborative notes, maps, voting, calendar, and more.</p>`
|
||||
: `<p style="color:#475569;line-height:1.6;">rSpace is a suite of privacy-first collaborative tools — notes, maps, voting, calendar, wallet, and more — powered by passkey authentication (no passwords).</p>`;
|
||||
const subjectLine = spaceSlug
|
||||
? `${payload.username} invited you to join "${spaceSlug}" on rSpace`
|
||||
: `${payload.username} invited you to join rSpace`;
|
||||
await smtpTransport.sendMail({
|
||||
from: CONFIG.smtp.from,
|
||||
to: email,
|
||||
subject: `${payload.username} invited you to join rSpace`,
|
||||
subject: subjectLine,
|
||||
html: `
|
||||
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 520px; margin: 0 auto; padding: 2rem;">
|
||||
<h2 style="color: #1a1a2e;">You've been invited to rSpace</h2>
|
||||
<p><strong>${escapeHtml(payload.username)}</strong> wants you to join the rSpace ecosystem — a suite of privacy-first tools powered by passkey authentication.</p>
|
||||
${message ? `<blockquote style="border-left: 3px solid #7c3aed; padding: 0.5rem 1rem; margin: 1rem 0; color: #475569; background: #f8fafc; border-radius: 0 0.5rem 0.5rem 0;">"${escapeHtml(message)}"</blockquote>` : ''}
|
||||
<p>Click below to claim your identity and set up your passkey:</p>
|
||||
<p style="text-align: center; margin: 2rem 0;">
|
||||
<a href="${joinLink}" style="display: inline-block; padding: 0.85rem 2rem; background: linear-gradient(90deg, #00d4ff, #7c3aed); color: #fff; text-decoration: none; border-radius: 0.5rem; font-weight: 600; font-size: 1rem;">Claim your rSpace</a>
|
||||
<div style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:520px;margin:0 auto;padding:2rem;">
|
||||
<h2 style="color:#1a1a2e;margin-bottom:0.5rem;">${spaceSlug ? `You've been invited to ${escapeHtml(spaceSlug)}` : `You've been invited to rSpace`}</h2>
|
||||
<p style="color:#475569;line-height:1.6;"><strong>${escapeHtml(payload.username)}</strong> wants you to join${spaceSlug ? ` the <strong>${escapeHtml(spaceSlug)}</strong> space on` : ''} rSpace.</p>
|
||||
${message ? `<blockquote style="border-left:3px solid #7c3aed;padding:0.5rem 1rem;margin:1rem 0;color:#475569;background:#f8fafc;border-radius:0 0.5rem 0.5rem 0;">"${escapeHtml(message)}"</blockquote>` : ''}
|
||||
${spaceInfo}
|
||||
<p style="color:#475569;line-height:1.6;">Click below to create your account and set up your passkey:</p>
|
||||
<p style="text-align:center;margin:2rem 0;">
|
||||
<a href="${joinLink}" style="display:inline-block;padding:0.85rem 2rem;background:linear-gradient(135deg,#14b8a6,#0d9488);color:#fff;text-decoration:none;border-radius:0.5rem;font-weight:600;font-size:1rem;">Accept Invitation</a>
|
||||
</p>
|
||||
<p style="color: #94a3b8; font-size: 0.85rem;">This invite expires in 7 days. If you didn't expect this, you can safely ignore it.</p>
|
||||
</div>
|
||||
`,
|
||||
<p style="color:#94a3b8;font-size:0.85rem;">This invite expires in 7 days. No passwords needed — you'll use a passkey to sign in securely.</p>
|
||||
</div>`,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('EncryptID: Failed to send invite email:', (err as Error).message);
|
||||
|
|
|
|||
|
|
@ -1406,6 +1406,84 @@
|
|||
display: none !important;
|
||||
}
|
||||
|
||||
/* First-time forgotten explainer tooltip */
|
||||
#forgotten-explainer {
|
||||
display: none;
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--rs-bg-surface, #1e293b);
|
||||
border: 1px solid var(--rs-border, rgba(255,255,255,0.1));
|
||||
border-radius: 14px;
|
||||
padding: 20px 24px;
|
||||
max-width: 420px;
|
||||
width: calc(100vw - 32px);
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.5);
|
||||
z-index: 300000;
|
||||
color: var(--rs-text-primary, #e2e8f0);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.55;
|
||||
animation: explainer-slide-up 0.35s ease-out;
|
||||
}
|
||||
@keyframes explainer-slide-up {
|
||||
from { opacity: 0; transform: translateX(-50%) translateY(20px); }
|
||||
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
}
|
||||
#forgotten-explainer.visible { display: block; }
|
||||
#forgotten-explainer h4 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 0.95rem;
|
||||
color: var(--rs-text-primary, #e2e8f0);
|
||||
}
|
||||
#forgotten-explainer p {
|
||||
margin: 0 0 8px;
|
||||
color: var(--rs-text-secondary, #94a3b8);
|
||||
}
|
||||
#forgotten-explainer .tip-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
#forgotten-explainer .tip-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 1rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
#forgotten-explainer .tip-label {
|
||||
font-size: 0.82rem;
|
||||
color: var(--rs-text-secondary, #94a3b8);
|
||||
}
|
||||
#forgotten-explainer .tip-label strong {
|
||||
color: var(--rs-text-primary, #e2e8f0);
|
||||
}
|
||||
#forgotten-explainer .proto-badge {
|
||||
display: inline-block;
|
||||
font-size: 0.68rem;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
background: rgba(124,58,237,0.15);
|
||||
color: #a78bfa;
|
||||
margin-left: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#forgotten-explainer .dismiss-btn {
|
||||
display: block;
|
||||
margin: 14px auto 0;
|
||||
padding: 7px 20px;
|
||||
background: rgba(20,184,166,0.12);
|
||||
border: 1px solid rgba(20,184,166,0.25);
|
||||
color: #14b8a6;
|
||||
border-radius: 8px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
#forgotten-explainer .dismiss-btn:hover {
|
||||
background: rgba(20,184,166,0.2);
|
||||
}
|
||||
|
||||
/* Cross-space shape styling — colored border + source badge */
|
||||
.rspace-cross-space-shape {
|
||||
outline: 2px dashed rgba(99, 102, 241, 0.5) !important;
|
||||
|
|
@ -2128,6 +2206,24 @@
|
|||
<!-- Hidden button for JS toggle reference (injected into identity dropdown) -->
|
||||
<button id="toggle-hide-forgotten" style="display:none" title="Hide forgotten items"></button>
|
||||
|
||||
<div id="forgotten-explainer">
|
||||
<h4>👻 Faded shapes are "forgotten"</h4>
|
||||
<p>When you or others close a shape, it doesn't disappear — it fades. This is <strong>collective memory</strong>.</p>
|
||||
<div class="tip-row">
|
||||
<span class="tip-icon">🖱</span>
|
||||
<span class="tip-label"><strong>Right-click</strong> a faded shape to <strong>Remember</strong> it (restore) or <strong>Forget permanently</strong> (delete)</span>
|
||||
</div>
|
||||
<div class="tip-row">
|
||||
<span class="tip-icon">👁</span>
|
||||
<span class="tip-label">Toggle <strong>Hide Forgotten</strong> in your profile menu to show/hide faded shapes</span>
|
||||
</div>
|
||||
<div class="tip-row">
|
||||
<span class="tip-icon">🔮</span>
|
||||
<span class="tip-label">The <strong>Collective Memory</strong> graph <span class="proto-badge">prototype</span> visualizes what the group remembers vs. forgets</span>
|
||||
</div>
|
||||
<button class="dismiss-btn" id="forgotten-explainer-dismiss">Got it</button>
|
||||
</div>
|
||||
|
||||
<div id="memory-panel">
|
||||
<div id="memory-panel-header">
|
||||
<h3>💭 Recent Changes</h3>
|
||||
|
|
@ -6068,6 +6164,33 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying quest
|
|||
if (memoryPanel.classList.contains("open")) renderMemoryPanel();
|
||||
});
|
||||
|
||||
// First-time forgotten explainer tooltip
|
||||
{
|
||||
const SEEN_KEY = 'rspace_forgotten_explainer_seen';
|
||||
let explainerShown = false;
|
||||
|
||||
function showForgottenExplainer() {
|
||||
if (explainerShown || localStorage.getItem(SEEN_KEY)) return;
|
||||
explainerShown = true;
|
||||
// Delay slightly so the faded shape is visible first
|
||||
setTimeout(() => {
|
||||
const el = document.getElementById('forgotten-explainer');
|
||||
if (el) el.classList.add('visible');
|
||||
}, 800);
|
||||
}
|
||||
|
||||
document.getElementById('forgotten-explainer-dismiss')?.addEventListener('click', () => {
|
||||
localStorage.setItem(SEEN_KEY, '1');
|
||||
const el = document.getElementById('forgotten-explainer');
|
||||
if (el) el.classList.remove('visible');
|
||||
});
|
||||
|
||||
sync.addEventListener("shape-state-changed", (e) => {
|
||||
if (e.detail?.state === 'forgotten') showForgottenExplainer();
|
||||
});
|
||||
sync.addEventListener("shape-forgotten", () => showForgottenExplainer());
|
||||
}
|
||||
|
||||
// Re-dispatch comment-pins-changed on window so header bell can update
|
||||
sync.addEventListener("comment-pins-changed", () => {
|
||||
window.dispatchEvent(new CustomEvent("comment-pins-changed"));
|
||||
|
|
|
|||
Loading…
Reference in New Issue