Feature/posts limit

This commit is contained in:
i.j 2024-03-21 09:27:16 +00:00
parent a78de16fd9
commit 410e83bce1
11 changed files with 438 additions and 273 deletions

View File

@ -1,3 +1,11 @@
v4.3.10 - 21/03/2024
- Allow to load more than 20 or 40 posts
- Add link preview description
- Allow to choose a maximum number of lines of text in preview description
- Fix carousel conflict with avatar and emojos images
- Fix possible error in aria-setsize with values greater than the total number of posts
- JS refactoring
v4.3.7 - 12/03/2024 v4.3.7 - 12/03/2024
- Display medias inside post using CSS grid - Display medias inside post using CSS grid
- Add a placeholder bg-color for images - Add a placeholder bg-color for images
@ -168,7 +176,7 @@ v3.1.1 - 28/01/2023
v3.1.0 - 21/01/2023 v3.1.0 - 21/01/2023
- Fix spoiler content show/hide - Fix spoiler content show/hide
- Add feature, choose a maximum number of lines of text - Allow to choose a maximum number of lines of text in posts
- Hide button to user page if 'btn_see_more' is empty - Hide button to user page if 'btn_see_more' is empty
v2.12.0 - 02/12/2022 v2.12.0 - 02/12/2022

View File

@ -65,11 +65,11 @@ This option allows you to start without the need to upload any files on your ser
Copy the following CSS and JS links to include them in your project: Copy the following CSS and JS links to include them in your project:
```html ```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@idotj/mastodon-embed-timeline@4.3.7/dist/mastodon-timeline.min.css" integrity="sha256-TxNxDe916jqa7iqnY5d3/1SuHlB+/4r9XEH0kOwh2Nc=" crossorigin="anonymous"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@idotj/mastodon-embed-timeline@4.3.10/dist/mastodon-timeline.min.css" crossorigin="anonymous">
``` ```
```html ```html
<script src="https://cdn.jsdelivr.net/npm/@idotj/mastodon-embed-timeline@4.3.7/dist/mastodon-timeline.umd.js" integrity="sha256-VK7I7SRA8gZaOzjlIQ6aeG0vOlkzuRnstJi2fgR3L80=" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/@idotj/mastodon-embed-timeline@4.3.10/dist/mastodon-timeline.umd.js" crossorigin="anonymous"></script>
``` ```
### Package manager ### Package manager
@ -182,48 +182,54 @@ Here you have all the options available to quickly setup and customize your time
```js ```js
// Id of the <div> containing the timeline // Id of the <div> containing the timeline
// Default: "mt-container"
mtContainerId: "mt-container", mtContainerId: "mt-container",
// Mastodon instance Url including https:// // Mastodon instance Url including https://
// Default: "https://mastodon.social"
instanceUrl: "https://mastodon.social", instanceUrl: "https://mastodon.social",
// Choose type of posts to show in the timeline: 'local', 'profile', 'hashtag' // Choose type of posts to show in the timeline: 'local', 'profile', 'hashtag'
// Default: local // Default: "local"
timelineType: "local", timelineType: "local",
// Your user ID number on Mastodon instance // Your user ID number on Mastodon instance
// Leave it empty if you didn't choose 'profile' as type of timeline // Leave it empty if you didn't choose 'profile' as type of timeline
// Default: ""
userId: "", userId: "",
// Your user name on Mastodon instance (including the @ symbol at the beginning) // Your user name on Mastodon instance (including the @ symbol at the beginning)
// Leave it empty if you didn't choose 'profile' as type of timeline // Leave it empty if you didn't choose 'profile' as type of timeline
// Default: ""
profileName: "", profileName: "",
// The name of the hashtag (not including the # symbol) // The name of the hashtag (not including the # symbol)
// Leave it empty if you didn't choose 'hashtag' as type of timeline // Leave it empty if you didn't choose 'hashtag' as type of timeline
// Default: ""
hashtagName: "", hashtagName: "",
// Class name for the loading spinner (also used in CSS file) // Class name for the loading spinner (also used in CSS file)
// Default: "mt-loading-spinner"
spinnerClass: "mt-loading-spinner", spinnerClass: "mt-loading-spinner",
// Preferred color theme: 'light', 'dark' or 'auto' // Preferred color theme: "light", "dark" or "auto"
// Default: auto // Default: "auto"
defaultTheme: "auto", defaultTheme: "auto",
// Maximum number of posts to request to the server // Maximum number of posts to request to the server
// Default: 20 // Default: "20"
maxNbPostFetch: "20", maxNbPostFetch: "20",
// Maximum number of posts to show in the timeline // Maximum number of posts to show in the timeline
// Default: 20 // Default: "20"
maxNbPostShow: "20", maxNbPostShow: "20",
// Specifies the format of the date according to the chosen language/country // Specifies the format of the date according to the chosen language/country
// Default: British English (day-month-year order) // Default: "en-GB" (British English: day-month-year order)
dateLocale: "en-GB", dateLocale: "en-GB",
// Customize the date format using the options // Customize the date format using the options for day, month and year
// Default: DD MMM YYYY // Default: day: "2-digit", month: "short", year: "numeric" (DD MMM YYYY)
dateOptions: { dateOptions: {
day: "2-digit", day: "2-digit",
month: "short", month: "short",
@ -231,47 +237,64 @@ Here you have all the options available to quickly setup and customize your time
}, },
// Hide unlisted posts // Hide unlisted posts
// Default: don't hide // Default: false (don't hide)
hideUnlisted: false, hideUnlisted: false,
// Hide boosted posts // Hide boosted posts
// Default: don't hide // Default: false (don't hide)
hideReblog: false, hideReblog: false,
// Hide replies posts // Hide replies posts
// Default: don't hide // Default: false (don't hide)
hideReplies: false, hideReplies: false,
// Hide pinned posts from the profile timeline // Hide pinned posts from the profile timeline
// Default: don't hide // Default: false (don't hide)
hidePinnedPosts: false, hidePinnedPosts: false,
// Hide the user account under the user name // Hide the user account under the user name
// Default: don't hide // Default: false (don't hide)
hideUserAccount: false, hideUserAccount: false,
// Limit the text content to a maximum number of lines
// Use "0" to show no text
// Default: "" (unlimited)
txtMaxLines: "",
// Customize the text of the button used for showing/hiding sensitive/spoiler text
btnShowMore: "SHOW MORE",
btnShowLess: "SHOW LESS",
// Converts Markdown symbol ">" at the beginning of a paragraph into a blockquote HTML tag
// Default: false (don't apply)
markdownBlockquote: false,
// Hide custom emojis available on the server // Hide custom emojis available on the server
// Default: don't hide // Default: false (don't hide)
hideEmojos: false, hideEmojos: false,
// Customize the text of the button used for showing sensitive/spoiler media content
btnShowContent: "SHOW CONTENT",
// Hide video image preview and load video player instead // Hide video image preview and load video player instead
// Default: don't hide // Default: false (don't hide)
hideVideoPreview: false, hideVideoPreview: false,
// Hide preview card if post contains a link, photo or video from a Url // Hide preview card if post contains a link, photo or video from a Url
// Default: don't hide // Default: false (don't hide)
hidePreviewLink: false, hidePreviewLink: false,
// Limit the preview text description to a maximum number of lines
// Use "0" to show no text
// Default: "" (unlimited)
previewMaxLines: "",
// Hide replies, boosts and favourites posts counter // Hide replies, boosts and favourites posts counter
// Default: don't hide // Default: false (don't hide)
hideCounterBar: false, hideCounterBar: false,
// Converts Markdown symbol ">" at the beginning of a paragraph into a blockquote HTML tag // Disable a carousel/lightbox when the user clicks on a picture in a post
// Default: don't apply // Default: false (not disabled)
markdownBlockquote: false,
// Show a carousel/lightbox when the user clicks on a picture in a post
// Default: not disabled
disableCarousel: false, disableCarousel: false,
// Customize the text of the buttons used for the carousel/lightbox // Customize the text of the buttons used for the carousel/lightbox
@ -279,17 +302,6 @@ Here you have all the options available to quickly setup and customize your time
carouselPrevTxt: "Previous media item", carouselPrevTxt: "Previous media item",
carouselNextTxt: "Next media item", carouselNextTxt: "Next media item",
// Limit the text content to a maximum number of lines
// Default: 0 (unlimited)
txtMaxLines: "0",
// Customize the text of the button used for showing/hiding sensitive/spoiler text
btnShowMore: "SHOW MORE",
btnShowLess: "SHOW LESS",
// Customize the text of the button used for showing sensitive/spoiler media content
btnShowContent: "SHOW CONTENT",
// Customize the text of the button pointing to the Mastodon page placed at the end of the timeline // Customize the text of the button pointing to the Mastodon page placed at the end of the timeline
// Leave the value empty to hide it // Leave the value empty to hide it
btnSeeMore: "See more posts at Mastodon", btnSeeMore: "See more posts at Mastodon",
@ -299,11 +311,11 @@ Here you have all the options available to quickly setup and customize your time
btnReload: "Refresh", btnReload: "Refresh",
// Keep searching for the main <div> container before building the timeline. Useful in some cases where extra time is needed to render the page // Keep searching for the main <div> container before building the timeline. Useful in some cases where extra time is needed to render the page
// Default: don't apply // Default: false (don't apply)
insistSearchContainer: false, insistSearchContainer: false,
// Defines the maximum time to continue searching for the main <div> container // Defines the maximum time to continue searching for the main <div> container
// Default: 3 seconds // Default: "3000" (3 seconds)
insistSearchContainerTime: "3000", insistSearchContainerTime: "3000",
``` ```

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -113,7 +113,7 @@
<h1>🐘 Mastodon embed timeline</h1> <h1>🐘 Mastodon embed timeline</h1>
<h2>Local timeline (customized)</h2> <h2>Local timeline (customized)</h2>
<p> <p>
This example shows 10 posts from the following instance: This example shows 42 posts from the following instance:
<br /> <br />
<a <a
href="https://mastodon.social/public/local" href="https://mastodon.social/public/local"
@ -183,6 +183,8 @@
const myTimeline = new MastodonTimeline.Init({ const myTimeline = new MastodonTimeline.Init({
instanceUrl: "https://mastodon.online", instanceUrl: "https://mastodon.online",
defaultTheme: "light", defaultTheme: "light",
maxNbPostFetch: "42",
maxNbPostShow: "42",
dateLocale: "en-CA", dateLocale: "en-CA",
dateOptions: { dateOptions: {
day: "2-digit", day: "2-digit",
@ -219,7 +221,8 @@
const myTimeline = new MastodonTimeline.Init({ const myTimeline = new MastodonTimeline.Init({
instanceUrl: "https://mastodon.online", instanceUrl: "https://mastodon.online",
defaultTheme: "light", defaultTheme: "light",
maxNbPostShow: "10", maxNbPostFetch: "42",
maxNbPostShow: "42",
dateLocale: "en-CA", dateLocale: "en-CA",
dateOptions: { dateOptions: {
day: "2-digit", day: "2-digit",

View File

@ -123,7 +123,8 @@
<h2>Theme API</h2> <h2>Theme API</h2>
<p> <p>
You can change your timeline color calling the function <strong>mtColorTheme()</strong> You can change your timeline color calling the function
<strong>mtColorTheme()</strong>
</p> </p>
<div class="dummy-buttons-container"> <div class="dummy-buttons-container">
<button onclick="myTimeline01.mtColorTheme('dark')"> <button onclick="myTimeline01.mtColorTheme('dark')">

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "@idotj/mastodon-embed-timeline", "name": "@idotj/mastodon-embed-timeline",
"version": "4.3.7", "version": "4.3.10",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@idotj/mastodon-embed-timeline", "name": "@idotj/mastodon-embed-timeline",
"version": "4.3.7", "version": "4.3.10",
"license": "GNU", "license": "GNU",
"devDependencies": { "devDependencies": {
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",

View File

@ -1,6 +1,6 @@
{ {
"name": "@idotj/mastodon-embed-timeline", "name": "@idotj/mastodon-embed-timeline",
"version": "4.3.7", "version": "4.3.10",
"description": "Displays Mastodon timeline with posts embed in your website. Very easy to setup, no dependencies, no trackers, cross-browser, WCAG compliant and fully responsive.", "description": "Displays Mastodon timeline with posts embed in your website. Very easy to setup, no dependencies, no trackers, cross-browser, WCAG compliant and fully responsive.",
"license": "GNU", "license": "GNU",
"author": { "author": {

View File

@ -1,4 +1,4 @@
/* Mastodon embed timeline v4.3.7 */ /* Mastodon embed timeline v4.3.10 */
/* More info at: */ /* More info at: */
/* https://gitlab.com/idotj/mastodon-embed-timeline */ /* https://gitlab.com/idotj/mastodon-embed-timeline */
@ -8,6 +8,7 @@
.mt-container[data-theme="light"], .mt-container[data-theme="light"],
.mt-dialog[data-theme="light"] { .mt-dialog[data-theme="light"] {
--mt-txt-max-lines: none; --mt-txt-max-lines: none;
--mt-preview-max-lines: none;
--mt-color-bg: #fff; --mt-color-bg: #fff;
--mt-color-bg-hover: #d9e1e8; --mt-color-bg-hover: #d9e1e8;
--mt-color-line-gray: #c0cdd9; --mt-color-line-gray: #c0cdd9;
@ -438,6 +439,23 @@ body:has(dialog.mt-dialog[open]) {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
gap: 0.5rem; gap: 0.5rem;
} }
.mt-post-preview-content:has(.mt-post-preview-description.truncate) {
align-self: unset;
}
.mt-post-preview-description {
display: block;
color: var(--mt-color-contrast-gray);
}
.mt-post-preview-description.truncate {
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: var(--mt-preview-max-lines);
-webkit-box-orient: vertical;
}
.mt-post-preview-description:not(.truncate) .ellipsis::after {
content: "...";
}
.mt-post-preview-title { .mt-post-preview-title {
font-weight: 600; font-weight: 600;
} }

View File

@ -1,7 +1,7 @@
/** /**
* Mastodon embed timeline * Mastodon embed timeline
* @author idotj * @author idotj
* @version 4.3.7 * @version 4.3.10
* @url https://gitlab.com/idotj/mastodon-embed-timeline * @url https://gitlab.com/idotj/mastodon-embed-timeline
* @license GNU AGPLv3 * @license GNU AGPLv3
*/ */
@ -31,19 +31,20 @@ export class Init {
hideReplies: false, hideReplies: false,
hidePinnedPosts: false, hidePinnedPosts: false,
hideUserAccount: false, hideUserAccount: false,
txtMaxLines: "",
btnShowMore: "SHOW MORE",
btnShowLess: "SHOW LESS",
markdownBlockquote: false,
hideEmojos: false, hideEmojos: false,
btnShowContent: "SHOW CONTENT",
hideVideoPreview: false, hideVideoPreview: false,
hidePreviewLink: false, hidePreviewLink: false,
previewMaxLines: "",
hideCounterBar: false, hideCounterBar: false,
markdownBlockquote: false,
disableCarousel: false, disableCarousel: false,
carouselCloseTxt: "Close carousel", carouselCloseTxt: "Close carousel",
carouselPrevTxt: "Previous media item", carouselPrevTxt: "Previous media item",
carouselNextTxt: "Next media item", carouselNextTxt: "Next media item",
txtMaxLines: "0",
btnShowMore: "SHOW MORE",
btnShowLess: "SHOW LESS",
btnShowContent: "SHOW CONTENT",
btnSeeMore: "See more posts at Mastodon", btnSeeMore: "See more posts at Mastodon",
btnReload: "Refresh", btnReload: "Refresh",
insistSearchContainer: false, insistSearchContainer: false,
@ -52,6 +53,9 @@ export class Init {
this.mtSettings = { ...this.defaultSettings, ...customSettings }; this.mtSettings = { ...this.defaultSettings, ...customSettings };
this.#checkMaxNbPost();
this.linkHeader = {};
this.mtContainerNode = ""; this.mtContainerNode = "";
this.mtBodyNode = ""; this.mtBodyNode = "";
this.fetchedData = {}; this.fetchedData = {};
@ -61,6 +65,21 @@ export class Init {
}); });
} }
/**
* Verify that the values of posts fetched and showed are consistent
*/
#checkMaxNbPost() {
if (
Number(this.mtSettings.maxNbPostShow) >
Number(this.mtSettings.maxNbPostFetch)
) {
console.error(
`Please check your settings! The maximum number of posts to show is bigger than the maximum number of posts to fetch. Changing the value of "maxNbPostFetch" to: ${this.mtSettings.maxNbPostShow}`
);
this.mtSettings.maxNbPostFetch = this.mtSettings.maxNbPostShow;
}
}
/** /**
* Trigger callback when DOM loaded or completed * Trigger callback when DOM loaded or completed
* @param {function} c Callback executed * @param {function} c Callback executed
@ -146,7 +165,7 @@ export class Init {
/** /**
* Apply the color theme in the timeline * Apply the color theme in the timeline
* @param {string} themeType Type of color theme * @param {string} themeType Type of color theme ('light' or 'dark')
*/ */
mtColorTheme(themeType) { mtColorTheme(themeType) {
this.#onDOMContentLoaded(() => { this.#onDOMContentLoaded(() => {
@ -178,51 +197,18 @@ export class Init {
*/ */
#getTimelineData() { #getTimelineData() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const instanceApiUrl = `${this.mtSettings.instanceUrl}/api/v1/`; const instanceApiUrl = this.mtSettings.instanceUrl
let urls = {}; ? `${this.mtSettings.instanceUrl}/api/v1/`
: this.#showError(
if (this.mtSettings.instanceUrl) { "Please check your <strong>instanceUrl</strong> value",
if (this.mtSettings.timelineType === "profile") {
if (this.mtSettings.userId) {
urls.timeline = `${instanceApiUrl}accounts/${this.mtSettings.userId}/statuses?limit=${this.mtSettings.maxNbPostFetch}`;
if (!this.mtSettings.hidePinnedPosts) {
urls.pinned = `${instanceApiUrl}accounts/${this.mtSettings.userId}/statuses?pinned=true`;
}
} else {
this.#showError(
"Please check your <strong>userId</strong> value",
"⚠️"
);
}
} else if (this.mtSettings.timelineType === "hashtag") {
if (this.mtSettings.hashtagName) {
urls.timeline = `${instanceApiUrl}timelines/tag/${this.mtSettings.hashtagName}?limit=${this.mtSettings.maxNbPostFetch}`;
} else {
this.#showError(
"Please check your <strong>hashtagName</strong> value",
"⚠️"
);
}
} else if (this.mtSettings.timelineType === "local") {
urls.timeline = `${instanceApiUrl}timelines/public?local=true&limit=${this.mtSettings.maxNbPostFetch}`;
} else {
this.#showError(
"Please check your <strong>timelineType</strong> value",
"⚠️" "⚠️"
); );
}
} else { const urls = this.#setUrls(instanceApiUrl);
this.#showError(
"Please check your <strong>instanceUrl</strong> value",
"⚠️"
);
}
if (!this.mtSettings.hideEmojos) {
urls.emojos = `${instanceApiUrl}custom_emojis`;
}
const urlsPromises = Object.entries(urls).map(([key, url]) => { const urlsPromises = Object.entries(urls).map(([key, url]) => {
return this.#fetchData(url) const headers = key === "timeline";
return this.#fetchData(url, headers)
.then((data) => ({ [key]: data })) .then((data) => ({ [key]: data }))
.catch((error) => { .catch((error) => {
reject( reject(
@ -234,35 +220,151 @@ export class Init {
}); });
// Fetch all urls simultaneously // Fetch all urls simultaneously
Promise.all(urlsPromises).then((dataObjects) => { Promise.all(urlsPromises).then(async (dataObjects) => {
this.fetchedData = dataObjects.reduce((result, dataItem) => { this.fetchedData = dataObjects.reduce((result, dataItem) => {
return { ...result, ...dataItem }; return { ...result, ...dataItem };
}, {}); }, {});
// console.log("Mastodon timeline data fetched: ", this.fetchedData); // Merge pinned posts with timeline posts
resolve(); if (
!this.mtSettings.hidePinnedPosts &&
this.fetchedData.pinned?.length !== undefined &&
this.fetchedData.pinned.length !== 0
) {
const pinnedPosts = this.fetchedData.pinned.map((obj) => ({
...obj,
pinned: true,
}));
this.fetchedData.timeline = [
...pinnedPosts,
...this.fetchedData.timeline,
];
}
// Fetch more posts if maxNbPostFetch is not reached
if (this.#isNbPostsFulfilled()) {
resolve();
} else {
do {
await this.#fetchMorePosts();
} while (!this.#isNbPostsFulfilled() && this.linkHeader.next);
resolve();
}
}); });
}); });
} }
/**
* Set all urls before fetching the data
* @param {string} Instance url api
* @returns {object}
*/
#setUrls(i) {
let urls = {};
if (this.mtSettings.timelineType === "profile") {
if (this.mtSettings.userId) {
urls.timeline = `${i}accounts/${this.mtSettings.userId}/statuses?limit=${this.mtSettings.maxNbPostFetch}`;
if (!this.mtSettings.hidePinnedPosts) {
urls.pinned = `${i}accounts/${this.mtSettings.userId}/statuses?pinned=true`;
}
} else {
this.#showError(
"Please check your <strong>userId</strong> value",
"⚠️"
);
}
} else if (this.mtSettings.timelineType === "hashtag") {
if (this.mtSettings.hashtagName) {
urls.timeline = `${i}timelines/tag/${this.mtSettings.hashtagName}?limit=${this.mtSettings.maxNbPostFetch}`;
} else {
this.#showError(
"Please check your <strong>hashtagName</strong> value",
"⚠️"
);
}
} else if (this.mtSettings.timelineType === "local") {
urls.timeline = `${i}timelines/public?local=true&limit=${this.mtSettings.maxNbPostFetch}`;
} else {
this.#showError(
"Please check your <strong>timelineType</strong> value",
"⚠️"
);
}
if (!this.mtSettings.hideEmojos) {
urls.emojos = `${i}custom_emojis`;
}
return urls;
}
/** /**
* Fetch data from server * Fetch data from server
* @param {string} url address to fetch * @param {string} u Url address to fetch
* @param {boolean} h gets the link header
* @returns {array} List of objects * @returns {array} List of objects
*/ */
async #fetchData(url) { async #fetchData(u, h = false) {
const response = await fetch(url); const response = await fetch(u);
if (!response.ok) { if (!response.ok) {
throw new Error(` throw new Error(`
Failed to fetch the following Url:<br/>${url}<hr>Error status: ${response.status}<hr>Error message: ${response.statusText} Failed to fetch the following Url:<br />${u}<hr />Error status: ${response.status}<hr />Error message: ${response.statusText}
`); `);
} }
const data = await response.json(); const data = await response.json();
// Get Link headers for pagination
if (h && response.headers.get("Link")) {
this.linkHeader = this.#parseLinkHeader(response.headers.get("Link"));
}
return data; return data;
} }
/**
* Check if there are enough posts to reach the value of maxNbPostFetch
* @returns {boolean}
*/
#isNbPostsFulfilled() {
return (
this.fetchedData.timeline.length >= Number(this.mtSettings.maxNbPostFetch)
);
}
/**
* Fetch extra posts
*/
#fetchMorePosts() {
return new Promise((resolve) => {
if (this.linkHeader.next) {
this.#fetchData(this.linkHeader.next, true).then((data) => {
this.fetchedData.timeline = [...this.fetchedData.timeline, ...data];
resolve();
});
} else {
resolve();
}
});
}
/**
* Parse link header into an object
* @param {string} l Link header
* @returns {object}
*/
#parseLinkHeader(l) {
const linkArray = l.split(", ").map((header) => header.split("; "));
const linkMap = linkArray.map((header) => {
const linkRel = header[1].replace(/"/g, "").replace("rel=", "");
const linkURL = header[0].slice(1, -1);
return [linkRel, linkURL];
});
return Object.fromEntries(linkMap);
}
/** /**
* Filter all fetched posts and append them on the timeline * Filter all fetched posts and append them on the timeline
* @param {string} t Type of build (new or reload) * @param {string} t Type of build (new or reload)
@ -270,67 +372,87 @@ export class Init {
async #buildTimeline(t) { async #buildTimeline(t) {
await this.#getTimelineData(); await this.#getTimelineData();
// Merge pinned posts with timeline posts // console.log("Mastodon timeline data fetched: ", this.fetchedData);
let posts;
if ( const posts = this.fetchedData.timeline;
!this.mtSettings.hidePinnedPosts && let nbPostToShow = 0;
this.fetchedData.pinned?.length !== undefined &&
this.fetchedData.pinned.length !== 0
) {
const pinnedPosts = this.fetchedData.pinned.map((obj) => ({
...obj,
pinned: true,
}));
posts = [...pinnedPosts, ...this.fetchedData.timeline];
} else {
posts = this.fetchedData.timeline;
}
// Empty container body
this.mtBodyNode.replaceChildren(); this.mtBodyNode.replaceChildren();
// Set posts counter to 0 posts.forEach((post) => {
let nbPostShow = 0; const isPublicOrUnlisted =
post.visibility === "public" ||
(!this.mtSettings.hideUnlisted && post.visibility === "unlisted");
const shouldHideReblog = this.mtSettings.hideReblog && post.reblog;
const shouldHideReplies =
this.mtSettings.hideReplies && post.in_reply_to_id;
for (let i in posts) { // Filter by (Public / Unlisted)
// First filter (Public / Unlisted) if (isPublicOrUnlisted && !shouldHideReblog && !shouldHideReplies) {
if ( if (nbPostToShow < this.mtSettings.maxNbPostShow) {
posts[i].visibility == "public" || this.#appendPost(post, nbPostToShow);
(!this.mtSettings.hideUnlisted && posts[i].visibility == "unlisted") nbPostToShow++;
) {
// Second filter (Reblog / Replies)
if (
(this.mtSettings.hideReblog && posts[i].reblog) ||
(this.mtSettings.hideReplies && posts[i].in_reply_to_id)
) {
// Nothing here (Don't append posts)
} else { } else {
if (nbPostShow < this.mtSettings.maxNbPostShow) { // Reached the limit of maximum number of posts to show
this.#appendPost(posts[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 // Check if there are posts to display or not
if (this.mtBodyNode.innerHTML === "") { if (this.mtBodyNode.innerHTML !== "") {
const errorMessage = `No posts to show<hr/>${
posts?.length || 0
} posts have been fetched from the server<hr/>This may be due to an incorrect configuration with the parameters or with the filters applied (to hide certains type of posts)`;
this.#showError(errorMessage, "📭");
} else {
if (t === "newTimeline") { if (t === "newTimeline") {
this.#manageSpinner(); this.#manageSpinner();
this.#setPostsInteracion(); this.#setCSSvariables();
this.#buildFooter(); this.#addAriaSetsize(nbPostToShow);
this.#addPostListener();
if (this.mtSettings.btnSeeMore || this.mtSettings.btnReload)
this.#buildFooter();
} else if (t === "updateTimeline") { } else if (t === "updateTimeline") {
this.#manageSpinner(); this.#manageSpinner();
} else { } else {
this.#showError("The function buildTimeline() was expecting a param"); this.#showError("The function buildTimeline() was expecting a param");
} }
} else {
const errorMessage = `No posts to show<hr />${
posts?.length || 0
} posts have been fetched from the server<hr />This may be due to an incorrect configuration with the parameters or with the filters applied (to hide certains type of posts)`;
this.#showError(errorMessage, "📭");
}
}
/**
* Establishes the defined CSS variables
*/
#setCSSvariables() {
if (
this.mtSettings.txtMaxLines !== "0" &&
this.mtSettings.txtMaxLines.length !== 0
) {
this.mtBodyNode.parentNode.style.setProperty(
"--mt-txt-max-lines",
this.mtSettings.txtMaxLines
);
}
if (
this.mtSettings.previewMaxLines !== "0" &&
this.mtSettings.previewMaxLines.length !== 0
) {
this.mtBodyNode.parentNode.style.setProperty(
"--mt-preview-max-lines",
this.mtSettings.previewMaxLines
);
}
}
/**
* Add the attribute Aria-setsize to all posts
* @param {number} n The total number of posts showed in the timeline
*/
#addAriaSetsize(n) {
const articles = this.mtBodyNode.getElementsByTagName("article");
for (let i = 0; i < n; i++) {
articles[i].setAttribute("aria-setsize", n);
} }
} }
@ -402,7 +524,7 @@ export class Init {
if (!this.mtSettings.hideUserAccount) { if (!this.mtSettings.hideUserAccount) {
accountName = accountName =
'<br/><span class="mt-post-header-user-account">@' + '<br /><span class="mt-post-header-user-account">@' +
c.reblog.account.username + c.reblog.account.username +
"@" + "@" +
new URL(c.reblog.account.url).hostname + new URL(c.reblog.account.url).hostname +
@ -464,7 +586,7 @@ export class Init {
if (!this.mtSettings.hideUserAccount) { if (!this.mtSettings.hideUserAccount) {
accountName = accountName =
'<br/><span class="mt-post-header-user-account">@' + '<br /><span class="mt-post-header-user-account">@' +
c.account.username + c.account.username +
"@" + "@" +
new URL(c.account.url).hostname + new URL(c.account.url).hostname +
@ -511,64 +633,60 @@ export class Init {
</div>`; </div>`;
// Main text // 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 = ""; let content = "";
if (c.spoiler_text !== "") { if (this.mtSettings.txtMaxLines !== "0") {
content = const txtCss =
'<div class="mt-post-txt">' + this.mtSettings.txtMaxLines.length !== 0 ? " truncate" : "";
c.spoiler_text +
' <button type="button" class="mt-btn-dark mt-btn-spoiler" aria-expanded="false">' + if (c.spoiler_text !== "") {
this.mtSettings.btnShowMore + content =
"</button>" + '<div class="mt-post-txt">' +
'<div class="spoiler-txt-hidden">' + c.spoiler_text +
this.#formatPostText(c.content) + ' <button type="button" class="mt-btn-dark mt-btn-spoiler" aria-expanded="false">' +
"</div>" + this.mtSettings.btnShowMore +
"</div>"; "</button>" +
} else if ( '<div class="spoiler-txt-hidden">' +
c.reblog && this.#formatPostText(c.content) +
c.reblog.content !== "" && "</div>" +
c.reblog.spoiler_text !== "" "</div>";
) { } else if (
content = c.reblog &&
'<div class="mt-post-txt">' + c.reblog.content !== "" &&
c.reblog.spoiler_text + c.reblog.spoiler_text !== ""
' <button type="button" class="mt-btn-dark mt-btn-spoiler" aria-expanded="false">' + ) {
this.mtSettings.btnShowMore + content =
"</button>" + '<div class="mt-post-txt">' +
'<div class="spoiler-txt-hidden">' + c.reblog.spoiler_text +
this.#formatPostText(c.reblog.content) + ' <button type="button" class="mt-btn-dark mt-btn-spoiler" aria-expanded="false">' +
"</div>" + this.mtSettings.btnShowMore +
"</div>"; "</button>" +
} else if ( '<div class="spoiler-txt-hidden">' +
c.reblog && this.#formatPostText(c.reblog.content) +
c.reblog.content !== "" && "</div>" +
c.reblog.spoiler_text === "" "</div>";
) { } else if (
content = c.reblog &&
'<div class="mt-post-txt' + c.reblog.content !== "" &&
txtCss + c.reblog.spoiler_text === ""
'">' + ) {
'<div class="mt-post-txt-wrapper">' + content =
this.#formatPostText(c.reblog.content) + '<div class="mt-post-txt' +
"</div>" + txtCss +
"</div>"; '">' +
} else { '<div class="mt-post-txt-wrapper">' +
content = this.#formatPostText(c.reblog.content) +
'<div class="mt-post-txt' + "</div>" +
txtCss + "</div>";
'">' + } else {
'<div class="mt-post-txt-wrapper">' + content =
this.#formatPostText(c.content) + '<div class="mt-post-txt' +
"</div>" + txtCss +
"</div>"; '">' +
'<div class="mt-post-txt-wrapper">' +
this.#formatPostText(c.content) +
"</div>" +
"</div>";
}
} }
// Media attachments // Media attachments
@ -643,8 +761,6 @@ export class Init {
const post = const post =
'<article class="mt-post" aria-posinset="' + '<article class="mt-post" aria-posinset="' +
(i + 1) + (i + 1) +
'" aria-setsize="' +
this.mtSettings.maxNbPostFetch +
'" data-location="' + '" data-location="' +
url + url +
'" tabindex="0">' + '" tabindex="0">' +
@ -795,8 +911,8 @@ export class Init {
* @param {boolean} s Sensitive/spoiler status * @param {boolean} s Sensitive/spoiler status
* @returns {string} Media in HTML format * @returns {string} Media in HTML format
*/ */
#createMedia(m, s) { #createMedia(m, s = false) {
const spoiler = s || false; const spoiler = s;
const type = m.type; const type = m.type;
let media = ""; let media = "";
@ -806,7 +922,7 @@ export class Init {
(spoiler ? "mt-post-media-spoiler " : "") + (spoiler ? "mt-post-media-spoiler " : "") +
this.mtSettings.spinnerClass + this.mtSettings.spinnerClass +
'" data-media-type="' + '" data-media-type="' +
m.type + type +
'" data-media-url-hd="' + '" data-media-url-hd="' +
m.url + m.url +
'" data-media-alt-txt="' + '" data-media-alt-txt="' +
@ -838,7 +954,7 @@ export class Init {
(spoiler ? "mt-post-media-spoiler " : "") + (spoiler ? "mt-post-media-spoiler " : "") +
this.mtSettings.spinnerClass + this.mtSettings.spinnerClass +
'" data-media-type="' + '" data-media-type="' +
m.type + type +
'" data-media-url-hd="' + '" data-media-url-hd="' +
m.preview_url + m.preview_url +
'" data-media-alt-txt="' + '" data-media-alt-txt="' +
@ -869,7 +985,7 @@ export class Init {
'<div class="mt-post-media ' + '<div class="mt-post-media ' +
(spoiler ? "mt-post-media-spoiler " : "") + (spoiler ? "mt-post-media-spoiler " : "") +
'" data-media-type="' + '" data-media-type="' +
m.type + type +
'">' + '">' +
(spoiler (spoiler
? '<button class="mt-btn-dark mt-btn-spoiler">' + ? '<button class="mt-btn-dark mt-btn-spoiler">' +
@ -890,7 +1006,7 @@ export class Init {
(spoiler ? "mt-post-media-spoiler " : "") + (spoiler ? "mt-post-media-spoiler " : "") +
this.mtSettings.spinnerClass + this.mtSettings.spinnerClass +
'" data-media-type="' + '" data-media-type="' +
m.type + type +
'" data-media-url-hd="' + '" data-media-url-hd="' +
m.url + m.url +
'" data-media-alt-txt="' + '" data-media-alt-txt="' +
@ -919,7 +1035,7 @@ export class Init {
'<div class="mt-post-media ' + '<div class="mt-post-media ' +
(spoiler ? "mt-post-media-spoiler " : "") + (spoiler ? "mt-post-media-spoiler " : "") +
'" data-media-type="' + '" data-media-type="' +
m.type + type +
'" data-media-url-hd="' + '" data-media-url-hd="' +
m.url + m.url +
'" data-media-alt-txt="' + '" data-media-alt-txt="' +
@ -968,7 +1084,7 @@ export class Init {
* Build a carousel/lightbox with the media content in the post clicked * Build a carousel/lightbox with the media content in the post clicked
* @param {event} e User interaction trigger * @param {event} e User interaction trigger
*/ */
#buildCarousel(e) { #showCarousel(e) {
// List all medias in the post and remove sensitive/spoiler medias // List all medias in the post and remove sensitive/spoiler medias
const mediaSiblings = Array.from( const mediaSiblings = Array.from(
e.target.parentNode.parentNode.children e.target.parentNode.parentNode.children
@ -1214,6 +1330,19 @@ export class Init {
* @returns {string} Preview link in HTML format * @returns {string} Preview link in HTML format
*/ */
#createPreviewLink(c) { #createPreviewLink(c) {
let previewDescription = "";
if (this.mtSettings.previewMaxLines !== "0" && c.description) {
const txtCss =
this.mtSettings.previewMaxLines.length !== 0 ? " truncate" : "";
previewDescription =
'<span class="mt-post-preview-description' +
txtCss +
'">' +
this.#parseHTMLstring(c.description) +
"</span>";
}
const card = const card =
'<a href="' + '<a href="' +
c.url + c.url +
@ -1237,6 +1366,7 @@ export class Init {
'<span class="mt-post-preview-title">' + '<span class="mt-post-preview-title">' +
c.title + c.title +
"</span>" + "</span>" +
previewDescription +
(c.author_name (c.author_name
? '<span class="mt-post-preview-author">' + ? '<span class="mt-post-preview-author">' +
this.#parseHTMLstring(c.author_name) + this.#parseHTMLstring(c.author_name) +
@ -1263,69 +1393,64 @@ export class Init {
* Build footer after last post * Build footer after last post
*/ */
#buildFooter() { #buildFooter() {
if (this.mtSettings.btnSeeMore || this.mtSettings.btnReload) { let btnSeeMoreHTML = "";
// Add footer container let btnReloadHTML = "";
this.mtBodyNode.parentNode.insertAdjacentHTML(
"beforeend",
'<div class="mt-footer"></div>'
);
const containerFooter = // Create button to open Mastodon page
this.mtContainerNode.getElementsByClassName("mt-footer")[0]; if (this.mtSettings.btnSeeMore) {
let btnSeeMorePath = "";
// Create button to open Mastodon page if (this.mtSettings.timelineType === "profile") {
if (this.mtSettings.btnSeeMore) { if (this.mtSettings.profileName) {
let btnSeeMorePath = ""; btnSeeMorePath = this.mtSettings.profileName;
if (this.mtSettings.timelineType === "profile") { } else {
if (this.mtSettings.profileName) { this.#showError(
btnSeeMorePath = this.mtSettings.profileName; "Please check your <strong>profileName</strong> value",
} else { "⚠️"
this.#showError( );
"Please check your <strong>profileName</strong> value",
"⚠️"
);
}
} else if (this.mtSettings.timelineType === "hashtag") {
btnSeeMorePath = "tags/" + this.mtSettings.hashtagName;
} else if (this.mtSettings.timelineType === "local") {
btnSeeMorePath = "public/local";
} }
const btnSeeMoreHTML = ` } else if (this.mtSettings.timelineType === "hashtag") {
btnSeeMorePath = "tags/" + this.mtSettings.hashtagName;
} else if (this.mtSettings.timelineType === "local") {
btnSeeMorePath = "public/local";
}
btnSeeMoreHTML = `
<a class="mt-btn-violet btn-see-more" href="${ <a class="mt-btn-violet btn-see-more" href="${
this.mtSettings.instanceUrl this.mtSettings.instanceUrl
}/${this.#escapeHTML( }/${this.#escapeHTML(
btnSeeMorePath btnSeeMorePath
)}" rel="nofollow noopener noreferrer" target="_blank"> )}" rel="nofollow noopener noreferrer" target="_blank">
${this.mtSettings.btnSeeMore} ${this.mtSettings.btnSeeMore}
</a>`; </a>`;
}
containerFooter.insertAdjacentHTML("beforeend", btnSeeMoreHTML); // Create button to refresh the timeline
} if (this.mtSettings.btnReload) {
btnReloadHTML = `
// Create button to refresh the timeline
if (this.mtSettings.btnReload) {
const btnReloadHTML = `
<button class="mt-btn-violet btn-refresh"> <button class="mt-btn-violet btn-refresh">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M21 3v5m0 0h-5m5 0l-3-2.708C16.408 3.867 14.305 3 12 3a9 9 0 1 0 0 18c4.283 0 7.868-2.992 8.777-7" stroke="var(--mt-color-btn-txt)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M21 3v5m0 0h-5m5 0l-3-2.708C16.408 3.867 14.305 3 12 3a9 9 0 1 0 0 18c4.283 0 7.868-2.992 8.777-7" stroke="var(--mt-color-btn-txt)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>
${this.mtSettings.btnReload} ${this.mtSettings.btnReload}
</button>`; </button>`;
containerFooter.insertAdjacentHTML("beforeend", btnReloadHTML); // Add footer container
this.mtBodyNode.parentNode.insertAdjacentHTML(
"beforeend",
'<div class="mt-footer">' + btnSeeMoreHTML + btnReloadHTML + "</div>"
);
const reloadBtn = // Add event listener to the button "Refresh"
this.mtContainerNode.getElementsByClassName("btn-refresh")[0]; const reloadBtn =
reloadBtn.addEventListener("click", () => { this.mtContainerNode.getElementsByClassName("btn-refresh")[0];
this.mtUpdate(); reloadBtn.addEventListener("click", () => {
}); this.mtUpdate();
} });
} }
} }
/** /**
* Add EventListeners for timeline interactions and trigger functions * Add EventListeners for timeline interactions and trigger functions
*/ */
#setPostsInteracion() { #addPostListener() {
this.mtBodyNode.addEventListener("click", (e) => { this.mtBodyNode.addEventListener("click", (e) => {
const target = e.target; const target = e.target;
const localName = target.localName; const localName = target.localName;
@ -1335,10 +1460,8 @@ export class Init {
if ( if (
localName == "article" || localName == "article" ||
target.offsetParent?.localName == "article" || target.offsetParent?.localName == "article" ||
(localName == "img" && (this.mtSettings.disableCarousel &&
this.mtSettings.disableCarousel && parentNode.getAttribute("data-media-type") === "image")
parentNode.getAttribute("data-media-type") !== "video" &&
parentNode.getAttribute("data-media-type") !== "gifv")
) { ) {
this.#openPostUrl(e); this.#openPostUrl(e);
} }
@ -1355,11 +1478,10 @@ export class Init {
if ( if (
!this.mtSettings.disableCarousel && !this.mtSettings.disableCarousel &&
localName == "img" && localName == "img" &&
!parentNode.classList.contains("mt-post-preview-image") && (parentNode.getAttribute("data-media-type") === "image" ||
parentNode.getAttribute("data-media-type") !== "video" && parentNode.getAttribute("data-media-type") === "audio")
parentNode.getAttribute("data-media-type") !== "gifv"
) { ) {
this.#buildCarousel(e); this.#showCarousel(e);
} }
// Check if video preview image or play icon/button was clicked // Check if video preview image or play icon/button was clicked
@ -1400,6 +1522,7 @@ export class Init {
e.target.className !== "mt-post-preview-noImage" && e.target.className !== "mt-post-preview-noImage" &&
e.target.parentNode.className !== "mt-post-avatar-image-big" && e.target.parentNode.className !== "mt-post-avatar-image-big" &&
e.target.parentNode.className !== "mt-post-avatar-image-small" && e.target.parentNode.className !== "mt-post-avatar-image-small" &&
e.target.parentNode.className !== "mt-post-header-user-name" &&
e.target.parentNode.className !== "mt-post-preview-image" && e.target.parentNode.className !== "mt-post-preview-image" &&
e.target.parentNode.className !== "mt-post-preview" && e.target.parentNode.className !== "mt-post-preview" &&
urlPost urlPost