Overview
| Comment: | more jabbascript improvements |
|---|---|
| Downloads: | Tarball | ZIP archive | SQL archive |
| Timelines: | family | ancestors | descendants | both | trunk |
| Files: | files | file ages | folders |
| SHA3-256: |
b6c2a79945ff2c35e061b4eecfd6ec79 |
| User & Date: | lexi on 2021-01-04 20:33:33 |
| Other Links: | manifest | tags |
Context
|
2021-01-05
| ||
| 22:10 | improve sql, js, docs, other tweaks check-in: 7bd78f9b1c user: lexi tags: trunk | |
|
2021-01-04
| ||
| 20:33 | more jabbascript improvements check-in: b6c2a79945 user: lexi tags: trunk | |
| 15:29 | add like + retweets buttons, keyboard nav check-in: b9cf14c14b user: lexi tags: trunk | |
Changes
Modified parsav.md from [bd4297f1d2] to [7a2c43b008].
1 1 # parsav 2 2 3 -**parsav** is a lightweight fediverse server 3 +**parsav** is a lightweight social media server written in [terra](https://terralang.org), intended to integrate to some degree with the fediverse. it is named for the [Ranuir](http://ʞ.cc/fic/spirals/ranuir) words *par* "speech, communication" and *sav* "unity, togetherness, solidarity". 4 4 5 5 ## backends 6 6 parsav is designed to be storage-agnostic, and can draw data from multiple backends at a time. backends can be enabled or disabled at compile time to avoid unnecessary dependencies. 7 7 8 8 * postgresql 9 9 10 10 ## dependencies ................................................................................ 130 130 * sqlite3 131 131 * generic odbc 132 132 * lua 133 133 * ldap for auth (and maybe actors?) 134 134 * cdb (for static content, maybe?) 135 135 * mariadb/mysql 136 136 * the various nosql horrors, e.g. redis, mongo, and so on 137 + 138 +parsav urgently needs an internationalization framework as well. right now everything is just hardcoded in english. yuck.
Modified render/docpage.t from [97c704e199] to [8016afcf69].
70 70 var [pages] = array([allpages]) 71 71 var started = false 72 72 for i=0,[pages.type.N] do 73 73 if pages[i].parent == idx+1 and (pages[i].priv:sz() == 0 or 74 74 (ps and pages[i].priv):sz() ~= 0) then 75 75 if not started then 76 76 started = true 77 - list:lpush('<ul>') 77 + list:lpush('<ol>') 78 78 end 79 - list:lpush('<li><a href="/doc/'):rpush(pages[i].name):lpush('">') 79 + list:lpush('<li><a accesskey="'):ipush(i):lpush('" href="/doc/'):rpush(pages[i].name):lpush('">') 80 80 :rpush(pages[i].title):lpush('</a>') 81 81 pushbranches(list, i, ps) 82 82 list:lpush('</li>') 83 83 end 84 84 end 85 - if started then list:lpush('</ul>') end 85 + if started then list:lpush('</ol>') end 86 86 end 87 87 88 88 local terra 89 89 render_docpage(co: &lib.srv.convo, pg: pref) 90 90 var nullprivs: lib.store.powerset nullprivs:clear() 91 91 if not pg then -- display index 92 92 var list: lib.str.acc list:compose('<ul>')
Modified render/nav.t from [4a737bb7ff] to [c8c1aa5dcb].
1 1 -- vim: ft=terra 2 2 local terra 3 3 render_nav(co: &lib.srv.convo) 4 4 var t: lib.str.acc t:init(64) 5 5 if co.who ~= nil or co.srv.cfg.pol_sec == lib.srv.secmode.public then 6 - t:lpush(' <a href="/">timeline</a>') 6 + t:lpush(' <a accesskey="t" href="/">timeline</a>') 7 7 end 8 8 if co.who ~= nil then 9 - t:lpush(' <a href="/compose">compose</a> <a href="/'):push(co.who.xid,0) 10 - t:lpush('">profile</a> <a href="/conf">configure</a> <a href="/doc">docs</a> <a href="/logout">log out</a>') 9 + t:lpush(' <a accesskey="c" href="/compose">compose</a> <a accesskey="p" href="/'):push(co.who.xid,0) 10 + t:lpush('">profile</a> <a accesskey="o" href="/conf">configure</a> <a accesskey="d" href="/doc">docs</a> <a accesskey="g" href="/logout">log out</a>') 11 11 else 12 - t:lpush(' <a href="/doc">docs</a> <a href="/login">log in</a>') 12 + t:lpush(' <a accesskey="d" href="/doc">docs</a> <a accesskey="g" href="/login">log in</a>') 13 13 end 14 14 return t:finalize() 15 15 end 16 16 return render_nav
Modified render/profile.t from [08f3a58ce1] to [531b5ac1cf].
5 5 end 6 6 7 7 local terra 8 8 render_profile(co: &lib.srv.convo, actor: &lib.store.actor) 9 9 var aux: lib.str.acc 10 10 var followed = false -- FIXME 11 11 if co.aid ~= 0 and co.who.id == actor.id then 12 - aux:compose('<a class="button" href="/conf/profile?go=/@',actor.handle,'">alter</a>') 12 + aux:compose('<a accesskey="a" class="button" href="/conf/profile?go=/@',actor.handle,'">alter</a>') 13 13 elseif co.aid ~= 0 then 14 14 if not followed then 15 - aux:compose('<button method="post" class="pos" name="act" value="follow">follow</button>') 15 + aux:compose('<button accesskey="f" method="post" class="pos" name="act" value="follow">follow</button>') 16 16 elseif followed then 17 - aux:compose('<button method="post" class="neg" name="act" value="unfollow">unfollow</button>') 17 + aux:compose('<button accesskey="f" method="post" class="neg" name="act" value="unfollow">unfollow</button>') 18 18 end 19 - aux:lpush('<a class="button" href="/'):push(actor.xid,0):lpush('/chat">chat</a>') 19 + aux:lpush('<a accesskey="h" class="button" href="/'):push(actor.xid,0):lpush('/chat">chat</a>') 20 20 if co.who.rights.powers:affect_users() and co.who:overpowers(actor) then 21 - aux:lpush('<a class="button" href="/'):push(actor.xid,0):lpush('/ctl">control</a>') 21 + aux:lpush('<a accesskey="n" class="button" href="/'):push(actor.xid,0):lpush('/ctl">control</a>') 22 22 end 23 23 else 24 - aux:compose('<a class="button" href="/', actor.xid, '/follow">remote follow</a>') 24 + aux:compose('<a accesskey="f" class="button" href="/', actor.xid, '/follow">remote follow</a>') 25 25 end 26 26 var auxp = aux:finalize() 27 27 var timestr: int8[26] lib.osclock.ctime_r(&actor.knownsince, ×tr[0]) 28 28 29 29 var strfbuf: int8[28*4] 30 30 var stats = co.srv:actor_stats(actor.id) 31 31 var sn_posts = cs(lib.math.decstr_friendly(stats.posts, &strfbuf[ [strfbuf.type.N - 1] ])) 32 32 var sn_follows = cs(lib.math.decstr_friendly(stats.follows, sn_posts.ptr - 1)) 33 33 var sn_followers = cs(lib.math.decstr_friendly(stats.followers, sn_follows.ptr - 1)) 34 34 var sn_mutuals = cs(lib.math.decstr_friendly(stats.mutuals, sn_followers.ptr - 1)) 35 - var bio = lib.str.plit "<em>tall, dark, and mysterious</em>" 35 + var bio = lib.str.plit '<em style="opacity:0.6">tall, dark, and mysterious</em>' 36 36 if actor.bio ~= nil then 37 37 bio = lib.smackdown.html(cs(actor.bio)) 38 38 end 39 39 var fullname = lib.render.nym(actor,0,nil,false) defer fullname:free() 40 40 var comments: lib.str.acc comments:init(64) 41 - -- this is really more what epithets are for, i think 42 - --if actor.rights.rank > 0 then comments:lpush('<li>staff member</li>') end 41 + 43 42 if co.srv.cfg.master == actor.id then 44 - comments:lpush('<li style="--co:-70">founder</li>') 43 + var foundertxt = lib.str.plit 'founder' 44 + if co.srv.cfg.ui_cue_founder:ref() then 45 + if co.srv.cfg.ui_cue_founder.ct == 0 -- empty string, suppress field 46 + then foundertxt = pstr.null() 47 + else foundertxt = co.srv.cfg.ui_cue_founder 48 + end 49 + end 50 + 51 + if foundertxt:ref() then 52 + comments:lpush('<li style="--co:-70">'):ppush(foundertxt):lpush('</li>') 53 + end 45 54 end 46 55 if co.aid ~= 0 and actor.rights.rank ~= 0 then 56 + var stafftxt = lib.str.plit 'site staff' 57 + if co.srv.cfg.ui_cue_staff:ref() then 58 + if co.srv.cfg.ui_cue_staff.ct == 0 -- empty string, suppress field 59 + then stafftxt = pstr.null() 60 + else stafftxt = co.srv.cfg.ui_cue_staff 61 + end 62 + end 63 + 64 + -- this is really more what epithets are for, i think 65 + if actor.rights.rank > 0 and stafftxt:ref() then 66 + comments:lpush('<li>'):ppush(stafftxt):lpush('</li>') 67 + end 68 + 47 69 if co.who:outranks(actor) then 48 70 comments:lpush('<li style="--co:50">underling</li>') 49 71 elseif actor:outranks(co.who) then 50 72 comments:lpush('<li style="--co:-50">outranks you</li>') 51 73 end 52 74 end 53 75
Modified render/tweet-page.t from [8729ddd689] to [57c7b287c5].
27 27 co: &lib.srv.convo, 28 28 path: lib.mem.ptr(pref), 29 29 p: &lib.store.post 30 30 ): {} 31 31 var livetime = co.srv:thread_latest_arrival_calc(p.id) 32 32 33 33 var pg: lib.str.acc pg:init(256) 34 + pg:lpush('<div data-live="10">') -- make the OP refresh too 34 35 lib.render.tweet(co, p, &pg) 36 + pg:lpush('</div>') 35 37 36 38 if co.aid ~= 0 then 37 39 pg:lpush('<form class="action-bar" method="post">') 38 40 if not co.srv:post_liked_uid(co.who.id, p.id) 39 - then pg:lpush('<button class="pos" name="act" value="like">like</button>') 40 - else pg:lpush('<button class="neg" name="act" value="dislike">dislike</button>') 41 + then pg:lpush('<button class="pos" name="act" accesskey="l" value="like">like</button>') 42 + else pg:lpush('<button class="neg" name="act" accesskey="l" value="dislike">dislike</button>') 41 43 end 42 - pg:lpush('<button class="pos" name="act" value="rt">retweet</button>') 44 + pg:lpush('<button class="pos" name="act" accesskey="r" value="rt">retweet</button>') 43 45 if p.author == co.who.id then 44 - pg:lpush('<a class="button" href="/post/'):rpush(path(1)):lpush('/edit">edit</a><a class="neg button" href="/post/'):rpush(path(1)):lpush('/del">delete</a>') 46 + pg:lpush('<a class="button" accesskey="e" href="/post/'):rpush(path(1)):lpush('/edit">edit</a><a class="neg button" accesskey="d" href="/post/'):rpush(path(1)):lpush('/del">delete</a>') 45 47 end 46 48 -- TODO list user's chosen reaction emoji 47 49 pg:lpush('</form>') 48 50 49 51 end 50 52 pg:lpush('<div id="convo" data-live="10">') 51 53 render_tweet_replies(co, &pg, p.id)
Modified render/tweet.t from [ee058ed0af] to [d7574b82d8].
63 63 s:lpush('<div class="stats">') 64 64 if p.rts > 0 then s:lpush('<div class="rt">' ):ipush(p.rts ):lpush('</div>') end 65 65 if p.likes > 0 then s:lpush('<div class="like">'):ipush(p.likes):lpush('</div>') end 66 66 s:lpush('</div>') 67 67 tpl.stats = s:finalize() 68 68 end 69 69 70 - var attrbuf: int8[32] 70 + var attrbuf: int8[48] 71 + var attrcur = &attrbuf[0] 71 72 if p.accent ~= -1 and p.accent ~= co.ui_hue then 72 73 var hdecbuf: int8[21] 73 74 var hdec = lib.math.decstr(p.accent, &hdecbuf[20]) 74 - lib.str.cpy(&attrbuf[0], ' style="--hue:') 75 - lib.str.cpy(&attrbuf[14], hdec) 76 - var len = &hdecbuf[20] - hdec 77 - lib.str.cpy(&attrbuf[14] + len, '"') 78 - tpl.attr = &attrbuf[0] 75 + attrcur = lib.str.cpy(attrcur,' style="--hue:') 76 + attrcur = lib.str.cpy(attrcur, hdec) 77 + -- var len = &hdecbuf[20] - hdec 78 + attrcur = lib.str.cpy(attrcur, '"') 79 79 end 80 + if p.author == co.who.id then attrcur = lib.str.cpy(attrcur, ' data-own') end 81 + 82 + if attrcur ~= &attrbuf[0] then tpl.attr = &attrbuf[0] end 80 83 81 84 defer tpl.permalink:free() 82 85 if acc ~= nil then 83 86 if retweeter ~= nil then push_promo_header(co, acc, retweeter, p.rtact) end 84 87 tpl:append(acc) 85 88 if retweeter ~= nil then acc:lpush('</div>') end 86 89 if p.rts + p.likes > 0 then tpl.stats:free() end
Modified srv.t from [e0d52a1828] to [d4dcecb4e5].
1 1 -- vim: ft=terra 2 2 local util = lib.util 3 3 local secmode = lib.enum { 'public', 'private', 'lockdown', 'isolate' } 4 4 local pstring = lib.mem.ptr(int8) 5 5 local struct srv 6 6 local struct cfgcache { 7 - secret: lib.mem.ptr(int8) 7 + secret: pstring 8 8 pol_sec: secmode.t 9 9 pol_reg: bool 10 10 credmgd: bool 11 11 maxupsz: intptr 12 - instance: lib.mem.ptr(int8) 12 + instance: pstring 13 13 overlord: &srv 14 + ui_cue_staff: pstring 15 + ui_cue_founder: pstring 14 16 ui_hue: uint16 15 17 nranks: uint16 16 18 maxinvites: uint16 17 19 master: uint64 18 20 } 19 21 local struct srv { 20 22 sources: lib.mem.ptr(lib.store.source) ................................................................................ 23 25 cfg: cfgcache 24 26 id: rawstring 25 27 } 26 28 27 29 terra cfgcache:free() -- :/ 28 30 self.secret:free() 29 31 self.instance:free() 32 + self.ui_cue_staff:free() 33 + self.ui_cue_founder:free() 30 34 end 31 35 32 36 terra srv:post_enum_author_uid(uid: uint64, r: lib.store.range): lib.mem.vec(lib.mem.ptr(lib.store.post)) 33 37 var all: lib.mem.vec(lib.mem.ptr(lib.store.post)) all:init(64) 34 38 for i=0,self.sources.ct do var src = self.sources.ptr + i 35 39 if src.handle ~= nil and src.backend.timeline_instance_fetch ~= nil then 36 40 var lst = src:post_enum_author_uid(uid,r) ................................................................................ 812 816 if not wma then 813 817 lib.warn('the webmaster specified in the configuration store does not seem to exist or is not known to this instance; preceding as if no master defined. if the master is a remote user, you can rectify this with the `actor ',{webmaster.ptr,webmaster.ct},' instantiate` and `conf refresh` commands') 814 818 else 815 819 self.master = wma(0).id 816 820 wma:free() 817 821 end 818 822 end 823 + 824 + self.ui_cue_staff = self.overlord:conf_get('ui-profile-cue-staff') 825 + self.ui_cue_founder = self.overlord:conf_get('ui-profile-cue-master') 819 826 end 820 827 821 828 return { 822 829 overlord = srv; 823 830 convo = convo; 824 831 route = route; 825 832 secmode = secmode; 826 833 }
Modified static/live.js from [9100f46c08] to [f01ba9ae01].
2 2 * if there are any UI elements unfortunate enough to need 3 3 * interactivity beyond what native HTML+CSS can provide. if so, 4 4 * we attach the appropriate listeners to them. */ 5 5 window.addEventListener('load', function() { 6 6 /* social media is less fun when you can't just click on a tweet 7 7 * to insta-like or -retweet it. this is unfortunately not possible 8 8 * (except in various hideously shitty ways) without javascript. */ 9 - function mk(elt) { return document.createElement(elt); } 10 - function posturl(post) { 11 - return post.querySelector('.permalink').attributes.getNamedItem('href').value; 12 - } 9 + let mk = elt => document.createElement(elt); 10 + let posturl = post => post.querySelector('.permalink').attributes.getNamedItem('href').value; 11 + let focused = () => document.querySelector('textarea:focus, input:focus, button:focus, select:focus, a[href]:focus') != null; 13 12 function postReq(url,act,elt) { 14 13 fetch(new Request(url, { 15 14 method: 'POST', 16 15 body: 'act='+act 17 16 })).then(function(resp) { 18 17 if (resp.ok && resp.status == 200) { 19 - var i = parseInt(elt.innerHTML) 18 + let i = parseInt(elt.innerHTML) 20 19 if (isNaN(i)) {i=0} 21 20 elt.innerHTML = (i+1).toString() 21 + elt.animate({ 22 + transform: ['scale(1.4)', 'scale(1.0)'], 23 + filter: ['brightness(1)','brightness(0)'] 24 + },200) 22 25 } 23 26 }) 24 27 } 25 28 26 - /* div-based like and rt aren't very keyboard-friendly. add a replacement */ 27 - if (document.querySelector('body.timeline, body.profile') != null) { 28 - window.addEventListener('keydown', function(event) { 29 + function onkey(elt, fn) { 30 + elt.addEventListener('keydown', function(ev) { 29 31 if (!window._liveTweetMap) { return; } 30 32 if (event.isComposing || event.keyCode === 229) { return; } // 🙄 31 - var cururl = window._liveTweetMap.cur; 32 - var nexturl = null; 33 + return fn(ev); 34 + }) 35 + } 36 + 37 + /* div-based like and rt aren't very keyboard-friendly. add a replacement */ 38 + if (document.querySelector('body.timeline, body.profile, body.post') != null) { 39 + onkey(window, function(event) { 40 + if (focused()) {return;} 41 + let cururl = window._liveTweetMap.cur; 42 + let nexturl = null; 33 43 if (event.key == 'j') { // down 34 44 if (cururl == null) { 35 45 nexturl = window._liveTweetMap.first 36 46 } else { 37 47 nexturl = window._liveTweetMap.map.get(cururl).next 38 48 } 39 49 } else if (event.key == 'k') { // up 40 50 if (cururl == null) { 41 51 nexturl = window._liveTweetMap.last 42 52 } else { 43 53 nexturl = window._liveTweetMap.map.get(cururl).prev 44 54 } 45 55 } else if (cururl != null) { 46 - var post = window._liveTweetMap.map.get(cururl).me 56 + let post = window._liveTweetMap.map.get(cururl).me 47 57 if (event.key == 'f') { // fave 48 58 postReq(cururl, 'like', post.querySelector('.stats>.like')) 49 59 } else if (event.key == 'r') { // rt 50 60 postReq(cururl, 'rt', post.querySelector('.stats>.rt')) 61 + } else if (event.key == 'd') { // rt 62 + if (post.attributes.getNamedItem('data-own')) { 63 + window.location = cururl + '/del'; 64 + } 51 65 } else if (event.key == 'Enter') { // nav 52 66 window.location = cururl; 53 67 return; 54 68 } 55 69 } 56 70 if (nexturl != null) { 57 71 if (cururl != null) { 58 - var cur = window._liveTweetMap.map.get(cururl); 72 + let cur = window._liveTweetMap.map.get(cururl); 59 73 cur.me.classList.remove('live-selected') 60 74 } 61 - var next = window._liveTweetMap.map.get(nexturl); 75 + let next = window._liveTweetMap.map.get(nexturl); 62 76 next.me.classList.add('live-selected') 63 77 window._liveTweetMap.cur = nexturl 64 78 } 65 79 }); 66 80 } 81 + 82 + /* make ctrl-enter submit poasts. why the fuck does this require jabbascript */ 83 + document.querySelectorAll('form').forEach(form => form.querySelectorAll('textarea').forEach(function(te) { 84 + let submitbtn = form.querySelector('button[name], input[type="submit"][name], input[type="image"][name]'); 85 + onkey(te, function(e) { 86 + if(e.ctrlKey && e.keyCode == 13) { 87 + if(submitbtn == null) { form.submit(); } else { submitbtn.click(); } 88 + // are you kidding me with this shit 89 + return true; 90 + } 91 + }) 92 + })); 93 + 94 + /* allow response to queries via the keyboard */ 95 + let queryform = document.querySelector('body.query form'); 96 + if(queryform != null) { 97 + okbtn = queryform.querySelector('button[name]'); 98 + nobtn = queryform.querySelector('.button.no, button.no'); 99 + onkey(window, function(e) { 100 + if (focused()) {return;} 101 + if (e.keyCode == 13 || e.key == 'y') { 102 + if (okbtn != null) { okbtn.click() } else { queryform.submit() } 103 + } else if (e.key == 'Escape' || e.key == 'n') { 104 + if (nobtn != null) { nobtn.click() } else { window.history.back() } 105 + } 106 + }); 107 + } 67 108 68 109 function attachButtons() { 69 - var last = null; 70 - var newmap = { cur: null, first: null, last: null, map: new Map() } 71 - document.querySelectorAll('body:not(.post) main article.post').forEach(function(post){ 110 + let last = null; 111 + let newmap = { cur: null, first: null, last: null, map: new Map() } 112 + document.querySelectorAll('main article.post').forEach(function(post){ 72 113 let url = posturl(post); 73 114 if (last == null) { newmap.first = url; } else { 74 115 newmap.map.get(last).next = url 75 116 } 76 117 newmap.map.set(url, {me: post, prev: last, next: null}) 77 118 last = url 78 119 if (window._liveTweetMap && window._liveTweetMap.cur == url) { 79 120 post.classList.add('live-selected'); 80 121 } 81 122 82 - var stats = post.querySelector('.stats'); 123 + let stats = post.querySelector('.stats'); 83 124 if (stats == null) { 84 125 /* no stats box; create one */ 85 - var n = mk('div'); 126 + let n = mk('div'); 86 127 n.classList.add('stats'); 87 128 post.appendChild(n); 88 129 stats = n 89 130 } 90 131 function ensureElt(cls, before) { 91 132 let s = stats.querySelector('.' + cls); 92 133 if (s == null) { 93 - var n = mk('div'); 134 + let n = mk('div'); 94 135 n.classList.add(cls); 95 136 if (before == null) { stats.appendChild(n) } else { 96 137 stats.insertBefore(n,stats.querySelector(before)) 97 138 } 98 139 return n 99 140 } else { return s } 100 141 } 101 - var like = ensureElt('like', null); 102 - var rt = ensureElt('rt','.like'); 142 + let like = ensureElt('like', null); 143 + let rt = ensureElt('rt','.like'); 103 144 function activate(elt,name) { 104 145 elt.addEventListener('click', function(e) { postReq(url,name,elt) }); 105 146 elt.style.setProperty('cursor','pointer'); 106 147 elt.setAttribute('tabindex','0'); 107 148 } 108 149 activate(like,'like'); 109 150 activate(rt,'rt'); 110 151 }); 111 152 newmap.last = last 112 153 if (window._liveTweetMap) { 113 154 newmap.cur = window._liveTweetMap.cur // TODO handle vanishments 114 155 } 115 - window._liveTweetMap = newmap 156 + window._liveTweetMap = newmap; 116 157 } 117 158 118 159 /* update hue-picker background when slider is adjusted */ 119 160 document.querySelectorAll('.color-picker').forEach(function(box) { 120 161 let slider = box.querySelector('[data-color-pick]'); 121 162 box.style.setProperty('--hue', slider.value); 122 163 slider.addEventListener('input', function(e) { ................................................................................ 131 172 * tree from its html, find the element in question, ferret out 132 173 * any deltas, and apply them. */ 133 174 document.querySelectorAll('*[data-live]').forEach(function(container) { 134 175 let interv = parseFloat(container.attributes.getNamedItem('data-live').nodeValue) * 1000; 135 176 container._liveLastArrival = 0; /* TODO include initial value in document */ 136 177 137 178 window.setInterval(function() { 138 - var req = new Request(window.location, { 179 + let req = new Request(window.location, { 139 180 method: 'GET', 140 - headers: { 141 - 'X-Live-Last-Arrival': container._liveLastArrival 142 - } 181 + headers: { 'X-Live-Last-Arrival': container._liveLastArrival } 143 182 }) 144 183 145 184 fetch(req).then(function(resp) { 146 185 if (!resp.ok) return; 147 186 let newest = parseInt(resp.headers.get('X-Live-Newest-Artifact')); 148 187 if (newest == container._liveLastArrival) { // != also handles some deletions 149 188 resp.body.cancel(); 150 189 return; 151 190 } 152 191 container._liveLastArrival = newest 153 192 154 193 resp.text().then(function(htmlbody) { 155 - var parser = new DOMParser(); 156 - var newdoc = parser.parseFromString(htmlbody,'text/html') 194 + let parser = new DOMParser(); 195 + let newdoc = parser.parseFromString(htmlbody,'text/html') 157 196 container.innerHTML = newdoc.getElementById(container.id).innerHTML; 158 197 attachButtons(); 159 198 }) 160 199 }) 161 200 }, interv) 162 201 }); 163 202 164 203 attachButtons(); 165 204 });
Modified static/style.scss from [ea5ab728f6] to [d34b63e467].
500 500 article.post { 501 501 @extend %box; 502 502 display: grid; 503 503 margin: unset; 504 504 grid-template-columns: 1in 1fr max-content max-content; 505 505 grid-template-rows: min-content max-content; 506 506 margin-bottom: 0.1in; 507 - transition: 0.3s; 507 + transition: 0.2s ease-out; 508 508 >.avatar { 509 509 grid-column: 1/2; grid-row: 1/2; 510 510 img { display: block; width: 1in; height: 1in; margin:0; } 511 511 background: linear-gradient(to bottom, tone(-53%), tone(-57%)); 512 512 } 513 513 >a[href].username { 514 514 display: block; ................................................................................ 543 543 grid-column: 3/4; grid-row: 2/3; 544 544 justify-content: center; 545 545 > .like, > .rt { 546 546 margin: 0.5em 0.3em; 547 547 padding-left: 1.3em; 548 548 background-size: 1.1em; 549 549 background-repeat: no-repeat; 550 - pointer-events: all; 551 550 min-width: 0.3em; 552 551 &:empty { 553 552 transition: 0.3s; 554 553 opacity: 0.0001; // qutebrowser won't show hints if opacity=0 :( 555 554 &:hover, &:focus { opacity: 0.6 !important; } 556 555 } 557 556 } 558 557 > .like { background-image: url(/s/heart.webp); } 559 558 > .rt { background-image: url(/s/retweet.webp); } 560 559 } 561 560 562 561 // used for keyboard navigation 563 562 &.live-selected { 564 - margin-left: 0.4in; 565 - margin-right: -0.4in; 563 + //margin-left: 0.4in; margin-right: -0.4in; 566 564 box-shadow: 0 0 0 1px tone(15%), 0 0 1in tone(5%, -0.5); 565 + transform: scale(1.05) translateX(0.1in); 567 566 } 568 567 } 569 568 570 569 article.post:hover div.stats { > .like, > .rt { &:empty {opacity: 0.3;} } } 571 570 572 571 a[href].rawlink { 573 572 @extend %teletype;
Modified view/confirm.tpl from [0d2952df9c] to [8b0d6acfba].
1 1 <form class="message" method="post"> 2 2 <img class="icon" src="/s/query.webp"> 3 3 <h1>@title</h1> 4 4 <p>@query</p> 5 5 <menu class="horizontal choice"> 6 - <a class="button" href="@:cancel">cancel</a> 6 + <a class="no button" href="@:cancel">cancel</a> 7 7 <button name="act" value="confirm">confirm</button> 8 8 </menu> 9 9 </form>
Modified view/docskel.tpl from [3229efd171] to [e84fb6bf18].
5 5 <link rel="stylesheet" type="text/css" href="/s/style.css"> 6 6 <script type="text/javascript" src="/s/live.js" async></script> 7 7 </head> 8 8 <body class="@class"@attr> 9 9 <header><div> 10 10 <h1>@title</h1> 11 11 <nav> 12 - <a href="/instance">instance</a> 12 + <a accesskey="i" href="/instance">instance</a> 13 13 @navlinks 14 14 </nav> 15 15 </div></header> 16 16 <main> 17 17 @body 18 18 </main> 19 19 </body> 20 20 </html>