Index: static/live.js ================================================================== --- static/live.js +++ static/live.js @@ -5,24 +5,80 @@ window.addEventListener('load', function() { /* social media is less fun when you can't just click on a tweet * to insta-like or -retweet it. this is unfortunately not possible * (except in various hideously shitty ways) without javascript. */ function mk(elt) { return document.createElement(elt); } + function posturl(post) { + return post.querySelector('.permalink').attributes.getNamedItem('href').value; + } + function postReq(url,act,elt) { + fetch(new Request(url, { + method: 'POST', + body: 'act='+act + })).then(function(resp) { + if (resp.ok && resp.status == 200) { + var i = parseInt(elt.innerHTML) + if (isNaN(i)) {i=0} + elt.innerHTML = (i+1).toString() + } + }) + } + + /* div-based like and rt aren't very keyboard-friendly. add a replacement */ + if (document.querySelector('body.timeline, body.profile') != null) { + window.addEventListener('keydown', function(event) { + if (!window._liveTweetMap) { return; } + if (event.isComposing || event.keyCode === 229) { return; } // 🙄 + var cururl = window._liveTweetMap.cur; + var nexturl = null; + if (event.key == 'j') { // down + if (cururl == null) { + nexturl = window._liveTweetMap.first + } else { + nexturl = window._liveTweetMap.map.get(cururl).next + } + } else if (event.key == 'k') { // up + if (cururl == null) { + nexturl = window._liveTweetMap.last + } else { + nexturl = window._liveTweetMap.map.get(cururl).prev + } + } else if (cururl != null) { + var post = window._liveTweetMap.map.get(cururl).me + if (event.key == 'f') { // fave + postReq(cururl, 'like', post.querySelector('.stats>.like')) + } else if (event.key == 'r') { // rt + postReq(cururl, 'rt', post.querySelector('.stats>.rt')) + } else if (event.key == 'Enter') { // nav + window.location = cururl; + return; + } + } + if (nexturl != null) { + if (cururl != null) { + var cur = window._liveTweetMap.map.get(cururl); + cur.me.classList.remove('live-selected') + } + var next = window._liveTweetMap.map.get(nexturl); + next.me.classList.add('live-selected') + window._liveTweetMap.cur = nexturl + } + }); + } + function attachButtons() { - document.querySelectorAll('body:not(.post) main div.post').forEach(function(post){ - let url = post.querySelector('.permalink').attributes.getNamedItem('href').value; - function postReq(act,elt) { - fetch(new Request(url, { - method: 'POST', - body: 'act='+act - })).then(function(resp) { - if (resp.ok && resp.status == 200) { - var i = parseInt(elt.innerHTML) - if (isNaN(i)) {i=0} - elt.innerHTML = (i+1).toString() - } - }) + var last = null; + var newmap = { cur: null, first: null, last: null, map: new Map() } + document.querySelectorAll('body:not(.post) main article.post').forEach(function(post){ + let url = posturl(post); + if (last == null) { newmap.first = url; } else { + newmap.map.get(last).next = url + } + newmap.map.set(url, {me: post, prev: last, next: null}) + last = url + if (window._liveTweetMap && window._liveTweetMap.cur == url) { + post.classList.add('live-selected'); } var stats = post.querySelector('.stats'); if (stats == null) { /* no stats box; create one */ @@ -43,16 +99,22 @@ } else { return s } } var like = ensureElt('like', null); var rt = ensureElt('rt','.like'); function activate(elt,name) { - elt.addEventListener('click', function(e) { postReq(name,elt) }); + elt.addEventListener('click', function(e) { postReq(url,name,elt) }); elt.style.setProperty('cursor','pointer'); + elt.setAttribute('tabindex','0'); } activate(like,'like'); activate(rt,'rt'); }); + newmap.last = last + if (window._liveTweetMap) { + newmap.cur = window._liveTweetMap.cur // TODO handle vanishments + } + window._liveTweetMap = newmap } /* update hue-picker background when slider is adjusted */ document.querySelectorAll('.color-picker').forEach(function(box) { let slider = box.querySelector('[data-color-pick]'); Index: static/style.scss ================================================================== --- static/style.scss +++ static/style.scss @@ -482,11 +482,11 @@ box-shadow: 1px 1px 1px black; } div.thread { margin-left: 0.3in; - & + div.post { margin-top: 0.3in; } + & + article.post { margin-top: 0.3in; } } a[href].username { >.nym { font-weight: bold; } color: tone(0%,-0.4); @@ -495,17 +495,18 @@ &:hover { > span.nym { color: white; } > span.handle { color: tone(15%) } } } -div.post { +article.post { @extend %box; display: grid; margin: unset; grid-template-columns: 1in 1fr max-content max-content; grid-template-rows: min-content max-content; margin-bottom: 0.1in; + transition: 0.3s; >.avatar { grid-column: 1/2; grid-row: 1/2; img { display: block; width: 1in; height: 1in; margin:0; } background: linear-gradient(to bottom, tone(-53%), tone(-57%)); } @@ -544,27 +545,31 @@ > .like, > .rt { margin: 0.5em 0.3em; padding-left: 1.3em; background-size: 1.1em; background-repeat: no-repeat; + pointer-events: all; min-width: 0.3em; &:empty { transition: 0.3s; - opacity: 0.1; - &:hover { opacity: 0.6 !important; } + opacity: 0.0001; // qutebrowser won't show hints if opacity=0 :( + &:hover, &:focus { opacity: 0.6 !important; } } } - > .like { - background-image: url(/s/heart.webp); - } - > .rt { - background-image: url(/s/retweet.webp); - } + > .like { background-image: url(/s/heart.webp); } + > .rt { background-image: url(/s/retweet.webp); } + } + + // used for keyboard navigation + &.live-selected { + margin-left: 0.4in; + margin-right: -0.4in; + box-shadow: 0 0 0 1px tone(15%), 0 0 1in tone(5%, -0.5); } } -div.post:hover div.stats { > .like, > .rt { &:empty {opacity: 0.3;} } } +article.post:hover div.stats { > .like, > .rt { &:empty {opacity: 0.3;} } } a[href].rawlink { @extend %teletype; } Index: view/tweet.tpl ================================================================== --- view/tweet.tpl +++ view/tweet.tpl @@ -1,10 +1,10 @@ -
+
@nym
@!subject
@text
@stats - -
+ +