-- app state + messages -- screens scr_boot=0 scr_timeline=1 scr_thread=2 scr_compose=3 scr_auth=4 scr_demo=5 scr_splash=6 scr=scr_splash scr_wt=-1 -- time() a screen change started its slide (-1 = none yet) lastscr=-1 -- last scr seen, for edge-detecting screen changes trans_dir=1 -- 1=forward (cards from right), -1=back (cards from left) prev_was_thread=false -- title to roll out on next transition prev_was_compose=false -- true when closing the compose drawer feed={} -- timeline posts thread={} -- open thread posts posts=feed -- active list pointer sel=1 scroll=0 prev_sel=1 prev_scroll=0 just_switched=false discard_loading=false -- true while discarding an in-flight REQ_MORE after screen change thread_focus=0 -- refid to auto-focus after a thread loads (0 = none) loading=true hasmore=false status="connecting to bluesky..." status_t=0 stik=0 -- compose state draft="" reply_to=0 -- 0 = new post, else refid quote_of=0 -- 0 = not a quote, else the quoted post's refid quote_name="" -- display name of the quoted post's author (compose header) maxchars=300 theme=0 -- 0=day,1=night last_pt=-1 -- last page-theme bit seen on pin 126 (edge-detect) mi_track=-1 mi_theme=-1 -- last track/theme reflected in the pause menu labels -- feed switcher: names from js (idx 1=following). active_feed is the picked -- index; loaded_feed is what the timeline currently shows (reload when they -- differ); mi_feed shadows the pause-menu label registration. feeds={"following"} feedsm={"following"} active_feed=1 loaded_feed=1 mi_feed=-1 alt_ref=0 -- refid whose alt-text marquee clock is running alt_t0=0 -- time() the active post's alt marquee started function set_status(s) status=s status_t=time() end function parse_post(s) local f=split(s,"\1",false) return { name=f[1] or "?", handle=f[2] or "", reltime=f[3] or "", text=f[4] or "", likes=f[5] or "0", reposts=f[6] or "0", replies=f[7] or "0", refid=tonum(f[8]) or 0, depth=tonum(f[9]) or 0, -- img_state: 0=no image, 1=loading, 2=ready img_state=(tonum(f[10]) or 0)==1 and 1 or 0, linktext=f[11] or "", liked=(tonum(f[12]) or 0)==1, -- viewer already liked this post? alt=f[13] or "", -- image alt text (marquee'd when long + selected) reposted=(tonum(f[14]) or 0)==1, -- viewer already reposted this post? avatar_pixels=nil, } end -- run fn(p) on every post matching id across the timeline + open thread - the -- same id can exist in both when uriRefs reuses a timeline id in a thread post. function eachpost(id,fn) for l in all({feed,thread}) do for p in all(l) do if p.refid==id then fn(p) end end end end -- unpack 2px/byte nibble-packed bytes (hi nibble first) into a 0-15 pixel list function unpack_px(packed) local px={} for b in all(packed) do add(px,(b>>4)&0xf) add(px,b&0xf) end return px end -- update all posts with this refid (image/avatar can land in feed + thread both); -- px is unpacked once on first match, then shared via the closure upvalue. function on_image(refid,packed) local px eachpost(refid,function(p) px=px or unpack_px(packed) p.pixels=px p.img_state=2 end) end function on_image_fail(refid) eachpost(refid,function(p) p.img_state=0 end) end function on_avatar(refid,packed) local px eachpost(refid,function(p) px=px or unpack_px(packed) p.avatar_pixels=px end) end function on_message(op,arg,s) if op==op_demo_stop then exit_demo() return end if not ONLINE then return end if op==op_resp_post then if not discard_loading then add(posts,parse_post(s)) loading=false end elseif op==op_resp_end then if discard_loading then discard_loading=false -- REQ_MORE fully discarded; next response is REQ_THREAD else loading=false hasmore=(arg!=0) if #posts>0 then sfx(8) end -- new-posts chime -- auto-focus a specific post after a thread loads (e.g. self-reply from timeline) if scr==scr_thread and thread_focus>0 then for i=1,#thread do if thread[i].refid==thread_focus then sel=i scroll=max(0,i-2) break end end thread_focus=0 end end elseif op==op_resp_ok then set_status"posted!" reset_draft() sfx(6) -- success open_timeline(true) elseif op==op_resp_error then loading=false set_status(sub(s,1,40)) sfx(7) -- error -- a failed send leaves the draft intact: drop back into compose so it can -- be edited and resent (a fetch error has no draft, so this is a no-op). if #draft>0 then scr=scr_compose poke(0x5f2d,1) end elseif op==op_resp_feeds then -- rebuild the feed switcher from js: names arrive interleaved [header,menu,...]. -- keep active/loaded in range and equal so arriving feeds never trigger a -- spurious reload; mi_feed=-1 forces re-register. local a=split(s,"\1",false) feeds={} feedsm={} for i=1,#a,2 do add(feeds,a[i]) add(feedsm,a[i+1]) end active_feed=mid(1,active_feed,#feeds) loaded_feed=active_feed mi_feed=-1 elseif op==op_resp_auth_lost then scr=scr_auth end end