/**
* You (DESIGN.md §12.2, §12.7): Signal with its receipts, First eyes
* bylines, your sealed captures and uploads, your posts and holograms,
* what you watch, calm-by-default notifications, location precision, the
* privacy line we don't cross, and settings.
*/
import { icon, FAMILY_ICON } from './icons.js';
import { $, $$, esc, ago, haptic, store, applyMotionPref } from './dom.js';
import { all, get } from '../capture/db.js';
import { deviceKey } from '../capture/provenance.js';
import { KIND, badgesFor } from './rooms.js';
import { llShort, utc } from './bands.js';
import { RULES, tierOf, TIERS } from './signal.js';
/** Shown, dimmed and labelled, until the person has receipts of their own. */
const EXAMPLE_LEDGER = [
{
delta: 40,
text: 'Fulfilled Need eyes: “Is the 7th St underpass flooded?”',
m: 1440,
},
{ delta: 50, text: 'First eyes on Heavy rain cell over Houston', m: 1440 },
{
delta: 20,
text: 'ADS-B: 4 aircraft within 8 km at the sealed time',
m: 1441,
},
{
delta: 5,
text: '@wxdan (Corroborated, ×1.9) corroborated your capture',
m: 1420,
},
{
delta: -25,
text: 'A bridged correction: your “Cat 5” claim used the wrong scale',
m: 4320,
},
];
const NOTIFY = [
{
id: 'severe',
label: 'Severe, near me',
sub: 'Only things that can hurt people, inside your alert radius',
},
{
id: 'notable',
label: 'Notable',
sub: 'Plus wonder: ISS passes, launches, aurora over you',
},
{
id: 'all',
label: 'Everything',
sub: 'Every situation inside the radius. Not recommended.',
},
];
export function createYou(
view,
{
bindScroll,
getSituations,
onOpenSituation,
onVerify,
onCredits,
rooms,
rt,
onOpenRoom,
onLocate,
core: coreIn,
},
) {
let core = coreIn || null;
const getCore = async () =>
(core =
core ||
(await import('./social.js')).socialCore({ rooms, rt, getSituations }));
let urls = [];
view.addEventListener(
'scroll',
() => view.classList.toggle('is-scrolled', view.scrollTop > 44),
{ passive: true },
);
bindScroll(view);
async function render() {
urls.forEach((u) => URL.revokeObjectURL(u));
urls = [];
const [captures, key, sits, uploads, posts] = await Promise.all([
all('captures').catch(() => []),
deviceKey().catch(() => null),
getSituations().catch(() => []),
all('uploads').catch(() => []),
rooms.myPosts().catch(() => []),
]);
const c = await getCore();
const [score, receipts] = await Promise.all([
c.signal.score(),
c.signal.receipts(),
]);
const tier = tierOf(score);
const firsts = receipts.filter((r) => r.rule === 'first');
const exampleLedger = !receipts.length;
const ledger = exampleLedger
? EXAMPLE_LEDGER.map((x) => ({
...x,
at: Date.now() - x.m * 60e3,
example: true,
}))
: receipts;
const loc = c.myLoc();
const needs = c.needs.near(loc, 60).filter((x) => x.status === 'open');
const notify = store.get('notifyLevel') || 'severe';
const asks = store.get('needAsks') || 'daily';
const quiet = store.get('quietHours') ?? true;
const prec = store.get('precisionDefault') || '100m';
const watchSummary = c.ctx.watch?.summary?.() || null;
uploads.sort((a, b) => b.addedAt - a.addedAt);
posts.sort((a, b) => b.at - a.at);
const sitOf = (id) => sits.find((x) => String(x.id) === id);
const upThumbs = uploads.slice(0, 12).map((u) => {
if (!u.blob) return '';
const url = URL.createObjectURL(u.blob);
urls.push(url);
return url;
});
const holos = posts.filter((p) => p.hologram);
const home = rt.me.home;
captures.sort((a, b) => b.seq - a.seq);
const followed = store.get('followed') || [];
const fol = followed
.map((id) => sits.find((s) => String(s.id) === id))
.filter(Boolean);
const contacts = followed.filter((id) => id.startsWith('contact:'));
const thumbs = await Promise.all(
captures.slice(0, 30).map(async (c) => {
const m = await get('media', c.id).catch(() => null);
if (!m?.blob) return '';
const u = URL.createObjectURL(m.blob);
urls.push(u);
return u;
}),
);
const q = store.get('quality');
const rm = store.get('reduceMotion');
const radius = store.get('alertRadiusKm');
view.innerHTML = `
Signal rises when your sealed captures are corroborated by people and by sensors, and falls when your claims are corrected. It weighs your votes and notes. No follower counts, ever.
Tiers: ${TIERS.map((t) => `${esc(t.name)} ${t.min}+ (×${t.weight})`).join(' · ')}
God’s Eye mobile prototype. The window through which you see the world, and the eye through which the world sees itself.
`; $$('.segmented', view).forEach(placeThumb); } function placeThumb(seg) { const b = $('[aria-pressed="true"]', seg) || $('button', seg); const th = $('.seg-thumb', seg); if (!b || !th) return; th.style.width = `${b.offsetWidth}px`; th.style.transform = `translateX(${b.offsetLeft}px)`; } view.addEventListener('click', async (e) => { const nd = e.target.closest('[data-need-act]'); if (nd) { (await getCore()).needs.act(nd.dataset.needAct, nd.dataset.need); return; } if (e.target.closest('[data-act="watch"]')) { (await getCore()).ctx.watch?.open?.(); return; } const ref = e.target.closest('[data-ref-room]'); if (ref && ref.dataset.refRoom) { const sits = await getSituations(); const st = sits.find((x) => String(x.id) === ref.dataset.refRoom); if (st) onOpenRoom(st, ref.dataset.refPost || undefined); return; } const cap = e.target.closest('[data-cap]'); if (cap) { const c = (await all('captures')).find((x) => x.id === cap.dataset.cap); if (c) onVerify(c); return; } const sit = e.target.closest('[data-sit]'); if (sit) { const s = (await getSituations()).find( (x) => String(x.id) === sit.dataset.sit, ); if (s) onOpenSituation(s); return; } const segBtn = e.target.closest('.segmented[data-set] button'); if (segBtn) { const seg = segBtn.parentElement; $$('button', seg).forEach((b) => b.setAttribute('aria-pressed', String(b === segBtn)), ); placeThumb(seg); store.set(seg.dataset.set, segBtn.dataset.v); if (seg.dataset.set === 'reduceMotion') applyMotionPref(); if (seg.dataset.set === 'notifyLevel') render(); haptic('tick'); return; } const sw = e.target.closest('[data-switch]'); if (sw) { const on = sw.getAttribute('aria-checked') !== 'true'; sw.setAttribute('aria-checked', String(on)); store.set(sw.dataset.switch, on); haptic('tick'); return; } if (e.target.closest('[data-act="credits"]')) onCredits(); const pr = e.target.closest('[data-post-room]'); if (pr) { const s = (await getSituations()).find( (x) => String(x.id) === pr.dataset.postRoom, ); if (s) onOpenRoom(s, pr.dataset.post); return; } if (e.target.closest('[data-act="home"]')) { const loc = await onLocate(); if (loc) { rt.setHome({ lat: Math.round(loc.lat * 10) / 10, lon: Math.round(loc.lon * 10) / 10, }); haptic('success'); render(); } } }); view.addEventListener('input', (e) => { if (!e.target.matches('.slider')) return; const v = Number(e.target.value); e.target.style.setProperty('--p', `${((v - 25) / 1975) * 100}%`); $('[data-radius]', view).textContent = `${v.toLocaleString()} km`; }); view.addEventListener('change', (e) => { if (e.target.matches('.slider')) store.set('alertRadiusKm', Number(e.target.value)); }); new ResizeObserver(() => $$('.segmented', view).forEach(placeThumb)).observe( view, ); // Signal changes (a corroboration, a fulfilled request) refresh You live. getCore() .then((c) => c.signal.on('change', () => { if (view.isConnected && !view.hidden) render(); }), ) .catch(() => {}); return { render }; }