From 4fdf962daa4cce96cc65c2924a96f1a292259231 Mon Sep 17 00:00:00 2001 From: Sebastian Steinhorst Date: Mon, 16 Oct 2023 18:37:41 +0200 Subject: [PATCH] Widget for ESI Mastodon Account --- index.html | 43 +++ mastodon-timeline.css | 384 ++++++++++++++++++++ mastodon-timeline.js | 810 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1237 insertions(+) create mode 100644 index.html create mode 100644 mastodon-timeline.css create mode 100644 mastodon-timeline.js diff --git a/index.html b/index.html new file mode 100644 index 0000000..f88eb24 --- /dev/null +++ b/index.html @@ -0,0 +1,43 @@ + + + + + Mastodon embed timeline + + + + + + + + + +
+
+
+
+
+
+
+ + + + diff --git a/mastodon-timeline.css b/mastodon-timeline.css new file mode 100644 index 0000000..65a2adb --- /dev/null +++ b/mastodon-timeline.css @@ -0,0 +1,384 @@ +/* Mastodon embed feed timeline v3.10.0 */ +/* More info at: */ +/* https://gitlab.com/idotj/mastodon-embed-feed-timeline */ + +/* Variables */ +:root { + --text-max-lines: none; +} + +/* Theme colors */ +:root, +html[data-theme="light"] { + --bg-color: #fff; + --bg-hover-color: #d9e1e8; + --line-gray-color: #c0cdd9; + --contrast-gray-color: #606984; + --content-text: #000; + --link-color: #3a3bff; + --error-text-color: #8b0000; +} +html[data-theme="dark"] { + --bg-color: #282c37; + --bg-hover-color: #313543; + --line-gray-color: #393f4f; + --contrast-gray-color: #606984; + --content-text: #fff; + --link-color: #8c8dff; + --error-text-color: #fe6c6c; +} + +/* Main container */ +.mt-container { + height: 100%; + overflow-y: auto; + position: relative; + background-color: var(--bg-color); + scrollbar-color: var(--line-gray-color) var(--bg-color); + scrollbar-width: thin; +} +.mt-container::-webkit-scrollbar { + width: 0.25rem; + height: 0.25rem; +} +.mt-container::-webkit-scrollbar-thumb { + background-color: var(--line-gray-color); + border: none; + border-radius: 3rem; +} +.mt-container::-webkit-scrollbar-thumb:hover, +.mt-container::-webkit-scrollbar-thumb:active { + background-color: var(--line-gray-color); +} +.mt-container::-webkit-scrollbar-track { + background-color: var(--bg-color); + border: none; + border-radius: 0; +} +.mt-container::-webkit-scrollbar-track:hover, +.mt-container::-webkit-scrollbar-track:active, +.mt-container::-webkit-scrollbar-corner { + background-color: var(--bg-color); +} +.mt-container a:link, +.mt-container a:active, +.mt-container a { + text-decoration: none; + color: var(--link-color); +} +.mt-container a:not(.mt-toot-preview):hover { + text-decoration: underline; +} +.mt-body { + padding: 1rem clamp(0.25rem, 4vw, 1.5rem); + white-space: pre-wrap; + word-wrap: break-word; +} +.mt-body .invisible { + font-size: 0; + line-height: 0; + display: inline-block; + width: 0; + height: 0; + position: absolute; +} + +/* Toot container */ +.mt-toot { + margin: 0.25rem; + padding: 1rem 0.5rem 1.5rem 0.5rem; + position: relative; + min-height: 3.75rem; + background-color: transparent; + border-bottom: 1px solid var(--line-gray-color); +} +.mt-toot:hover, +.mt-toot:focus { + cursor: pointer; + background-color: var(--bg-hover-color); +} +.mt-toot p:last-child { + margin-bottom: 0; +} + +/* User avatar */ +.mt-toot-avatar { + margin-right: 0.75rem; +} +.mt-toot-avatar-standard { + width: 2.25rem; + height: 2.25rem; +} +.mt-toot-avatar-boosted { + width: 3rem; + height: 3rem; + position: relative; +} +.mt-toot-avatar-image-big img { + aspect-ratio: 1/1; + width: 2.25rem; + height: 2.25rem; + border-radius: 0.25rem; + overflow: hidden; +} +.mt-toot-avatar-image-small img { + aspect-ratio: 1/1; + width: 1.5rem; + height: 1.5rem; + top: 1.5rem; + left: 1.5rem; + position: absolute; + border-radius: 0.25rem; + overflow: hidden; +} + +/* User name and date */ +.mt-toot-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1rem; +} +.mt-toot-header-user { + font-weight: 600; + margin-top: 0.5rem; + padding-right: 1rem; +} +.mt-toot-header-user > a { + display: flex; + align-items: flex-start; + color: var(--content-text) !important; + overflow-wrap: anywhere; +} +.mt-toot-header-date { + font-size: 0.75rem; + text-align: right; + margin: 0.5rem 0 0 auto; +} +.mt-toot-header-date > a { + color: var(--contrast-gray-color) !important; +} + +/* Text */ +.mt-toot-text { + margin-bottom: 1rem; + color: var(--content-text); +} +.mt-toot-text .spoiler-btn { + display: inline-block; +} +.mt-toot-text .spoiler-text-hidden { + display: none; +} +.mt-toot-text.truncate { + display: -webkit-box; + overflow: hidden; + -webkit-line-clamp: var(--text-max-lines); + -webkit-box-orient: vertical; +} +.mt-toot-text:not(.truncate) .ellipsis::after { + content: "..."; +} +.mt-toot-text blockquote { + border-left: 0.25rem solid var(--line-gray-color); + margin-left: 0; + padding-left: 0.5rem; +} +.mt-toot-header-user .custom-emoji, +.mt-toot-text .custom-emoji { + height: 1.5rem; + min-width: 1.5rem; + margin-bottom: -0.25rem; + width: auto; +} + +/* Poll */ +.mt-toot-poll { + margin-bottom: 1rem; + color: var(--content-text); +} +.mt-toot-poll ul { + list-style: none; + padding: 0; + margin: 0; +} +.mt-toot-poll ul li { + font-size: 0.9rem; + margin-bottom: 0.5rem; +} +.mt-toot-poll ul li:not(:last-child) { + margin-bottom: 0.25rem; +} +.mt-toot-poll ul li:before { + content: "◯"; + padding-right: 0.5rem; +} + +/* Medias */ +.mt-toot-media { + overflow: hidden; + margin-bottom: 1rem; +} +.mt-toot-media > .spoiler-btn { + position: absolute; + top: 50%; + left: 50%; + z-index: 1; + transform: translate(-50%, -50%); +} +.mt-toot-media-spoiler > img { + filter: blur(2rem); +} +.img-ratio14_7 { + position: relative; + padding-top: 56.95%; + width: 100%; +} +.img-ratio14_7 > img { + width: 100%; + height: auto; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; +} + +/* Preview link */ +.mt-toot-preview { + min-height: 4rem; + display: flex; + flex-direction: row; + border: 1px solid var(--line-gray-color); + border-radius: 0.5rem; + color: var(--link-color); + font-size: 0.8rem; + margin: 1rem 0; + overflow: hidden; +} +.mt-toot-preview-image { + width: 40%; + align-self: stretch; +} +.mt-toot-preview-image img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} +.mt-toot-preview-noImage { + width: 40%; + font-size: 1.5rem; + align-self: center; + text-align: center; +} +.mt-toot-preview-content { + width: 60%; + display: flex; + align-self: center; + flex-direction: column; + padding: 0.5rem 1rem; + gap: 0.5rem; +} +.mt-toot-preview-title { + font-weight: 600; +} + +/* Spoiler button */ +.spoiler-btn { + border-radius: 2px; + background-color: var(--line-gray-color); + border: 0; + color: var(--content-text); + font-weight: 700; + font-size: 0.7rem; + padding: 0 0.35rem; + text-transform: uppercase; + line-height: 1.25rem; + cursor: pointer; + vertical-align: top; +} + +/* Counter bar */ +.mt-toot-counter-bar { + display: flex; + min-width: 6rem; + max-width: 40rem; + justify-content: space-between; + color: var(--contrast-gray-color); +} +.mt-toot-counter-bar-replies, +.mt-toot-counter-bar-reblog, +.mt-toot-counter-bar-favorites { + display: flex; + font-size: 0.75rem; + gap: 0.25rem; + align-items: center; + opacity: 0.5; +} +.mt-toot-counter-bar-replies > svg, +.mt-toot-counter-bar-reblog > svg, +.mt-toot-counter-bar-favorites > svg { + width: 1rem; + fill: var(--contrast-gray-color); +} + +/* Error */ +.mt-error { + position: absolute; + display: flex; + flex-direction: column; + height: calc(100% - 3.5rem); + width: calc(100% - 4.5rem); + justify-content: center; + align-items: center; + color: var(--error-text-color); + padding: 0.75rem; + text-align: center; +} +.mt-error-icon { + font-size: 2rem; +} +.mt-error-message { + padding: 1rem 0; +} +.mt-error-message hr { + color: var(--line-gray-color); +} + +/* Loading spinner */ +.mt-body > .loading-spinner { + position: absolute; + width: 3rem; + height: 3rem; + margin: auto; + top: calc(50% - 1.5rem); + right: calc(50% - 1.5rem); +} +.loading-spinner { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns:svg='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.0' viewBox='0 0 128 128' %3E%3Cg%3E%3Cpath d='M64 128A64 64 0 0 1 18.34 19.16L21.16 22a60 60 0 1 0 52.8-17.17l.62-3.95A64 64 0 0 1 64 128z' fill='%23404040'/%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 64 64' to='360 64 64' dur='1000ms' repeatCount='indefinite'%3E%3C/animateTransform%3E%3C/g%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center center; + background-color: transparent; + background-size: min(2.5rem, calc(100% - 0.5rem)); +} + +/* Footer (See more link) */ +.mt-footer { + margin: 1rem auto 2rem auto; + padding: 0 2rem; + text-align: center; +} + +/* Hidden elements */ +.visually-hidden { + position: absolute !important; + width: 1px !important; + height: 1px !important; + padding: 0 !important; + margin: -1px !important; + overflow: hidden !important; + clip: rect(0, 0, 0, 0) !important; + white-space: nowrap !important; + border: 0 !important; +} diff --git a/mastodon-timeline.js b/mastodon-timeline.js new file mode 100644 index 0000000..58f5fd9 --- /dev/null +++ b/mastodon-timeline.js @@ -0,0 +1,810 @@ +/** + * Mastodon embed feed timeline v3.10.0 + * More info at: + * https://gitlab.com/idotj/mastodon-embed-feed-timeline + */ + +/** + * Timeline settings + * Adjust these parameters to customize your timeline + */ +window.addEventListener("load", () => { + const mastodonTimeline = new MastodonApi({ + // Id of the
containing the timeline + container_body_id: "mt-body", + + // Class name for the loading spinner (also used in CSS file) + spinner_class: "loading-spinner", + + // Preferred color theme: 'light', 'dark' or 'auto'. Default: auto + default_theme: "auto", + + // Your Mastodon instance + instance_url: "https://mastodon.social", + + // Choose type of toots to show in the timeline: 'local', 'profile', 'hashtag'. Default: local + timeline_type: "profile", + + // Your user ID on Mastodon instance. Leave empty if you didn't choose 'profile' as type of timeline + user_id: "111245589313877734", + + // Your user name on Mastodon instance. Leave empty if you didn't choose 'profile' as type of timeline + profile_name: "@tum_embeddediot@mastodon.social", + + // The name of the hashtag. Leave empty if you didn't choose 'hashtag' as type of timeline + hashtag_name: "", + + // Maximum amount of toots to get. Default: 20 + toots_limit: "20", + + // Hide unlisted toots. Default: don't hide + hide_unlisted: false, + + // Hide boosted toots. Default: don't hide + hide_reblog: false, + + // Hide replies toots. Default: don't hide + hide_replies: false, + + // Hide preview card if toot contains a link, photo or video from a URL. Default: don't hide + hide_preview_link: false, + + // Hide custom emojis available on the server. Default: don't hide + hide_emojos: false, + + // Converts Markdown symbol ">" at the beginning of a paragraph into a blockquote HTML tag. Ddefault: don't apply + markdown_blockquote: false, + + // Hide replies, boosts and favourites toots counter. Default: don't hide + hide_counter_bar: false, + + // Limit the text content to a maximum number of lines. Default: 0 (unlimited) + text_max_lines: "0", + + // Customize the text of the link pointing to the Mastodon page (appears after the last toot) + link_see_more: "See more posts at Mastodon", + }); +}); + +/** + * Set all variables with customized values or use default ones + * @param {object} params_ User customized values + * Trigger main function to build the timeline + */ +const MastodonApi = function (params_) { + this.CONTAINER_BODY_ID = params_.container_body_id || "mt-body"; + this.SPINNER_CLASS = params_.spinner_class || "loading-spinner"; + this.DEFAULT_THEME = params_.default_theme || "auto"; + this.INSTANCE_URL = params_.instance_url; + this.USER_ID = params_.user_id || ""; + this.PROFILE_NAME = this.USER_ID ? params_.profile_name : ""; + this.TIMELINE_TYPE = params_.timeline_type || "local"; + this.HASHTAG_NAME = params_.hashtag_name || ""; + this.TOOTS_LIMIT = params_.toots_limit || "20"; + this.HIDE_UNLISTED = + typeof params_.hide_unlisted !== "undefined" + ? params_.hide_unlisted + : false; + this.HIDE_REBLOG = + typeof params_.hide_reblog !== "undefined" ? params_.hide_reblog : false; + this.HIDE_REPLIES = + typeof params_.hide_replies !== "undefined" ? params_.hide_replies : false; + this.HIDE_PREVIEW_LINK = + typeof params_.hide_preview_link !== "undefined" + ? params_.hide_preview_link + : false; + this.HIDE_EMOJOS = + typeof params_.hide_emojos !== "undefined" ? params_.hide_emojos : false; + this.MARKDOWN_BLOCKQUOTE = + typeof params_.markdown_blockquote !== "undefined" + ? params_.markdown_blockquote + : false; + this.HIDE_COUNTER_BAR = + params_.hide_counter_bar !== "undefined" ? params_.hide_counter_bar : false; + this.TEXT_MAX_LINES = params_.text_max_lines || "0"; + this.LINK_SEE_MORE = params_.link_see_more; + this.FETCHED_DATA = {}; + + this.mtBodyContainer = document.getElementById(this.CONTAINER_BODY_ID); + + this.buildTimeline(); +}; + +/** + * Trigger functions and construct timeline + */ +MastodonApi.prototype.buildTimeline = async function () { + // Apply color theme + this.setTheme(); + + // Get server data + await this.getTimelineData(); + + // Empty the
container + this.mtBodyContainer.innerHTML = ""; + + for (let i in this.FETCHED_DATA.timeline) { + // First filter (Public / Unlisted) + if ( + this.FETCHED_DATA.timeline[i].visibility == "public" || + (!this.HIDE_UNLISTED && + this.FETCHED_DATA.timeline[i].visibility == "unlisted") + ) { + // Second filter (Reblog / Replies) + if ( + (this.HIDE_REBLOG && this.FETCHED_DATA.timeline[i].reblog) || + (this.HIDE_REPLIES && this.FETCHED_DATA.timeline[i].in_reply_to_id) + ) { + // Nothing here (Don't append toots) + } else { + // Append toots + this.appendToot(this.FETCHED_DATA.timeline[i], Number(i)); + } + } + } + + // Check if there are toots in the container (due to filters applied) + if (this.mtBodyContainer.innerHTML === "") { + this.mtBodyContainer.setAttribute("role", "none"); + this.mtBodyContainer.innerHTML = + '
📭
Sorry, no toots to show
Got ' + + this.FETCHED_DATA.timeline.length + + ' toots from the server but due to the "hide filters" applied, no toot is shown
'; + } else { + // Insert link after last toot to visit Mastodon page + if (this.LINK_SEE_MORE) { + let linkSeeMorePath = ""; + if (this.TIMELINE_TYPE === "profile") { + linkSeeMorePath = this.PROFILE_NAME; + } else if (this.TIMELINE_TYPE === "hashtag") { + linkSeeMorePath = "tags/" + this.HASHTAG_NAME; + } else if (this.TIMELINE_TYPE === "local") { + linkSeeMorePath = "public/local"; + } + let linkSeeMore = + '"; + this.mtBodyContainer.parentNode.insertAdjacentHTML( + "beforeend", + linkSeeMore + ); + } + + // Control loading spinners + this.manageSpinner(); + } + + // Toot interactions + this.mtBodyContainer.addEventListener("click", function (e) { + // Check if toot cointainer was clicked + if ( + e.target.localName == "article" || + e.target.offsetParent?.localName == "article" || + e.target.localName == "img" + ) { + openTootURL(e); + } + // Check if Show More/Less button was clicked + if (e.target.localName == "button" && e.target.className == "spoiler-btn") { + toogleSpoiler(e); + } + }); + this.mtBodyContainer.addEventListener("keydown", function (e) { + // Check if Enter key was pressed with focus in an article + if (e.key === "Enter" && e.target.localName == "article") { + openTootURL(e); + } + }); + + /** + * Open toot in a new page avoiding any other natural link + * @param {event} e User interaction trigger + */ + const openTootURL = function (e) { + let urlToot = e.target.closest(".mt-toot").dataset.location; + if ( + e.target.localName !== "a" && + e.target.localName !== "span" && + e.target.localName !== "button" && + e.target.localName !== "time" && + e.target.className !== "mt-toot-preview-noImage" && + e.target.parentNode.className !== "mt-toot-avatar-image-big" && + e.target.parentNode.className !== "mt-toot-avatar-image-small" && + e.target.parentNode.className !== "mt-toot-preview-image" && + urlToot + ) { + window.open(urlToot, "_blank"); + } + }; + + /** + * Spoiler button + * @param {event} e User interaction trigger + */ + const toogleSpoiler = function (e) { + const nextSibling = e.target.nextSibling; + if (nextSibling.localName === "img") { + e.target.parentNode.classList.remove("mt-toot-media-spoiler"); + e.target.style.display = "none"; + } else if ( + nextSibling.classList.contains("spoiler-text-hidden") || + nextSibling.classList.contains("spoiler-text-visible") + ) { + if (e.target.textContent == "Show more") { + nextSibling.classList.remove("spoiler-text-hidden"); + nextSibling.classList.add("spoiler-text-visible"); + e.target.setAttribute("aria-expanded", "true"); + e.target.textContent = "Show less"; + } else { + nextSibling.classList.remove("spoiler-text-visible"); + nextSibling.classList.add("spoiler-text-hidden"); + e.target.setAttribute("aria-expanded", "false"); + e.target.textContent = "Show more"; + } + } + }; +}; + +/** + * Set the theme style chosen by the user or by the browser/OS + */ +MastodonApi.prototype.setTheme = function () { + /** + * Set the theme value in the tag using the attribute "data-theme" + * @param {string} theme Type of theme to apply: dark or light + */ + const setTheme = function (theme) { + document.documentElement.setAttribute("data-theme", theme); + }; + + if (this.DEFAULT_THEME === "auto") { + let systemTheme = window.matchMedia("(prefers-color-scheme: dark)"); + systemTheme.matches ? setTheme("dark") : setTheme("light"); + // Update the theme if user change browser/OS preference + systemTheme.addEventListener("change", (e) => { + e.matches ? setTheme("dark") : setTheme("light"); + }); + } else { + setTheme(this.DEFAULT_THEME); + } +}; + +/** + * Requests to the server to get all the data + */ +MastodonApi.prototype.getTimelineData = async function () { + return new Promise((resolve, reject) => { + /** + * Fetch data from server + * @param {string} url address to fetch + * @returns {object} List of objects + */ + async function fetchData(url) { + const response = await fetch(url); + + if (!response.ok) { + throw new Error( + "Failed to fetch the following URL: " + + url + + "
" + + "Error status: " + + response.status + + "
" + + "Error message: " + + response.statusText + ); + } + + const data = await response.json(); + return data; + } + + // URLs to fetch + let urls = {}; + if (this.TIMELINE_TYPE === "profile") { + urls.timeline = `${this.INSTANCE_URL}/api/v1/accounts/${this.USER_ID}/statuses?limit=${this.TOOTS_LIMIT}`; + } else if (this.TIMELINE_TYPE === "hashtag") { + urls.timeline = `${this.INSTANCE_URL}/api/v1/timelines/tag/${this.HASHTAG_NAME}?limit=${this.TOOTS_LIMIT}`; + } else if (this.TIMELINE_TYPE === "local") { + urls.timeline = `${this.INSTANCE_URL}/api/v1/timelines/public?local=true&limit=${this.TOOTS_LIMIT}`; + } + if (!this.HIDE_EMOJOS) { + urls.emojos = this.INSTANCE_URL + "/api/v1/custom_emojis"; + } + + const urlsPromises = Object.entries(urls).map(([key, url]) => { + return fetchData(url) + .then((data) => ({ [key]: data })) + .catch((error) => { + reject(new Error("Something went wrong fetching data")); + this.mtBodyContainer.innerHTML = + '

Sorry, request failed:
' + + error.message + + "
"; + this.mtBodyContainer.setAttribute("role", "none"); + return { [key]: [] }; + }); + }); + + // Fetch all urls simultaneously + Promise.all(urlsPromises).then((dataObjects) => { + this.FETCHED_DATA = dataObjects.reduce((result, dataItem) => { + return { ...result, ...dataItem }; + }, {}); + + // console.log("Timeline data fetched: ", this.FETCHED_DATA); + resolve(); + }); + }); +}; + +/** + * Inner function to add each toot in timeline container + * @param {object} c Toot content + * @param {number} i Index of toot + */ +MastodonApi.prototype.appendToot = function (c, i) { + this.mtBodyContainer.insertAdjacentHTML("beforeend", this.assambleToot(c, i)); +}; + +/** + * Build toot structure + * @param {object} c Toot content + * @param {number} i Index of toot + */ +MastodonApi.prototype.assambleToot = function (c, i) { + let avatar, user, userName, url, date, formattedDate; + + if (c.reblog) { + // BOOSTED toot + // Toot url + url = c.reblog.url; + + // Boosted avatar + avatar = + '' + + '
' + + '
' + + '' +
+      c.reblog.account.username +
+      ' avatar' + + "
" + + '
' + + '' +
+      c.account.username +
+      ' avatar' + + "
" + + "
" + + "
"; + + // User name and url + userName = this.showEmojos( + c.reblog.account.display_name + ? c.reblog.account.display_name + : c.reblog.account.username, + this.FETCHED_DATA.emojos + ); + user = + '"; + + // Date + date = c.reblog.created_at; + } else { + // STANDARD toot + // Toot url + url = c.url; + + // Avatar + avatar = + '' + + '
' + + '
' + + '' +
+      c.account.username +
+      ' avatar' + + "
" + + "
" + + "
"; + + // User name and url + userName = this.showEmojos( + c.account.display_name ? c.account.display_name : c.account.username, + this.FETCHED_DATA.emojos + ); + user = + '"; + + // Date + date = c.created_at; + } + + // Date + formattedDate = this.formatDate(date); + let timestamp = + '"; + + // Main text + let text_css = ""; + if (this.TEXT_MAX_LINES !== "0") { + text_css = "truncate"; + document.documentElement.style.setProperty( + "--text-max-lines", + this.TEXT_MAX_LINES + ); + } + + let content = ""; + if (c.spoiler_text !== "") { + content = + '
' + + c.spoiler_text + + ' ' + + '
' + + this.formatTootText(c.content) + + "
" + + "
"; + } else if ( + c.reblog && + c.reblog.content !== "" && + c.reblog.spoiler_text !== "" + ) { + content = + '
' + + c.reblog.spoiler_text + + ' ' + + '
' + + this.formatTootText(c.reblog.content) + + "
" + + "
"; + } else if ( + c.reblog && + c.reblog.content !== "" && + c.reblog.spoiler_text === "" + ) { + content = + '
' + + '
' + + this.formatTootText(c.reblog.content) + + "
" + + "
"; + } else { + content = + '
' + + '
' + + this.formatTootText(c.content) + + "
" + + "
"; + } + + // Media attachments + let media = ""; + if (c.media_attachments.length > 0) { + for (let picid in c.media_attachments) { + media = this.placeMedias(c.media_attachments[picid], c.sensitive); + } + } + if (c.reblog && c.reblog.media_attachments.length > 0) { + for (let picid in c.reblog.media_attachments) { + media = this.placeMedias( + c.reblog.media_attachments[picid], + c.reblog.sensitive + ); + } + } + + // Preview link + let previewLink = ""; + if (!this.HIDE_PREVIEW_LINK && c.card) { + previewLink = this.placePreviewLink(c.card); + } + + // Poll + let poll = ""; + let pollOption = ""; + if (c.poll) { + for (let i in c.poll.options) { + pollOption += "
  • " + c.poll.options[i].title + "
  • "; + } + poll = + '
    ' + "
      " + pollOption + "
    " + "
    "; + } + + // Counter bar + let counterBar = ""; + if (!this.HIDE_COUNTER_BAR) { + let repliesCount = + '
    ' + + '' + + c.replies_count + + "
    "; + + let reblogCount = + '
    ' + + '' + + c.reblogs_count + + "
    "; + + let favoritesCount = + '
    ' + + '' + + c.favourites_count + + "
    "; + + counterBar = + '
    ' + + repliesCount + + reblogCount + + favoritesCount + + "
    "; + } + + // Add all to main toot container + let toot = + '
    ' + + '
    ' + + avatar + + user + + timestamp + + "
    " + + content + + media + + previewLink + + poll + + counterBar + + "
    "; + + return toot; +}; + +/** + * Handle text changes made to toots + * @param {string} c Text content + * @returns {string} Text content modified + */ +MastodonApi.prototype.formatTootText = function (c) { + let content = c; + + // Format hashtags and mentions + content = this.addTarget2hashtagMention(content); + + // Convert emojos shortcode into images + if (!this.HIDE_EMOJOS) { + content = this.showEmojos(content, this.FETCHED_DATA.emojos); + } + + // Convert markdown styles into HTML + if (this.MARKDOWN_BLOCKQUOTE) { + content = this.replaceHTMLtag( + content, + "

    >", + "

    ", + "

    ", + "

    " + ); + } + + return content; +}; + +/** + * Add target="_blank" to all #hashtags and @mentions in the toot + * @param {string} c Text content + * @returns {string} Text content modified + */ +MastodonApi.prototype.addTarget2hashtagMention = function (c) { + let content = c.replaceAll('rel="tag"', 'rel="tag" target="_blank"'); + content = content.replaceAll( + 'class="u-url mention"', + 'class="u-url mention" target="_blank"' + ); + + return content; +}; + +/** + * Find all custom emojis shortcode and replace by image + * @param {string} c Text content + * @param {array} e List with all custom emojis + * @returns {string} Text content modified + */ +MastodonApi.prototype.showEmojos = function (c, e) { + if (c.includes(":")) { + for (const emojo of e) { + const regex = new RegExp(`\\:${emojo.shortcode}\\:`, "g"); + c = c.replace( + regex, + `Emoji ${emojo.shortcode}` + ); + } + + return c; + } else { + return c; + } +}; + +/** + * Find all start/end and replace them by another start/end + * @param {string} c Text content + * @param {string} initialTagOpen Start HTML tag to replace + * @param {string} initialTagClose End HTML tag to replace + * @param {string} replacedTagOpen New start HTML tag + * @param {string} replacedTagClose New end HTML tag + * @returns {string} Text in HTML format + */ +MastodonApi.prototype.replaceHTMLtag = function ( + c, + initialTagOpen, + initialTagClose, + replacedTagOpen, + replacedTagClose +) { + if (c.includes(initialTagOpen)) { + const regex = new RegExp(initialTagOpen + "(.*?)" + initialTagClose, "gi"); + + return c.replace(regex, replacedTagOpen + "$1" + replacedTagClose); + } else { + return c; + } +}; + +/** + * Place media + * @param {object} m Media content + * @param {boolean} s Spoiler/Sensitive status + * @returns {string} Media in HTML format + */ +MastodonApi.prototype.placeMedias = function (m, s) { + let spoiler = s || false; + const pic = + '
    ' + + (spoiler ? '' : "") + + '' +
+    (m.description ? m.description : ' + + "
    "; + + return pic; +}; + +/** + * Place preview link + * @param {object} c Preview link content + * @returns {string} Preview link in HTML format + */ +MastodonApi.prototype.placePreviewLink = function (c) { + let card = + '' + + (c.image + ? '
    ' + : '
    📄
    ') + + "
    " + + '
    ' + + (c.provider_name + ? '' + c.provider_name + "" + : "") + + '' + + c.title + + "" + + (c.author_name + ? 'By ' + c.author_name + "" + : "") + + "
    " + + "
    "; + + return card; +}; + +/** + * Format date + * @param {string} d Date in ISO format (YYYY-MM-DDTHH:mm:ss.sssZ) + * @returns {string} Date formated (MM DD, YYYY) + */ +MastodonApi.prototype.formatDate = function (d) { + const monthNames = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + + let date = new Date(d); + + const displayDate = + monthNames[date.getMonth()] + + " " + + date.getDate() + + ", " + + date.getFullYear(); + + return displayDate; +}; + +/** + * Add/Remove event listener for loading spinner + */ +MastodonApi.prototype.manageSpinner = function () { + // Remove CSS class to container and listener to images + const spinnerCSS = this.SPINNER_CLASS; + const removeSpinner = function () { + this.parentNode.classList.remove(spinnerCSS); + this.removeEventListener("load", removeSpinner); + this.removeEventListener("error", removeSpinner); + }; + + // Add listener to images + this.mtBodyContainer + .querySelectorAll(`.${this.SPINNER_CLASS} > img`) + .forEach((e) => { + e.addEventListener("load", removeSpinner); + e.addEventListener("error", removeSpinner); + }); +};