parsav  Check-in [8f954221a1]

Overview
Comment:look ma, im tweetin
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 8f954221a11a00d23ad86812516851d3b92e2e428d45c5284bee854a244c84d4
User & Date: lexi on 2020-12-27 04:08:20
Other Links: manifest | tags
Context
2020-12-28
23:42
vastly improve the setup process check-in: d228cd7fcb user: lexi tags: trunk
2020-12-27
04:08
look ma, im tweetin check-in: 8f954221a1 user: lexi tags: trunk
02:31
permissions work now check-in: bbfea467bf user: lexi tags: trunk
Changes

Modified backend/pgsql.t from [36b1398523] to [2c2a215381].

   205    205   				$1::bigint, case when $2::text = '' then null else $2::text end,
   206    206   				$3::text, $4::text, 
   207    207   				now(), now(), array[]::bigint[], array[]::bigint[]
   208    208   			) returning id
   209    209   		]]; -- TODO array handling
   210    210   	};
   211    211   
   212         -	instance_timeline_fetch = {
          212  +	post_enum_author_uid = {
          213  +		params = {uint64,uint64,uint64,uint64, uint64}, sql = [[
          214  +			select a.origin is null,
          215  +				p.id, p.author, p.subject, p.acl, p.body,
          216  +				extract(epoch from p.posted    )::bigint,
          217  +				extract(epoch from p.discovered)::bigint,
          218  +				p.parent, p.convoheaduri
          219  +			from parsav_posts as p
          220  +				inner join parsav_actors as a on p.author = a.id
          221  +			where p.author = $5::bigint and
          222  +				($1::bigint = 0 or p.posted <= to_timestamp($1::bigint)) and
          223  +				($2::bigint = 0 or to_timestamp($2::bigint) < p.posted)
          224  +			order by (p.posted, p.discovered) desc
          225  +			limit case when $3::bigint = 0 then null
          226  +			           else $3::bigint end
          227  +			offset $4::bigint
          228  +		]]
          229  +	};
          230  +
          231  +	-- maybe there's some way to unify these two, idk, im tired
          232  +
          233  +	timeline_instance_fetch = {
   213    234   		params = {uint64, uint64, uint64, uint64}, sql = [[
   214    235   			select true,
   215    236   				p.id, p.author, p.subject, p.acl, p.body,
   216    237   				extract(epoch from p.posted    )::bigint,
   217    238   				extract(epoch from p.discovered)::bigint,
   218    239   				p.parent, null::text
   219    240   			from parsav_posts as p
................................................................................
   394    415   		else
   395    416   			return pqr {ct, res}
   396    417   		end
   397    418   	end
   398    419   end
   399    420   
   400    421   local terra row_to_post(r: &pqr, row: intptr): lib.mem.ptr(lib.store.post)
   401         -	--lib.io.fmt("body ptr %p  len %llu\n", r:string(row,5), r:len(row,5))
   402         -	--lib.io.fmt("acl ptr %p  len %llu\n", r:string(row,4), r:len(row,4))
   403    422   	var subj: rawstring, sblen: intptr
          423  +	var cvhu: rawstring, cvhlen: intptr
   404    424   	if r:null(row,3)
   405    425   		then subj = nil sblen = 0
   406    426   		else subj = r:string(row,3) sblen = r:len(row,3)+1
   407    427   	end
          428  +	if r:null(row,9)
          429  +		then cvhu = nil cvhlen = 0
          430  +		else cvhu = r:string(row,9) cvhlen = r:len(row,9)+1
          431  +	end
   408    432   	var p = [ lib.str.encapsulate(lib.store.post, {
   409    433   		subject = { `subj, `sblen };
   410    434   		acl = {`r:string(row,4), `r:len(row,4)+1};
   411    435   		body = {`r:string(row,5), `r:len(row,5)+1};
   412         -		--convoheaduri = { `nil, `0 }; --FIXME
          436  +		convoheaduri = { `cvhu, `cvhlen }; --FIXME
   413    437   	}) ]
   414    438   	p.ptr.id = r:int(uint64,row,1)
   415    439   	p.ptr.author = r:int(uint64,row,2)
   416    440   	p.ptr.posted = r:int(uint64,row,6)
   417    441   	p.ptr.discovered = r:int(uint64,row,7)
   418    442   	if r:null(row,8)
   419    443   		then p.ptr.parent = 0
................................................................................
   682    706   		var r = queries.post_create.exec(src,post.author,post.subject,post.acl,post.body) 
   683    707   		if r.sz == 0 then return 0 end
   684    708   		defer r:free()
   685    709   		var id = r:int(uint64,0,0)
   686    710   		return id
   687    711   	end];
   688    712   
   689         -	instance_timeline_fetch = [terra(src: &lib.store.source, rg: lib.store.range)
          713  +	timeline_instance_fetch = [terra(src: &lib.store.source, rg: lib.store.range)
          714  +		var r = pqr { sz = 0 }
          715  +		var A,B,C,D = rg:matrix() -- :/
          716  +		r = queries.timeline_instance_fetch.exec(src,A,B,C,D)
          717  +		
          718  +		var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz)
          719  +		for i=0,r.sz do ret.ptr[i] = row_to_post(&r, i) end -- MUST FREE ALL
          720  +
          721  +		return ret
          722  +	end];
          723  +
          724  +	post_enum_author_uid = [terra(
          725  +		src: &lib.store.source,
          726  +		uid: uint64,
          727  +		rg: lib.store.range
          728  +	): lib.mem.ptr(lib.mem.ptr(lib.store.post))
   690    729   		var r = pqr { sz = 0 }
   691         -		if rg.mode == 0 then
   692         -			r = queries.instance_timeline_fetch.exec(src,rg.from_time,rg.to_time,0,0)
   693         -		elseif rg.mode == 1 then
   694         -			r = queries.instance_timeline_fetch.exec(src,rg.from_time,0,rg.to_idx,0)
   695         -		elseif rg.mode == 2 then
   696         -			r = queries.instance_timeline_fetch.exec(src,0,rg.to_time,0,rg.from_idx)
   697         -		elseif rg.mode == 3 then
   698         -			r = queries.instance_timeline_fetch.exec(src,0,0,rg.to_idx,rg.from_idx)
   699         -		end
          730  +		var A,B,C,D = rg:matrix() -- :/
          731  +		r = queries.post_enum_author_uid.exec(src,A,B,C,D,uid)
   700    732   		
   701    733   		var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz)
   702    734   		for i=0,r.sz do ret.ptr[i] = row_to_post(&r, i) end -- MUST FREE ALL
   703    735   
   704    736   		return ret
   705    737   	end];
   706    738   

Modified doc/acl.md from [ea4e893c12] to [53956f184b].

     7      7   * **mutuals**: matches users you follow who also follow you
     8      8   * **followed**: matches users you follow
     9      9   * **followers**: matches users who follow you
    10     10   * **groupies**: matches users who follow you, but whom you do not follow
    11     11   * **mentioned**: matches users who are mentioned in the post
    12     12   * **staff**: matches instance staff (equivalent to `~%0`)
    13     13   * **admin**: matches the individual named as the instance administrator, if any
    14         -* **@**`handle`: matches the user `handle`
    15         -* **+**`circle`: matches users you have categorized under `circle`
    16         -* **#**`room`: matches users who are members of `room`
    17         -* **%**`rank`: matches users of `rank` or higher (e.g. `%3` matches users of rank 3, 2, and 1). as a special case, `%0` matches ordinary users
    18         -* **#**`room`**%**`rank`: matches users who hold `rank` in `room`
    19         -* **<**`title`**>**: matches peers of the net who have been created `title` by the sovereign
    20         -* **#**`room`**<**`title`**>**: matches peers of the chat who have been created `title` by `room` staff
           14  +* **@**_handle_: matches the user *handle*
           15  +* **+**_circle_: matches users you have categorized under *circle*
           16  +* **#**_room_: matches users who are members of *room*
           17  +* **%**_rank_: matches users of *rank* or higher (e.g. `%3` matches users of rank 3, 2, and 1). as a special case, `%0` matches ordinary users
           18  +* **#**_room_**%**_rank_: matches users who hold *rank* in *room*
           19  +* **<**_title_**>**: matches peers of the net who have been created *title* by the sovereign
           20  +* **#**_room_**<**_title_**>**: matches peers of the chat who have been created *title* by *room* staff
    21     21   
    22     22   to evaluate an ACL expression, `parsav` reads each term from start to finish. for each term, it considers whether it describes the user who is attempting to access the content. if the term matches, its policy is applied and the expression completes. if the term doesn't match, the server proceeds on to the next term and the process repeats until it finds a matching term or runs out of terms, applying the fallback policy.
    23     23   
    24     24   **policy** is whether a term grants or denies access. the default term policy is **allow**, but you can control the policy with the keywords `allow` and `deny`. if a term finishes evaluating without any match being found, a fallback policy is applied; this fallback is the opposite of whatever the current policy is. this sounds confusing but makes ACL expressions much more intuitive; `allow @bob` and `deny trent` do exactly what you'd expect — the former allows bob and only bob in; the latter denies access only to trent, but grants access to the rest of the world.
    25     25   
    26     26   expressions must contain at least one term to be valid. if they consist only of policy keywords, they will be rejected.
    27     27   

Modified parsav.t from [8c471232dd] to [1c10e6f8f8].

   369    369   end
   370    370   
   371    371   lib.load {
   372    372   	'srv';
   373    373   	'render:nav';
   374    374   	'render:login';
   375    375   	'render:profile';
   376         -	'render:userpage';
   377    376   	'render:compose';
   378    377   	'render:tweet';
          378  +	'render:userpage';
   379    379   	'render:timeline';
   380    380   	'render:docpage';
   381    381   	'route';
   382    382   }
   383    383   
   384    384   do
   385    385   	local p = string.format('parsav: %s\nbuilt on %s\n', config.build.str, config.build.when)

Modified render/profile.t from [d063b74f92] to [efe49adad0].

    10     10   	var auxp: pstr
    11     11   	if co.aid ~= 0 and co.who.id == actor.id then
    12     12   		auxp = lib.str.plit '<a href="/conf/profile">alter</a>'
    13     13   	elseif co.aid ~= 0 then
    14     14   		aux:compose('<a href="/', actor.xid, '/follow">follow</a><a href="/',
    15     15   			actor.xid, '/chat">chat</a>')
    16     16   		if co.who.rights.powers:affect_users() then
    17         -			aux:push('<a href="/',11):push(actor.xid,0):push('/ctl">control</a>',17)
           17  +			aux:lpush('<a href="/'):push(actor.xid,0):lpush('/ctl">control</a>')
    18     18   		end
    19     19   		auxp = aux:finalize()
    20     20   	else
    21     21   		aux:compose('<a href="/', actor.xid, '/follow">remote follow</a>')
    22     22   		auxp = aux:finalize()
    23     23   	end
    24     24   	var avistr: lib.str.acc if actor.origin == 0 then

Modified render/timeline.t from [e2a4558814] to [873aeea861].

    13     13   	var stoptime = lib.osclock.time(nil)
    14     14   
    15     15   	var posts = [lib.mem.vec(lib.mem.ptr(lib.store.post))] { 
    16     16   		sz = 0, run = 0
    17     17   	}
    18     18   	if mode == modes.follow then
    19     19   	elseif mode == modes.srvlocal then
    20         -		posts = co.srv:instance_timeline_fetch(lib.store.range {
           20  +		posts = co.srv:timeline_instance_fetch(lib.store.range {
    21     21   			mode = 1; -- T->I
    22     22   			from_time = stoptime;
    23     23   			to_idx = 64;
    24     24   		})
    25     25   	elseif mode == modes.fediglobal then
    26     26   	elseif mode == modes.circle then
    27     27   	end

Modified render/tweet.t from [71eb9101d9] to [00c7b6fd89].

     4      4   	return pstr { ptr = s, ct = lib.str.sz(s) }
     5      5   end
     6      6   
     7      7   local terra 
     8      8   render_tweet(co: &lib.srv.convo, p: &lib.store.post, acc: &lib.str.acc)
     9      9   	var author: &lib.store.actor
    10     10   	for j = 0, co.actorcache.top do
    11         -		lib.io.fmt('scanning cache for author %llu (%llu/%llu)\n', p.author, j, co.actorcache.top)
    12     11   		if p.author == co.actorcache(j).ptr.id then
    13     12   			author = co.actorcache(j).ptr
    14         -			lib.io.fmt('cache hit on idx %llu, skipping db lookup\n', j) 
    15     13   			goto foundauth
    16     14   		end
    17     15   	end
    18         -	lib.io.fmt('cache miss, checking db for id %llu\n', p.author) 
    19     16   	author = co.actorcache:insert(co.srv:actor_fetch_uid(p.author)).ptr
    20         -	lib.io.fmt('got author %s\n', author.handle) 
    21     17   
    22     18   	::foundauth::
    23     19   	var avistr: lib.str.acc if author.origin == 0 then
    24     20   		avistr:compose('/avi/',author.handle)
    25     21   	end
    26     22   	var timestr: int8[26] lib.osclock.ctime_r(&p.posted, &timestr[0])
    27         -	lib.io.fmt('got body %s\n', author.handle) 
    28     23   
    29     24   	var bhtml = lib.smackdown.html([lib.mem.ptr(int8)] {ptr=p.body,ct=0}) defer bhtml:free()
    30     25   
    31     26   	var idbuf: int8[lib.math.shorthand.maxlen]
    32     27   	var idlen = lib.math.shorthand.gen(p.id, idbuf)
    33     28   	var permalink: lib.str.acc permalink:compose('/post/',{idbuf,idlen})
    34     29   

Modified render/userpage.t from [9774faf032] to [edce0da550].

     3      3   render_userpage(co: &lib.srv.convo, actor: &lib.store.actor)
     4      4   	var ti: lib.str.acc 
     5      5   	if co.aid ~= 0 and co.who.id == actor.id then
     6      6   		ti:compose('my profile')
     7      7   	else
     8      8   		ti:compose('profile :: ', actor.handle)
     9      9   	end
    10         -	var pftxt = lib.render.profile(co,actor) defer pftxt:free()
    11     10   	var tiptr = ti:finalize()
           11  +
           12  +	var acc: lib.str.acc acc:init(1024)
           13  +	var pftxt = lib.render.profile(co,actor) defer pftxt:free()
           14  +	acc:ppush(pftxt)
           15  +
           16  +	var stoptime = lib.osclock.time(nil)
           17  +	var posts = co.srv:post_enum_author_uid(actor.id, lib.store.range {
           18  +		mode = 1; -- T->I
           19  +		from_time = stoptime;
           20  +		to_idx = 64;
           21  +	})
           22  +
           23  +	for i = 0, posts.sz do
           24  +		lib.render.tweet(co, posts(i).ptr, &acc)
           25  +		posts(i):free()
           26  +	end
           27  +	posts:free()
           28  +
           29  +	var bdf = acc:finalize()
    12     30   	co:stdpage([lib.srv.convo.page] {
    13         -		title = tiptr; body = pftxt;
           31  +		title = tiptr; body = bdf;
    14     32   		class = lib.str.plit 'profile';
    15     33   	})
    16     34   
    17     35   	tiptr:free()
           36  +	bdf:free()
    18     37   end
    19     38   
    20     39   return render_userpage

Modified srv.t from [8c264568c9] to [8808dbd7a5].

    18     18   }
    19     19   
    20     20   terra cfgcache:free() -- :/
    21     21   	self.secret:free()
    22     22   	self.instance:free()
    23     23   end
    24     24   
    25         -terra srv:instance_timeline_fetch(r: lib.store.range): lib.mem.vec(lib.mem.ptr(lib.store.post))
           25  +terra srv:post_enum_author_uid(uid: uint64, r: lib.store.range): lib.mem.vec(lib.mem.ptr(lib.store.post))
           26  +	var all: lib.mem.vec(lib.mem.ptr(lib.store.post)) all:init(64)
           27  +	for i=0,self.sources.ct do var src = self.sources.ptr + i
           28  +		if src.handle ~= nil and src.backend.timeline_instance_fetch ~= nil then
           29  +			var lst = src:post_enum_author_uid(uid,r)
           30  +			all:assure(all.sz + lst.ct)
           31  +			for j=0, lst.ct do all:push(lst.ptr[j]) end
           32  +			lst:free()
           33  +		end
           34  +	end
           35  +	return all
           36  +end
           37  +
           38  +terra srv:timeline_instance_fetch(r: lib.store.range): lib.mem.vec(lib.mem.ptr(lib.store.post))
    26     39   	var all: lib.mem.vec(lib.mem.ptr(lib.store.post)) all:init(64)
    27     40   	for i=0,self.sources.ct do var src = self.sources.ptr + i
    28         -		if src.handle ~= nil and src.backend.instance_timeline_fetch ~= nil then
    29         -			var lst = src:instance_timeline_fetch(r)
           41  +		if src.handle ~= nil and src.backend.timeline_instance_fetch ~= nil then
           42  +			var lst = src:timeline_instance_fetch(r)
    30     43   			all:assure(all.sz + lst.ct)
    31     44   			for j=0, lst.ct do all:push(lst.ptr[j]) end
    32     45   			lst:free()
    33     46   		end
    34     47   	end
    35     48   	return all
    36     49   end

Modified static/style.scss from [9520b92c84] to [b0a8082b0c].

   157    157   		background-color: adjust-color($color, $lightness: -53%, $alpha: -0.7);
   158    158   	}
   159    159   	@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
   160    160   		background-color: adjust-color($color, $lightness: -53%, $alpha: -0.1);
   161    161   	}
   162    162   }
   163    163   
          164  +h1 { margin-top: 0 }
          165  +
   164    166   header {
   165    167   	position: fixed;
   166    168   	height: min-content;
   167    169   	width: 100vw;
   168    170   	margin: 0;
   169    171   	padding: 0;
   170    172   	border-bottom: 1px solid black;
................................................................................
   191    193   			all: unset;
   192    194   			display: flex;
   193    195   			justify-content: flex-end;
   194    196   			align-items: center;
   195    197   			grid-column: 2/3; grid-row: 1/2;
   196    198   			> a[href] {
   197    199   				display: block;
   198         -				padding: 0.25in 0.15in;
          200  +				padding: 0.25in 0.10in;
   199    201   				//padding: calc((25% - 1em)/2) 0.15in;
   200    202   				&, &::after { transition: 0.3s; }
   201    203   				text-shadow: 1px 1px 1px black;
   202    204   				&:hover{
   203    205   					transform: scale(120%);
   204    206   				}
   205    207   			}
................................................................................
   221    223   	border: {
   222    224   		left: 1px solid black;
   223    225   		right: 1px solid black;
   224    226   	}
   225    227   }
   226    228   
   227    229   div.profile {
   228         -	@extend %box;
   229    230   	padding: 0.1in;
   230    231   	position: relative;
   231    232   	display: grid;
          233  +	margin-bottom: 0.4in;
   232    234   	grid-template-columns: 2fr 1fr;
   233         -	grid-template-rows: 1fr 1fr;
          235  +	grid-template-rows: max-content 1fr;
   234    236   	width: 100%;
   235    237   	> .banner {
   236    238   		grid-column: 1 / 3;
   237    239   		grid-row: 1 / 2;
   238    240   		display: grid;
   239    241   		grid-template-columns: 1.1in 1fr;
   240         -		grid-template-rows: 0.3in 1fr;
          242  +		grid-template-rows: max-content 1fr;
   241    243   		> .avatar {
   242    244   			display: block;
   243    245   			width: 1in; height: 1in;
   244    246   			grid-column: 1 / 2;
   245    247   			grid-row: 1 / 3;
   246    248   			border: 1px solid black;
   247    249   		}
................................................................................
   265    267   		}
   266    268   	}
   267    269   	> .stats {
   268    270   		grid-column: 3 / 4;
   269    271   		grid-row: 1 / 3;
   270    272   	}
   271    273   	> .menu {
   272         -		grid-column: 1 / 3;
   273         -		grid-row: 2 / 3;
          274  +		grid-column: 1 / 3; grid-row: 2 / 3;
          275  +		padding-top: 0.075in;
          276  +		flex-wrap: wrap;
   274    277   		display: flex;
   275    278   		justify-content: center;
   276    279   		align-items: center;
   277    280   		> a[href] {
   278    281   			@extend %button;
   279    282   			display: block;
   280         -			margin: 0 0.05in;
          283  +			margin: 0.025in 0.05in;
   281    284   		}
   282    285   		> hr {
   283    286   			all: unset;
   284    287   			display: block;
   285    288   			height: 0.3in;
   286    289   			width: 1px;
   287    290   			border-left: 1px solid rgba(0,0,0,0.6);
................................................................................
   413    416   		padding: 0.1in;
   414    417   		&:hover { font-weight: bold; }
   415    418   	}
   416    419   }
   417    420   
   418    421   code {
   419    422   	@extend %teletype;
   420         -	background: tone(-50%);
   421         -	border: 1px solid tone(-20%);
          423  +	background: tone(-55%);
          424  +	border: 1px inset tone(-20%);
   422    425   	padding: 2px 6px;
   423         -	text-shadow: 2px 2px black;
          426  +	font-size: 1.5ex !important;
          427  +	letter-spacing: 1.3px;
          428  +	padding-bottom: 3px;
          429  +	border-radius: 2px;
          430  +	vertical-align: baseline;
          431  +	box-shadow: 1px 1px 1px black;
   424    432   }
   425    433   
   426    434   div.post {
   427    435   	@extend %box;
   428    436   	display: grid;
   429    437   	grid-template-columns: 1in 1fr max-content;
   430         -	grid-template-rows: 1fr max-content;
          438  +	grid-template-rows: min-content max-content;
   431    439   	margin-bottom: 0.1in;
   432    440   	>.avatar {
   433    441   		grid-column: 1/2; grid-row: 1/2;
   434         -		width: 1in;
          442  +		img { display: block; width: 1in; margin:0; }
          443  +		background: linear-gradient(to bottom, tone(-53%), tone(-57%));
   435    444   	}
   436    445   	>a[href].username {
   437    446   		display: block;
   438    447   		grid-column: 1/3;
   439    448   		grid-row: 2/3;
   440    449   		text-align: left;
   441    450   		text-decoration: none;
................................................................................
   469    478   	@extend %teletype;
   470    479   }
   471    480   
   472    481   body.doc main {
   473    482   	@extend %serif;
   474    483   	li { margin-top: 0.05in; }
   475    484   	li:first-child { margin-top: 0; }
          485  +	h1, h2, h3, h4, h5, h6 {
          486  +		background: linear-gradient(to right, tone(-50%), transparent);
          487  +		margin-left: -0.4in;
          488  +		padding-left: 0.2in;
          489  +		text-shadow: 0 2px 0 black;
          490  +	}
   476    491   }

Modified store.t from [5b8778f17f] to [4959208545].

    91     91   		from_idx: uint64
    92     92   	}
    93     93   	union {
    94     94   		to_time: m.timepoint
    95     95   		to_idx: uint64
    96     96   	}
    97     97   }
           98  +
           99  +terra m.range:matrix()
          100  +	if self.mode == 0 then
          101  +		return self.from_time,self.to_time,0,0
          102  +	elseif self.mode == 1 then
          103  +		return self.from_time,0,self.to_idx,0
          104  +	elseif self.mode == 2 then
          105  +		return 0,self.to_time,0,self.from_idx
          106  +	elseif self.mode == 3 then
          107  +		return 0,0,self.to_idx,self.from_idx
          108  +	else lib.bail('invalid mode on timeline range!') end
          109  +end
    98    110   
    99    111   struct m.post {
   100    112   	id: uint64
   101    113   	author: uint64
   102    114   	subject: str
   103    115   	body: str
   104    116   	acl: str
   105    117   	posted: m.timepoint
   106    118   	discovered: m.timepoint
   107    119   	mentions: lib.mem.ptr(uint64)
   108    120   	circles: lib.mem.ptr(uint64) --only meaningful if scope is set to circle
   109         -	convo: uint64
          121  +	convoheaduri: str
   110    122   	parent: uint64
   111    123   -- ephemera
   112    124   	localpost: bool
   113    125   	source: &m.source
   114    126   }
   115    127   
   116    128   local cnf = terralib.memoize(function(ty,rty)
................................................................................
   222    234   			-- uid: uint64
   223    235   
   224    236   	actor_conf_str: cnf(rawstring, lib.mem.ptr(int8))
   225    237   	actor_conf_int: cnf(intptr, lib.stat(intptr))
   226    238   
   227    239   	post_save: {&m.source, &m.post} -> {}
   228    240   	post_create: {&m.source, &m.post} -> uint64
   229         -	actor_post_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(m.post)
          241  +	post_enum_author_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
   230    242   	convo_fetch_xid: {&m.source,rawstring} -> lib.mem.ptr(m.post)
   231    243   	convo_fetch_uid: {&m.source,uint64} -> lib.mem.ptr(m.post)
   232    244   
   233         -	actor_timeline_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
   234         -	instance_timeline_fetch: {&m.source, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
          245  +	timeline_actor_fetch_uid: {&m.source, uint64, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
          246  +	timeline_instance_fetch: {&m.source, m.range} -> lib.mem.ptr(lib.mem.ptr(m.post))
   235    247   }
   236    248   
   237    249   struct m.source {
   238    250   	backend: &m.backend
   239    251   	id: lib.mem.ptr(int8)
   240    252   	handle: &opaque
   241    253   	string: lib.mem.ptr(int8)

Modified view/tweet.tpl from [e1b2184d52] to [43ed0b36e9].

     1      1   <div class="post">
     2         -	<img class="avatar" src="@avatar">
            2  +	<div class="avatar"><img src="@avatar"></div>
     3      3   	<a class="username" href="/@acctlink">
     4      4   		<span class="nym">@nym</span> [<span class="handle">@xid</span>]
     5      5   	</a>
     6      6   	<div class="content">
     7      7   		<div class="subject">@subject</div>
     8      8   		<div class="text">@text</div>
     9      9   	</div>
    10     10   	<a class="permalink" href="@permalink">@when</a>
    11     11   </div>