Index: backend/pgsql.t ================================================================== --- backend/pgsql.t +++ backend/pgsql.t @@ -1517,14 +1517,35 @@ var q = queries.post_reacts_fetch_uid.exec(src,uid,post,'like') if q.sz > 0 then q:free() return true end return false end]; - timeline_instance_fetch = [terra(src: &lib.store.source, rg: lib.store.range) + timeline_instance_fetch = [terra( + src: &lib.store.source, + rg: lib.store.range + ): lib.mem.lstptr(lib.store.post) var r = pqr { sz = 0 } var A,B,C,D = rg:matrix() -- :/ r = queries.timeline_instance_fetch.exec(src,A,B,C,D) + + var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz) + for i=0,r.sz do + ret.ptr[i] = row_to_post(&r, i) -- MUST FREE ALL + ret.ptr[i].ptr.source = src + end + + return ret + end]; + + timeline_actor_fetch_uid = [terra( + src: &lib.store.source, + uid: uint64, + rg: lib.store.range + ): lib.mem.lstptr(lib.store.post) + var r = pqr { sz = 0 } + var A,B,C,D = rg:matrix() -- :/ + r = queries.timeline_actor_fetch.exec(src,uid,A,B,C,D) var ret: lib.mem.ptr(lib.mem.ptr(lib.store.post)) ret:init(r.sz) for i=0,r.sz do ret.ptr[i] = row_to_post(&r, i) -- MUST FREE ALL ret.ptr[i].ptr.source = src @@ -1610,19 +1631,19 @@ var recip = queries.actor_rel_enum.exec(src,relatee,relator) if res.sz > 0 then defer res:free() for i = 0, res.sz do var bit = res:int(uint16, i, 0)-1 - if bit < [#lib.store.relation.members] then r.rel:setbit(bit, true) + if bit < [#lib.store.relation.members] then r.rel:setbit(bit,true) else lib.warn('unknown relationship type in database') end end end if recip.sz > 0 then defer recip:free() for i = 0, recip.sz do var bit = recip:int(uint16, i, 0)-1 - if bit < [#lib.store.relation.members] then r.recip:setbit(bit, true) + if bit < [#lib.store.relation.members] then r.recip:setbit(bit,true) else lib.warn('unknown relationship type in database') end end end return r Index: doc/acl.md ================================================================== --- doc/acl.md +++ doc/acl.md @@ -31,6 +31,6 @@ * `+illuminati deny groupies`: allows access to everyone but groupies (unless they're in the illuminati) * `@eve @alice@nowhere.tld deny @bob @trent@witches.live`: grants access to eve and alice, but locks out bob and trent * ` #4th-intl`: restricts the post to the eyes of the Fourth International's secret cabal of anointed comrades and the grand dukes of the Empire * `deny ~%3`: blocks a post from being seen by anyone with a staff rank level below 3 -**limitations:** to inhibit potential denial-of-service attacks, ACL expressions can be a maximum of 128 characters, can contain at most 16 words, and cannot trigger queries against other servers. all information needed to evaluate an ACL expression must be known locally. this is particularly relevant with respect to rooms. +**limitations:** to inhibit potential denial-of-service attacks, ACL expressions can be a maximum of 256 characters, can contain at most 16 words, and cannot trigger queries against other servers. all information needed to evaluate an ACL expression must be known locally. this is particularly relevant with respect to rooms. Index: parsav.t ================================================================== --- parsav.t +++ parsav.t @@ -242,10 +242,15 @@ n.name = string.format("stat<%s>", ty.name) n.stat_basetype = ty return n end) lib.enum = function(tbl) + if type(tbl) == 'string' then -- shorthand syntax + local t = {} + for w in tbl:gmatch('(%g+)') do t[#t+1] = w end + tbl = t + end local ty = uint8 if #tbl >= 2^32 then ty = uint64 -- hey, can't be too safe elseif #tbl >= 2^16 then ty = uint32 elseif #tbl >= 2^8 then ty = uint16 end local o = { t = ty, members = tbl } @@ -267,10 +272,12 @@ local struct set { _store: uint8[bytes] } local struct bit { _v: intptr _set: &set} terra set:clear() for i=0,bytes do self._store[i] = 0 end end terra set:fill() for i=0,bytes do self._store[i] = 0xFF end end set.members = tbl + set.idvmap = o + set.null = quote var s: set s:clear() in s end set.name = string.format('set<%s>', table.concat(tbl, '|')) set.metamethods.__entrymissing = macro(function(val, obj) if o[val] == nil then error('value ' .. val .. ' not in set') end return `bit { _v=[o[val] - 1], _set = &(obj) } end) @@ -308,14 +315,38 @@ self._store[i/8] = self._store[i/8] or (1 << (i % 8)) else self._store[i/8] = self._store[i/8] and not (1 << (i % 8)) end end + local qksetexp = function(self,idx,rhs) + local ch,bit + if terralib.isconstant(idx) then + ch = math.floor(idx/8) + bit = idx%8 + else + ch = `[idx]/8 + bit = `[idx]%8 + end + + if terralib.isconstant(rhs) then + local b = rhs:asvalue() + if b == true then return quote + self._store[ch] = self._store[ch] or (1 << bit) + end elseif b == false then return quote + self._store[ch] = self._store[ch] and not (1 << bit) + end end + else + return quote self:setbit([ch], rhs) end + end + end + set.metamethods.__update = macro(qksetexp) + set.metamethods.__setentry = macro(function(field,self,rhs) + return `self:setbit([o[field]-1],rhs) + --return qksetexp(self, o[field], rhs) + end) set.bits = {} - set.idvmap = {} for i,v in ipairs(tbl) do - set.idvmap[v] = i set.bits[v] = quote var b: set b:clear() b:setbit([i-1], true) in b end end set.metamethods.__add = macro(function(self,other) local new = symbol(set) local q = quote var [new] new:clear() end Index: render/conf/users.t ================================================================== --- render/conf/users.t +++ render/conf/users.t @@ -138,11 +138,11 @@ house home prison jail box pit hole haven town trump putin truth liberty zone land ranch butt butts sex pimp cop mail slut goblin goblins no good bad only gtfo electro electric dragon space mars earth venus neptune pluto saturn star moon lunar catastrophe catastro cuck honk war lap cuddle - planet + planet pride ]] var tlds = splitwords [[ tld club town space xxx house land ranch horse com io online shop site vip ltd win men lgbt cat adult army analytics art associates bar bible biz black blog broker cam camp careers @@ -153,11 +153,11 @@ jobs land law life limited live lol mom network now party porn productions pub rehab rocks school sex sexy singles social software solutions spot store sucks supplies cuck uwu systems university vacations ventures wang website work wow wtf world xyz soy live gym park worship orb zone mail - war honk derp planet + war honk derp planet pride ]] var sub = rnd(uint8,0,10) == 0 if sub then a:ppush(words[rnd(intptr,0,[words.type.N])]):lpush('.') end a:ppush(words[rnd(intptr,0,[words.type.N])]) if rnd(uint8,0,3) == 0 or not sub then Index: render/docpage.t ================================================================== --- render/docpage.t +++ render/docpage.t @@ -32,11 +32,11 @@ local restrict = symbol(lib.store.powerset) local setbits = quote restrict:clear() end if t.meta.priv then if type(t.meta.priv) ~= 'table' then t.meta.priv = {t.meta.priv} end for _,v in pairs(t.meta.priv) do - setbits = quote [setbits]; (restrict.[v] << true) end + setbits = quote [setbits]; restrict.[v] = true end end end allpages[i] = quote var [restrict]; [setbits] in pgpair { name = [v]; parent = par; Index: render/media-gallery.t ================================================================== --- render/media-gallery.t +++ render/media-gallery.t @@ -10,19 +10,19 @@ render_media_gallery(co: &lib.srv.convo, path: lib.mem.ptr(lib.mem.ref(int8)), uid: uint64, acc: &lib.str.acc) -- note that when calling this function, path must be adjusted so that path(0) -- eq "media" var owner = false if co.aid ~= 0 and co.who.id == uid then owner = true end - var ou = co.srv:actor_fetch_uid(uid) - if not ou then goto e404 end - do defer ou:free() + var ou = co:uid2actor(uid) + if ou == nil then goto e404 end + do -- defer ou:free() var pfx = pstr.null() if not owner then var pa = co:stra(32) pa:lpush('/') - if ou(0).origin ~= 0 then pa:lpush('@') end - pa:push(ou(0).xid,0) + if ou.origin ~= 0 then pa:lpush('@') end + pa:push(ou.xid,0) pfx = pa:finalize() end if path.ct >= 3 and path(1):cmp('a') then var id, idok = lib.math.shorthand.parse(path(2).ptr, path(2).ct) @@ -129,11 +129,11 @@ var fa = co:stra(128) var fldr = co:pgetv('folder') for i=0,folders.ct do var ule = lib.html.urlenc(&co.srv.pool,folders(i), true) -- defer ule:free() var san = lib.html.sanitize(&co.srv.pool,folders(i), true) -- defer san:free() - fa:lpush(''):ppush(san):lpush('') lib.dbg('checking folder ',{fldr.ptr,fldr.ct},' against ',{folders(i).ptr,folders(i).ct}) if fldr:ref() and folders(i):cmp(fldr) then folder = folders(i) lib.dbg('folder match ',{fldr.ptr,fldr.ct}) else folders(i):free() @@ -143,11 +143,11 @@ view.folders = fa:finalize() folders:free() end if owner then - view.menu = 'upload
' + view.menu = 'upload
' end var md = co.srv:artifact_enum_uid(uid, folder) var gallery: lib.str.acc gallery:pool(&co.srv.pool,256) var files: lib.str.acc files:pool(&co.srv.pool,256) Index: render/profile.t ================================================================== --- render/profile.t +++ render/profile.t @@ -1,36 +1,138 @@ -- vim: ft=terra local pstr = lib.mem.ptr(int8) local terra cs(s: rawstring) return pstr { ptr = s, ct = lib.str.sz(s) } end + +local relkinds = { + pos = { + { id = 'follow', start = { + text = 'follow'; + desc = "this user's posts will appear in your timeline"; + }, stop = { + text = 'unfollow'; + desc = "this user's posts will no longer appear in your timeline"; + }}; + + { id = 'sub', start = { + text = 'subscribe'; + desc = "you will get a notification whenever this user posts"; + }, stop = { + text = 'unsubscribe'; + desc = "you will no longer get notifications when this user posts"; + }}; + }; + neg = { + { id = 'mute', start = { + text = 'mute'; + desc = "this user's posts will no longer appear anywhere"; + }, stop = { + text = 'unmute'; + desc = "this user's posts will once again be visible to you"; + }}; + + { id = 'exclude', start = { + text = 'exclude'; + desc = "this user will not be able to see your posts"; + }, stop = { + text = 'reinclude'; + desc = "this user will again be able to see your posts"; + }}; + + { id = 'avoid', start = { + text = 'avoid'; + desc = "this user's posts will not appear on your timeline even if you follow them and you will not receive notices from them, but they will still be visible in threads"; + }, stop = { + text = 'reconcile'; + desc = "this user will once again be able to originate notices to you, and will be shown on your timeline"; + }}; + + { id = 'collapse', start = { + text = 'collapse'; + desc = "this user's posts will still appear but will be collapsed by default, so you only see the text if you choose to expand each post"; + }, stop = { + text = 'expand'; + desc = "this user's posts will no longer be collapsed"; + }}; + + { id = 'disemvowel', start = { + text = 'disemvowel'; -- translations should not translate this literally + desc = "this user's posts will be ritually mutilated in a humorous fashion as appropriate to the script in which they are written; e.g. the removal of vowels in roman text and deletion of kana in japanese text"; + }, stop = { + text = 're-emvowel'; + desc = "this user's posts will once again appear normally"; + }}; + + { id = 'block', start = { + text = 'block'; + desc = "this user will not be able to interact with you in any fashion and they will be forced to unfollow you"; + }, stop = { + text = 'unblock'; + desc = "this user will once again be able to interact with you"; + }}; + }; +} + +local function +btnhtml(kind) + local function sb(class) + local b = kind[class] + return string.format('
' .. + '' .. + '

%s

' .. + '
', + (class == 'stop' and ' on' or ''), + (class == 'stop' and 'un' or '') .. kind.id, + b.text, b.desc) + end + return sb('start'), sb('stop') +end local terra render_profile( co: &lib.srv.convo, actor: &lib.store.actor, relationship: &lib.store.relationship ): pstr - var aux: lib.str.acc + var aux = co:stra(128) var followed = false -- FIXME if co.aid ~= 0 and co.who.id == actor.id then - aux:pcompose(&co.srv.pool,'alter') + aux:lpush('alter') elseif co.aid ~= 0 then - if not relationship.rel.follow() then - aux:pcompose(&co.srv.pool,'') - elseif relationship.rel.follow() then - aux:pcompose(&co.srv.pool,'') - end - aux:lpush(' chat') if co.who.rights.powers:affect_users() and co.who:overpowers(actor) then aux:lpush(' control') end else - aux:pcompose(&co.srv.pool,' remote follow') + aux:lpush(' remote follow') end var auxp = aux:finalize() var timestr: int8[26] lib.osclock.ctime_r(&actor.knownsince, ×tr[0]) + + var relbtns = co:stra(256) + var sancbtns = co:stra(256) + [(function() + local allkinds = {} + for kind, rels in pairs(relkinds) do + for i,v in ipairs(rels) do + v.kind = kind + allkinds[#allkinds + 1] = v + end + end + local br = {} + for i,v in ipairs(allkinds) do + local off, on = btnhtml(v) + local target = v.kind == 'pos' and relbtns or sancbtns + br[#br+1] = quote + if relationship.rel.[v.id]() + then target:ppush(lib.str.plit([on])) + else target:ppush(lib.str.plit([off])) + end + end + end + return br + end)()] 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)) @@ -99,10 +201,12 @@ timephrase = lib.trn(actor.origin == 0, pstr 'joined', pstr 'known since'); remarks = ''; auxbtn = auxp; + relations = relbtns:finalize(); + sanctions = sancbtns:finalize(); } if comments.sz > 0 then profile.remarks = comments:finalize() end var ret = profile:poolstr(&co.srv.pool) -- auxp:free() Index: render/timeline.t ================================================================== --- render/timeline.t +++ render/timeline.t @@ -1,43 +1,73 @@ -- vim: ft=terra -local modes = lib.enum {'follow','mutual','srvlocal','fediglobal','circle'} +local pstr = lib.str.t +local modes = lib.enum [[follow mutual local fedi circle]] +local terra +requires_login(m: modes.t): bool + return m == modes.follow + or m == modes.mutual + or m == modes.circle +end + local terra render_timeline(co: &lib.srv.convo, modestr: lib.mem.ref(int8)) - var mode = modes.srvlocal + var mode = modes.follow var circle: uint64 = 0 - -- if modestr:cmpl('local') then mode = modes.srvlocal - -- elseif modestr:cmpl('mutual') then mode = modes.mutual - -- elseif modestr:cmpl('global') then mode = modes.fediglobal - -- elseif modestr:cmpl('circle') then mode = modes.circle - -- end + if modestr:cmp('local') then mode = [modes['local']] + elseif modestr:cmp('mutual') then mode = modes.mutual + elseif modestr:cmp('fedi') then mode = modes.fedi + elseif modestr:cmp('circle') then mode = modes.circle + end + if requires_login(mode) and co.aid == 0 then mode = [modes['local']] end + var stoptime = lib.osclock.time(nil) var posts = [lib.mem.vec(lib.mem.ptr(lib.store.post))] { sz = 0, run = 0 } - if mode == modes.follow then - elseif mode == modes.srvlocal then - posts = co.srv:timeline_instance_fetch(lib.store.range { - mode = 1; -- T->I - from_time = stoptime; - to_idx = 64; - }) - elseif mode == modes.fediglobal then + var fetchmode = lib.store.range { + mode = 1; -- T->I + from_time = stoptime; + to_idx = 64; + } + if mode == modes.follow or mode == modes.mutual then + posts = co.srv:timeline_actor_fetch_uid(co.who.id,fetchmode) + elseif mode == [modes['local']] then + posts = co.srv:timeline_instance_fetch(fetchmode) + elseif mode == modes.fedi then elseif mode == modes.circle then end var acc = co:stra(1024) + var modelabels = arrayof(pstr, 'followed', 'mutuals', 'local instance', 'fediverse', 'circle') + var modelinks = arrayof(pstr, [modes.members]) + acc:lpush('
showing ') + for i=0, [modelabels.type.N] do + if co.aid ~= 0 or not requires_login(i) then + if i > 0 then acc:lpush(' ยท ') end + if i == mode then + acc:lpush(''):ppush(modelabels[i]):lpush('') + else + acc:lpush(''):ppush(modelabels[i]):lpush('') + end + end + end + acc:lpush('
') acc:lpush('
') var newest: lib.store.timepoint = 0 for i = 0, posts.sz do + if mode == modes.mutual and posts(i).ptr.author ~= co.who.id then + var author = co:uid2actor(posts(i).ptr.author) + if not author.relationship.recip.follow() then goto skip end + end lib.render.tweet(co, posts(i).ptr, &acc) var t = lib.math.biggest(lib.math.biggest(posts(i).ptr.posted, posts(i).ptr.discovered),posts(i).ptr.edited) if t > newest then newest = t end - posts(i):free() + ::skip:: posts(i):free() end - posts:free() + if posts.run > 0 then posts:free() end acc:lpush('
') var doc = [lib.srv.convo.page] { title = 'timeline'; body = acc:finalize(); Index: render/tweet.t ================================================================== --- render/tweet.t +++ render/tweet.t @@ -26,14 +26,14 @@ if author ~= nil and (p.rtdby == 0 or retweeter ~= nil) then goto foundauth end end if author == nil then - author = co.actorcache:insert(co.srv:actor_fetch_uid(p.author)).ptr + author = co.actorcache:insert(co:uid2actor_live(p.author)).ptr end if p.rtdby ~= 0 and retweeter == nil then - retweeter = co.actorcache:insert(co.srv:actor_fetch_uid(p.rtdby)).ptr + retweeter = co.actorcache:insert(co:uid2actor_live(p.rtdby)).ptr end ::foundauth:: var timestr: int8[26] lib.osclock.ctime_r(&p.posted, ×tr[0]) for i=0,26 do if timestr[i] == @'\n' then timestr[i] = 0 break end end -- ๐Ÿ™„ Index: render/user-page.t ================================================================== --- render/user-page.t +++ render/user-page.t @@ -3,17 +3,16 @@ render_userpage( co : &lib.srv.convo, actor : &lib.store.actor, relationship: &lib.store.relationship ): {} - var ti: lib.str.acc + var ti: lib.str.t if co.aid ~= 0 and co.who.id == actor.id then - ti:compose('my profile') + ti = 'my profile' else - ti:compose('profile :: ', actor.handle) + ti = co:qstr('profile :: ', actor.handle) end - var tiptr = ti:finalize() var acc: lib.str.acc acc:pool(&co.srv.pool, 1024) var pftxt = lib.render.profile(co,actor,relationship) --defer pftxt:free() acc:ppush(pftxt) @@ -35,15 +34,15 @@ posts:free() acc:lpush('') var bdf = acc:finalize() co:livepage([lib.srv.convo.page] { - title = tiptr; body = bdf; + title = ti; body = bdf; class = 'profile'; cache = false; }, newest) - tiptr:free() + --tiptr:free() --bdf:free() end return render_userpage Index: route.t ================================================================== --- route.t +++ route.t @@ -14,28 +14,28 @@ if co.aid ~= 0 then rel = co.srv:actor_rel_calc(co.who.id, actor.id) if meth == method.post then var act = co:ppostv('act') if rel.recip.block() then - if act:cmp( 'follow') or act:cmp( 'subscribe') then + if act:cmp('follow') or act:cmp('subscribe') then co:complain(403,'blocked','you cannot follow a user you are blocked by') return end end - if act:cmp( 'block') and not rel.rel.block() then - (rel.rel.block << true) ; (rel.recip.follow << false) + if act:cmp('block') and not rel.rel.block() then + rel.rel.block = true rel.recip.follow = false co.srv:actor_rel_create([lib.store.relation.idvmap.block], co.who.id, actor.id) co.srv:actor_rel_destroy([lib.store.relation.idvmap.follow], actor.id, co.who.id) - else + elseif not act:cmp('report') then [(function() local tests = quote co:complain(400,'bad request','the action you have attempted on this user is not meaningful') return end for i,v in ipairs(lib.store.relation.members) do tests = quote - if [v ~= 'block'] and act:cmp(([v])) and not rel.rel.[v]() then -- rely on dead code elimination :/ - (rel.rel.[v] << true) + if [v ~= 'block'] and act:cmp(lib.str.plit([v])) and not rel.rel.[v]() then -- rely on dead code elimination :/ + rel.rel.[v] = true co.srv:actor_rel_create([lib.store.relation.idvmap[v]], co.who.id, actor.id) - elseif act:cmp((['un'..v])) and rel.rel.[v]() then - (rel.rel.[v] << false) + elseif act:cmp(lib.str.plit(['un'..v])) and rel.rel.[v]() then + rel.rel.[v] = false co.srv:actor_rel_destroy([lib.store.relation.idvmap[v]], co.who.id, actor.id) else [tests] end end end return tests Index: srv.t ================================================================== --- srv.t +++ srv.t @@ -47,23 +47,32 @@ end end return all end -terra srv:timeline_instance_fetch(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:timeline_instance_fetch(r) - all:assure(all.sz + lst.ct) - for j=0, lst.ct do all:push(lst.ptr[j]) end - lst:free() +local function deftlfetch(fnname, ...) + local args = {} + for i,ty in ipairs{...} do args[#args + 1] = symbol(ty) end + args[#args + 1] = symbol(lib.store.range) + fnname = 'timeline_' .. fnname + srv.methods[fnname] = terra(self: &srv, [args]): 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.[fnname] ~= nil then + var lst = src:[fnname]([args]) + all:assure(all.sz + lst.ct) + for j=0, lst.ct do all:push(lst.ptr[j]) end + lst:free() + end end + return all end - return all end +deftlfetch('instance_fetch') +deftlfetch('actor_fetch_uid', uint64) + srv.metamethods.__methodmissing = macro(function(meth, self, ...) local primary, ptr, stat, simple, oid = 0,1,2,3,4 local tk, rt = primary local expr = {...} for _,f in pairs(lib.store.backend.entries) do @@ -170,10 +179,40 @@ } terra getpeer(con: &lib.net.mg_connection) return [&strucheader](con).peer end end + +terra convo:uid2actor_live(uid: uint64) + var actor = self.srv:actor_fetch_uid(uid) + if actor:ref() then + if self.aid ~= 0 and self.who.id ~= uid then + actor(0).relationship = self.srv:actor_rel_calc(self.who.id, uid) + else -- defensive branch + actor(0).relationship = lib.store.relationship { + agent = 0, patient = uid; + rel = [lib.store.relation.null], + recip = [lib.store.relation.null], + } + end + end + return actor +end + +terra convo:uid2actor(uid: uint64) + var actor: &lib.store.actor = nil + for j = 0, self.actorcache.top do + if uid == self.actorcache(j).ptr.id then + actor = self.actorcache(j).ptr + break + end + end + if actor == nil then + actor = self.actorcache:insert(self:uid2actor_live(uid)).ptr + end + return actor +end terra convo:rawpage(code: uint16, pg: convo.page, hdrs: lib.mem.ptr(lib.http.header)) var doc = data.view.docskel { instance = self.srv.cfg.instance; title = pg.title; ADDED static/followreq.svg Index: static/followreq.svg ================================================================== --- static/followreq.svg +++ static/followreq.svg @@ -0,0 +1,745 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Index: static/style.scss ================================================================== --- static/style.scss +++ static/style.scss @@ -270,10 +270,40 @@ color: tone(20%,-0.1); font-size: 80%; vertical-align: text-top; } } + +body.profile { + #rel { + menu { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: repeat(max-content); + grid-gap: 0.1in; + > .opt { + padding: 0.1in; + border-radius: 5px; + border: 1px solid transparent; + &.on { + background-color: tone(-30%, -0.7); + box-shadow: 0 0 10px tone(-30%); + border-color: tone(-20%); + } + > button, > p { display: block; } + > p { text-align: center; font-size: 80%; margin: 0; margin-top: 0.1in; } + > button { + margin: auto; + } + &:last-child:nth-child(2n-1) { + grid-column: 1/3; + } + } + + } + } +} div.profile { padding: 0.1in; position: relative; display: grid; @@ -292,10 +322,11 @@ width: 1in; height: 1in; object-fit: cover; grid-column: 1 / 2; grid-row: 1 / 3; border: 1px solid black; + background-color: tone(-57%); } > .id { grid-column: 2 / 3; grid-row: 1 / 2; } @@ -413,11 +444,17 @@ width: max-content; margin: auto; background: tone(-20%,-0.3); border: 1px solid black; color: tone(-50%); padding: 0.1in; - > img { display: block; width: 1in; height: 1in; margin: auto; border: 1px solid black; } + > img { + display: block; + width: 1in; height: 1in; + margin: auto; + border: 1px solid black; + background-color: tone(-50%); + } > .name { @extend %serif; text-align: center; font-size: 130%; font-weight: bold; margin-top: 0.08in; } } >form { display: grid; grid-template-columns: 1fr 1fr; @@ -475,11 +512,11 @@ background-position: 0.05in 50%; }; padding-left: 0.40in; } -div.modal { +.modal { @extend %box; position: fixed; display: none; left: 0; right: 0; bottom: 0; top: 0; max-width: 7in; ADDED static/sub.svg Index: static/sub.svg ================================================================== --- static/sub.svg +++ static/sub.svg @@ -0,0 +1,516 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + Index: store.t ================================================================== --- store.t +++ store.t @@ -4,16 +4,18 @@ scope = lib.enum { 'public', 'private', 'local'; 'personal', 'direct', 'circle'; }; noticetype = lib.enum { - 'none', 'mention', 'reply', 'like', 'rt', 'react', 'follow' + -- only add new values to the end of this list! the numerical value + -- is stored in the database and must be kept synchronized across versions + 'none', 'mention', 'reply', 'like', 'rt', 'react', 'follow', 'followreq' }; relation = lib.set { 'follow', - 'subscribe', -- get a notification for every post + 'sub', -- get a notification for every post 'mute', -- posts will be completely hidden at all times 'block', -- no interactions will be permitted, but posts will remain visible 'silence', -- messages will not be accepted 'collapse', -- posts will be collapsed by default 'disemvowel', -- posts will be ritually humiliated, but shown @@ -100,10 +102,17 @@ (pow.account << true) (pow.edit << true) (pow.snitch << true) return m.rights { rank = 0, quota = 1000, invites = 0, powers = pow; } end + +struct m.relationship { + agent: uint64 + patient: uint64 + rel: m.relation -- agent โ†’ patient + recip: m.relation -- patient โ†’ agent +} struct m.actor { id: uint64 nym: str handle: str @@ -117,10 +126,11 @@ key: lib.mem.ptr(uint8) -- ephemera xid: str source: &m.source + relationship: m.relationship -- relationship to the logged-in user, if any } terra m.actor:outranks(other: &m.actor) -- this predicate determines where two users stand relative to -- each other in the formal staff hierarchy. it is used in @@ -332,17 +342,10 @@ netmask: m.inet privs: m.privset blacklist: bool } -struct m.relationship { - agent: uint64 - patient: uint64 - rel: m.relation -- agent โ†’ patient - recip: m.relation -- patient โ†’ agent -} - -- backends only handle content on the local server struct m.backend { id: rawstring open: &m.source -> &opaque close: &m.source -> {} dbsetup: &m.source -> bool -- creates the schema needed to call conprep (called only once per database e.g. with `parsav db init`) Index: view/media-gallery.tpl ================================================================== --- view/media-gallery.tpl +++ view/media-gallery.tpl @@ -1,19 +1,19 @@ @menu - new uploads - unfiled + new uploads + unfiled
@folders - all uploads - all images - all videos - all text files - all others + all uploads + all images + all videos + all text files + all others
@directory
Index: view/profile.tpl ================================================================== --- view/profile.tpl +++ view/profile.tpl @@ -23,9 +23,29 @@ archive media associates
+ options @auxbtn
+ +