aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
Diffstat (limited to 'src/features')
-rw-r--r--src/features/messages.js67
1 files changed, 41 insertions, 26 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index a9b21b0..8001d39 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -50,6 +50,7 @@ import { qrSvg } from '../qr.js';
// The coinos community's join material lives in ../community.js — shared
// with the public read-only chat page so the two can never drift.
import { COMMUNITY, EPOCH } from '../community.js';
+import COMMUNITY_DIRECTORY from '../community-directory.json';
const DM_RELAYS = ['wss://relay.coinos.io', 'wss://nos.lol'];
const NOTIFIER = 'https://nwcpush.coinos.io';
const APP_BASE = 'https://v3.coinos.io';
@@ -1857,30 +1858,50 @@ export function messagesFeature(ctx) {
scheduleRepaint();
}
- // ---- community discovery: the Vector Hub's public listing as suggestions
- // on Messages and under Browse. Fetched when Messages opens and kept
- // for the session; every listing is a CORD-05 link, which is exactly what
- // the paste box takes, so a tap previews it like a pasted invite.
+ // Public suggestions render immediately, including on a first/offline visit.
+ // The bundled snapshot is a fallback; the device cache refreshes once a day.
+ // Invite previews still check revocation and expiration on the relays.
const HUB_URL = 'https://vectorapp.io/api/hub';
- const hub = { state: 'idle', list: [] };
+ const HUB_CACHE_KEY = 'coinos-community-directory-v1';
+ const HUB_REFRESH_MS = 86400_000;
+ function normalizeHub(list) {
+ return list.filter((c) => c && c.name && parseInviteLink(c.invite_url || ''))
+ .map((c) => ({
+ name: String(c.name).slice(0, 64), description: String(c.description || '').slice(0, 200),
+ category: String(c.category || '').slice(0, 64), invite: c.invite_url,
+ members: Math.max(0, Number(c.member_count) || 0),
+ icon: /^data:image\//.test(c.icon_url || '') && c.icon_url.length <= 32_768 ? c.icon_url : null,
+ }))
+ .sort((a, b) => b.members - a.members);
+ }
+ const hub = { list: normalizeHub(COMMUNITY_DIRECTORY), at: 0, attempted: false, loading: false };
+ try {
+ const cached = JSON.parse(localStorage.getItem(HUB_CACHE_KEY) || 'null');
+ if (cached && Array.isArray(cached.list) && Number.isFinite(cached.at)) {
+ const list = normalizeHub(cached.list);
+ if (list.length || !cached.list.length) { hub.list = list; hub.at = cached.at; }
+ }
+ } catch {}
async function loadHub() {
- if (hub.state !== 'idle') return;
- hub.state = 'loading'; scheduleRepaint();
+ if (hub.loading || hub.attempted || (hub.at > 0 && Date.now() - hub.at < HUB_REFRESH_MS)) return;
+ hub.attempted = true; hub.loading = true;
try {
const res = await fetch(HUB_URL, { signal: AbortSignal.timeout(20_000) });
if (!res.ok) throw new Error('http ' + res.status);
- const list = await res.json();
- hub.list = (Array.isArray(list) ? list : [])
- .filter((c) => c && c.name && parseInviteLink(c.invite_url || ''))
- .map((c) => ({
- name: String(c.name).slice(0, 64), description: String(c.description || '').slice(0, 200),
- category: String(c.category || ''), invite: c.invite_url, members: Number(c.member_count) || 0,
- icon: /^data:image\//.test(c.icon_url || '') ? c.icon_url : null, official: !!c.official,
- }))
- .sort((a, b) => b.members - a.members);
- hub.state = 'ready';
- } catch { hub.state = 'error'; }
- scheduleRepaint();
+ const raw = await res.json();
+ if (!Array.isArray(raw)) throw new Error('invalid directory');
+ const list = normalizeHub(raw);
+ if (raw.length && !list.length) throw new Error('invalid invites');
+ hub.list = list; hub.at = Date.now();
+ try {
+ // Bound icon sizes so public suggestions do not crowd out wallet data.
+ const cached = list.map((c) => ({ name: c.name, description: c.description, category: c.category,
+ invite_url: c.invite, member_count: c.members, icon_url: c.icon }));
+ localStorage.setItem(HUB_CACHE_KEY, JSON.stringify({ at: hub.at, list: cached }));
+ } catch {}
+ scheduleRepaint();
+ } catch { /* Keep the usable directory if refresh fails or we're offline. */ }
+ finally { hub.loading = false; }
}
function hubRows({ preview = false } = {}) {
const q = preview ? '' : (ui.msgJoinText || '').trim().toLowerCase();
@@ -1908,13 +1929,7 @@ export function messagesFeature(ctx) {
h('div', { class: 'row between', style: 'align-items:baseline' },
h('h3', { style: 'margin:0' }, t(preview ? 'msgPopularCommunities' : 'msgDiscoverTitle')),
preview ? h('button', { class: 'btn-sm', onClick: (e) => { e.stopPropagation(); ui.msgJoinText = ''; ui.msgHomePanel = 'join'; render(); } }, t('msgBrowseCommunities')) : null),
- h('a', { class: 'small muted', href: 'https://vectorapp.io/hub/', target: '_blank', rel: 'noopener noreferrer' }, t('msgHubCredit')),
- hub.state === 'error' ? h('div', { class: 'row gap6', style: 'align-items:center' },
- h('span', { class: 'small muted grow' }, t('msgHubFailed')),
- h('button', { class: 'btn-sm', onClick: (e) => { e.stopPropagation(); hub.state = 'idle'; loadHub().catch(() => {}); } }, t('retry')))
- : hub.state !== 'ready' ? h('div', { class: 'row gap6', style: 'align-items:center;padding:6px 0' }, h('span', { class: 'spinner sm' }), h('span', { class: 'small muted' }, t('msgHubLoading')))
- : h('div', { class: 'list' + (preview ? '' : ' hub-list') }, hubRows({ preview })),
- h('div', { class: 'small muted' }, t('msgWhiteNoiseDiscovery')));
+ h('div', { class: 'list' + (preview ? '' : ' hub-list') }, hubRows({ preview })));
}
function joinFromText(text) {