Skip to content

Commit 0788f94

Browse files
Add Mastodon comments feature. Fixes #157 (#158)
* Add Mastodon comments feature. Fixes #157 * Move comments' related code to individual files * Move JS code in separate file * Pass id as string to avoid issue with js.Build params --------- Co-authored-by: Léo Cazenave <[email protected]>
1 parent ad430e4 commit 0788f94

File tree

7 files changed

+333
-6
lines changed

7 files changed

+333
-6
lines changed

code/comments.js

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import * as params from '@params';
2+
3+
function escapeHtml(unsafe) {
4+
return unsafe
5+
.replace(/&/g, "&amp;")
6+
.replace(/</g, "&lt;")
7+
.replace(/>/g, "&gt;")
8+
.replace(/"/g, "&quot;")
9+
.replace(/'/g, "&#039;");
10+
}
11+
function emojify(input, emojis) {
12+
let output = input;
13+
14+
emojis.forEach(emoji => {
15+
let picture = document.createElement("picture");
16+
17+
let source = document.createElement("source");
18+
source.setAttribute("srcset", escapeHtml(emoji.url));
19+
source.setAttribute("media", "(prefers-reduced-motion: no-preference)");
20+
21+
let img = document.createElement("img");
22+
img.className = "emoji";
23+
img.setAttribute("src", escapeHtml(emoji.static_url));
24+
img.setAttribute("alt", `:${emoji.shortcode}:`);
25+
img.setAttribute("title", `:${emoji.shortcode}:`);
26+
img.setAttribute("width", "20");
27+
img.setAttribute("height", "20");
28+
29+
picture.appendChild(source);
30+
picture.appendChild(img);
31+
32+
output = output.replace(`:${emoji.shortcode}:`, picture.outerHTML);
33+
});
34+
35+
return output;
36+
}
37+
38+
function loadComments() {
39+
let commentsWrapper = document.getElementById("comments-wrapper");
40+
document.getElementById("load-comment").innerHTML = "Chargement";
41+
fetch(`https://${params.host}/api/v1/statuses/${params.id}/context`)
42+
.then(function (response) {
43+
return response.json();
44+
})
45+
.then(function (data) {
46+
document.getElementById("load-comment").innerHTML = "Charger les commentaires";
47+
48+
if (data.error) {
49+
commentsWrapper.innerHTML = "Une erreur est survenue pendant le chargement des commentaires.";
50+
return;
51+
}
52+
53+
let descendants = data['descendants'];
54+
if (!descendants || !Array.isArray(descendants) || descendants.length <= 0) {
55+
commentsWrapper.innerHTML = "Pas encore de commentaires.";
56+
return;
57+
}
58+
59+
commentsWrapper.innerHTML = "";
60+
61+
descendants.forEach(function (status) {
62+
if (status.account.display_name.length > 0) {
63+
status.account.display_name = escapeHtml(status.account.display_name);
64+
status.account.display_name = emojify(status.account.display_name, status.account.emojis);
65+
} else {
66+
status.account.display_name = status.account.username;
67+
};
68+
69+
let instance = "";
70+
if (status.account.acct.includes("@")) {
71+
instance = status.account.acct.split("@")[1];
72+
} else {
73+
instance = params.host;
74+
}
75+
76+
const isReply = status.in_reply_to_id !== params.id;
77+
78+
status.content = emojify(status.content, status.emojis);
79+
80+
let avatarSource = document.createElement("source");
81+
avatarSource.setAttribute("srcset", escapeHtml(status.account.avatar));
82+
avatarSource.setAttribute("media", "(prefers-reduced-motion: no-preference)");
83+
84+
let avatarImg = document.createElement("img");
85+
avatarImg.className = "avatar";
86+
avatarImg.setAttribute("src", escapeHtml(status.account.avatar_static));
87+
avatarImg.setAttribute("alt", `@${status.account.username}@${instance} avatar`);
88+
89+
let avatarPicture = document.createElement("picture");
90+
avatarPicture.appendChild(avatarSource);
91+
avatarPicture.appendChild(avatarImg);
92+
93+
let avatar = document.createElement("a");
94+
avatar.className = "avatar-link";
95+
avatar.setAttribute("href", status.account.url);
96+
avatar.setAttribute("rel", "external nofollow");
97+
avatar.setAttribute("title", `Voir le profil sur @${status.account.username}@${instance}`);
98+
avatar.appendChild(avatarPicture);
99+
100+
let instanceBadge = document.createElement("a");
101+
instanceBadge.className = "instance";
102+
instanceBadge.setAttribute("href", status.account.url);
103+
instanceBadge.setAttribute("title", `@${status.account.username}@${instance}`);
104+
instanceBadge.setAttribute("rel", "external nofollow");
105+
instanceBadge.textContent = instance;
106+
107+
let display = document.createElement("span");
108+
display.className = "display";
109+
display.setAttribute("itemprop", "author");
110+
display.setAttribute("itemtype", "http://schema.org/Person");
111+
display.innerHTML = status.account.display_name;
112+
113+
let header = document.createElement("header");
114+
header.className = "author";
115+
header.appendChild(display);
116+
header.appendChild(instanceBadge);
117+
118+
let permalink = document.createElement("a");
119+
permalink.setAttribute("href", status.url);
120+
permalink.setAttribute("itemprop", "url");
121+
permalink.setAttribute("title", `Voir le commentaire sur ${instance}`);
122+
permalink.setAttribute("rel", "external nofollow");
123+
permalink.textContent = new Date(status.created_at).toLocaleString('fr-FR', {
124+
dateStyle: "long",
125+
timeStyle: "short",
126+
});
127+
128+
let timestamp = document.createElement("time");
129+
timestamp.setAttribute("datetime", status.created_at);
130+
timestamp.appendChild(permalink);
131+
132+
let main = document.createElement("main");
133+
main.setAttribute("itemprop", "text");
134+
main.innerHTML = status.content;
135+
136+
let interactions = document.createElement("footer");
137+
if (status.favourites_count > 0) {
138+
let faves = document.createElement("a");
139+
faves.className = "faves";
140+
faves.setAttribute("href", `${status.url}/favourites`);
141+
faves.setAttribute("title", `Favoris depuis ${instance}`);
142+
faves.textContent = status.favourites_count;
143+
144+
interactions.appendChild(faves);
145+
}
146+
147+
let comment = document.createElement("article");
148+
comment.id = `comment-${status.id}`;
149+
comment.className = isReply ? "comment comment-reply" : "comment";
150+
comment.setAttribute("itemprop", "comment");
151+
comment.setAttribute("itemtype", "http://schema.org/Comment");
152+
comment.appendChild(avatar);
153+
comment.appendChild(header);
154+
comment.appendChild(timestamp);
155+
comment.appendChild(main);
156+
comment.appendChild(interactions);
157+
158+
commentsWrapper.innerHTML += DOMPurify.sanitize(comment.outerHTML);
159+
});
160+
});
161+
}
162+
163+
document.getElementById("load-comment").addEventListener("click", loadComments);

code/purify.min.js

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

www/assets/css/comments.css

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/**
2+
* ╭───────────────────────────────────────────────────────────────╮
3+
* │ Comments │
4+
* ╰───────────────────────────────────────────────────────────────╯
5+
**/
6+
7+
#comments-wrapper {
8+
margin: 1.5em 0;
9+
padding: 0 var(25px);
10+
}
11+
12+
.comment {
13+
display: grid;
14+
column-gap: 1rem;
15+
grid-template-areas: "avatar name" "avatar time" "avatar post" "...... interactions";
16+
grid-template-columns: min-content;
17+
justify-items: start;
18+
margin: 0 auto 0 -1em;
19+
padding: .5em;
20+
}
21+
22+
.comment.comment-reply {
23+
margin: 0 auto 0 1em;
24+
}
25+
26+
.comment .avatar-link {
27+
grid-area: avatar;
28+
height: 4rem;
29+
position: relative;
30+
width: 4rem;
31+
}
32+
33+
.comment .avatar-link .avatar {
34+
height: 100%;
35+
width: 100%;
36+
}
37+
38+
.comment .author {
39+
align-items: center;
40+
display: flex;
41+
font-weight: 700;
42+
gap: .5em;
43+
grid-area: name;
44+
}
45+
46+
.comment .author .instance {
47+
background-color: var(--bg-accent);
48+
color: var(--fg-accent);
49+
border-radius: 9999px;
50+
font-size: smaller;
51+
font-weight: 400;
52+
padding: .25em .75em;
53+
}
54+
55+
.comment .author .instance:hover {
56+
opacity: .8;
57+
text-decoration: none;
58+
}
59+
60+
.comment time {
61+
grid-area: time;
62+
line-height: 3.5rem;
63+
}
64+
65+
.comment main {
66+
grid-area: post;
67+
}
68+
69+
.comment main p:first-child {
70+
margin-top: .25em;
71+
}
72+
73+
.comment main p:last-child {
74+
margin-bottom: 0;
75+
}
76+
77+
.comment footer {
78+
grid-area: interactions;
79+
border: none;
80+
margin-top: 1em;
81+
}
82+
83+
.comment footer .faves {
84+
color: inheritE
85+
}
86+
87+
.comment footer .faves:hover {
88+
opacity: .8;
89+
text-decoration: none;
90+
}
91+
92+
.comment footer .faves::before {
93+
color: red;
94+
content: "♥";
95+
font-size: 1rem;
96+
margin-inline-end: .25em;
97+
}
98+
99+
.comment .emoji {
100+
display: inline;
101+
height: 1.25em;
102+
vertical-align: middle;
103+
width: 1.25em;
104+
}
105+
106+
.comment .invisible {
107+
display: none;
108+
}
109+
110+
.comment .ellipsis::after {
111+
content: "…";
112+
}

www/assets/css/theme.css

+1-6
Original file line numberDiff line numberDiff line change
@@ -483,9 +483,4 @@ code { /* character, digrams, trigrams */
483483
header + nav li:has(.active) a.active {
484484
color: var(--fg-banner);
485485
}
486-
487-
figure figcaption,
488-
figure img {
489-
max-width: 45em;
490-
}
491-
}
486+
}

www/content/articles/bienvenue.md

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ title = "Bienvenue chez les Ergonautes !"
33
date = 2024-03-18T22:01:23+01:00
44
author = "nuclear_squid"
55
tags = ["communauté"]
6+
[comments]
7+
host = "mastodon.social"
8+
username = "fabi1cazenave"
9+
id = "112124416010685631"
610
+++
711

812
Après plus de quatre ans de travail et avec une version 1.0 en approche, il

www/layouts/_default/single.html

+4
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,8 @@
1414

1515
{{ .Content }}
1616

17+
{{ with .Params.comments }}
18+
{{ partial "comments.html" . }}
19+
{{ end }}
20+
1721
{{ end }}

www/layouts/partials/comments.html

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{{- with resources.Get "css/comments.css" }}
2+
{{- if eq hugo.Environment "development" }}
3+
<link rel="stylesheet" href="{{ .RelPermalink }}">
4+
{{- else }}
5+
{{- with . | minify | fingerprint }}
6+
<link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
7+
{{- end }}
8+
{{- end }}
9+
{{- end }}
10+
11+
{{- with resources.Get "js/purify.min.js" }}
12+
{{- if eq hugo.Environment "development" }}
13+
<script src="{{ relURL .RelPermalink }}" type="text/javascript"></script>
14+
{{- else }}
15+
{{- with . | minify | fingerprint }}
16+
<script src="{{ relURL .RelPermalink }}" integrity="{{- .Data.Integrity }}" crossorigin="anonymous" type="text/javascript"></script>
17+
{{- end }}
18+
{{- end }}
19+
{{- end }}
20+
21+
<section id="comments" class="article-content">
22+
<h2>Commentaires</h2>
23+
<p>Avec un compte sur le Fediverse ou Mastodon, vous pouvez répondre à ce <a
24+
href="https://{{ .host }}/@{{ .username }}/{{ .id }}">post</a>. Mastodon étant décentralisé, vous pouvez utiliser
25+
votre compte existant hébergé sur un autre serveur Mastodon ou une plateforme compatible si vous n'avez pas de
26+
compte sur celui-ci.</p>
27+
28+
<p id="mastodon-comments-list"><button id="load-comment">Charger les commentaires</button></p>
29+
<div id="comments-wrapper">
30+
<noscript>
31+
<p>Charger les commentaires nécessite JavaScript. Essayez d'activer Javascript et rechargez la page, ou visitez <a
32+
href="https://{{ .host }}/@{{ .username }}/{{ .id }}">le post original</a> sur Mastodon.</p>
33+
</noscript>
34+
</div>
35+
36+
{{- with resources.Get "js/comments.js" | js.Build (dict "params" (dict "host" .host "id" .id)) }}
37+
{{- if eq hugo.Environment "development" }}
38+
<script src="{{ relURL .RelPermalink }}" type="text/javascript"></script>
39+
{{- else }}
40+
{{- with . | minify | fingerprint }}
41+
<script src="{{ relURL .RelPermalink }}" integrity="{{- .Data.Integrity }}" crossorigin="anonymous" type="text/javascript"></script>
42+
{{- end }}
43+
{{- end }}
44+
{{- end }}
45+
46+
</section>

0 commit comments

Comments
 (0)