Skip to content

Commit

Permalink
implement toot embedding
Browse files Browse the repository at this point in the history
  • Loading branch information
hexylena committed Nov 13, 2023
1 parent b0719d5 commit 23a499a
Show file tree
Hide file tree
Showing 8 changed files with 1,956 additions and 31 deletions.
55 changes: 24 additions & 31 deletions _layouts/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,23 @@ <h2>Galaxy for Contributors and Instructors</h2>
</tbody>
</table>

<!-- Our awesome Contributors -->
<h2>Contributor Hall of Fame</h2>

<p>
{% snippet faqs/gtn/gtn_stats.md compact=true %}
</p>
<p>
This project would not be possible without the many amazing community contributors!
</p>
<p>
<a class="btn btn-info" href="{{ site.baseurl }}/hall-of-fame">View the Hall of Fame</a>

<a class="btn btn-warning" href="{{ site.baseurl }}/stats">View all GTN stats</a>

</p>
<p>Do you want to help with this project and join our Hall of Fame? Please see our <a href="{{ site.baseurl }}/topics/contributing/">dedicated tutorials</a> or our <a href="{{ site.baseurl }}/faq">Frequently Asked Questions</a> to get you started.</p>

</div>

<div class="col-lg-5 col-md-5 col-sm-12">
Expand Down Expand Up @@ -186,41 +203,17 @@ <h2>Meet & Join the Community!</h2>
<!-- GTN Tweets -->
<div class="col-12">
<h2>GTN Toots</h2>
<a rel="me" href="https://mstdn.science/@gtn">Follow us on Mastodon</a>

<link rel="stylesheet" type="text/css" href="{{ site.baseurl }}/assets/js/emfed/toots.css">
<script type="module" src="{{ site.baseurl }}/assets/js/emfed/emfed.js"></script>
<a rel="me" class="mastodon-feed"
href="https://mstdn.science/@gtn"
data-toot-limit="3"
>Follow us on the Mastodon/Fediverse</a>
</div>
</div>
</div>

<div class="row">
<div class="col-lg-7 col-md-7 col-sm-12">
<!-- Our awesome Contributors -->
<div class="col-12">
<h2>Contributor Hall of Fame</h2>

<p>
{% snippet faqs/gtn/gtn_stats.md compact=true %}
</p>
<p>
This project would not be possible without the many amazing community contributors!
</p>
<p>
<a class="btn btn-info" href="{{ site.baseurl }}/hall-of-fame">View the Hall of Fame</a>

<a class="btn btn-warning" href="{{ site.baseurl }}/stats">View all GTN stats</a>

</p>
<p>Do you want to help with this project and join our Hall of Fame? Please see our <a href="{{ site.baseurl }}/topics/contributing/">dedicated tutorials</a> or our <a href="{{ site.baseurl }}/faq">Frequently Asked Questions</a> to get you started.</p>

</div>
</div>
<div class="col-lg-5 col-md-5 col-sm-12">
<div class="col-12">
<h2>The Galaxy Training Network</h2>
{% include _includes/map.html height=300 %}

</div>
</div>
</div>
<h2 id="acknowledgment-and-funding">Acknowledgment and Funding</h2>
<p>
More information about this project can be found in our <a href="https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1010752">publication in PLOS Computational Biology</a>.
Expand Down
15 changes: 15 additions & 0 deletions assets/css/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,21 @@ img {
margin: 2.5%;
}

ol.toots {
li.toot {
display: flex;
flex-direction: column;
}
img {
width: 46px !important;
height: 46px !important;
}
a.user.boost img {
width: 23px !important;
height: 23px !important;
}
}

div.main-content {
img {
height: auto;
Expand Down
1,665 changes: 1,665 additions & 0 deletions assets/js/dompurify/purify.es.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions assets/js/dompurify/purify.es.js.map

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions assets/js/emfed/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Fetch recent toots for a user, given their Mastodon URL.
*/
export async function getToots(userURL, accountId, limit, excludeReplies) {
const url = new URL(userURL);
// Either use the account id specified or look it up based on the username
// in the link.
const userId = accountId ??
(await (async () => {
// Extract username from URL.
const parts = /@(\w+)$/.exec(url.pathname);
if (!parts) {
throw "not a Mastodon user URL";
}
const username = parts[1];
// Look up user ID from username.
const lookupURL = Object.assign(new URL(url), {
pathname: "/api/v1/accounts/lookup",
search: `?acct=${username}`,
});
return (await (await fetch(lookupURL)).json())["id"];
})());
// Fetch toots.
const tootURL = Object.assign(new URL(url), {
pathname: `/api/v1/accounts/${userId}/statuses`,
search: `?limit=${limit ?? 5}&exclude_replies=${!!excludeReplies}`,
});
return await (await fetch(tootURL)).json();
}
114 changes: 114 additions & 0 deletions assets/js/emfed/core.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { getToots } from "./client.js";
import DOMPurify from "../dompurify/purify.es.js";
/**
* Mark a string as safe for inclusion in HTML.
*/
function safe(s) {
return Object.assign(new String(s), { __safe: null });
}
/**
* Format a value as a string for templating.
*/
function flat(v) {
if (typeof v === "undefined" || v === null) {
return "";
}
else if (typeof v === "string" || v instanceof String) {
if (v.hasOwnProperty("__safe")) {
return v;
}
else {
// Escape strings for inclusion in HTML.
return v
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
}
else {
return v.map(flat).join("");
}
}
/**
* The world's dumbest templating system.
*/
function html(strings, ...subs) {
let out = strings[0];
for (let i = 1; i < strings.length; ++i) {
out += flat(subs[i - 1]);
out += strings[i];
}
return safe(out);
}
/**
* Render a single toot object as an HTML string.
*/
function renderToot(toot) {
// Is this a boost (reblog)?
let boost = null;
if (toot.reblog) {
boost = {
avatar: toot.account.avatar,
username: toot.account.username,
display_name: toot.account.display_name,
user_url: toot.account.url,
};
toot = toot.reblog; // Show the "inner" toot instead.
}
const date = new Date(toot.created_at).toLocaleString();
const images = toot.media_attachments.filter((att) => att.type === "image");
return html `<li class="toot">
<a class="permalink" href="${toot.url}">
<time datetime="${toot.created_at}">${date}</time>
</a>
${boost &&
html ` <a class="user boost" href="${boost.user_url}">
<img class="avatar" width="23" height="23" src="${boost.avatar}" />
<span class="display-name">${boost.display_name}</span>
<span class="username">@${boost.username}</span>
</a>`}
<a class="user" href="${toot.account.url}">
<img class="avatar" width="46" height="46" src="${toot.account.avatar}" />
<span class="display-name">${toot.account.display_name}</span>
<span class="username">@${toot.account.username}</span>
</a>
<div class="body">${safe(DOMPurify.sanitize(toot.content))}</div>
${images.map((att) => html ` <a
class="attachment"
href="${att.url}"
target="_blank"
rel="noopener noreferrer"
>
<img
class="attachment"
src="${att.preview_url}"
alt="${att.description}"
/>
</a>`)}
</li>`.toString();
}
/**
* Get the toots for an HTML element and replace that element with the
* rendered toot list.
*/
export async function loadToots(element) {
// Fetch toots based on the element's `data-toot-*` attributes.
const el = element;
const toots = await getToots(el.href, el.dataset.tootAccountId, Number(el.dataset.tootLimit ?? 5), el.dataset.excludeReplies === "true");
// Construct the HTML content.
const list = document.createElement("ol");
list.classList.add("toots");
el.replaceWith(list);
for (const toot of toots) {
const html = renderToot(toot);
list.insertAdjacentHTML("beforeend", html);
}
}
/**
* Transform all links on the page marked with the `mastodon-feed` class.
*/
export function loadAll() {
document.querySelectorAll("a.mastodon-feed").forEach(loadToots);
}
2 changes: 2 additions & 0 deletions assets/js/emfed/emfed.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { loadAll } from "./core.js";
loadAll();
106 changes: 106 additions & 0 deletions assets/js/emfed/toots.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
.toots {
max-width: 400px;
list-style: none;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Cantarell, Ubuntu, roboto, noto, arial, sans-serif;
}

.toot {
border-bottom: 1px solid #ccc;
padding: 1rem;
}

/* Posting user. */
.toot .user {
display: flex;
flex-flow: column wrap;
justify-content: space-evenly;
align-content: flex-start;
height: 46px; /* Avatar height. */
column-gap: 0.5rem;

text-decoration: none;
color: inherit;
}

.toot .avatar {
border-radius: 4px;
}

.toot .display-name {
font-weight: bold;
display: block;
}

.toot .user:hover .display-name {
text-decoration: underline;
}

.toot .username {
display: block;
margin-right: 1em;
color: #999;
}

/* Boosting user is smaller and above the posting user. */
.toot .boost {
height: 23px;
margin-bottom: 0.25rem;
column-gap: 0.25rem;
}

.toot .boost:before {
content: "♺";
font-size: 140%;
}

.toot .boost .username {
display: none;
}

.toot .permalink {
text-decoration: none;
display: block;
color: #999;
float: right;
}

.toot .permalink:hover {
text-decoration: underline;
}

.toot .body {
clear: both;
margin-top: 1em;
}

.toot .body a {
overflow-wrap: anywhere;
}

/* Weird trick to keep the text in the page but not display it. */
.toot .body .invisible {
display: inline-block;
font-size: 0;
line-height: 0;
width: 0;
height: 0;
position: absolute;
}

.toot .body .ellipsis::after {
content: "…";
}

.toot .attachment {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
border-radius: 4px;
}

.toot .attachment img {
width: 100%;
height: 100%;
object-fit: cover;
}

0 comments on commit 23a499a

Please sign in to comment.