mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat(mobile): rooms, live and upload capture, and the altitude-band identity
The Now feed mixes sensor, eye, track, number, witness and room cards across Orbit, Sky, Ground and Inside, ordered so neighbours differ, with composited thumbnails and live public-camera frames. Every situation has a room: typed, place- and time-anchored posts, evidence badges, bridging-rated notes and a cited Known / Disputed / Unknown local synthesis, with cross-tab presence over BroadcastChannel. Example posts are labelled as such. Capture adds Live (10 s segments sealed into a hash chain, shown on the globe as a view frustum) and Upload (EXIF placement, Unverified tier), and an attach sheet that files any capture into a nearby situation. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01VVYRAqiQj9Y1V9ZfG1RcDH
This commit is contained in:
38 files changed
+9837
-1081
No files matched your search
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Cross-tab realtime over BroadcastChannel: presence, posts, reactions and
|
||||
* live broadcasts. Two tabs of the app on one device see each other live,
|
||||
* which is the whole multiplayer loop, minus a server.
|
||||
*/
|
||||
|
||||
import { emitter } from './dom.js';
|
||||
|
||||
const CHANNEL = 'gev-mobile-live-v1';
|
||||
const ME_KEY = 'gev.mobile.me.v1';
|
||||
const HEARTBEAT = 4000;
|
||||
const EXPIRE = 12000;
|
||||
|
||||
function loadMe() {
|
||||
try {
|
||||
const v = JSON.parse(localStorage.getItem(ME_KEY) || 'null');
|
||||
if (v?.handle) return v;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const id = Math.random().toString(36).slice(2, 6);
|
||||
const me = { handle: `@you.${id}`, home: null, createdAt: Date.now() };
|
||||
try {
|
||||
localStorage.setItem(ME_KEY, JSON.stringify(me));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return me;
|
||||
}
|
||||
|
||||
export function createRealtime() {
|
||||
const ev = emitter();
|
||||
const tab = Math.random().toString(36).slice(2, 8);
|
||||
const me = loadMe();
|
||||
const peers = new Map(); // tab → { handle, room, at, watching }
|
||||
const lives = new Map(); // live id → { live, frame, at, viewers:Map }
|
||||
let room = null;
|
||||
let watching = null; // live id this tab is watching
|
||||
let ch = null;
|
||||
try {
|
||||
ch = new BroadcastChannel(CHANNEL);
|
||||
} catch {
|
||||
ch = null;
|
||||
}
|
||||
|
||||
const send = (msg) => {
|
||||
try {
|
||||
ch?.postMessage({ ...msg, tab, handle: me.handle, at: Date.now() });
|
||||
} catch {
|
||||
/* ignore (e.g. un-cloneable) */
|
||||
}
|
||||
};
|
||||
|
||||
function hello() {
|
||||
send({ t: 'here', room, watching, visible: !document.hidden });
|
||||
}
|
||||
|
||||
function prune() {
|
||||
const now = Date.now();
|
||||
let changed = false;
|
||||
for (const [k, p] of peers)
|
||||
if (now - p.at > EXPIRE) {
|
||||
peers.delete(k);
|
||||
changed = true;
|
||||
}
|
||||
for (const [id, l] of lives)
|
||||
if (now - l.at > EXPIRE) {
|
||||
lives.delete(id);
|
||||
ev.emit('live-end', { id });
|
||||
changed = true;
|
||||
} else
|
||||
for (const [v, at] of l.viewers)
|
||||
if (now - at > EXPIRE) l.viewers.delete(v);
|
||||
if (changed) ev.emit('presence', presence(room));
|
||||
}
|
||||
|
||||
ch?.addEventListener('message', (e) => {
|
||||
const m = e.data;
|
||||
if (!m || m.tab === tab) return;
|
||||
if (m.t === 'here' || m.t === 'hello') {
|
||||
const prev = peers.get(m.tab);
|
||||
peers.set(m.tab, {
|
||||
handle: m.handle,
|
||||
room: m.room,
|
||||
at: Date.now(),
|
||||
watching: m.watching,
|
||||
});
|
||||
if (m.watching)
|
||||
for (const l of lives.values())
|
||||
if (l.live.id === m.watching) l.viewers.set(m.tab, Date.now());
|
||||
if (!prev || prev.room !== m.room) ev.emit('presence', presence(room));
|
||||
if (m.t === 'hello') hello();
|
||||
} else if (m.t === 'bye') {
|
||||
peers.delete(m.tab);
|
||||
ev.emit('presence', presence(room));
|
||||
} else if (m.t === 'post') ev.emit('post', m.post);
|
||||
else if (m.t === 'react')
|
||||
ev.emit('react', { room: m.room, kind: m.kind, remote: true });
|
||||
else if (m.t === 'rate') ev.emit('rate', m);
|
||||
else if (m.t === 'live') {
|
||||
const cur = lives.get(m.live.id);
|
||||
lives.set(m.live.id, {
|
||||
live: m.live,
|
||||
frame: m.frame || cur?.frame || null,
|
||||
at: Date.now(),
|
||||
viewers: cur?.viewers || new Map(),
|
||||
});
|
||||
ev.emit('live', { ...m.live, frame: m.frame || cur?.frame || null });
|
||||
} else if (m.t === 'live-end') {
|
||||
lives.delete(m.id);
|
||||
ev.emit('live-end', { id: m.id });
|
||||
} else if (m.t === 'watch') {
|
||||
ev.emit('watch', { live: m.live, tab: m.tab });
|
||||
}
|
||||
});
|
||||
|
||||
send({ t: 'hello', room, watching });
|
||||
setInterval(() => {
|
||||
hello();
|
||||
prune();
|
||||
}, HEARTBEAT);
|
||||
addEventListener('pagehide', () => send({ t: 'bye' }));
|
||||
document.addEventListener('visibilitychange', hello);
|
||||
|
||||
function presence(r) {
|
||||
const list = [{ tab, handle: me.handle, self: true }];
|
||||
for (const [k, p] of peers)
|
||||
if (!r || p.room === r)
|
||||
list.push({ tab: k, handle: p.handle, self: false });
|
||||
return list;
|
||||
}
|
||||
|
||||
return {
|
||||
tab,
|
||||
me,
|
||||
on: ev.on,
|
||||
off: ev.off,
|
||||
setHome(home) {
|
||||
me.home = home;
|
||||
try {
|
||||
localStorage.setItem(ME_KEY, JSON.stringify(me));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
},
|
||||
join(r) {
|
||||
room = r;
|
||||
hello();
|
||||
ev.emit('presence', presence(room));
|
||||
},
|
||||
leave() {
|
||||
room = null;
|
||||
hello();
|
||||
},
|
||||
get room() {
|
||||
return room;
|
||||
},
|
||||
presence,
|
||||
/** Tabs on this device currently anywhere in the app. */
|
||||
tabs: () => peers.size + 1,
|
||||
post: (post) => send({ t: 'post', post }),
|
||||
react: (r, kind) => send({ t: 'react', room: r, kind }),
|
||||
rate: (payload) => send({ t: 'rate', ...payload }),
|
||||
/* live broadcasts */
|
||||
liveTick: (live, frame) => send({ t: 'live', live, frame }),
|
||||
liveEnd: (id) => send({ t: 'live-end', id }),
|
||||
watch(id) {
|
||||
watching = id;
|
||||
hello();
|
||||
},
|
||||
lives: () =>
|
||||
[...lives.values()].map((l) => ({ ...l.live, frame: l.frame })),
|
||||
/** Viewers of my own broadcast (other tabs that report watching it). */
|
||||
viewersOf(id) {
|
||||
let n = 0;
|
||||
const now = Date.now();
|
||||
for (const p of peers.values())
|
||||
if (p.watching === id && now - p.at < EXPIRE) n += 1;
|
||||
return n;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user