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