parsav  Check-in [9cc5d48cd8]

Overview
Comment:notifications work now
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 9cc5d48cd80df268d79a98772788050764957de51848144865aa4be80ff72455
User & Date: lexi on 2021-01-06 22:18:59
Other Links: manifest | tags
Context
2021-01-06
22:31
unfuck last commit check-in: 8d8ab01573 user: lexi tags: trunk
22:18
notifications work now check-in: 9cc5d48cd8 user: lexi tags: trunk
2021-01-05
22:48
adjusted bell check-in: 0faa879398 user: lexi tags: trunk
Changes

Modified backend/pgsql.t from [7305c1c258] to [80ca2205e4].

   693    693   			return buf
   694    694   		end
   695    695   	end;
   696    696   }
   697    697   
   698    698   local sqlvars = {}
   699    699   for i, n in ipairs(lib.store.noticetype.members) do
   700         -	sqlvars['notice:' .. n] = lib.store.noticetype[n]
          700  +	sqlvars['notice:' .. n] = lib.store.noticetype[n]:asvalue()
   701    701   end
   702    702   
   703    703   for i, n in ipairs(lib.store.relation.members) do
   704    704   	sqlvars['rel:' .. n] = lib.store.relation.idvmap[n]
   705    705   end
   706    706   
   707    707   local con = symbol(&lib.pq.PGconn)
................................................................................
   847    847   		else p.ptr.chgcount = r:int(uint32,row,11)
   848    848   	end 
   849    849   	p.ptr.accent = r:int(int16,row,12)
   850    850   	p.ptr.rtdby = r:int(uint64,row,13)
   851    851   	p.ptr.rtact = r:int(uint64,row,14)
   852    852   	p.ptr.likes = r:int(uint32,row,15)
   853    853   	p.ptr.rts = r:int(uint32,row,16)
          854  +	p.ptr.isreply = r:bool(row,17)
   854    855   	p.ptr.localpost = r:bool(row,0)
   855    856   
   856    857   	return p
   857    858   end
   858    859   local terra row_to_actor(r: &pqr, row: intptr): lib.mem.ptr(lib.store.actor)
   859    860   	var a: lib.mem.ptr(lib.store.actor)
   860    861   	var av: rawstring, avlen: intptr
................................................................................
  1416   1417   		return r
  1417   1418   	end];
  1418   1419   
  1419   1420   	actor_purge_uid = [terra(
  1420   1421   		src: &lib.store.source,
  1421   1422   		uid: uint64
  1422   1423   	) queries.actor_purge_uid.exec(src,uid) end];
         1424  +
         1425  +	actor_notice_enum = [terra(
         1426  +		src: &lib.store.source,
         1427  +		uid: uint64
         1428  +	): lib.mem.ptr(lib.store.notice)
         1429  +		var r = queries.actor_notice_enum.exec(src,uid);
         1430  +		if r.sz == 0 then return [lib.mem.ptr(lib.store.notice)].null() end
         1431  +		defer r:free()
         1432  +
         1433  +		var notes = lib.mem.heapa(lib.store.notice, r.sz)
         1434  +		for i=0, r.sz do
         1435  +			var n = notes.ptr + i
         1436  +			n.kind = r:int(uint16,i,0)
         1437  +			n.when = r:int(int64,i,1)
         1438  +			n.who = r:int(int64,i,2)
         1439  +			n.what = r:int(uint64,i,3)
         1440  +			if n.kind == lib.store.noticetype.reply then
         1441  +				n.reply = r:int(uint64,i,4)
         1442  +			elseif n.kind == lib.store.noticetype.react then
         1443  +				var react = r:_string(i,5)
         1444  +				lib.str.ncpy(n.reaction, react.ptr, lib.math.smallest(react.ct,[(`n.reaction).tree.type.N]))
         1445  +			end
         1446  +		end
         1447  +
         1448  +		return notes
         1449  +	end];
  1423   1450   
  1424   1451   	auth_enum_uid = [terra(
  1425   1452   		src: &lib.store.source,
  1426   1453   		uid: uint64
  1427   1454   	): lib.mem.ptr(lib.mem.ptr(lib.store.auth))
  1428   1455   		var r = queries.auth_enum_uid.exec(src,uid)
  1429   1456   		if r.sz == 0 then return [lib.mem.ptr(lib.mem.ptr(lib.store.auth))].null() end

Modified backend/schema/pgsql-views.sql from [54b7851d27] to [d567971842].

    70     70   	body		text,
    71     71   	posted		bigint,
    72     72   	discovered	bigint,
    73     73   	edited		bigint,
    74     74   	parent		bigint,
    75     75   	convoheaduri text,
    76     76   	chgcount	integer,
           77  +-- ephemeral
    77     78   	accent		smallint,
    78     79   	rtdby		bigint, -- note that these must be 0 if the record
    79     80   	rtid		bigint, -- in question does not represent an RT!
    80     81   	n_likes		integer,
    81         -	n_rts		integer
           82  +	n_rts		integer,
           83  +	isreply		bool -- true if parent in (table posts); saves us a bunch of queries
    82     84   );
    83     85   
    84     86   create or replace function
    85     87   pg_temp.parsavpg_translate_post(parsav_posts,bigint,bigint)
    86     88   returns pg_temp.parsavpg_intern_post as $$
    87     89   	select a.origin is null,
    88     90   		($1).id,     ($1).author,
    89     91   		($1).subject,($1).acl,         ($1).body,
    90     92   		($1).posted, ($1).discovered,  ($1).edited,
    91     93   		($1).parent, ($1).convoheaduri,($1).chgcount,
    92     94   		coalesce(c.value, -1)::smallint,
    93     95   		$2 as rtdby, $3 as rtid,
    94         -		re.likes, re.rts
           96  +		re.likes, re.rts,
           97  +		($1).parent in (select id from parsav_posts)
    95     98   	from parsav_actors as a 
    96     99   		left join parsav_actor_conf_ints as c
    97    100   		          on c.key = 'ui-accent' and
    98    101   		             c.uid = a.id
    99    102   		left join pg_temp.parsavpg_post_react_counts as re
   100    103   		          on re.post = ($1).id
   101    104   	where a.id = ($1).author

Modified config.lua from [5768880cb8] to [eb323f3b45].

    56     56   		{'default-avatar.webp', 'image/webp'}; -- needs inkscape-exclusive svg features
    57     57   		{'bell.svg', 'image/svg+xml'};
    58     58   		{'heart.webp', 'image/webp'};
    59     59   		{'retweet.webp', 'image/webp'};
    60     60   		{'padlock.svg', 'image/svg+xml'};
    61     61   		{'warn.svg', 'image/svg+xml'};
    62     62   		{'query.webp', 'image/webp'};
           63  +		{'reply.webp', 'image/webp'};
           64  +		-- keep in mind before you add anything to this list: these are not
           65  +		-- just files parsav can access, they are files that are *kept in
           66  +		-- memory* for fast access the entire time parsav is running, and
           67  +		-- which need to be loaded into memory before the program can even
           68  +		-- start. it's imperative to keep these as small and few in number
           69  +		-- as is realistically possible.
    63     70   	};
    64     71   	default_ui_accent = tonumber(default('parsav_ui_default_accent',323));
    65     72   }
    66     73   if os.getenv('parsav_let_me_be_an_idiot') == "i know what i'm doing" then
    67     74   	conf.braingeniousmode = true -- SOUND GENERAL QUARTERS
    68     75   end
    69     76   if u.ping '.fslckout' or u.ping '_FOSSIL_' then

Modified makefile from [5c5158676d] to [0e9ec3efb8].

     1      1   dl = git
     2      2   dbg-flags = $(if $(dbg),-g)
     3      3   
     4         -images = static/default-avatar.webp static/query.webp static/heart.webp static/retweet.webp
            4  +images = static/default-avatar.webp static/query.webp static/heart.webp static/retweet.webp static/reply.webp
     5      5   #$(addsuffix .webp, $(basename $(wildcard static/*.svg)))
     6      6   styles = $(addsuffix .css, $(basename $(wildcard static/*.scss)))
     7      7   
     8      8   parsav parsavd: parsav.t config.lua pkgdata.lua $(images) $(styles)
     9      9   	terra $(dbg-flags) $<
    10     10   parsav.o parsavd.o: parsav.t config.lua pkgdata.lua $(images) $(styles)
    11     11   	env parsav_link=no terra $(dbg-flags) $<

Modified parsav.t from [5b9672ebb4] to [8bdefbaeb6].

   238    238   	local ty = uint8
   239    239   	if #tbl >= 2^32 then ty = uint64 -- hey, can't be too safe
   240    240   	elseif #tbl >= 2^16 then ty = uint32
   241    241   	elseif #tbl >= 2^8 then ty = uint16 end
   242    242   	local o = { t = ty, members = tbl }
   243    243   	local strings = {}
   244    244   	for i, name in ipairs(tbl) do
   245         -		o[name] = i - 1
          245  +		o[name] = `[ty]([i - 1])
   246    246   		strings[i] = `[lib.mem.ref(int8)]{ptr=[name], ct=[#name]}
   247    247   	end
   248    248   	o._str = terra(val: ty)
   249    249   		var l = array([strings])
   250    250   		return l[val]
   251    251   	end
   252    252   	return o
................................................................................
   433    433   	'render:login';
   434    434   	'render:profile';
   435    435   	'render:compose';
   436    436   	'render:tweet';
   437    437   	'render:tweet-page';
   438    438   	'render:user-page';
   439    439   	'render:timeline';
          440  +	'render:notices';
   440    441   
   441    442   	'render:docpage';
   442    443   
   443    444   	'render:conf:profile';
   444    445   	'render:conf:sec';
   445    446   	'render:conf:users';
   446    447   	'render:conf';

Added render/notices.t version [99b348e685].

            1  +-- vim: ft=terra
            2  +local pstr = lib.mem.ptr(int8)
            3  +local P = lib.str.plit
            4  +local terra cs(s: rawstring)
            5  +	return pstr { ptr = s, ct = lib.str.sz(s) }
            6  +end
            7  +
            8  +local terra 
            9  +render_notices(
           10  +	co: &lib.srv.convo
           11  +): {}
           12  +	var notes = co.srv:actor_notice_enum(co.who.id)
           13  +	
           14  +	if notes.ct == 0 then
           15  +		co:complain(200,'no news is good news',"you don't have any notices to review")
           16  +		return
           17  +	end
           18  +	defer notes:free()
           19  +
           20  +	var pg: lib.str.acc pg:init(512) defer pg:free()
           21  +	var pflink: lib.str.acc pflink:init(64)
           22  +	var body: lib.str.acc body:init(256)
           23  +	var latest: lib.store.timepoint = 0
           24  +	for i=0,notes.ct do
           25  +		if notes(i).when > latest then latest = notes(i).when end
           26  +		var who = co.srv:actor_fetch_uid(notes(i).who) defer who:free()
           27  +		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
           28  +		pflink:cue(lib.str.sz(who(0).xid) + 4)
           29  +		if who(0).origin == 0 then pflink:lpush('/')
           30  +		                      else pflink:lpush('/@') end
           31  +		pflink:push(who(0).xid,0)
           32  +		var n = data.view.notice {
           33  +			avatar = cs(who(0).avatar);
           34  +			nym = lib.render.nym(who.ptr,0,nil,true);
           35  +			pflink = pstr{ptr = pflink.buf; ct = pflink.sz};
           36  +		}
           37  +		var notweet = true
           38  +		var what = co.srv:post_fetch(notes(i).what) defer what:free()
           39  +		switch notes(i).kind do
           40  +			case lib.store.noticetype.rt then
           41  +				n.kind = P'rt'
           42  +				n.act = P'retweeted your post'
           43  +			end
           44  +			case lib.store.noticetype.like then
           45  +				n.kind = P'like'
           46  +				n.act = P'likes your post'
           47  +			end
           48  +			case lib.store.noticetype.reply then
           49  +				n.kind = P'reply'
           50  +				n.act = P'replied to your post'
           51  +				notweet = false
           52  +			end
           53  +		else goto skip end
           54  +		do var idbuf: int8[lib.math.shorthand.maxlen]
           55  +			var idlen = lib.math.shorthand.gen(notes(i).what, idbuf)
           56  +			var b = lib.smackdown.html(pstr {ptr=what(0).body,ct=0},true) defer b:free()
           57  +			body:lpush(' <a class="quote" href="/post/'):push(&idbuf[0],idlen):lpush('">'):ppush(b):lpush('</a>')
           58  +		end
           59  +		if not notweet then
           60  +			var reply = co.srv:post_fetch(notes(i).reply)
           61  +				lib.render.tweet(co,reply.ptr,&body)
           62  +			reply:free()
           63  +		end
           64  +		n.ref = pstr {ptr = body.buf, ct = body.sz}
           65  +
           66  +		n:append(&pg)
           67  +		::skip:: n.nym:free()
           68  +		         pflink:reset()
           69  +				 body:reset()
           70  +	end
           71  +	pflink:free()
           72  +	pg:lpush('<form method="post"><button name="act" value="clear">clear all notices</button></form>')
           73  +	co:livepage([lib.srv.convo.page] {
           74  +		title = P'notices', class = P'notices';
           75  +		body = pstr {ptr = pg.buf, ct = pg.sz};
           76  +		cache = false;
           77  +	}, latest)
           78  +end
           79  +
           80  +return render_notices

Modified render/profile.t from [edefc21455] to [3525ca58bc].

    34     34   	var stats = co.srv:actor_stats(actor.id)
    35     35   		var sn_posts     = cs(lib.math.decstr_friendly(stats.posts, &strfbuf[ [strfbuf.type.N - 1] ]))
    36     36   		var sn_follows   = cs(lib.math.decstr_friendly(stats.follows, sn_posts.ptr - 1))
    37     37   		var sn_followers = cs(lib.math.decstr_friendly(stats.followers, sn_follows.ptr - 1))
    38     38   		var sn_mutuals   = cs(lib.math.decstr_friendly(stats.mutuals, sn_followers.ptr - 1))
    39     39   	var bio = lib.str.plit '<em style="opacity:0.6">tall, dark, and mysterious</em>'
    40     40   	if actor.bio ~= nil then
    41         -		bio = lib.smackdown.html(cs(actor.bio))
           41  +		bio = lib.smackdown.html(cs(actor.bio),false)
    42     42   	end
    43     43   	var fullname = lib.render.nym(actor,0,nil,false) defer fullname:free()
    44     44   	var comments: lib.str.acc comments:init(64)
    45     45   
    46     46   	if co.srv.cfg.master == actor.id then
    47     47   		var foundertxt = lib.str.plit 'founder'
    48     48   		if co.srv.cfg.ui_cue_founder:ref() then

Modified render/tweet.t from [8bee6c6ab8] to [0700e85db2].

    37     37   	::foundauth::
    38     38   	var avistr: lib.str.acc if author.origin == 0 then
    39     39   		avistr:compose('/avi/',author.handle)
    40     40   	end
    41     41   	var timestr: int8[26] lib.osclock.ctime_r(&p.posted, &timestr[0])
    42     42   	for i=0,26 do if timestr[i] == @'\n' then timestr[i] = 0 break end end -- 🙄
    43     43   
    44         -	var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0})
           44  +	var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0},false)
    45     45   	defer bhtml:free()
    46     46   
    47     47   	var idbuf: int8[lib.math.shorthand.maxlen]
    48     48   	var idlen = lib.math.shorthand.gen(p.id, idbuf)
    49     49   	var permalink: lib.str.acc permalink:compose('/post/',{idbuf,idlen})
    50     50   	var fullname = lib.render.nym(author,0,nil, false) defer fullname:free()
    51     51   	var tpl = data.view.tweet {
................................................................................
    54     54   		nym = fullname;
    55     55   		when = cs(&timestr[0]);
    56     56   		avatar = cs(author.avatar);
    57     57   		acctlink = cs(author.xid);
    58     58   		permalink = permalink:finalize();
    59     59   		attr = pstr{'',0};
    60     60   		stats = pstr{'',0};
           61  +		extra = pstr{'',0};
    61     62   	}
           63  +	if p.isreply then
           64  +		var parent = co.srv:post_fetch(p.parent) defer parent:free()
           65  +		if not parent then
           66  +			lib.bail('schema integrity violation - could not match post to parent')
           67  +		end
           68  +		var pauth = co.srv:actor_fetch_uid(parent(0).author) defer pauth:free()
           69  +		var pidbuf: int8[lib.math.shorthand.maxlen]
           70  +		var pidlen = lib.math.shorthand.gen(p.parent, pidbuf)
           71  +		var pa: lib.str.acc pa:init(128)
           72  +		pa:lpush('<small>in reply to <a class="username" href="'):push(&pidbuf[0],pidlen):lpush('">')
           73  +		lib.render.nym(pauth.ptr,0,&pa,true)
           74  +		pa:lpush('</a></small>')
           75  +		tpl.extra = pa:finalize()
           76  +	end
    62     77   	if p.rts + p.likes > 0 then
    63     78   		var s: lib.str.acc s:init(128)
    64     79   		s:lpush('<div class="stats">')
    65     80   		if p.rts   > 0 then s:lpush('<div class="rt">'  ):dpush(p.rts  ):lpush('</div>') end
    66     81   		if p.likes > 0 then s:lpush('<div class="like">'):dpush(p.likes):lpush('</div>') end
    67     82   		s:lpush('</div>')
    68     83   		tpl.stats = s:finalize()
................................................................................
    85    100   
    86    101   	defer tpl.permalink:free()
    87    102   	if acc ~= nil then
    88    103   		if retweeter ~= nil then push_promo_header(co, acc, retweeter, p.rtact) end
    89    104   		tpl:append(acc)
    90    105   		if retweeter ~= nil then acc:lpush('</div>') end
    91    106   		if p.rts + p.likes > 0 then tpl.stats:free() end
          107  +		if tpl.extra.ct > 0 then tpl.extra:free() end
    92    108   		return [lib.mem.ptr(int8)]{ptr=nil,ct=0}
    93    109   	end
    94    110   
    95    111   	if retweeter ~= nil then
    96    112   		var rta: lib.str.acc rta:init(512)
    97    113   		push_promo_header(co, &rta, retweeter, p.rtact)
    98    114   		tpl:append(&rta) rta:lpush('</div>')
          115  +		if tpl.extra.ct > 0 then tpl.extra:free() end
    99    116   		return rta:finalize()
   100    117   	else
   101    118   		var txt = tpl:tostr()
          119  +		if tpl.extra.ct > 0 then tpl.extra:free() end
   102    120   		if p.rts + p.likes > 0 then tpl.stats:free() end
   103    121   		return txt
   104    122   	end
   105    123   end
   106    124   return render_tweet

Modified route.t from [cc5cf170ae] to [e8c2143a0c].

   164    164   			return
   165    165   		end
   166    166   		if subj == nil then subj = '' end
   167    167   
   168    168   		var p = lib.store.post {
   169    169   			author = co.who.id, acl = acl;
   170    170   			body = text, subject = subj;
          171  +			parent = 0;
   171    172   		}
   172    173   		var newid = p:publish(co.srv)
   173    174   
   174    175   		var idbuf: int8[lib.math.shorthand.maxlen]
   175    176   		var idlen = lib.math.shorthand.gen(newid, idbuf)
   176    177   		var redirto: lib.str.acc redirto:compose('/post/',{idbuf,idlen}) defer redirto:free()
   177    178   		co:reroute(redirto.buf)
................................................................................
   380    381   		end
   381    382   	end
   382    383   	lib.render.conf(co,path,msg)
   383    384   	do return end
   384    385   
   385    386   	::nopriv:: co:complain(403,'insufficient privileges','you do not have the necessary powers to perform this action')
   386    387   end
          388  +
          389  +terra http.user_notices(co: &lib.srv.convo, meth: method.t)
          390  +	if meth == method.post then
          391  +		var act = co:ppostv('act')
          392  +		if act:cmp(lib.str.plit'clear') then
          393  +			co.srv:actor_conf_int_set(co.who.id, 'notice-clear-time', lib.osclock.time(nil))
          394  +			co:reroute('/')
          395  +			return
          396  +		else goto badop end
          397  +	end
          398  +
          399  +	lib.render.notices(co)
          400  +	do return end
          401  +
          402  +	::badop :: do co:complain(405, 'invalid operation', 'the operation you have attempted on this post is not meaningful') return end
          403  +end
   387    404   
   388    405   do local branches = quote end
   389    406   	local filename, flen = symbol(&int8), symbol(intptr)
   390    407   	local page = symbol(lib.http.page)
   391    408   	local send = label()
   392    409   	local storage = data.stmap
   393    410   	for i,e in ipairs(config.embeds) do local id,mime = e[1],e[2]
................................................................................
   442    459   	elseif uri.ptr[1] == @'@' then
   443    460   		http.actor_profile_xid(co, uri, meth)
   444    461   	elseif uri.ptr[1] == @'s' and uri.ptr[2] == @'/' and uri.ct > 3 then
   445    462   		if not meth_get(meth) then goto wrongmeth end
   446    463   		if not http.static_content(co, uri.ptr + 3, uri.ct - 3) then goto notfound end
   447    464   	elseif lib.str.ncmp('/avi/', uri.ptr, 5) == 0 then
   448    465   		http.local_avatar(co, [lib.mem.ptr(int8)] {ptr = uri.ptr + 5, ct = uri.ct - 5})
   449         -	elseif lib.str.ncmp('/compose', uri.ptr, lib.math.biggest(uri.ct,8)) == 0 then
          466  +	elseif uri:cmp(lib.str.plit '/notices') then
          467  +		if co.aid == 0 then co:reroute('/login') return end
          468  +		http.user_notices(co,meth)
          469  +	elseif uri:cmp(lib.str.plit '/compose') then
   450    470   		if co.aid == 0 then co:reroute('/login') return end
   451    471   		http.post_compose(co,meth)
   452         -	elseif lib.str.ncmp('/login', uri.ptr, lib.math.biggest(uri.ct,6)) == 0 then
          472  +	elseif uri:cmp(lib.str.plit '/login') then
   453    473   		if co.aid == 0
   454    474   			then http.login_form(co, meth)
   455    475   			else co:reroute('/')
   456    476   		end
   457         -	elseif lib.str.ncmp('/logout', uri.ptr, lib.math.biggest(uri.ct,7)) == 0 then
          477  +	elseif uri:cmp(lib.str.plit '/logout') then
   458    478   		if co.aid == 0
   459    479   			then goto notfound
   460    480   			else co:reroute_cookie('/','auth=; Path=/')
   461    481   		end
   462    482   	else -- hierarchical routes
   463    483   		var path = lib.http.hier(uri) defer path:free()
   464    484   		if path.ct > 1 and path(0):cmp(lib.str.lit('user')) then

Modified smackdown.t from [c51eb8a6b2] to [926ec15b69].

    52     52   local terra scanline_wordend(l: rawstring, max: intptr, n: rawstring, nc: intptr)
    53     53   	var sl = scanline(l,max,n,nc)
    54     54   	if sl == nil then return nil else sl = sl + nc end
    55     55   	if sl >= l+max or isws(@sl) then return sl-nc end
    56     56   	return nil
    57     57   end
    58     58   
    59         -terra m.html(input: pstr)
           59  +terra m.html(input: pstr, firstline: bool)
    60     60   	if input.ct == 0 then input.ct = lib.str.sz(input.ptr) end
    61     61   
    62     62   	var md = lib.html.sanitize(input,false)
    63     63   
    64     64   	var styled: lib.str.acc styled:init(md.ct)
    65     65   
    66     66   	do var i = 0 while i < md.ct do

Added static/reply.svg version [0a0b88e351].

            1  +<?xml version="1.0" encoding="UTF-8" standalone="no"?>
            2  +<!-- Created with Inkscape (http://www.inkscape.org/) -->
            3  +
            4  +<svg
            5  +   xmlns:dc="http://purl.org/dc/elements/1.1/"
            6  +   xmlns:cc="http://creativecommons.org/ns#"
            7  +   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
            8  +   xmlns:svg="http://www.w3.org/2000/svg"
            9  +   xmlns="http://www.w3.org/2000/svg"
           10  +   xmlns:xlink="http://www.w3.org/1999/xlink"
           11  +   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
           12  +   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
           13  +   width="20"
           14  +   height="20"
           15  +   viewBox="0 0 5.2916664 5.2916665"
           16  +   version="1.1"
           17  +   id="svg8"
           18  +   inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
           19  +   sodipodi:docname="reply.svg">
           20  +  <defs
           21  +     id="defs2">
           22  +    <linearGradient
           23  +       inkscape:collect="always"
           24  +       id="linearGradient954">
           25  +      <stop
           26  +         style="stop-color:#ffffff;stop-opacity:1;"
           27  +         offset="0"
           28  +         id="stop950" />
           29  +      <stop
           30  +         style="stop-color:#ffffff;stop-opacity:0;"
           31  +         offset="1"
           32  +         id="stop952" />
           33  +    </linearGradient>
           34  +    <linearGradient
           35  +       inkscape:collect="always"
           36  +       id="linearGradient938">
           37  +      <stop
           38  +         style="stop-color:#d9fff6;stop-opacity:1;"
           39  +         offset="0"
           40  +         id="stop934" />
           41  +      <stop
           42  +         style="stop-color:#d9fff6;stop-opacity:0;"
           43  +         offset="1"
           44  +         id="stop936" />
           45  +    </linearGradient>
           46  +    <linearGradient
           47  +       inkscape:collect="always"
           48  +       id="linearGradient1403">
           49  +      <stop
           50  +         style="stop-color:#ccaaff;stop-opacity:1;"
           51  +         offset="0"
           52  +         id="stop1399" />
           53  +      <stop
           54  +         style="stop-color:#ccaaff;stop-opacity:0;"
           55  +         offset="1"
           56  +         id="stop1401" />
           57  +    </linearGradient>
           58  +    <linearGradient
           59  +       id="linearGradient1395"
           60  +       inkscape:collect="always">
           61  +      <stop
           62  +         id="stop1391"
           63  +         offset="0"
           64  +         style="stop-color:#ff1616;stop-opacity:1" />
           65  +      <stop
           66  +         id="stop1393"
           67  +         offset="1"
           68  +         style="stop-color:#ff1d1d;stop-opacity:0" />
           69  +    </linearGradient>
           70  +    <linearGradient
           71  +       inkscape:collect="always"
           72  +       id="linearGradient1383">
           73  +      <stop
           74  +         style="stop-color:#980000;stop-opacity:1;"
           75  +         offset="0"
           76  +         id="stop1379" />
           77  +      <stop
           78  +         style="stop-color:#980000;stop-opacity:0;"
           79  +         offset="1"
           80  +         id="stop1381" />
           81  +    </linearGradient>
           82  +    <linearGradient
           83  +       inkscape:collect="always"
           84  +       id="linearGradient832">
           85  +      <stop
           86  +         style="stop-color:#ffcfcf;stop-opacity:1;"
           87  +         offset="0"
           88  +         id="stop828" />
           89  +      <stop
           90  +         style="stop-color:#ffcfcf;stop-opacity:0;"
           91  +         offset="1"
           92  +         id="stop830" />
           93  +    </linearGradient>
           94  +    <radialGradient
           95  +       inkscape:collect="always"
           96  +       xlink:href="#linearGradient832"
           97  +       id="radialGradient834"
           98  +       cx="3.2286437"
           99  +       cy="286.62921"
          100  +       fx="3.2286437"
          101  +       fy="286.62921"
          102  +       r="1.0866126"
          103  +       gradientTransform="matrix(1.8608797,0.8147617,-0.38242057,0.87343168,106.71446,33.692223)"
          104  +       gradientUnits="userSpaceOnUse" />
          105  +    <radialGradient
          106  +       inkscape:collect="always"
          107  +       xlink:href="#linearGradient1383"
          108  +       id="radialGradient1385"
          109  +       cx="4.1787109"
          110  +       cy="286.89261"
          111  +       fx="4.1787109"
          112  +       fy="286.89261"
          113  +       r="1.2260786"
          114  +       gradientTransform="matrix(1.7016464,0,0,1.6348586,-2.9319775,-182.10895)"
          115  +       gradientUnits="userSpaceOnUse" />
          116  +    <radialGradient
          117  +       inkscape:collect="always"
          118  +       xlink:href="#linearGradient1395"
          119  +       id="radialGradient1389"
          120  +       gradientUnits="userSpaceOnUse"
          121  +       gradientTransform="matrix(0.66230313,-1.6430738,1.0154487,0.40931507,-290.06307,177.39489)"
          122  +       cx="4.02069"
          123  +       cy="287.79269"
          124  +       fx="4.02069"
          125  +       fy="287.79269"
          126  +       r="1.0866126" />
          127  +    <linearGradient
          128  +       inkscape:collect="always"
          129  +       xlink:href="#linearGradient1403"
          130  +       id="linearGradient1405"
          131  +       x1="8.3939333"
          132  +       y1="288.1091"
          133  +       x2="7.0158253"
          134  +       y2="287.32819"
          135  +       gradientUnits="userSpaceOnUse" />
          136  +    <linearGradient
          137  +       inkscape:collect="always"
          138  +       xlink:href="#linearGradient938"
          139  +       id="linearGradient940"
          140  +       x1="7.609839"
          141  +       y1="288.73215"
          142  +       x2="7.609839"
          143  +       y2="283.78305"
          144  +       gradientUnits="userSpaceOnUse" />
          145  +    <linearGradient
          146  +       inkscape:collect="always"
          147  +       xlink:href="#linearGradient954"
          148  +       id="linearGradient956"
          149  +       x1="3.0150654"
          150  +       y1="285.94464"
          151  +       x2="3.0150654"
          152  +       y2="282.40109"
          153  +       gradientUnits="userSpaceOnUse" />
          154  +    <linearGradient
          155  +       inkscape:collect="always"
          156  +       xlink:href="#linearGradient938"
          157  +       id="linearGradient960"
          158  +       gradientUnits="userSpaceOnUse"
          159  +       x1="7.609839"
          160  +       y1="288.73215"
          161  +       x2="7.609839"
          162  +       y2="283.78305" />
          163  +    <linearGradient
          164  +       inkscape:collect="always"
          165  +       xlink:href="#linearGradient954"
          166  +       id="linearGradient1138"
          167  +       gradientUnits="userSpaceOnUse"
          168  +       x1="3.0150654"
          169  +       y1="285.94464"
          170  +       x2="3.0150654"
          171  +       y2="284.62277" />
          172  +    <linearGradient
          173  +       inkscape:collect="always"
          174  +       xlink:href="#linearGradient938"
          175  +       id="linearGradient1150"
          176  +       gradientUnits="userSpaceOnUse"
          177  +       x1="7.609839"
          178  +       y1="288.73215"
          179  +       x2="7.609839"
          180  +       y2="283.78305" />
          181  +    <linearGradient
          182  +       inkscape:collect="always"
          183  +       xlink:href="#linearGradient938"
          184  +       id="linearGradient1152"
          185  +       gradientUnits="userSpaceOnUse"
          186  +       x1="7.609839"
          187  +       y1="288.73215"
          188  +       x2="7.609839"
          189  +       y2="283.78305"
          190  +       gradientTransform="translate(-3.1738256,6.8821903)" />
          191  +    <linearGradient
          192  +       inkscape:collect="always"
          193  +       xlink:href="#linearGradient954"
          194  +       id="linearGradient1154"
          195  +       gradientUnits="userSpaceOnUse"
          196  +       x1="3.0150654"
          197  +       y1="285.94464"
          198  +       x2="3.0150654"
          199  +       y2="282.40109"
          200  +       gradientTransform="translate(-3.1738256,6.8821903)" />
          201  +    <linearGradient
          202  +       inkscape:collect="always"
          203  +       xlink:href="#linearGradient954"
          204  +       id="linearGradient1156"
          205  +       gradientUnits="userSpaceOnUse"
          206  +       x1="3.0150654"
          207  +       y1="285.94464"
          208  +       x2="3.0150654"
          209  +       y2="284.62277" />
          210  +    <linearGradient
          211  +       inkscape:collect="always"
          212  +       xlink:href="#linearGradient938"
          213  +       id="linearGradient1170"
          214  +       gradientUnits="userSpaceOnUse"
          215  +       x1="7.609839"
          216  +       y1="288.73215"
          217  +       x2="7.609839"
          218  +       y2="283.78305"
          219  +       gradientTransform="matrix(0.93088299,0,0,0.93088299,0.40825643,19.623427)" />
          220  +    <linearGradient
          221  +       inkscape:collect="always"
          222  +       xlink:href="#linearGradient954"
          223  +       id="linearGradient1172"
          224  +       gradientUnits="userSpaceOnUse"
          225  +       x1="3.0150654"
          226  +       y1="285.94464"
          227  +       x2="3.0150654"
          228  +       y2="282.40109"
          229  +       gradientTransform="matrix(0.93088299,0,0,0.93088299,0.40825643,19.623427)" />
          230  +    <linearGradient
          231  +       inkscape:collect="always"
          232  +       xlink:href="#linearGradient954"
          233  +       id="linearGradient1174"
          234  +       gradientUnits="userSpaceOnUse"
          235  +       x1="3.0150654"
          236  +       y1="285.94464"
          237  +       x2="3.0150654"
          238  +       y2="284.62277" />
          239  +  </defs>
          240  +  <sodipodi:namedview
          241  +     id="base"
          242  +     pagecolor="#181818"
          243  +     bordercolor="#666666"
          244  +     borderopacity="1.0"
          245  +     inkscape:pageopacity="0"
          246  +     inkscape:pageshadow="2"
          247  +     inkscape:zoom="3.959798"
          248  +     inkscape:cx="0.97863268"
          249  +     inkscape:cy="46.024492"
          250  +     inkscape:document-units="mm"
          251  +     inkscape:current-layer="layer1"
          252  +     showgrid="false"
          253  +     units="px"
          254  +     inkscape:window-width="1920"
          255  +     inkscape:window-height="1042"
          256  +     inkscape:window-x="0"
          257  +     inkscape:window-y="38"
          258  +     inkscape:window-maximized="0"
          259  +     showguides="false"
          260  +     fit-margin-top="0"
          261  +     fit-margin-left="0"
          262  +     fit-margin-right="0"
          263  +     fit-margin-bottom="0" />
          264  +  <metadata
          265  +     id="metadata5">
          266  +    <rdf:RDF>
          267  +      <cc:Work
          268  +         rdf:about="">
          269  +        <dc:format>image/svg+xml</dc:format>
          270  +        <dc:type
          271  +           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
          272  +        <dc:title></dc:title>
          273  +      </cc:Work>
          274  +    </rdf:RDF>
          275  +  </metadata>
          276  +  <g
          277  +     inkscape:label="Layer 1"
          278  +     inkscape:groupmode="layer"
          279  +     id="layer1"
          280  +     transform="translate(-2.6134661,-283.36966)">
          281  +    <g
          282  +       id="g1168"
          283  +       transform="matrix(0.92817904,-0.24870482,0.24870482,0.92817904,-70.834504,21.905842)"
          284  +       style="stroke-width:1.04066753">
          285  +      <path
          286  +         d="m 3.1152344,284.35547 c -0.1591337,0 -0.2949219,0.13579 -0.2949219,0.29492 v 2.9668 c 0,0.15913 0.1357882,0.29492 0.2949219,0.29492 h 4.3945312 c 0.1591337,0 0.2949219,-0.13579 0.2949219,-0.29492 v -2.9668 c 0,-0.15913 -0.1357882,-0.29492 -0.2949219,-0.29492 z"
          287  +         id="rect958"
          288  +         style="opacity:0.25200004;vector-effect:none;fill:url(#linearGradient960);fill-opacity:1;stroke:none;stroke-width:0.18445945;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
          289  +         inkscape:original="M 3.1152344 284.55078 C 3.0598344 284.55078 3.015625 284.59499 3.015625 284.65039 L 3.015625 287.61719 C 3.015625 287.67259 3.0598344 287.7168 3.1152344 287.7168 L 7.5097656 287.7168 C 7.5651656 287.7168 7.609375 287.67259 7.609375 287.61719 L 7.609375 284.65039 C 7.609375 284.59499 7.5651656 284.55078 7.5097656 284.55078 L 3.1152344 284.55078 z "
          290  +         inkscape:radius="0.19498466"
          291  +         sodipodi:type="inkscape:offset"
          292  +         transform="matrix(0.93088299,0,0,0.93088299,0.40825643,19.623427)" />
          293  +      <rect
          294  +         rx="0.093088299"
          295  +         y="284.50693"
          296  +         x="3.2149298"
          297  +         height="2.9467573"
          298  +         width="4.2771964"
          299  +         id="rect932"
          300  +         style="opacity:1;vector-effect:none;fill:url(#linearGradient1170);fill-opacity:1;stroke:none;stroke-width:0.17171016;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
          301  +      <path
          302  +         d="m 3.015625,284.38867 a 0.16537863,0.16537863 0 0 0 -0.089844,0.30469 l 2.296875,1.50391 a 0.16537863,0.16537863 0 0 0 0.1796876,0 l 2.296875,-1.50391 a 0.16537863,0.16537863 0 0 0 -0.089844,-0.30469 z"
          303  +         id="path1136"
          304  +         style="opacity:0.13699999;fill:url(#linearGradient1174);fill-opacity:1;stroke:none;stroke-width:0.18445943;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
          305  +         inkscape:original="M 3.015625 284.55469 L 5.3125 286.05859 L 7.609375 284.55469 L 3.015625 284.55469 z "
          306  +         inkscape:radius="0.16536209"
          307  +         sodipodi:type="inkscape:offset"
          308  +         transform="matrix(0.93088299,0,0,0.93088299,0.40825643,19.623427)" />
          309  +      <path
          310  +         sodipodi:nodetypes="cccc"
          311  +         inkscape:connector-curvature="0"
          312  +         d="M 7.4105013,284.507 5.3535277,285.91094 3.3022018,284.5076 Z"
          313  +         style="fill:url(#linearGradient1172);fill-opacity:1;stroke:none;stroke-width:0.17171013;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
          314  +         id="path929" />
          315  +    </g>
          316  +    <g
          317  +       id="g1162">
          318  +      <path
          319  +         transform="translate(-3.1738256,6.8821903)"
          320  +         sodipodi:type="inkscape:offset"
          321  +         inkscape:radius="0.19498466"
          322  +         inkscape:original="M 3.1152344 284.55078 C 3.0598344 284.55078 3.015625 284.59499 3.015625 284.65039 L 3.015625 287.61719 C 3.015625 287.67259 3.0598344 287.7168 3.1152344 287.7168 L 7.5097656 287.7168 C 7.5651656 287.7168 7.609375 287.67259 7.609375 287.61719 L 7.609375 284.65039 C 7.609375 284.59499 7.5651656 284.55078 7.5097656 284.55078 L 3.1152344 284.55078 z "
          323  +         style="opacity:0.25200004;vector-effect:none;fill:url(#linearGradient1150);fill-opacity:1;stroke:none;stroke-width:0.16500001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
          324  +         id="path1142"
          325  +         d="m 3.1152344,284.35547 c -0.1591337,0 -0.2949219,0.13579 -0.2949219,0.29492 v 2.9668 c 0,0.15913 0.1357882,0.29492 0.2949219,0.29492 h 4.3945312 c 0.1591337,0 0.2949219,-0.13579 0.2949219,-0.29492 v -2.9668 c 0,-0.15913 -0.1357882,-0.29492 -0.2949219,-0.29492 z" />
          326  +      <rect
          327  +         style="opacity:1;vector-effect:none;fill:url(#linearGradient1152);fill-opacity:1;stroke:none;stroke-width:0.16500001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
          328  +         id="rect1144"
          329  +         width="4.5947733"
          330  +         height="3.1655507"
          331  +         x="-0.15875995"
          332  +         y="291.43301"
          333  +         rx="0.1" />
          334  +      <path
          335  +         id="path1146"
          336  +         style="fill:url(#linearGradient1154);fill-opacity:1;stroke:none;stroke-width:0.16499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
          337  +         d="m 4.4360131,291.43669 -2.2973866,1.50456 -2.29738662,-1.50456 z"
          338  +         inkscape:connector-curvature="0"
          339  +         sodipodi:nodetypes="cccc" />
          340  +      <path
          341  +         transform="translate(-3.1738256,6.8821903)"
          342  +         sodipodi:type="inkscape:offset"
          343  +         inkscape:radius="0.16536209"
          344  +         inkscape:original="M 3.015625 284.55469 L 5.3125 286.05859 L 7.609375 284.55469 L 3.015625 284.55469 z "
          345  +         style="opacity:0.13699999;fill:url(#linearGradient1156);fill-opacity:1;stroke:none;stroke-width:0.16499999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
          346  +         id="path1148"
          347  +         d="m 3.015625,284.38867 a 0.16537863,0.16537863 0 0 0 -0.089844,0.30469 l 2.296875,1.50391 a 0.16537863,0.16537863 0 0 0 0.1796876,0 l 2.296875,-1.50391 a 0.16537863,0.16537863 0 0 0 -0.089844,-0.30469 z" />
          348  +    </g>
          349  +  </g>
          350  +</svg>

Modified static/style.scss from [dc435bcc7e] to [fa60ad7c82].

   224    224   					transform: scale(1.2);
   225    225   				}
   226    226   			}
   227    227   			> a[href].bell {
   228    228   				content: url(/s/bell.svg);
   229    229   				height: 2em;
   230    230   				padding: 0.125in 0.10in;
          231  +				filter: drop-shadow(1px 1px 3px tone(-5%));
   231    232   				&:hover {
   232         -					filter: drop-shadow(0 0 10px tone(-5%));
          233  +					filter: drop-shadow(1px 1px 3px tone(-5%))
          234  +						drop-shadow(0 0 10px tone(-5%));
   233    235   				}
   234    236   			}
   235    237   		}
   236    238   	}
   237    239   }
   238    240   
   239    241   main {
................................................................................
   380    382   	padding: 0.4in;
   381    383   	> .msg {
   382    384   		text-align: center;
   383    385   		padding: 0.3in;
   384    386   	}
   385    387   	> .msg:first-child { padding-top: 0; }
   386    388   	> .user {
   387         -		width: min-content; margin: auto;
          389  +		width: max-content; margin: auto;
   388    390   		background: tone(-20%,-0.3);
   389    391   		border: 1px solid black;
   390    392   		color: tone(-50%);
   391    393   		padding: 0.1in;
   392         -		> img { width: 1in; height: 1in; border: 1px solid black; }
          394  +		> img { display: block; width: 1in; height: 1in; margin: auto; border: 1px solid black; }
   393    395   		> .name { @extend %serif; text-align: center; font-size: 130%; font-weight: bold; margin-top: 0.08in; }
   394    396   	}
   395    397   	>form {
   396    398   		display: grid;
   397    399   		grid-template-columns: 1fr 1fr;
   398    400   		grid-template-rows: 1.2em 1fr 1fr;
   399    401   		grid-gap: 5px;
................................................................................
   535    537   		font-size: 110%;
   536    538   		text-align: justify;
   537    539   		color: tone(25%);
   538    540   	}
   539    541   	> a[href].permalink {
   540    542   		display: block;
   541    543   		grid-column: 4/5; grid-row: 2/3;
   542         -		font-size: 80%;
          544  +		font-size: 90%;
   543    545   		text-align: right;
   544    546   		padding: 0.1in;
   545    547   		padding-right: 0.15in;
   546    548   		font-style: italic;
   547    549   		background: linear-gradient(to left, tone(-55%,-0.5), transparent);
   548    550   	}
   549    551   	div.stats {
................................................................................
   555    557   			padding-left: 1.3em;
   556    558   			background-size: 1.1em;
   557    559   			background-repeat: no-repeat;
   558    560   			min-width: 0.3em;
   559    561   			&:focus {
   560    562   				outline: none;
   561    563   				opacity: 0.9 !important;
   562         -				filter: brightness(1.7) drop-shadow(0 0 15px rgb(255,150,200));
          564  +				filter: drop-shadow(0 0 7px tone(-10%));
   563    565   			}
   564    566   			&:empty {
   565    567   				transition: 0.3s;
   566    568   				opacity: 0.0001; // qutebrowser won't show hints if opacity=0 :(
   567    569   				&:hover, &:focus { opacity: 0.6 !important; }
   568    570   			}
   569    571   		}
................................................................................
   949    951   		grid-row: 1/2; grid-column: 2/3;
   950    952   		text-decoration: none;
   951    953   	}
   952    954   	> .post {
   953    955   		grid-row: 2/3; grid-column: 1/3;
   954    956   	}
   955    957   }
          958  +
          959  +body.notices {
          960  +	form { text-align: center; }
          961  +	div.notice {
          962  +		padding: 0.15in;
          963  +		background: linear-gradient(to bottom, tone(10%, -0.9), transparent);
          964  +		border: 1px solid tone(-60%);
          965  +		& + div.notice { border-top: none; }
          966  +		&.rt, &.like, &.reply { &::before {
          967  +			display: inline-block;
          968  +			width: 1em; height: 1em;
          969  +			margin-right: 1ex;
          970  +			background-size: contain;
          971  +			vertical-align: bottom;
          972  +			content: ""; // 🙄
          973  +		}}
          974  +		&.rt::before    { background-image: url(/s/retweet.webp); }
          975  +		&.like::before  { background-image: url(/s/heart.webp);   }
          976  +		&.reply::before { background-image: url(/s/reply.webp);   }
          977  +		> .action {
          978  +			display: inline-block;
          979  +			color: tone(5%);
          980  +			> .id {
          981  +				display: inline-block;
          982  +				> img {
          983  +					width: 1em; height: 1em;
          984  +					vertical-align: middle;
          985  +					margin-right: 0.5ex;
          986  +				}
          987  +			}
          988  +		}
          989  +		> a[href].quote {
          990  +			&::before { content: "“"; }
          991  +			&::after { content: "”"; }
          992  +			font-style: italic; color: tone(20%);
          993  +			text-decoration: none;
          994  +		}
          995  +		> article.post {
          996  +			margin: 0.1in 0.2in;
          997  +			margin-left: 0.4in;
          998  +		}
          999  +	}
         1000  +}

Modified store.t from [e1429862ec] to [54fed43947].

     7      7   	};
     8      8   	noticetype = lib.enum {
     9      9   		'none', 'mention', 'reply', 'like', 'rt', 'react'
    10     10   	};
    11     11   
    12     12   	relation = lib.set {
    13     13   		'follow',
           14  +		'subscribe', -- get a notification for every post
    14     15   		'mute', -- posts will be completely hidden at all times
    15     16   		'block', -- no interactions will be permitted, but posts will remain visible
    16     17   		'silence', -- messages will not be accepted
    17     18   		'collapse', -- posts will be collapsed by default
    18     19   		'disemvowel', -- posts will be ritually humiliated, but shown
    19     20   		'avoid', -- posts will be kept out of the timeline but will show on users' posts and in conversations
    20     21   		'exclude', -- own posts will not be visible to this user
................................................................................
   220    221   -- ephemera
   221    222   	localpost: bool
   222    223   	accent: int16
   223    224   	rts: uint32
   224    225   	likes: uint32
   225    226   	rtdby: uint64 -- 0 if not rt
   226    227   	rtact: uint64 -- 0 if not rt, id of rt action otherwise
          228  +	isreply: bool
   227    229   	source: &m.source
   228    230   
   229    231   	-- save :: bool -> {} (defined in acl.t due to dep. hell)
   230    232   }
   231    233   
   232    234   m.user_conf_funcs = function(be,n,ty,rty,rty2)
   233    235   	rty = rty or ty
................................................................................
   251    253   struct m.notice {
   252    254   	kind: m.noticetype.t
   253    255   	when: uint64
   254    256   	who: uint64
   255    257   	what: uint64
   256    258   	union {
   257    259   		reply: uint64
   258         -		reaction: int8[16]
          260  +		reaction: int8[32] -- are you shitting me, unichode
   259    261   	}
   260    262   }
   261    263   
   262    264   struct m.inet {
   263    265   	pv: uint8 -- 0 = null, 4 = ipv4, 6 = ipv6
   264    266   	union {
   265    267   		v4: uint8[4]
................................................................................
   389    391   			-- origin: inet
   390    392   			-- cookie issue time: m.timepoint
   391    393   	actor_auth_register_uid: {&m.source, uint64, uint64} -> {}
   392    394   		-- notifies the backend module of the UID that has been assigned for
   393    395   		-- an authentication ID
   394    396   			-- aid: uint64
   395    397   			-- uid: uint64
   396         -	actor_notice_enum: {&m.source, uint64} -> lib.mem.lstptr(m.notice)
          398  +	actor_notice_enum: {&m.source, uint64} -> lib.mem.ptr(m.notice)
   397    399   	actor_rel_create: {&m.source, uint16, uint64, uint64} -> {}
   398    400   	actor_rel_destroy: {&m.source, uint16, uint64, uint64} -> {}
   399    401   	actor_rel_calc: {&m.source, uint64, uint64} -> m.relationship
   400    402   
   401    403   	auth_enum_uid:    {&m.source, uint64}    -> lib.mem.lstptr(m.auth)
   402    404   	auth_enum_handle: {&m.source, rawstring} -> lib.mem.lstptr(m.auth)
   403    405   	auth_attach_pw: {&m.source, uint64, bool, pstr, pstr} -> {}

Modified str.t from [7fbe47e2ae] to [004fceba8a].

   160    160   	if sz <= self.run then return end
   161    161   	self.run = sz
   162    162   	if self.space - self.sz < self.run then
   163    163   		self.space = self.sz + self.run
   164    164   		self.buf = [rawstring](lib.mem.heapr_raw(self.buf, self.space))
   165    165   	end
   166    166   end
          167  +
          168  +terra m.acc:reset() -- semantic convenience function
          169  +	self.sz = 0
          170  +end
   167    171   
   168    172   terra m.acc:push(str: rawstring, len: intptr)
   169    173   	--var llen = len
   170    174   	if str == nil then return self end
   171    175   	--if str[len - 1] == 0xA then llen = llen - 1 end -- don't display newlines in debug output
   172    176   	-- lib.dbg('pushing "',{str,llen},'" onto accumulator')
   173    177   	if self.buf == nil then self:init(self.run) end

Modified view/load.lua from [dd2878563c] to [1dbf5e584d].

     5      5   local path = ...
     6      6   local sources = {
     7      7   	'docskel';
     8      8   	'confirm';
     9      9   	'tweet';
    10     10   	'profile';
    11     11   	'compose';
           12  +	'notice';
    12     13   
    13     14   	'login-username';
    14     15   	'login-challenge';
    15     16   
    16     17   	'conf';
    17     18   	'conf-profile';
    18     19   	'conf-sec';

Added view/notice.tpl version [6c1a6f12b0].

            1  +<div class="notice @kind">
            2  +	<div class="action">
            3  +		<div class="id">
            4  +			<img src="@avatar">
            5  +			<a class="username" href="@pflink">@nym</a>
            6  +		</div> @act
            7  +	</div> @ref
            8  +</div>

Modified view/tweet.tpl from [80bcf01f8a] to [5d1f823e24].

     1      1   <article class="post"@attr>
     2      2   	<div class="avatar"><img src="@:avatar"></div>
     3      3   	<a class="username" href="/@:acctlink">@nym</a>
     4         -	<div class="content">
            4  +	<div class="content">@extra
     5      5   		<div class="subject">@!subject</div>
     6      6   		<div class="text">@text</div>
     7      7   	</div>
     8      8   	@stats
     9      9   	<a class="permalink" href="@permalink"><time>@when</time></a>
    10     10   </article>