Merge branch 'feature/posts-limit' into 'master'

Feature/posts limit

See merge request idotj/mastodon-embed-timeline!34
This commit is contained in:
i.j 2024-03-21 09:27:16 +00:00
commit 87c9d5b9fb
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
- Display medias inside post using CSS grid
- Add a placeholder bg-color for images
@ -168,7 +176,7 @@ v3.1.1 - 28/01/2023
v3.1.0 - 21/01/2023
- 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
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:
```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
<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
@ -182,48 +182,54 @@ Here you have all the options available to quickly setup and customize your time
```js
// Id of the <div> containing the timeline
// Default: "mt-container"
mtContainerId: "mt-container",
// Mastodon instance Url including https://
// Default: "https://mastodon.social"
instanceUrl: "https://mastodon.social",
// Choose type of posts to show in the timeline: 'local', 'profile', 'hashtag'
// Default: local
// Default: "local"
timelineType: "local",
// Your user ID number on Mastodon instance
// Leave it empty if you didn't choose 'profile' as type of timeline
// Default: ""
userId: "",
// 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
// Default: ""
profileName: "",
// The name of the hashtag (not including the # symbol)
// Leave it empty if you didn't choose 'hashtag' as type of timeline
// Default: ""
hashtagName: "",
// Class name for the loading spinner (also used in CSS file)
// Default: "mt-loading-spinner"
spinnerClass: "mt-loading-spinner",
// Preferred color theme: 'light', 'dark' or 'auto'
// Default: auto
// Preferred color theme: "light", "dark" or "auto"
// Default: "auto"
defaultTheme: "auto",
// Maximum number of posts to request to the server
// Default: 20
// Default: "20"
maxNbPostFetch: "20",
// Maximum number of posts to show in the timeline
// Default: 20
// Default: "20"
maxNbPostShow: "20",
// 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",
// Customize the date format using the options
// Default: DD MMM YYYY
// Customize the date format using the options for day, month and year
// Default: day: "2-digit", month: "short", year: "numeric" (DD MMM YYYY)
dateOptions: {
day: "2-digit",
month: "short",
@ -231,47 +237,64 @@ Here you have all the options available to quickly setup and customize your time
},
// Hide unlisted posts
// Default: don't hide
// Default: false (don't hide)
hideUnlisted: false,
// Hide boosted posts
// Default: don't hide
// Default: false (don't hide)
hideReblog: false,
// Hide replies posts
// Default: don't hide
// Default: false (don't hide)
hideReplies: false,
// Hide pinned posts from the profile timeline
// Default: don't hide
// Default: false (don't hide)
hidePinnedPosts: false,
// Hide the user account under the user name
// Default: don't hide
// Default: false (don't hide)
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
// Default: don't hide
// Default: false (don't hide)
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
// Default: don't hide
// Default: false (don't hide)
hideVideoPreview: false,
// 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,
// 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
// Default: don't hide
// Default: false (don't hide)
hideCounterBar: false,
// Converts Markdown symbol ">" at the beginning of a paragraph into a blockquote HTML tag
// Default: don't apply
markdownBlockquote: false,
// Show a carousel/lightbox when the user clicks on a picture in a post
// Default: not disabled
// Disable a carousel/lightbox when the user clicks on a picture in a post
// Default: false (not disabled)
disableCarousel: false,
// 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",
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
// Leave the value empty to hide it
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",
// 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,
// Defines the maximum time to continue searching for the main <div> container
// Default: 3 seconds
// Default: "3000" (3 seconds)
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>
<h2>Local timeline (customized)</h2>
<p>
This example shows 10 posts from the following instance:
This example shows 42 posts from the following instance:
<br />
<a
href="https://mastodon.social/public/local"
@ -183,6 +183,8 @@
const myTimeline = new MastodonTimeline.Init({
instanceUrl: "https://mastodon.online",
defaultTheme: "light",
maxNbPostFetch: "42",
maxNbPostShow: "42",
dateLocale: "en-CA",
dateOptions: {
day: "2-digit",
@ -219,7 +221,8 @@
const myTimeline = new MastodonTimeline.Init({
instanceUrl: "https://mastodon.online",
defaultTheme: "light",
maxNbPostShow: "10",
maxNbPostFetch: "42",
maxNbPostShow: "42",
dateLocale: "en-CA",
dateOptions: {
day: "2-digit",

View File

@ -123,7 +123,8 @@
<h2>Theme API</h2>
<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>
<div class="dummy-buttons-container">
<button onclick="myTimeline01.mtColorTheme('dark')">

4
package-lock.json generated
View File

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

View File

@ -1,6 +1,6 @@
{
"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.",
"license": "GNU",
"author": {

View File

@ -1,4 +1,4 @@
/* Mastodon embed timeline v4.3.7 */
/* Mastodon embed timeline v4.3.10 */
/* More info at: */
/* https://gitlab.com/idotj/mastodon-embed-timeline */
@ -8,6 +8,7 @@
.mt-container[data-theme="light"],
.mt-dialog[data-theme="light"] {
--mt-txt-max-lines: none;
--mt-preview-max-lines: none;
--mt-color-bg: #fff;
--mt-color-bg-hover: #d9e1e8;
--mt-color-line-gray: #c0cdd9;
@ -438,6 +439,23 @@ body:has(dialog.mt-dialog[open]) {
padding: 0.5rem 1rem;
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 {
font-weight: 600;
}

View File

@ -1,7 +1,7 @@
/**
* Mastodon embed timeline
* @author idotj
* @version 4.3.7
* @version 4.3.10
* @url https://gitlab.com/idotj/mastodon-embed-timeline
* @license GNU AGPLv3
*/
@ -31,19 +31,20 @@ export class Init {
hideReplies: false,
hidePinnedPosts: false,
hideUserAccount: false,
txtMaxLines: "",
btnShowMore: "SHOW MORE",
btnShowLess: "SHOW LESS",
markdownBlockquote: false,
hideEmojos: false,
btnShowContent: "SHOW CONTENT",
hideVideoPreview: false,
hidePreviewLink: false,
previewMaxLines: "",
hideCounterBar: false,
markdownBlockquote: false,
disableCarousel: false,
carouselCloseTxt: "Close carousel",
carouselPrevTxt: "Previous media item",
carouselNextTxt: "Next media item",
txtMaxLines: "0",
btnShowMore: "SHOW MORE",
btnShowLess: "SHOW LESS",
btnShowContent: "SHOW CONTENT",
btnSeeMore: "See more posts at Mastodon",
btnReload: "Refresh",
insistSearchContainer: false,
@ -52,6 +53,9 @@ export class Init {
this.mtSettings = { ...this.defaultSettings, ...customSettings };
this.#checkMaxNbPost();
this.linkHeader = {};
this.mtContainerNode = "";
this.mtBodyNode = "";
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
* @param {function} c Callback executed
@ -146,7 +165,7 @@ export class Init {
/**
* 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) {
this.#onDOMContentLoaded(() => {
@ -178,51 +197,18 @@ export class Init {
*/
#getTimelineData() {
return new Promise((resolve, reject) => {
const instanceApiUrl = `${this.mtSettings.instanceUrl}/api/v1/`;
let urls = {};
if (this.mtSettings.instanceUrl) {
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",
const instanceApiUrl = this.mtSettings.instanceUrl
? `${this.mtSettings.instanceUrl}/api/v1/`
: this.#showError(
"Please check your <strong>instanceUrl</strong> value",
"⚠️"
);
}
} else {
this.#showError(
"Please check your <strong>instanceUrl</strong> value",
"⚠️"
);
}
if (!this.mtSettings.hideEmojos) {
urls.emojos = `${instanceApiUrl}custom_emojis`;
}
const urls = this.#setUrls(instanceApiUrl);
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 }))
.catch((error) => {
reject(
@ -234,35 +220,151 @@ export class Init {
});
// Fetch all urls simultaneously
Promise.all(urlsPromises).then((dataObjects) => {
Promise.all(urlsPromises).then(async (dataObjects) => {
this.fetchedData = dataObjects.reduce((result, dataItem) => {
return { ...result, ...dataItem };
}, {});
// console.log("Mastodon timeline data fetched: ", this.fetchedData);
resolve();
// Merge pinned posts with timeline posts
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
* @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
*/
async #fetchData(url) {
const response = await fetch(url);
async #fetchData(u, h = false) {
const response = await fetch(u);
if (!response.ok) {
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();
// Get Link headers for pagination
if (h && response.headers.get("Link")) {
this.linkHeader = this.#parseLinkHeader(response.headers.get("Link"));
}
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
* @param {string} t Type of build (new or reload)
@ -270,67 +372,87 @@ export class Init {
async #buildTimeline(t) {
await this.#getTimelineData();
// Merge pinned posts with timeline posts
let posts;
if (
!this.mtSettings.hidePinnedPosts &&
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;
}
// console.log("Mastodon timeline data fetched: ", this.fetchedData);
const posts = this.fetchedData.timeline;
let nbPostToShow = 0;
// Empty container body
this.mtBodyNode.replaceChildren();
// Set posts counter to 0
let nbPostShow = 0;
posts.forEach((post) => {
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) {
// First filter (Public / Unlisted)
if (
posts[i].visibility == "public" ||
(!this.mtSettings.hideUnlisted && posts[i].visibility == "unlisted")
) {
// 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)
// Filter by (Public / Unlisted)
if (isPublicOrUnlisted && !shouldHideReblog && !shouldHideReplies) {
if (nbPostToShow < this.mtSettings.maxNbPostShow) {
this.#appendPost(post, nbPostToShow);
nbPostToShow++;
} else {
if (nbPostShow < this.mtSettings.maxNbPostShow) {
this.#appendPost(posts[i], Number(i));
nbPostShow++;
} else {
// Nothing here (Reached the limit of maximum number of posts to show)
}
// 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<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 {
// Check if there are posts to display or not
if (this.mtBodyNode.innerHTML !== "") {
if (t === "newTimeline") {
this.#manageSpinner();
this.#setPostsInteracion();
this.#buildFooter();
this.#setCSSvariables();
this.#addAriaSetsize(nbPostToShow);
this.#addPostListener();
if (this.mtSettings.btnSeeMore || this.mtSettings.btnReload)
this.#buildFooter();
} else if (t === "updateTimeline") {
this.#manageSpinner();
} else {
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) {
accountName =
'<br/><span class="mt-post-header-user-account">@' +
'<br /><span class="mt-post-header-user-account">@' +
c.reblog.account.username +
"@" +
new URL(c.reblog.account.url).hostname +
@ -464,7 +586,7 @@ export class Init {
if (!this.mtSettings.hideUserAccount) {
accountName =
'<br/><span class="mt-post-header-user-account">@' +
'<br /><span class="mt-post-header-user-account">@' +
c.account.username +
"@" +
new URL(c.account.url).hostname +
@ -511,64 +633,60 @@ export class Init {
</div>`;
// 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 =
'<div class="mt-post-txt">' +
c.spoiler_text +
' <button type="button" class="mt-btn-dark mt-btn-spoiler" aria-expanded="false">' +
this.mtSettings.btnShowMore +
"</button>" +
'<div class="spoiler-txt-hidden">' +
this.#formatPostText(c.content) +
"</div>" +
"</div>";
} else if (
c.reblog &&
c.reblog.content !== "" &&
c.reblog.spoiler_text !== ""
) {
content =
'<div class="mt-post-txt">' +
c.reblog.spoiler_text +
' <button type="button" class="mt-btn-dark mt-btn-spoiler" aria-expanded="false">' +
this.mtSettings.btnShowMore +
"</button>" +
'<div class="spoiler-txt-hidden">' +
this.#formatPostText(c.reblog.content) +
"</div>" +
"</div>";
} else if (
c.reblog &&
c.reblog.content !== "" &&
c.reblog.spoiler_text === ""
) {
content =
'<div class="mt-post-txt' +
txtCss +
'">' +
'<div class="mt-post-txt-wrapper">' +
this.#formatPostText(c.reblog.content) +
"</div>" +
"</div>";
} else {
content =
'<div class="mt-post-txt' +
txtCss +
'">' +
'<div class="mt-post-txt-wrapper">' +
this.#formatPostText(c.content) +
"</div>" +
"</div>";
if (this.mtSettings.txtMaxLines !== "0") {
const txtCss =
this.mtSettings.txtMaxLines.length !== 0 ? " truncate" : "";
if (c.spoiler_text !== "") {
content =
'<div class="mt-post-txt">' +
c.spoiler_text +
' <button type="button" class="mt-btn-dark mt-btn-spoiler" aria-expanded="false">' +
this.mtSettings.btnShowMore +
"</button>" +
'<div class="spoiler-txt-hidden">' +
this.#formatPostText(c.content) +
"</div>" +
"</div>";
} else if (
c.reblog &&
c.reblog.content !== "" &&
c.reblog.spoiler_text !== ""
) {
content =
'<div class="mt-post-txt">' +
c.reblog.spoiler_text +
' <button type="button" class="mt-btn-dark mt-btn-spoiler" aria-expanded="false">' +
this.mtSettings.btnShowMore +
"</button>" +
'<div class="spoiler-txt-hidden">' +
this.#formatPostText(c.reblog.content) +
"</div>" +
"</div>";
} else if (
c.reblog &&
c.reblog.content !== "" &&
c.reblog.spoiler_text === ""
) {
content =
'<div class="mt-post-txt' +
txtCss +
'">' +
'<div class="mt-post-txt-wrapper">' +
this.#formatPostText(c.reblog.content) +
"</div>" +
"</div>";
} else {
content =
'<div class="mt-post-txt' +
txtCss +
'">' +
'<div class="mt-post-txt-wrapper">' +
this.#formatPostText(c.content) +
"</div>" +
"</div>";
}
}
// Media attachments
@ -643,8 +761,6 @@ export class Init {
const post =
'<article class="mt-post" aria-posinset="' +
(i + 1) +
'" aria-setsize="' +
this.mtSettings.maxNbPostFetch +
'" data-location="' +
url +
'" tabindex="0">' +
@ -795,8 +911,8 @@ export class Init {
* @param {boolean} s Sensitive/spoiler status
* @returns {string} Media in HTML format
*/
#createMedia(m, s) {
const spoiler = s || false;
#createMedia(m, s = false) {
const spoiler = s;
const type = m.type;
let media = "";
@ -806,7 +922,7 @@ export class Init {
(spoiler ? "mt-post-media-spoiler " : "") +
this.mtSettings.spinnerClass +
'" data-media-type="' +
m.type +
type +
'" data-media-url-hd="' +
m.url +
'" data-media-alt-txt="' +
@ -838,7 +954,7 @@ export class Init {
(spoiler ? "mt-post-media-spoiler " : "") +
this.mtSettings.spinnerClass +
'" data-media-type="' +
m.type +
type +
'" data-media-url-hd="' +
m.preview_url +
'" data-media-alt-txt="' +
@ -869,7 +985,7 @@ export class Init {
'<div class="mt-post-media ' +
(spoiler ? "mt-post-media-spoiler " : "") +
'" data-media-type="' +
m.type +
type +
'">' +
(spoiler
? '<button class="mt-btn-dark mt-btn-spoiler">' +
@ -890,7 +1006,7 @@ export class Init {
(spoiler ? "mt-post-media-spoiler " : "") +
this.mtSettings.spinnerClass +
'" data-media-type="' +
m.type +
type +
'" data-media-url-hd="' +
m.url +
'" data-media-alt-txt="' +
@ -919,7 +1035,7 @@ export class Init {
'<div class="mt-post-media ' +
(spoiler ? "mt-post-media-spoiler " : "") +
'" data-media-type="' +
m.type +
type +
'" data-media-url-hd="' +
m.url +
'" data-media-alt-txt="' +
@ -968,7 +1084,7 @@ export class Init {
* Build a carousel/lightbox with the media content in the post clicked
* @param {event} e User interaction trigger
*/
#buildCarousel(e) {
#showCarousel(e) {
// List all medias in the post and remove sensitive/spoiler medias
const mediaSiblings = Array.from(
e.target.parentNode.parentNode.children
@ -1214,6 +1330,19 @@ export class Init {
* @returns {string} Preview link in HTML format
*/
#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 =
'<a href="' +
c.url +
@ -1237,6 +1366,7 @@ export class Init {
'<span class="mt-post-preview-title">' +
c.title +
"</span>" +
previewDescription +
(c.author_name
? '<span class="mt-post-preview-author">' +
this.#parseHTMLstring(c.author_name) +
@ -1263,69 +1393,64 @@ export class Init {
* Build footer after last post
*/
#buildFooter() {
if (this.mtSettings.btnSeeMore || this.mtSettings.btnReload) {
// Add footer container
this.mtBodyNode.parentNode.insertAdjacentHTML(
"beforeend",
'<div class="mt-footer"></div>'
);
let btnSeeMoreHTML = "";
let btnReloadHTML = "";
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 <strong>profileName</strong> value",
"⚠️"
);
}
} else if (this.mtSettings.timelineType === "hashtag") {
btnSeeMorePath = "tags/" + this.mtSettings.hashtagName;
} else if (this.mtSettings.timelineType === "local") {
btnSeeMorePath = "public/local";
// 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 <strong>profileName</strong> value",
"⚠️"
);
}
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="${
this.mtSettings.instanceUrl
}/${this.#escapeHTML(
btnSeeMorePath
)}" rel="nofollow noopener noreferrer" target="_blank">
btnSeeMorePath
)}" rel="nofollow noopener noreferrer" target="_blank">
${this.mtSettings.btnSeeMore}
</a>`;
}
containerFooter.insertAdjacentHTML("beforeend", btnSeeMoreHTML);
}
// Create button to refresh the timeline
if (this.mtSettings.btnReload) {
const btnReloadHTML = `
// Create button to refresh the timeline
if (this.mtSettings.btnReload) {
btnReloadHTML = `
<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>
${this.mtSettings.btnReload}
</button>`;
containerFooter.insertAdjacentHTML("beforeend", btnReloadHTML);
// Add footer container
this.mtBodyNode.parentNode.insertAdjacentHTML(
"beforeend",
'<div class="mt-footer">' + btnSeeMoreHTML + btnReloadHTML + "</div>"
);
const reloadBtn =
this.mtContainerNode.getElementsByClassName("btn-refresh")[0];
reloadBtn.addEventListener("click", () => {
this.mtUpdate();
});
}
// Add event listener to the button "Refresh"
const reloadBtn =
this.mtContainerNode.getElementsByClassName("btn-refresh")[0];
reloadBtn.addEventListener("click", () => {
this.mtUpdate();
});
}
}
/**
* Add EventListeners for timeline interactions and trigger functions
*/
#setPostsInteracion() {
#addPostListener() {
this.mtBodyNode.addEventListener("click", (e) => {
const target = e.target;
const localName = target.localName;
@ -1335,10 +1460,8 @@ export class Init {
if (
localName == "article" ||
target.offsetParent?.localName == "article" ||
(localName == "img" &&
this.mtSettings.disableCarousel &&
parentNode.getAttribute("data-media-type") !== "video" &&
parentNode.getAttribute("data-media-type") !== "gifv")
(this.mtSettings.disableCarousel &&
parentNode.getAttribute("data-media-type") === "image")
) {
this.#openPostUrl(e);
}
@ -1355,11 +1478,10 @@ export class Init {
if (
!this.mtSettings.disableCarousel &&
localName == "img" &&
!parentNode.classList.contains("mt-post-preview-image") &&
parentNode.getAttribute("data-media-type") !== "video" &&
parentNode.getAttribute("data-media-type") !== "gifv"
(parentNode.getAttribute("data-media-type") === "image" ||
parentNode.getAttribute("data-media-type") === "audio")
) {
this.#buildCarousel(e);
this.#showCarousel(e);
}
// 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.parentNode.className !== "mt-post-avatar-image-big" &&
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" &&
urlPost