diff --git a/css/mastodon-timeline-4.0.3-1.css b/css/mastodon-timeline-4.0.3-1.css
new file mode 100644
index 0000000..199de49
--- /dev/null
+++ b/css/mastodon-timeline-4.0.3-1.css
@@ -0,0 +1,473 @@
+/* Mastodon embed feed timeline v4.0.3-1*/
+/* More info at: */
+/* https://gitlab.com/clvgt12/mastodon-embed-feed-timeline */
+
+/* Variables */
+.mt-container,
+.mt-container[data-theme="light"] {
+ --mt-txt-max-lines: none;
+ --mt-color-bg: #fff;
+ --mt-color-bg-hover: #d9e1e8;
+ --mt-color-line-gray: #c0cdd9;
+ --mt-color-contrast-gray: #606984;
+ --mt-color-content-txt: #000;
+ --mt-color-link: #3a3bff;
+ --mt-color-error-txt: #8b0000;
+ --mt-color-btn-bg: #6364ff;
+ --mt-color-btn-bg-hover: #563acc;
+ --mt-color-btn-txt: #fff;
+}
+.mt-container[data-theme="dark"] {
+ --mt-color-bg: #fff;
+ --mt-color-bg-hover: #d9e1e8;
+ --mt-color-line-gray: #c0cdd9;
+ --mt-color-contrast-gray: #606984;
+ --mt-color-content-txt: #000;
+ --mt-color-link: #3a3bff;
+ --mt-color-error-txt: #8b0000;
+}
+
+/* Reset CSS */
+.mt-container button {
+ font: inherit;
+}
+.mt-container a,
+.mt-container button {
+ cursor: pointer;
+}
+
+/* Main container */
+.mt-container {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow-y: auto;
+ position: relative;
+ background-color: var(--mt-color-bg);
+ scrollbar-color: var(--mt-color-contrast-gray) var(--mt-color-bg);
+ scrollbar-width: auto;
+}
+.mt-container::-webkit-scrollbar {
+ width: 0.25rem;
+ height: 0.25rem;
+}
+.mt-container::-webkit-scrollbar-thumb {
+ background-color: var(--mt-color-contrast-gray);
+ border: none;
+ border-radius: 3rem;
+}
+.mt-container::-webkit-scrollbar-thumb:hover,
+.mt-container::-webkit-scrollbar-thumb:active {
+ background-color: var(--mt-color-contrast-gray);
+}
+.mt-container::-webkit-scrollbar-track {
+ background-color: var(--mt-color-bg);
+ 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(--mt-color-bg);
+}
+.mt-container a:link,
+.mt-container a:active,
+.mt-container a {
+ text-decoration: none;
+ color: var(--mt-color-link);
+}
+.mt-container a:not(.mt-post-preview):hover {
+ text-decoration: underline;
+}
+.mt-body {
+ padding: 1rem clamp(0.25rem, 4vw, 1rem);
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ margin-bottom: 1rem;
+}
+.mt-body .invisible {
+ font-size: 0;
+ line-height: 0;
+ display: inline-block;
+ width: 0;
+ height: 0;
+ position: absolute;
+}
+.mt-body a {
+ color: var(--mt-color-link);
+ text-decoration: none;
+}
+.mt-body a:hover {
+color: var(--mt-color-link);
+text-decoration: underline;
+}
+
+/* Post container */
+.mt-post {
+ margin: 0.25rem;
+ padding: 1rem 0.5rem;
+ position: relative;
+ min-height: 3.75rem;
+ background-color: transparent;
+ border-bottom: 1px solid var(--mt-color-line-gray);
+}
+.mt-post:hover,
+.mt-post:focus {
+ cursor: pointer;
+ background-color: var(--mt-color-bg-hover);
+}
+.mt-post p:last-child {
+ margin-bottom: 0;
+}
+
+/* User avatar */
+.mt-post-avatar {
+ margin-right: 0.75rem;
+}
+.mt-post-avatar-standard {
+ width: 2.25rem;
+ height: 2.25rem;
+}
+.mt-post-avatar-boosted {
+ width: 3rem;
+ height: 3rem;
+ position: relative;
+}
+.mt-post-avatar-image-big img {
+ aspect-ratio: 1/1;
+ width: 2.25rem;
+ height: 2.25rem;
+ border-radius: 0.25rem;
+ overflow: hidden;
+}
+.mt-post-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-post-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+}
+.mt-post-header-user {
+ font-weight: 600;
+ padding-right: 1rem;
+}
+.mt-post-header-user > a {
+ display: flex;
+ align-items: flex-start;
+ color: var(--mt-color-content-txt) !important;
+ overflow-wrap: anywhere;
+ text-decoration: none;
+}
+.mt-post-header-date {
+ font-size: 0.75rem;
+ text-align: right;
+ margin: 0 0 0 auto;
+}
+.mt-post-header-date > a {
+ color: var(--mt-color-contrast-gray) !important;
+}
+
+/* Text */
+.mt-post-txt {
+ margin-bottom: 1rem;
+ color: var(--mt-color-content-txt);
+}
+.mt-post-txt .spoiler-txt-hidden {
+ display: none;
+}
+.mt-post-txt.truncate {
+ display: -webkit-box;
+ overflow: hidden;
+ -webkit-line-clamp: var(--mt-txt-max-lines);
+ -webkit-box-orient: vertical;
+}
+.mt-post-txt:not(.truncate) .ellipsis::after {
+ content: "...";
+}
+.mt-post-txt blockquote {
+ border-left: 0.25rem solid var(--mt-color-line-gray);
+ margin-left: 0;
+ padding-left: 0.5rem;
+}
+.mt-post-header-user .mt-custom-emoji,
+.mt-post-txt .mt-custom-emoji {
+ height: 1.5rem;
+ min-width: 1.5rem;
+ margin-bottom: -0.25rem;
+ width: auto;
+}
+
+/* Poll */
+.mt-post-poll {
+ margin-bottom: 1rem;
+ color: var(--mt-color-content-txt);
+}
+.mt-post-poll ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+.mt-post-poll ul li {
+ font-size: 0.9rem;
+ margin-bottom: 0.5rem;
+}
+.mt-post-poll.mt-post-poll-expired ul li {
+ color: var(--mt-color-contrast-gray);
+}
+.mt-post-poll ul li:not(:last-child) {
+ margin-bottom: 0.25rem;
+}
+.mt-post-poll ul li:before {
+ content: "◯";
+ padding-right: 0.5rem;
+}
+.mt-post-poll.mt-post-poll-expired ul li:before {
+ content: "";
+ padding-right: 0;
+}
+
+/* Medias */
+.mt-post-media {
+ position: relative;
+ overflow: hidden;
+ margin-bottom: 1rem;
+ border-radius: 0.75rem;
+}
+.mt-post-media-spoiler > img,
+.mt-post-media-spoiler > audio,
+.mt-post-media-spoiler > video,
+.mt-post-media-spoiler > .mt-post-media-play-icon {
+ filter: blur(2rem);
+ pointer-events: none;
+}
+.mt-post-media > audio {
+ width: 100%;
+ position: relative;
+ z-index: 1;
+}
+.mt-post-media > img,
+.mt-post-media > video {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ text-align: center;
+ color: var(--mt-color-content-txt);
+}
+.mt-post-media.mt-loading-spinner .mt-post-media-play-icon {
+ display: none;
+}
+.mt-post-media-play-icon {
+ display: flex;
+ position: absolute;
+ width: 3rem;
+ height: 3rem;
+ top: calc(50% - 1.5rem);
+ left: calc(50% - 1.5rem);
+ justify-content: center;
+ align-items: center;
+ background-color: transparent;
+ border: none;
+ cursor: pointer;
+}
+.mt-post-media-play-icon > svg {
+ width: 2.5rem;
+ height: 2.5rem;
+ fill: var(--mt-color-bg);
+ stroke: var(--mt-color-content-txt);
+ stroke-width: 1px;
+}
+
+/* Preview link */
+.mt-post-preview {
+ min-height: 4rem;
+ display: flex;
+ flex-direction: column;
+ border: 1px solid var(--mt-color-line-gray);
+ border-radius: 0.5rem;
+ color: var(--mt-color-link);
+ font-size: 0.8rem;
+ margin: 1rem 0;
+ overflow: hidden;
+}
+.mt-post-preview-image {
+ width: 100%;
+ align-self: stretch;
+}
+.mt-post-preview-image img {
+ display: block;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ color: var(--mt-color-content-txt);
+ border-bottom: 1px solid var(--mt-color-line-gray);
+}
+.mt-post-preview-noImage {
+ width: 100%;
+ font-size: 1.5rem;
+ align-self: inherit;
+ text-align: left;
+ display: none;
+}
+.mt-post-preview-content {
+ width: 100%;
+ display: flex;
+ align-self: center;
+ flex-direction: column;
+ padding: 0.5rem 1rem;
+ gap: 0.5rem;
+}
+.mt-post-preview-title {
+ font-weight: 600;
+ font-size: 1.0rem;
+ font-family: serif;
+}
+
+/* Counter bar */
+.mt-post-counter-bar {
+ display: flex;
+ min-width: 6rem;
+ max-width: 40rem;
+ justify-content: space-between;
+ color: var(--mt-color-contrast-gray);
+}
+.mt-post-counter-bar-replies,
+.mt-post-counter-bar-reblog,
+.mt-post-counter-bar-favorites {
+ display: flex;
+ font-size: 0.75rem;
+ gap: 0.25rem;
+ align-items: center;
+ opacity: 0.5;
+}
+.mt-post-counter-bar-replies > svg,
+.mt-post-counter-bar-reblog > svg,
+.mt-post-counter-bar-favorites > svg {
+ width: 1rem;
+ fill: var(--mt-color-contrast-gray);
+}
+
+/* Buttons */
+.mt-container .mt-btn-dark {
+ display: flex;
+ border-radius: 0.25rem;
+ background-color: var(--mt-color-line-gray);
+ border: 0;
+ color: var(--mt-color-content-txt);
+ font-weight: 600;
+ font-size: 0.75rem;
+ text-align: center;
+ padding: 0 0.5rem;
+ line-height: 1.25rem;
+
+ vertical-align: top;
+}
+.mt-container .mt-btn-violet,
+.mt-container a.mt-btn-violet {
+ display: flex;
+ gap: 0.5rem;
+ border-radius: 0.25rem;
+ border: 0.5rem;
+ padding: 0.5rem 0.75rem;
+ font-size: 1rem;
+ font-weight: 600;
+ text-align: center;
+ background-color: var(--mt-color-btn-bg);
+ color: var(--mt-color-btn-txt);
+}
+.mt-container .mt-btn-violet:hover,
+.mt-container a.mt-btn-violet:hover {
+ background-color: var(--mt-color-btn-bg-hover);
+ text-decoration: none;
+}
+.mt-post-txt .mt-btn-spoiler {
+ display: inline-block;
+}
+.mt-post-media.mt-loading-spinner > .mt-btn-spoiler {
+ display: none;
+}
+.mt-post-media > .mt-btn-spoiler {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ z-index: 2;
+ transform: translate(-50%, -50%);
+}
+
+/* Error */
+.mt-error {
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ color: var(--mt-color-error-txt);
+ padding: 0.75rem;
+ text-align: center;
+}
+.mt-error-icon {
+ font-size: 2rem;
+}
+.mt-error-message {
+ width: 100%;
+ padding: 1rem 0;
+}
+.mt-error-message hr {
+ color: var(--mt-color-line-gray);
+}
+
+/* Loading spinner */
+.mt-body > .mt-loading-spinner {
+ position: absolute;
+ width: 3rem;
+ height: 3rem;
+ margin: auto;
+ top: calc(50% - 1.5rem);
+ right: calc(50% - 1.5rem);
+}
+.mt-loading-spinner {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 128 128'%3E%3Cg%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 64 64' to='360 64 64' dur='1000ms' repeatCount='indefinite'/%3E%3Cpath d='M64 6.69a57.3 57.3 0 1 1 0 114.61A57.3 57.3 0 0 1 6.69 64' fill='none' stroke='%23404040' stroke-width='12'/%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 */
+.mt-footer {
+ display: flex;
+ flex-flow: wrap;
+ margin: auto auto 2rem auto;
+ padding: 0 1.5rem;
+ gap: 1.5rem;
+ align-items: center;
+ justify-content: 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;
+}
\ No newline at end of file
diff --git a/js/mastodon-timeline-4.0.3-1.js b/js/mastodon-timeline-4.0.3-1.js
new file mode 100644
index 0000000..758ef92
--- /dev/null
+++ b/js/mastodon-timeline-4.0.3-1.js
@@ -0,0 +1,1111 @@
+/**
+ * Mastodon embed feed timeline
+ * @author clvgt12
+ * @version 4.0.3-1
+ * @url https://gitlab.com/clvgt12/mastodon-embed-feed-timeline
+ * @license GNU AGPLv3
+ */
+
+/**
+ * Mastodon embed feed timeline
+ * @author idotj
+ * @version 4.0.3
+ * @url https://gitlab.com/idotj/mastodon-embed-feed-timeline
+ * @license GNU AGPLv3
+ */
+"use strict";
+
+class MastodonTimeline {
+ constructor(customSettings = {}) {
+ this.defaultSettings = {
+ mtContainerId: "mt-container",
+ instanceUrl: "https://mastodon.social",
+ timelineType: "local",
+ userId: "",
+ profileName: "",
+ hashtagName: "",
+ spinnerClass: "mt-loading-spinner",
+ defaultTheme: "auto",
+ maxNbPostFetch: "20",
+ maxNbPostShow: "20",
+ hideUnlisted: false,
+ hideReblog: false,
+ hideReplies: false,
+ hideVideoPreview: false,
+ hidePreviewLink: false,
+ hideEmojos: false,
+ markdownBlockquote: false,
+ hideCounterBar: false,
+ txtMaxLines: "0",
+ btnShowMore: "SHOW MORE",
+ btnShowLess: "SHOW LESS",
+ btnShowContent: "SHOW CONTENT",
+ btnSeeMore: "See more posts at Mastodon",
+ btnReload: "Refresh",
+ };
+
+ this.mtSettings = { ...this.defaultSettings, ...customSettings };
+
+ this.mtContainerNode = "";
+ this.mtBodyNode = "";
+ this.fetchedData = {};
+
+ this.mtInit();
+ }
+
+ /**
+ * Trigger callback when DOM loaded or complete
+ * @param {function} c Callback executed
+ */
+ onDOMContentLoaded(c) {
+ if (
+ typeof document !== "undefined" &&
+ (document.readyState === "complete" ||
+ document.readyState === "interactive")
+ ) {
+ c();
+ } else if (
+ typeof document !== "undefined" &&
+ (document.readyState !== "complete" ||
+ document.readyState !== "interactive")
+ ) {
+ document.addEventListener("DOMContentLoaded", c);
+ }
+ }
+
+ /**
+ * Initialize and build the timeline
+ */
+ mtInit() {
+ // console.log("Creating Mastodon timeline with settings: ", this.mtSettings);
+
+ this.onDOMContentLoaded(() => {
+ // Register container node
+ this.mtContainerNode = document.getElementById(
+ this.mtSettings.mtContainerId
+ );
+
+ // Register body node
+ this.mtBodyNode =
+ this.mtContainerNode.getElementsByClassName("mt-body")[0];
+
+ this.#loadColorTheme();
+ this.#buildTimeline("newTimeline");
+ });
+ }
+
+ /**
+ * Reload the timeline by fetching the lastest posts
+ */
+ mtUpdate() {
+ this.onDOMContentLoaded(() => {
+ this.mtBodyNode.replaceChildren();
+ this.mtBodyNode.insertAdjacentHTML(
+ "afterbegin",
+ '
'
+ );
+ this.#buildTimeline("updateTimeline");
+ });
+ }
+
+ /**
+ * Apply the color theme in the timeline
+ * @param {string} themeType Type of color theme
+ */
+ mtColorTheme(themeType) {
+ this.onDOMContentLoaded(() => {
+ this.mtContainerNode.setAttribute("data-theme", themeType);
+ });
+ }
+
+ /**
+ * Get the theme style chosen by the user or by the browser/OS
+ */
+ #loadColorTheme() {
+ if (this.mtSettings.defaultTheme === "auto") {
+ let systemTheme = window.matchMedia("(prefers-color-scheme: dark)");
+ systemTheme.matches
+ ? this.mtColorTheme("dark")
+ : this.mtColorTheme("light");
+ // Update the theme if user change browser/OS preference
+ systemTheme.addEventListener("change", (e) => {
+ e.matches ? this.mtColorTheme("dark") : this.mtColorTheme("light");
+ });
+ } else {
+ this.mtColorTheme(this.mtSettings.defaultTheme);
+ }
+ }
+
+ /**
+ * Requests to the server to collect all the data
+ * @returns {object} Data container
+ */
+ #fetchTimelineData() {
+ return new Promise((resolve, reject) => {
+ /**
+ * Fetch data from server
+ * @param {string} url address to fetch
+ * @returns {array} 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.mtSettings.instanceUrl) {
+ if (this.mtSettings.timelineType === "profile") {
+ if (this.mtSettings.userId) {
+ urls.timeline = `${this.mtSettings.instanceUrl}/api/v1/accounts/${this.mtSettings.userId}/statuses?limit=${this.mtSettings.maxNbPostFetch}`;
+ } else {
+ this.#showError(
+ "Please check your userId value",
+ "⚠️"
+ );
+ }
+ } else if (this.mtSettings.timelineType === "hashtag") {
+ if (this.mtSettings.hashtagName) {
+ urls.timeline = `${this.mtSettings.instanceUrl}/api/v1/timelines/tag/${this.mtSettings.hashtagName}?limit=${this.mtSettings.maxNbPostFetch}`;
+ } else {
+ this.#showError(
+ "Please check your hashtagName value",
+ "⚠️"
+ );
+ }
+ } else if (this.mtSettings.timelineType === "local") {
+ urls.timeline = `${this.mtSettings.instanceUrl}/api/v1/timelines/public?local=true&limit=${this.mtSettings.maxNbPostFetch}`;
+ } else {
+ this.#showError(
+ "Please check your timelineType value",
+ "⚠️"
+ );
+ }
+ } else {
+ this.#showError(
+ "Please check your instanceUrl value",
+ "⚠️"
+ );
+ }
+ if (!this.mtSettings.hideEmojos) {
+ urls.emojos = this.mtSettings.instanceUrl + "/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 from: " + url)
+ );
+ this.#showError(error.message);
+ return { [key]: [] };
+ });
+ });
+
+ // Fetch all urls simultaneously
+ Promise.all(urlsPromises).then((dataObjects) => {
+ this.mtSettings.fetchedData = dataObjects.reduce((result, dataItem) => {
+ return { ...result, ...dataItem };
+ }, {});
+
+ // console.log("Timeline data fetched: ", this.mtSettings.fetchedData);
+ resolve();
+ });
+ });
+ }
+
+ /**
+ * Filter all fetched posts and append them on the timeline
+ * @param {string} t Type of build (new or reload)
+ */
+ async #buildTimeline(t) {
+ await this.#fetchTimelineData();
+
+ // Empty container body
+ this.mtBodyNode.replaceChildren();
+
+ // Set posts counter to 0
+ let nbPostShow = 0;
+
+ for (let i in this.mtSettings.fetchedData.timeline) {
+ // First filter (Public / Unlisted)
+ if (
+ this.mtSettings.fetchedData.timeline[i].visibility == "public" ||
+ (!this.mtSettings.hideUnlisted &&
+ this.mtSettings.fetchedData.timeline[i].visibility == "unlisted")
+ ) {
+ // Second filter (Reblog / Replies)
+ if (
+ (this.mtSettings.hideReblog &&
+ this.mtSettings.fetchedData.timeline[i].reblog) ||
+ (this.mtSettings.hideReplies &&
+ this.mtSettings.fetchedData.timeline[i].in_reply_to_id)
+ ) {
+ // Nothing here (Don't append posts)
+ } else {
+ if (nbPostShow < this.mtSettings.maxNbPostShow) {
+ this.#appendPost(
+ this.mtSettings.fetchedData.timeline[i],
+ Number(i)
+ );
+ nbPostShow++;
+ } else {
+ // Nothing here (Reached the limit of maximum number of posts to show)
+ }
+ }
+ }
+ }
+
+ // If there are no posts to display, show an error message
+ if (this.mtBodyNode.innerHTML === "") {
+ const errorMessage =
+ "No posts to show
" +
+ (this.mtSettings.fetchedData.timeline?.length || 0) +
+ " posts have been fetched from the server
This may be due to an incorrect configuration in the parameters or to filters applied (to hide certains type of posts)";
+ this.#showError(errorMessage, "📭");
+ } else {
+ if (t === "newTimeline") {
+ this.#manageSpinner();
+ this.#setPostsInteracion();
+ this.#buildFooter();
+ } else if (t === "updateTimeline") {
+ this.#manageSpinner();
+ } else {
+ this.#showError("The function buildTimeline() was expecting a param");
+ }
+ }
+ }
+
+ /**
+ * Add each post in the timeline container
+ * @param {object} c Post content
+ * @param {number} i Index of post
+ */
+ #appendPost(c, i) {
+ this.mtBodyNode.insertAdjacentHTML("beforeend", this.#assamblePost(c, i));
+ }
+
+ /**
+ * Build post structure
+ * @param {object} c Post content
+ * @param {number} i Index of post
+ */
+ #assamblePost(c, i) {
+ let avatar,
+ user,
+ userName,
+ url,
+ date,
+ formattedDate,
+ favoritesCount,
+ reblogCount,
+ repliesCount;
+
+ if (c.reblog) {
+ // BOOSTED post
+ // Post url
+ url = c.reblog.url;
+
+ // Boosted avatar
+ avatar =
+ '' +
+ '' +
+ '
' +
+ '
' +
+ "
" +
+ '
' +
+ '
' +
+ "
" +
+ "
" +
+ "";
+
+ // User name and url
+ userName = c.reblog.account.display_name
+ ? c.reblog.account.display_name
+ : c.reblog.account.username;
+ if (!this.mtSettings.hideEmojos) {
+ userName = this.#createEmoji(
+ userName,
+ this.mtSettings.fetchedData.emojos
+ );
+ }
+ user =
+ '";
+
+ // Date
+ date = c.reblog.created_at;
+
+ // Counter bar
+ repliesCount = c.reblog.replies_count;
+ reblogCount = c.reblog.reblogs_count;
+ favoritesCount = c.reblog.favourites_count;
+ } else {
+ // STANDARD post
+ // Post url
+ url = c.url;
+
+ // Avatar
+ avatar =
+ '' +
+ '' +
+ '
' +
+ '
' +
+ "
" +
+ "
" +
+ "";
+
+ // User name and url
+ userName = c.account.display_name
+ ? c.account.display_name
+ : c.account.username;
+ if (!this.mtSettings.hideEmojos) {
+ userName = this.#createEmoji(
+ userName,
+ this.mtSettings.fetchedData.emojos
+ );
+ }
+ user =
+ '";
+
+ // Date
+ date = c.created_at;
+
+ // Counter bar
+ repliesCount = c.replies_count;
+ reblogCount = c.reblogs_count;
+ favoritesCount = c.favourites_count;
+ }
+
+ // Date
+ formattedDate = this.#formatDate(date);
+ const timestamp =
+ '";
+
+ // Main text
+ let txtCss = "";
+ if (this.mtSettings.txtMaxLines !== "0") {
+ txtCss = " truncate";
+ this.mtBodyNode.parentNode.style.setProperty(
+ "--mt-txt-max-lines",
+ this.mtSettings.txtMaxLines
+ );
+ }
+
+ let content = "";
+ if (c.spoiler_text !== "") {
+ content =
+ '' +
+ c.spoiler_text +
+ '
" +
+ '
' +
+ this.#formatPostText(c.content) +
+ "
" +
+ "
";
+ } else if (
+ c.reblog &&
+ c.reblog.content !== "" &&
+ c.reblog.spoiler_text !== ""
+ ) {
+ content =
+ '' +
+ c.reblog.spoiler_text +
+ '
" +
+ '
' +
+ this.#formatPostText(c.reblog.content) +
+ "
" +
+ "
";
+ } else if (
+ c.reblog &&
+ c.reblog.content !== "" &&
+ c.reblog.spoiler_text === ""
+ ) {
+ content =
+ '' +
+ '
' +
+ this.#formatPostText(c.reblog.content) +
+ "
" +
+ "
";
+ } else {
+ content =
+ '' +
+ '
' +
+ this.#formatPostText(c.content) +
+ "
" +
+ "
";
+ }
+
+ // Media attachments
+ let media = [];
+ if (c.media_attachments.length > 0) {
+ for (let i in c.media_attachments) {
+ media.push(this.#createMedia(c.media_attachments[i], c.sensitive));
+ }
+ }
+ if (c.reblog && c.reblog.media_attachments.length > 0) {
+ for (let i in c.reblog.media_attachments) {
+ media.push(
+ this.#createMedia(c.reblog.media_attachments[i], c.reblog.sensitive)
+ );
+ }
+ }
+
+ // Preview link
+ let previewLink = "";
+ if (!this.mtSettings.hidePreviewLink && c.card) {
+ previewLink = this.#createPreviewLink(c.card);
+ }
+
+ // Poll
+ let poll = "";
+ if (c.poll) {
+ let pollOption = "";
+ for (let i in c.poll.options) {
+ pollOption += "" + c.poll.options[i].title + "";
+ }
+ poll =
+ '";
+ }
+
+ // Counter bar
+ let counterBar = "";
+ if (!this.mtSettings.hideCounterBar) {
+ const repliesTag =
+ '' +
+ '
' +
+ repliesCount +
+ "
";
+
+ const reblogTag =
+ '' +
+ '
' +
+ reblogCount +
+ "
";
+
+ const favoritesTag =
+ '' +
+ '
' +
+ favoritesCount +
+ "
";
+
+ counterBar =
+ '' +
+ repliesTag +
+ reblogTag +
+ favoritesTag +
+ "
";
+ }
+
+ // Add all to main post container
+ const post =
+ '' +
+ '" +
+ content +
+ media.join("") +
+ previewLink +
+ poll +
+ counterBar +
+ "";
+
+ return post;
+ }
+
+ /**
+ * Handle text changes made to posts
+ * @param {string} c Text content
+ * @returns {string} Text content modified
+ */
+ #formatPostText(c) {
+ let content = c;
+
+ // Format hashtags and mentions
+ content = this.#addTarget2hashtagMention(content);
+
+ // Convert emojos shortcode into images
+ if (!this.mtSettings.hideEmojos) {
+ content = this.#createEmoji(content, this.mtSettings.fetchedData.emojos);
+ }
+
+ // Convert markdown styles into HTML
+ if (this.mtSettings.markdownBlockquote) {
+ content = this.#replaceHTMLtag(
+ content,
+ ">",
+ "
",
+ "",
+ "
"
+ );
+ }
+
+ return content;
+ }
+
+ /**
+ * Add target="_blank" to all #hashtags and @mentions in the post
+ * @param {string} c Text content
+ * @returns {string} Text content modified
+ */
+ #addTarget2hashtagMention(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 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
+ */
+ #replaceHTMLtag(
+ 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;
+ }
+ }
+
+ /**
+ * Escape quotes and other special characters, to make them safe to add
+ * to HTML content and attributes as plain text
+ * @param {string} s String
+ * @returns {string} String
+ */
+ #escapeHtml(s) {
+ return (s ?? "")
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'");
+ }
+
+ /**
+ * 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
+ */
+ #createEmoji(c, e) {
+ if (c.includes(":")) {
+ for (const emojo of e) {
+ const regex = new RegExp(`\\:${emojo.shortcode}\\:`, "g");
+ c = c.replace(
+ regex,
+ ``
+ );
+ }
+
+ return c;
+ } else {
+ return c;
+ }
+ }
+
+ /**
+ * Format date
+ * @param {string} d Date in ISO format (YYYY-MM-DDTHH:mm:ss.sssZ)
+ * @returns {string} Date formated (MM DD, YYYY)
+ */
+ #formatDate(d) {
+ const monthNames = [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ ];
+
+ const date = new Date(d);
+
+ const displayDate =
+ monthNames[date.getMonth()] +
+ " " +
+ date.getDate() +
+ ", " +
+ date.getFullYear();
+
+ return displayDate;
+ }
+
+ /**
+ * Create media element
+ * @param {object} m Media content
+ * @param {boolean} s Spoiler/Sensitive status
+ * @returns {string} Media in HTML format
+ */
+ #createMedia(m, s) {
+ const spoiler = s || false;
+ const type = m.type;
+ let media = "";
+
+ if (type === "image") {
+ media =
+ '' +
+ (spoiler
+ ? '
"
+ : "") +
+ '
' +
+ "
";
+ }
+
+ if (type === "audio") {
+ if (m.preview_url) {
+ media =
+ '' +
+ (spoiler
+ ? '
"
+ : "") +
+ '
' +
+ '
' +
+ "
";
+ } else {
+ media =
+ '' +
+ (spoiler
+ ? '
"
+ : "") +
+ '
' +
+ "
";
+ }
+ }
+
+ if (type === "video" || type === "gifv") {
+ if (!this.mtSettings.hideVideoPreview) {
+ media =
+ '' +
+ (spoiler
+ ? '
"
+ : "") +
+ '
' +
+ '
' +
+ "
";
+ } else {
+ media =
+ '' +
+ (spoiler
+ ? '"
+ : "") +
+ '' +
+ "
";
+ }
+ }
+
+ return media;
+ }
+
+ /**
+ * Replace the video preview image by the video player
+ * @param {event} e User interaction trigger
+ */
+ #loadPostVideo(e) {
+ const parentNode = e.target.closest("[data-video-url]");
+ const videoUrl = parentNode.dataset.videoUrl;
+ parentNode.replaceChildren();
+ parentNode.innerHTML =
+ '';
+ }
+
+ /**
+ * Spoiler button
+ * @param {event} e User interaction trigger
+ */
+ #toogleSpoiler(e) {
+ const nextSibling = e.target.nextSibling;
+ if (
+ nextSibling.localName === "img" ||
+ nextSibling.localName === "audio" ||
+ nextSibling.localName === "video"
+ ) {
+ e.target.parentNode.classList.remove("mt-post-media-spoiler");
+ e.target.style.display = "none";
+ } else if (
+ nextSibling.classList.contains("spoiler-txt-hidden") ||
+ nextSibling.classList.contains("spoiler-txt-visible")
+ ) {
+ if (e.target.textContent == this.mtSettings.btnShowMore) {
+ nextSibling.classList.remove("spoiler-txt-hidden");
+ nextSibling.classList.add("spoiler-txt-visible");
+ e.target.setAttribute("aria-expanded", "true");
+ e.target.textContent = this.mtSettings.btnShowLess;
+ } else {
+ nextSibling.classList.remove("spoiler-txt-visible");
+ nextSibling.classList.add("spoiler-txt-hidden");
+ e.target.setAttribute("aria-expanded", "false");
+ e.target.textContent = this.mtSettings.btnShowMore;
+ }
+ }
+ }
+
+ /**
+ * Create preview link
+ * @param {object} c Preview link content
+ * @returns {string} Preview link in HTML format
+ */
+ #createPreviewLink(c) {
+ const card =
+ '' +
+ (c.image
+ ? ''
+ : '📄
') +
+ "" +
+ '' +
+ (c.provider_name
+ ? '' +
+ this.#parseHTMLstring(c.provider_name) +
+ ""
+ : "") +
+ '' +
+ c.title +
+ "" +
+ (c.author_name
+ ? '' +
+ this.#parseHTMLstring(c.author_name) +
+ ""
+ : "") +
+ "
" +
+ "";
+
+ return card;
+ }
+
+ /**
+ * Parse HTML string
+ * @param {string} s HTML string
+ * @returns {string} Plain text
+ */
+ #parseHTMLstring(s) {
+ const parser = new DOMParser();
+ const txt = parser.parseFromString(s, "text/html");
+ return txt.body.textContent;
+ }
+
+ /**
+ * Build footer after last post
+ */
+ #buildFooter() {
+ if (this.mtSettings.btnSeeMore || this.mtSettings.btnReload) {
+ // Add footer container
+ this.mtBodyNode.parentNode.insertAdjacentHTML(
+ "beforeend",
+ ''
+ );
+
+ const containerFooter =
+ this.mtContainerNode.getElementsByClassName("mt-footer")[0];
+
+ // Create button to open Mastodon page
+ if (this.mtSettings.btnSeeMore) {
+ let btnSeeMorePath = "";
+ if (this.mtSettings.timelineType === "profile") {
+ if (this.mtSettings.profileName) {
+ btnSeeMorePath = this.mtSettings.profileName;
+ } else {
+ this.#showError(
+ "Please check your profileName value",
+ "⚠️"
+ );
+ }
+ } else if (this.mtSettings.timelineType === "hashtag") {
+ btnSeeMorePath = "tags/" + this.mtSettings.hashtagName;
+ } else if (this.mtSettings.timelineType === "local") {
+ btnSeeMorePath = "public/local";
+ }
+ const btnSeeMoreHTML =
+ '' +
+ this.mtSettings.btnSeeMore +
+ "";
+
+ containerFooter.insertAdjacentHTML("beforeend", btnSeeMoreHTML);
+ }
+
+ // Create button to refresh the timeline
+ if (this.mtSettings.btnReload) {
+ const btnReloadHTML =
+ '";
+
+ containerFooter.insertAdjacentHTML("beforeend", btnReloadHTML);
+
+ const reloadBtn =
+ this.mtContainerNode.getElementsByClassName("btn-refresh")[0];
+ reloadBtn.addEventListener("click", () => {
+ this.mtUpdate();
+ });
+ }
+ }
+ }
+
+ /**
+ * Add EventListeners for timeline interactions and trigger functions
+ */
+ #setPostsInteracion() {
+ this.mtBodyNode.addEventListener("click", (e) => {
+ // Check if post cointainer was clicked
+ if (
+ e.target.localName == "article" ||
+ e.target.offsetParent?.localName == "article" ||
+ (e.target.localName == "img" &&
+ !e.target.parentNode.getAttribute("data-video-url"))
+ ) {
+ this.#openPostUrl(e);
+ }
+ // Check if Show More/Less button was clicked
+ if (
+ e.target.localName == "button" &&
+ e.target.classList.contains("mt-btn-spoiler")
+ ) {
+ this.#toogleSpoiler(e);
+ }
+ // Check if video preview image or play icon/button was clicked
+ if (
+ e.target.className == "mt-post-media-play-icon" ||
+ (e.target.localName == "svg" &&
+ e.target.parentNode.className == "mt-post-media-play-icon") ||
+ (e.target.localName == "path" &&
+ e.target.parentNode.parentNode.className ==
+ "mt-post-media-play-icon") ||
+ (e.target.localName == "img" &&
+ e.target.parentNode.getAttribute("data-video-url"))
+ ) {
+ this.#loadPostVideo(e);
+ }
+ });
+ this.mtBodyNode.addEventListener("keydown", (e) => {
+ // Check if Enter key was pressed with focus in an article
+ if (e.key === "Enter" && e.target.localName == "article") {
+ this.#openPostUrl(e);
+ }
+ });
+ }
+
+ /**
+ * Open post in a new page avoiding any other natural link
+ * @param {event} e User interaction trigger
+ */
+ #openPostUrl(e) {
+ const urlPost = e.target.closest(".mt-post").dataset.location;
+ if (
+ e.target.localName !== "a" &&
+ e.target.localName !== "span" &&
+ e.target.localName !== "button" &&
+ e.target.localName !== "time" &&
+ e.target.className !== "mt-post-preview-noImage" &&
+ e.target.parentNode.className !== "mt-post-avatar-image-big" &&
+ e.target.parentNode.className !== "mt-post-avatar-image-small" &&
+ e.target.parentNode.className !== "mt-post-preview-image" &&
+ e.target.parentNode.className !== "mt-post-preview" &&
+ urlPost
+ ) {
+ window.open(urlPost, "_blank", "noopener");
+ }
+ }
+
+ /**
+ * Add/Remove EventListeners for loading spinner
+ */
+ #manageSpinner() {
+ // Remove EventListener and CSS class to container
+ const removeSpinner = (e) => {
+ e.target.parentNode.classList.remove(this.mtSettings.spinnerClass);
+ e.target.removeEventListener("load", removeSpinner);
+ e.target.removeEventListener("error", removeSpinner);
+ };
+ // Add EventListener to images
+ this.mtBodyNode
+ .querySelectorAll(`.${this.mtSettings.spinnerClass} > img`)
+ .forEach((e) => {
+ e.addEventListener("load", removeSpinner);
+ e.addEventListener("error", removeSpinner);
+ });
+ }
+
+ /**
+ * Show an error on the timeline
+ * @param {string} e Error message
+ * @param {string} i Icon
+ */
+ #showError(t, i) {
+ const icon = i || "❌";
+ this.mtBodyNode.innerHTML =
+ '' +
+ icon +
+ 'Oops, something\'s happened:' +
+ t +
+ "
";
+ this.mtBodyNode.setAttribute("role", "none");
+ throw new Error(
+ "Stopping the script due to an error building the timeline."
+ );
+ }
+}