Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of UI for webmention reactions. #2572

Merged
merged 16 commits into from
Nov 25, 2021
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/server/csp.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
],
'connect-src': [
'\'self\'',
'webmention.io',
'discuss.httparchive.org',
'www.google-analytics.com',
'www.googletagmanager.com'
Expand Down
88 changes: 86 additions & 2 deletions src/static/css/page.css
Original file line number Diff line number Diff line change
Expand Up @@ -250,15 +250,18 @@
}

.authors h2,
.chapter-links h2 {
.chapter-links h2,
.webmentions h2 {
padding: 16px 0;
padding: 1rem 0;
}

.authors,
.authors h2,
.chapter-links,
.chapter-links h2 {
.chapter-links h2,
.webmentions,
.webmentions h2 {
border-bottom: 1px solid #1a2b490a;
font-size: 17px;
font-size: 1.0625rem;
Expand Down Expand Up @@ -858,6 +861,87 @@ pre {
width: 100%;
}

/* Webmention code */
.reaction-tabs [role="tab"][aria-selected="true"] {
z-index: 3;
font-weight: bold;
border-bottom: 0.5rem solid #a8caba;
}

.reaction-tabs [role="tab"] {
position: relative;
z-index: 1;
background: white;
border: 0;
padding: 0rem 0.75rem 0.25rem;
SaptakS marked this conversation as resolved.
Show resolved Hide resolved
}

.reactions [role="tabpanel"] {
margin-top: 1.5rem;
}

tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
.reaction-tabs > button > span {
pointer-events: none;
}

.webmention-likes,
.webmention-reposts {
display: flex;
flex-wrap: wrap;
}

.webmentions .reactions ul {
padding: 0;
}

.webmentions .reactions ul > li {
margin-right: 1rem;
}

.webmentions .reactions ul > li::before {
display: none;
}

.webmentions a.webmention-author {
text-decoration: none;
}

.webmentions a.webmention-author:hover {
text-decoration: underline;
}

.webmention-author .webmention-author-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
background: grey;
}

.webmention-meta {
font-size: 0.875rem;
color: #515660;
}

.webmention-meta a.webmention-source {
color: #515660;
}

.webmention-replies-item,
.webmention-mentions-item {
display: flex;
flex-direction: column;
position: relative;
padding: 1rem 0 1rem 60px;
}

.webmention-replies-item .webmention-author .webmention-author-avatar,
.webmention-mentions-item .webmention-author .webmention-author-avatar {
position: absolute;
top: 1rem;
left: 0;
}

/* Needed for Safari to hide the index properly */
@media print {
.main {
Expand Down
211 changes: 211 additions & 0 deletions src/static/js/webmentions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
// Code related to parsing and showing webmentions

// Gets the webmentions json data from current URL
async function getWebmentions(targetURL) {
const apiURL = `https://webmention.io/api/mentions.jf2?perPage=500&target=${targetURL}`;
SaptakS marked this conversation as resolved.
Show resolved Hide resolved
let mentions = []
SaptakS marked this conversation as resolved.
Show resolved Hide resolved
try {
const response = await window.fetch(apiURL);
if (response.status >= 200 && response.status < 300) {
const json = await response.json();
mentions = json.children;
} else {
gtag('event', 'error', { 'event_category': 'webmentions.js', 'event_label': response.statusText, 'value': 1 });
}
} catch(error) {
gtag('event', 'error', { 'event_category': 'webmentions.js', 'event_label': error, 'value': 1 });
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved
}
return mentions;
}

// Format published date into human readable form
function formatDate(dateString){
const options = { year: "numeric", month: "long", day: "numeric" }
return new Date(dateString).toLocaleDateString(undefined, options)
SaptakS marked this conversation as resolved.
Show resolved Hide resolved
}

// Parse webmentions for individual category
function parseMentions(webmentions, mentionType) {
let filteredMentions = []
webmentions.forEach(function(mention){
if (mention["wm-property"] == mentionType) {
filteredMentions.push(mention)
}
});
return filteredMentions;
}
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved

// Renders webmention into different sections, based on the type
function renderReactions(webmentions, reactionType) {
// Process webmentions
const reactionMap = {
likes: "like-of",
reposts: "repost-of",
replies: "in-reply-to",
mentions: "mention-of"
}
const reactions = parseMentions(webmentions, reactionMap[reactionType]);
if (!reactions.length) {
return;
}

// Add the count to the reaction tab
document.querySelector(`#${reactionType}-count`).textContent = reactions.length;
const reactionLabel = document.querySelector(`#${reactionType}-label`);
if (reactions && reactions.length ===1) {
reactionLabel.textContent = reactionLabel.getAttribute("data-singular");
}

// Render logic for the reaction types
const webmentionReactionsList = document.createElement("ul");
webmentionReactionsList.setAttribute("class", `webmention-${reactionType}`);
reactions.forEach(function(reaction) {
const reactionLI = document.createElement("li");
reactionLI.setAttribute("class", `webmention-${reactionType}-item`);

const reactionA = document.createElement("a");
reactionA.setAttribute("class", "webmention-author");
reactionA.setAttribute("href", reaction["url"]);
reactionA.setAttribute("title", reaction["author"]["name"]);
reactionA.setAttribute("aria-label", reaction["author"]["name"]);

const reactionIMG = document.createElement("img");
reactionIMG.setAttribute("class", "webmention-author-avatar");
reactionIMG.setAttribute("src", reaction["author"]["photo"]);
reactionIMG.setAttribute("alt", reaction["author"]["name"]);
reactionIMG.setAttribute("loading", "lazy");
reactionIMG.setAttribute("width", "60");
reactionIMG.setAttribute("height", "60");

reactionA.appendChild(reactionIMG);

// Replies and mentions have some extra HTML
const reactionDIVContent = document.createElement("div");
const reactionDIVMeta = document.createElement("div");
if (reactionType === "replies" || reactionType === "mentions") {
const reactionSTRONG = document.createElement("strong");
reactionSTRONG.setAttribute("class", "webmention-author-name");
reactionSTRONG.textContent = reaction["author"]["name"];
reactionA.appendChild(reactionSTRONG);

reactionDIVContent.setAttribute("class", "webmention-content");
reactionDIVContent.textContent = reaction["content"]["text"];

reactionDIVMeta.setAttribute("class", "webmention-meta");

const reactionTIME = document.createElement("time");
reactionTIME.setAttribute("class", "webmention-pub-date");
reactionTIME.setAttribute("datetime", reaction["published"]);
reactionTIME.textContent = formatDate(reaction["published"]);

const reactionSPAN = document.createElement("span");
reactionSPAN.setAttribute("class", "webmention-divider");
reactionSPAN.setAttribute("aria-hidden", "true");
reactionSPAN.textContent = " ⋅ ";

const reactionASource = document.createElement("a");
reactionASource.setAttribute("class", "webmention-source");
reactionASource.setAttribute("href", reaction["url"]);
reactionASource.textContent = document.querySelector(".reactions").getAttribute("data-source");

reactionDIVMeta.appendChild(reactionTIME);
reactionDIVMeta.appendChild(reactionSPAN);
reactionDIVMeta.appendChild(reactionASource);
}

reactionLI.appendChild(reactionA);
if (reactionType === "replies" || reactionType === "mentions") {
reactionLI.appendChild(reactionDIVContent);
reactionLI.appendChild(reactionDIVMeta);
}
webmentionReactionsList.appendChild(reactionLI);
});
document.querySelector(`#${reactionType}-panel`).appendChild(webmentionReactionsList);
}

// Parses and renders mentions into likes, reposts, replies and mentions
function renderWebmentions(webmentions) {
if (!webmentions.length) {
return;
}

renderReactions(webmentions, "likes");
renderReactions(webmentions, "reposts");
renderReactions(webmentions, "replies");
renderReactions(webmentions, "mentions");

}
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved

// Process webmention promise
function processWebmentions(targetURL) {
getWebmentions(targetURL)
.then(webmentions => renderWebmentions(webmentions))
.catch(e => gtag('event', 'error', { 'event_category': 'webmentions.js', 'event_label': e, 'value': 1 }))
}

// Change tabs for webmentions UI
function changeTabs(target) {
const parent = target.parentNode;
const grandparent = parent.parentNode;

// Remove all current selected tabs
parent
.querySelectorAll('[aria-selected="true"]')
.forEach(t => t.setAttribute("aria-selected", false));

// Set this tab as selected
target.setAttribute("aria-selected", true);

// Hide all tab panels
grandparent
.querySelectorAll('[role="tabpanel"]')
.forEach(p => p.setAttribute("hidden", true));

// Show the selected panel
grandparent.parentNode
.querySelector(`#${target.getAttribute("aria-controls")}`)
.removeAttribute("hidden");
}

function addTabListeners() {
const tabs = document.querySelectorAll('.reactions [role="tab"]');
const tabList = document.querySelector('.reactions [role="tablist"]');

SaptakS marked this conversation as resolved.
Show resolved Hide resolved
// Add a click event handler to each tab
tabs.forEach(tab => {
tab.addEventListener("click", function(e){changeTabs(e.target)});
});

// Enable arrow navigation between tabs in the tab list
let tabFocus = 0;
tunetheweb marked this conversation as resolved.
Show resolved Hide resolved

tabList.addEventListener("keydown", e => {
if (e.key === "ArrowRight" || e.key === "ArrowLeft") {
tabs[tabFocus].setAttribute("tabindex", -1);
// Move right
if (e.key === "ArrowRight") {
tabFocus++;
// If we're at the end, go to the start
if (tabFocus >= tabs.length) {
tabFocus = 0;
}
// Move left
} else if (e.key === "ArrowLeft") {
tabFocus--;
// If we're at the start, move to the end
if (tabFocus < 0) {
tabFocus = tabs.length - 1;
}
}

tabs[tabFocus].setAttribute("tabindex", 0);
tabs[tabFocus].focus();
changeTabs(tabs[tabFocus]);
}
});
}


addTabListeners();
const BASE_URL = "https://almanac.httparchive.org";
processWebmentions(BASE_URL + window.location.pathname);
Loading