Index: backend/pgsql.t ================================================================== --- backend/pgsql.t +++ backend/pgsql.t @@ -695,11 +695,11 @@ end; } local sqlvars = {} for i, n in ipairs(lib.store.noticetype.members) do - sqlvars['notice:' .. n] = lib.store.noticetype[n] + sqlvars['notice:' .. n] = lib.store.noticetype[n]:asvalue() end for i, n in ipairs(lib.store.relation.members) do sqlvars['rel:' .. n] = lib.store.relation.idvmap[n] end @@ -849,10 +849,11 @@ p.ptr.accent = r:int(int16,row,12) p.ptr.rtdby = r:int(uint64,row,13) p.ptr.rtact = r:int(uint64,row,14) p.ptr.likes = r:int(uint32,row,15) p.ptr.rts = r:int(uint32,row,16) + p.ptr.isreply = r:bool(row,17) p.ptr.localpost = r:bool(row,0) return p end local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor) @@ -1418,10 +1419,36 @@ actor_purge_uid = [terra( src: &lib.store.source, uid: uint64 ) queries.actor_purge_uid.exec(src,uid) end]; + + actor_notice_enum = [terra( + src: &lib.store.source, + uid: uint64 + ): lib.mem.ptr(lib.store.notice) + var r = queries.actor_notice_enum.exec(src,uid); + if r.sz == 0 then return [lib.mem.ptr(lib.store.notice)].null() end + defer r:free() + + var notes = lib.mem.heapa(lib.store.notice, r.sz) + for i=0, r.sz do + var n = notes.ptr + i + n.kind = r:int(uint16,i,0) + n.when = r:int(int64,i,1) + n.who = r:int(int64,i,2) + n.what = r:int(uint64,i,3) + if n.kind == lib.store.noticetype.reply then + n.reply = r:int(uint64,i,4) + elseif n.kind == lib.store.noticetype.react then + var react = r:_string(i,5) + lib.str.ncpy(n.reaction, react.ptr, lib.math.smallest(react.ct,[(`n.reaction).tree.type.N])) + end + end + + return notes + end]; auth_enum_uid = [terra( src: &lib.store.source, uid: uint64 ): lib.mem.ptr(lib.mem.ptr(lib.store.auth)) Index: backend/schema/pgsql-views.sql ================================================================== --- backend/schema/pgsql-views.sql +++ backend/schema/pgsql-views.sql @@ -72,15 +72,17 @@ discovered bigint, edited bigint, parent bigint, convoheaduri text, chgcount integer, +-- ephemeral accent smallint, rtdby bigint, -- note that these must be 0 if the record rtid bigint, -- in question does not represent an RT! n_likes integer, - n_rts integer + n_rts integer, + isreply bool -- true if parent in (table posts); saves us a bunch of queries ); create or replace function pg_temp.parsavpg_translate_post(parsav_posts,bigint,bigint) returns pg_temp.parsavpg_intern_post as $$ @@ -89,11 +91,12 @@ ($1).subject,($1).acl, ($1).body, ($1).posted, ($1).discovered, ($1).edited, ($1).parent, ($1).convoheaduri,($1).chgcount, coalesce(c.value, -1)::smallint, $2 as rtdby, $3 as rtid, - re.likes, re.rts + re.likes, re.rts, + ($1).parent in (select id from parsav_posts) from parsav_actors as a left join parsav_actor_conf_ints as c on c.key = 'ui-accent' and c.uid = a.id left join pg_temp.parsavpg_post_react_counts as re Index: config.lua ================================================================== --- config.lua +++ config.lua @@ -58,10 +58,17 @@ {'heart.webp', 'image/webp'}; {'retweet.webp', 'image/webp'}; {'padlock.svg', 'image/svg+xml'}; {'warn.svg', 'image/svg+xml'}; {'query.webp', 'image/webp'}; + {'reply.webp', 'image/webp'}; + -- keep in mind before you add anything to this list: these are not + -- just files parsav can access, they are files that are *kept in + -- memory* for fast access the entire time parsav is running, and + -- which need to be loaded into memory before the program can even + -- start. it's imperative to keep these as small and few in number + -- as is realistically possible. }; default_ui_accent = tonumber(default('parsav_ui_default_accent',323)); } if os.getenv('parsav_let_me_be_an_idiot') == "i know what i'm doing" then conf.braingeniousmode = true -- SOUND GENERAL QUARTERS Index: makefile ================================================================== --- makefile +++ makefile @@ -1,9 +1,9 @@ dl = git dbg-flags = $(if $(dbg),-g) -images = static/default-avatar.webp static/query.webp static/heart.webp static/retweet.webp +images = static/default-avatar.webp static/query.webp static/heart.webp static/retweet.webp static/reply.webp #$(addsuffix .webp, $(basename $(wildcard static/*.svg))) styles = $(addsuffix .css, $(basename $(wildcard static/*.scss))) parsav parsavd: parsav.t config.lua pkgdata.lua $(images) $(styles) terra $(dbg-flags) $< Index: parsav.t ================================================================== --- parsav.t +++ parsav.t @@ -240,11 +240,11 @@ elseif #tbl >= 2^16 then ty = uint32 elseif #tbl >= 2^8 then ty = uint16 end local o = { t = ty, members = tbl } local strings = {} for i, name in ipairs(tbl) do - o[name] = i - 1 + o[name] = `[ty]([i - 1]) strings[i] = `[lib.mem.ref(int8)]{ptr=[name], ct=[#name]} end o._str = terra(val: ty) var l = array([strings]) return l[val] @@ -435,10 +435,11 @@ 'render:compose'; 'render:tweet'; 'render:tweet-page'; 'render:user-page'; 'render:timeline'; + 'render:notices'; 'render:docpage'; 'render:conf:profile'; 'render:conf:sec'; ADDED render/notices.t Index: render/notices.t ================================================================== --- render/notices.t +++ render/notices.t @@ -0,0 +1,80 @@ +-- vim: ft=terra +local pstr = lib.mem.ptr(int8) +local P = lib.str.plit +local terra cs(s: rawstring) + return pstr { ptr = s, ct = lib.str.sz(s) } +end + +local terra +render_notices( + co: &lib.srv.convo +): {} + var notes = co.srv:actor_notice_enum(co.who.id) + + if notes.ct == 0 then + co:complain(200,'no news is good news',"you don't have any notices to review") + return + end + defer notes:free() + + var pg: lib.str.acc pg:init(512) defer pg:free() + var pflink: lib.str.acc pflink:init(64) + var body: lib.str.acc body:init(256) + var latest: lib.store.timepoint = 0 + for i=0,notes.ct do + if notes(i).when > latest then latest = notes(i).when end + var who = co.srv:actor_fetch_uid(notes(i).who) defer who:free() + if not who then lib.bail('schema integrity violation: nonexistent actor referenced in notification, this is almost certainly an SQL error or bug in the backend implementation') end + pflink:cue(lib.str.sz(who(0).xid) + 4) + if who(0).origin == 0 then pflink:lpush('/') + else pflink:lpush('/@') end + pflink:push(who(0).xid,0) + var n = data.view.notice { + avatar = cs(who(0).avatar); + nym = lib.render.nym(who.ptr,0,nil,true); + pflink = pstr{ptr = pflink.buf; ct = pflink.sz}; + } + var notweet = true + var what = co.srv:post_fetch(notes(i).what) defer what:free() + switch notes(i).kind do + case lib.store.noticetype.rt then + n.kind = P'rt' + n.act = P'retweeted your post' + end + case lib.store.noticetype.like then + n.kind = P'like' + n.act = P'likes your post' + end + case lib.store.noticetype.reply then + n.kind = P'reply' + n.act = P'replied to your post' + notweet = false + end + else goto skip end + do var idbuf: int8[lib.math.shorthand.maxlen] + var idlen = lib.math.shorthand.gen(notes(i).what, idbuf) + var b = lib.smackdown.html(pstr {ptr=what(0).body,ct=0},true) defer b:free() + body:lpush(' '):ppush(b):lpush('') + end + if not notweet then + var reply = co.srv:post_fetch(notes(i).reply) + lib.render.tweet(co,reply.ptr,&body) + reply:free() + end + n.ref = pstr {ptr = body.buf, ct = body.sz} + + n:append(&pg) + ::skip:: n.nym:free() + pflink:reset() + body:reset() + end + pflink:free() + pg:lpush('
') + co:livepage([lib.srv.convo.page] { + title = P'notices', class = P'notices'; + body = pstr {ptr = pg.buf, ct = pg.sz}; + cache = false; + }, latest) +end + +return render_notices Index: render/profile.t ================================================================== --- render/profile.t +++ render/profile.t @@ -36,11 +36,11 @@ var sn_follows = cs(lib.math.decstr_friendly(stats.follows, sn_posts.ptr - 1)) var sn_followers = cs(lib.math.decstr_friendly(stats.followers, sn_follows.ptr - 1)) var sn_mutuals = cs(lib.math.decstr_friendly(stats.mutuals, sn_followers.ptr - 1)) var bio = lib.str.plit 'tall, dark, and mysterious' if actor.bio ~= nil then - bio = lib.smackdown.html(cs(actor.bio)) + bio = lib.smackdown.html(cs(actor.bio),false) end var fullname = lib.render.nym(actor,0,nil,false) defer fullname:free() var comments: lib.str.acc comments:init(64) if co.srv.cfg.master == actor.id then Index: render/tweet.t ================================================================== --- render/tweet.t +++ render/tweet.t @@ -39,11 +39,11 @@ avistr:compose('/avi/',author.handle) end var timestr: int8[26] lib.osclock.ctime_r(&p.posted, ×tr[0]) for i=0,26 do if timestr[i] == @'\n' then timestr[i] = 0 break end end -- 🙄 - var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0}) + var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0},false) defer bhtml:free() var idbuf: int8[lib.math.shorthand.maxlen] var idlen = lib.math.shorthand.gen(p.id, idbuf) var permalink: lib.str.acc permalink:compose('/post/',{idbuf,idlen}) @@ -56,11 +56,26 @@ avatar = cs(author.avatar); acctlink = cs(author.xid); permalink = permalink:finalize(); attr = pstr{'',0}; stats = pstr{'',0}; + extra = pstr{'',0}; } + if p.isreply then + var parent = co.srv:post_fetch(p.parent) defer parent:free() + if not parent then + lib.bail('schema integrity violation - could not match post to parent') + end + var pauth = co.srv:actor_fetch_uid(parent(0).author) defer pauth:free() + var pidbuf: int8[lib.math.shorthand.maxlen] + var pidlen = lib.math.shorthand.gen(p.parent, pidbuf) + var pa: lib.str.acc pa:init(128) + pa:lpush('in reply to ') + lib.render.nym(pauth.ptr,0,&pa,true) + pa:lpush('') + tpl.extra = pa:finalize() + end if p.rts + p.likes > 0 then var s: lib.str.acc s:init(128) s:lpush('