From b38b400e04e1654fd539c7dc888de9df479529a9 Mon Sep 17 00:00:00 2001 From: Clement Bois Date: Fri, 7 Jun 2019 15:05:12 +0200 Subject: [PATCH] Initial version --- README.md | 99 ++++++++++++++++++++++++++++++++++++++++ comtodon.js | 108 ++++++++++++++++++++++++++++++++++++++++++++ comtodon.min.js | 1 + index.html | 33 ++++++++++++++ index.pug | 26 +++++++++++ style.css | 117 ++++++++++++++++++++++++++++++++++++++++++++++++ style.css.map | 9 ++++ style.sass | 72 +++++++++++++++++++++++++++++ 8 files changed, 465 insertions(+) create mode 100644 README.md create mode 100644 comtodon.js create mode 100644 comtodon.min.js create mode 100644 index.html create mode 100644 index.pug create mode 100644 style.css create mode 100644 style.css.map create mode 100644 style.sass diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c60f25 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# Comtodon + +A minimal commenting system for static blogs using external [Mastodon](https://joinmastodon.org) or API [compatible](https://pleroma.social) server. + +- **painless:** use already existing fediverse accounts +- **serverless:** hosted on static server or cdn +- **lightweight:** any useless features *(IMO)* +- **no dependencies:** pure native js +- **personal:** no style, no tracking + +```html +
+ +``` + +See [index.html](index.html) + +## Deep + +Create a tree by adding `data-deep=N` + - 0: Full tree + - 1: Only "direct" replies + - 2: With replies to replies + - And so on + +## Moderation ? + +Add `data-moderator="{moderator_id}"` to display only approved *(replied)* comments + +Note: Can't use fav, it requires authentication + +## Hugo +Put `comtodon.min.js` in `static` folder + +In your site config +```yaml +params: + comtodon: + domain: mastodon.social + # moderator: 358957 +``` + +In your single page layout +```html +{{ if .Params.comtodon }} +
+ +{{- end }} +``` + +In your content header +```yaml +comtodon: 100745593232538751 +``` + +**Style it** + +## Mastodon + +Only use `/api/v1/statuses/:id/context`. See [doc](https://docs.joinmastodon.org/api/rest/statuses/#get-api-v1-statuses-id-context) + +> No authentication required + +``` +{ + descendants: [{ + account: { + acct, + avatar_static, + display_name, + id, + emojis: [{shortcode, static_url}], + url + }, + created_at, + content, + emojis: [{shortcode, static_url}], + id, + in_reply_to_id, + sensitive, + spoiler_text + }] +} +``` + +## No js ? + +Simple placeholder +```html +
+

Enable JavaScript to see comments

+
+``` + +## TODO + +- Use personal status as sample +- Create proxy backend for cache and moderating with fav +- Auto-magically create statuses from hugo ??? \ No newline at end of file diff --git a/comtodon.js b/comtodon.js new file mode 100644 index 0000000..f921b42 --- /dev/null +++ b/comtodon.js @@ -0,0 +1,108 @@ +(function () { + function fail(el, text) { + el.innerHTML = 'Loading fail =(' + console.error(text, el) + } + + function required(el, field) { + const val = el.dataset[field] + if (val) { + return val + } + fail(el, `Missing data-${field} attribut`) + } + + function h(classes, content, tag = 'div') { + return `<${tag} class="${classes}">${content}` + } + + const TIMES = new Map([ + ['second', 1000], + ['minute', 60], + ['hour', 60], + ['day', 24], + ['month', 30.5], + ['year', 12], + ['century', 100] + ]) + + function ago(date) { + const now = Date.now() + const target = Number(new Date(date)) + + const prefix = target > now ? 'in ' : '' + const milliseconds = Math.floor(Math.abs(target - now)) + + let cur = 0 + let divider = 1 + let name = 'millisecond' + for (const time of TIMES) { + divider *= time[1] + const next = Math.floor(milliseconds / divider) + if (next <= 0) { + return `${prefix}${cur} ${name}${cur > 1 ? 's' : ''} ago` + } + name = time[0] + cur = next + } + return `${prefix}a long time ago` + } + + function moji(text, emojis) { + for (const emoji of emojis) { + text = text.split(`:${emoji.shortcode}:`).join( + `${emoji.shortcode}` + ) + } + return text + } + + function tree(statuses, parent, limit) { + const [replies, others] = statuses.reduce(([c, o], s) => (s.in_reply_to_id == parent.id ? [[...c, s], o] : [c, [...o, s]]), [[], []]) + parent.replies = limit ? replies.map(r => tree(others, r, limit - 1)) : [] + return parent + } + + function html(statuses, domain) { + return statuses.map(({ account, created_at, content, id, emojis, sensitive, spoiler_text, replies }) => + h('status', h('date', ago(created_at), 'p') + + h('author" href="' + account.url, `` + + h('name', moji(account.display_name, account.emojis), 'span') + + h('acct', account.acct, 'span'), 'a') + + (spoiler_text || sensitive ? h('spoiler', spoiler_text || h('spoiler-empty', 'Sensitive', 'span')) : '') + + h('content' + (spoiler_text || sensitive ? ' sensitive' : ''), moji(content, emojis)) + + (replies ? h('replies', html(replies, domain)) : '') + + h(`reply" href="https://${domain}/interact/${id}?type=reply`, 'Reply', 'a'))).join('') + } + + function moderate(statuses, id) { + if (!id) { + return statuses + } + const valids = statuses.filter(s => s.account.id == id).map(s => s.in_reply_to_id) + return statuses.filter(s => valids.includes(s.id)) + } + + for (const el of document.getElementsByClassName('comtodon')) { + el.innerHTML = '
Loading...
' + + const domain = required(el, 'domain') + const status = required(el, 'status') + if (!domain || !status) { + return + } + + fetch(`https://${domain}/api/v1/statuses/${status}/context`) + .then(res => res.json()) + .then(res => { + el.innerHTML = h(`reply-main" href="https://${domain}/interact/${status}?type=reply`, 'Comment', 'a') + const statuses = moderate(res.descendants, el.dataset.moderator) + if (statuses) { + el.innerHTML += html('deep' in el.dataset ? tree(statuses, { id: status }, el.dataset.deep || -1).replies : statuses, domain) + } else { + el.innerHTML += h('empty', 'Any comment') + } + }) + .catch(() => fail(el, 'Request fail')) + } +})() \ No newline at end of file diff --git a/comtodon.min.js b/comtodon.min.js new file mode 100644 index 0000000..c7be8b4 --- /dev/null +++ b/comtodon.min.js @@ -0,0 +1 @@ +(function(){function a(a,b){a.innerHTML="Loading fail =(",console.error(b,a)}function b(b,c){const d=b.dataset[c];return d?d:void a(b,`Missing data-${c} attribut`)}function c(a,b,c="div"){return`<${c} class="${a}">${b}`}function d(a){const b=Date.now(),c=+new Date(a),d=c>b?"in ":"",e=Math.floor(Math.abs(c-b));let f=0,g=1,h="millisecond";for(const b of i){g*=b[1];const a=Math.floor(e/g);if(0>=a)return`${d}${f} ${h}${1`);return a}function f(a,b,c){const[d,e]=a.reduce(([a,c],d)=>d.in_reply_to_id==b.id?[[...a,d],c]:[a,[...c,d]],[[],[]]);return b.replies=c?d.map(a=>f(e,a,c-1)):[],b}function g(a,b){return a.map(({account:a,created_at:f,content:h,id:i,emojis:j,sensitive:k,spoiler_text:l,replies:m})=>c("status",c("date",d(f),"p")+c("author\" href=\""+a.url,``+c("name",e(a.display_name,a.emojis),"span")+c("acct",a.acct,"span"),"a")+(l||k?c("spoiler",l||c("spoiler-empty","Sensitive","span")):"")+c("content"+(l||k?" sensitive":""),e(h,j))+(m?c("replies",g(m,b)):"")+c(`reply" href="https://${b}/interact/${i}?type=reply`,"Reply","a"))).join("")}function h(a,b){if(!b)return a;const c=a.filter(a=>a.account.id==b).map(a=>a.in_reply_to_id);return a.filter(a=>c.includes(a.id))}const i=new Map([["second",1e3],["minute",60],["hour",60],["day",24],["month",30.5],["year",12],["century",100]]);for(const d of document.getElementsByClassName("comtodon")){d.innerHTML="
Loading...
";const e=b(d,"domain"),i=b(d,"status");if(!e||!i)return;fetch(`https://${e}/api/v1/statuses/${i}/context`).then(a=>a.json()).then(a=>{d.innerHTML=c(`reply-main" href="https://${e}/interact/${i}?type=reply`,"Comment","a");const b=h(a.descendants,d.dataset.moderator);d.innerHTML+=b?g("deep"in d.dataset?f(b,{id:i},d.dataset.deep||-1).replies:b,e):c("empty","Any comment")}).catch(()=>a(d,"Request fail"))}})(); \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..a3edff9 --- /dev/null +++ b/index.html @@ -0,0 +1,33 @@ + + + + + + Comtodon samples + + + + +

Comtodon samples

+
+
+

Simple

+
+
+
+

Tree

+
+
+
+

Direct

+
+
+
+

Moderation

+
+
+
+ + + + \ No newline at end of file diff --git a/index.pug b/index.pug new file mode 100644 index 0000000..0a07938 --- /dev/null +++ b/index.pug @@ -0,0 +1,26 @@ +doctype html +html(lang="en") + head + meta(charset="UTF-8") + title Comtodon samples + link(rel="stylesheet" href="style.css") + body + h1 Comtodon samples + main + section + h2 Simple + .comtodon(data-domain="mastodon.social" data-status="100745593232538751") + + section + h2 Tree + .comtodon(data-domain="mastodon.social" data-status="100745593232538751" data-deep) + + section + h2 Direct + .comtodon(data-domain="mastodon.social" data-status="100745593232538751" data-deep="1") + + section + h2 Moderation + .comtodon(data-domain="mastodon.social" data-status="100745593232538751" data-deep="1" data-moderator="358957") + + script(src="comtodon.js" defer) \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..f561b54 --- /dev/null +++ b/style.css @@ -0,0 +1,117 @@ +@charset "UTF-8"; +* { + font-family: sans-serif; +} + +h1, h2 { + text-align: center; +} + +main { + display: -ms-grid; + display: grid; + -ms-grid-columns: (1fr)[2]; + grid-template-columns: repeat(2, 1fr); + grid-gap: 2em; + padding: 0 2em; +} + +.comtodon .status { + position: relative; +} + +.comtodon .content, .comtodon .reply-main { + margin-bottom: .5em; +} + +.comtodon .content { + border-left: 2px solid #777; +} + +.comtodon .content.sensitive { + content: 'Sensitive'; +} + +.comtodon .replies { + margin-left: 1em; +} + +.comtodon p { + margin: .1em; +} + +.comtodon a { + text-decoration: none; + color: #777; +} + +.comtodon .emoji { + height: 1em; +} + +.comtodon .author { + display: -ms-grid; + display: grid; + -ms-grid-columns: 3.3em auto; + grid-template-columns: 3.3em auto; + -ms-grid-rows: (1em)[3]; + grid-template-rows: repeat(3, 1em); + grid-gap: .1em; +} + +.comtodon .author .avatar { + border-radius: 20%; + -ms-grid-row: 1; + -ms-grid-row-span: 3; + -ms-grid-column: 1; + grid-area: 1 / 1 / 4 / 1; + height: 100%; +} + +.comtodon .author .name { + color: black; + font-weight: bold; +} + +.comtodon .author .acct:before { + content: '@'; +} + +.comtodon .date { + float: right; +} + +.comtodon .reply-main { + display: inline-block; + color: white; + background-color: #777; + border-radius: .5em; + padding: .1em .3em; +} + +.comtodon .reply-main:after { + content: '✉'; + margin-left: .5em; +} + +.comtodon .reply { + visibility: hidden; + position: absolute; + right: .1em; + top: 2em; +} + +.comtodon .reply:after { + content: '✉'; + visibility: visible; + display: block; + position: absolute; + right: 0; + bottom: 0; + color: white; + background-color: #777; + border-radius: .5em; + font-size: .8em; + padding: .1em .3em; +} +/*# sourceMappingURL=style.css.map */ \ No newline at end of file diff --git a/style.css.map b/style.css.map new file mode 100644 index 0000000..5e6306a --- /dev/null +++ b/style.css.map @@ -0,0 +1,9 @@ +{ + "version": 3, + "mappings": ";AAAA,AAAA,CAAC,CAAC;EACA,WAAW,EAAE,UAAU;CAAG;;AAE5B,AAAA,EAAE,EAAE,EAAE,CAAC;EACL,UAAU,EAAE,MAAM;CAAG;;AAEvB,AAAA,IAAI,CAAC;EACH,OAAO,EAAE,IAAI;EACb,qBAAqB,EAAE,cAAc;EACrC,QAAQ,EAAE,GAAG;EACb,OAAO,EAAE,KAAK;CAAG;;AAEnB,AACE,SADO,CACP,OAAO,CAAC;EACN,QAAQ,EAAE,QAAQ;CAAG;;AAFzB,AAGE,SAHO,CAGP,QAAQ,EAHV,SAAS,CAGG,WAAW,CAAC;EACpB,aAAa,EAAE,IAAI;CAAG;;AAJ1B,AAKE,SALO,CAKP,QAAQ,CAAC;EACP,WAAW,EAAE,cAAc;CAEA;;AAR/B,AAOI,SAPK,CAKP,QAAQ,AAEL,UAAU,CAAC;EACV,OAAO,EAAE,WAAW;CAAG;;AAR7B,AASE,SATO,CASP,QAAQ,CAAC;EACP,WAAW,EAAE,GAAG;CAAG;;AAVvB,AAWE,SAXO,CAWP,CAAC,CAAC;EACA,MAAM,EAAE,IAAI;CAAG;;AAZnB,AAaE,SAbO,CAaP,CAAC,CAAC;EACA,eAAe,EAAE,IAAI;EACrB,KAAK,EAAE,IAAI;CAAG;;AAflB,AAgBE,SAhBO,CAgBP,MAAM,CAAC;EACL,MAAM,EAAE,GAAG;CAAG;;AAjBlB,AAkBE,SAlBO,CAkBP,OAAO,CAAC;EACN,OAAO,EAAE,IAAI;EACb,qBAAqB,EAAE,UAAU;EACjC,kBAAkB,EAAE,cAAc;EAClC,QAAQ,EAAE,IAAI;CASK;;AA/BvB,AAuBI,SAvBK,CAkBP,OAAO,CAKL,OAAO,CAAC;EACN,aAAa,EAAE,GAAG;EAClB,SAAS,EAAE,aAAa;EACxB,MAAM,EAAE,IAAI;CAAG;;AA1BrB,AA2BI,SA3BK,CAkBP,OAAO,CASL,KAAK,CAAC;EACJ,KAAK,EAAE,KAAK;EACZ,WAAW,EAAE,IAAI;CAAG;;AA7B1B,AA8BI,SA9BK,CAkBP,OAAO,CAYL,KAAK,AAAA,OAAO,CAAC;EACX,OAAO,EAAE,GAAG;CAAG;;AA/BrB,AAgCE,SAhCO,CAgCP,KAAK,CAAC;EACJ,KAAK,EAAE,KAAK;CAAG;;AAjCnB,AAkCE,SAlCO,CAkCP,WAAW,CAAC;EACV,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,KAAK;EACZ,gBAAgB,EAAE,IAAI;EACtB,aAAa,EAAE,IAAI;EACnB,OAAO,EAAE,SAAS;CAGM;;AA1C5B,AAwCI,SAxCK,CAkCP,WAAW,AAMR,MAAM,CAAC;EACN,OAAO,EAAE,IAAI;EACb,WAAW,EAAE,IAAI;CAAG;;AA1C1B,AA2CE,SA3CO,CA2CP,MAAM,CAAC;EACL,UAAU,EAAE,MAAM;EAClB,QAAQ,EAAE,QAAQ;EAClB,KAAK,EAAE,IAAI;EACX,GAAG,EAAE,GAAG;CAYiB;;AA3D7B,AAgDI,SAhDK,CA2CP,MAAM,AAKH,MAAM,CAAC;EACN,OAAO,EAAE,IAAI;EACb,UAAU,EAAE,OAAO;EACnB,OAAO,EAAE,KAAK;EACd,QAAQ,EAAE,QAAQ;EAClB,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;EACT,KAAK,EAAE,KAAK;EACZ,gBAAgB,EAAE,IAAI;EACtB,aAAa,EAAE,IAAI;EACnB,SAAS,EAAE,IAAI;EACf,OAAO,EAAE,SAAS;CAAG", + "sources": [ + "style.sass" + ], + "names": [], + "file": "style.css" +} \ No newline at end of file diff --git a/style.sass b/style.sass new file mode 100644 index 0000000..9f7736f --- /dev/null +++ b/style.sass @@ -0,0 +1,72 @@ +* + font-family: sans-serif + +h1, h2 + text-align: center + +main + display: grid + grid-template-columns: repeat(2, 1fr) + grid-gap: 2em + padding: 0 2em + +.comtodon + .status + position: relative + .content, .reply-main + margin-bottom: .5em + .content + border-left: 2px solid #777 + &.sensitive + content: 'Sensitive' + .replies + margin-left: 1em + p + margin: .1em + a + text-decoration: none + color: #777 + .emoji + height: 1em + .author + display: grid + grid-template-columns: 3.3em auto + grid-template-rows: repeat(3, 1em) + grid-gap: .1em + .avatar + border-radius: 20% + grid-area: 1 / 1 / 4 / 1 + height: 100% + .name + color: black + font-weight: bold + .acct:before + content: '@' + .date + float: right + .reply-main + display: inline-block + color: white + background-color: #777 + border-radius: .5em + padding: .1em .3em + &:after + content: '✉' + margin-left: .5em + .reply + visibility: hidden + position: absolute + right: .1em + top: 2em + &:after + content: '✉' + visibility: visible + display: block + position: absolute + right: 0 + bottom: 0 + color: white + background-color: #777 + border-radius: .5em + font-size: .8em + padding: .1em .3em