Merge branch 'feature/posts-limit' into 'master'
Feature/posts limit See merge request idotj/mastodon-embed-timeline!34
This commit is contained in:
commit
87c9d5b9fb
10
CHANGELOG
10
CHANGELOG
@ -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
|
||||
|
88
README.md
88
README.md
@ -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",
|
||||
|
||||
```
|
||||
|
4
dist/mastodon-timeline.esm.js
vendored
4
dist/mastodon-timeline.esm.js
vendored
File diff suppressed because one or more lines are too long
2
dist/mastodon-timeline.min.css
vendored
2
dist/mastodon-timeline.min.css
vendored
File diff suppressed because one or more lines are too long
4
dist/mastodon-timeline.umd.js
vendored
4
dist/mastodon-timeline.umd.js
vendored
File diff suppressed because one or more lines are too long
@ -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",
|
||||
|
@ -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
4
package-lock.json
generated
@ -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",
|
||||
|
@ -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": {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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",
|
||||
"⚠️"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.#showError(
|
||||
const instanceApiUrl = this.mtSettings.instanceUrl
|
||||
? `${this.mtSettings.instanceUrl}/api/v1/`
|
||||
: 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,44 +220,12 @@ 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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch data from server
|
||||
* @param {string} url address to fetch
|
||||
* @returns {array} List of objects
|
||||
*/
|
||||
async #fetchData(url) {
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`
|
||||
Failed to fetch the following Url:<br/>${url}<hr>Error status: ${response.status}<hr>Error message: ${response.statusText}
|
||||
`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter all fetched posts and append them on the timeline
|
||||
* @param {string} t Type of build (new or reload)
|
||||
*/
|
||||
async #buildTimeline(t) {
|
||||
await this.#getTimelineData();
|
||||
|
||||
// Merge pinned posts with timeline posts
|
||||
let posts;
|
||||
if (
|
||||
!this.mtSettings.hidePinnedPosts &&
|
||||
this.fetchedData.pinned?.length !== undefined &&
|
||||
@ -281,56 +235,224 @@ export class Init {
|
||||
...obj,
|
||||
pinned: true,
|
||||
}));
|
||||
posts = [...pinnedPosts, ...this.fetchedData.timeline];
|
||||
} else {
|
||||
posts = this.fetchedData.timeline;
|
||||
this.fetchedData.timeline = [
|
||||
...pinnedPosts,
|
||||
...this.fetchedData.timeline,
|
||||
];
|
||||
}
|
||||
|
||||
// Empty container body
|
||||
// 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} u Url address to fetch
|
||||
* @param {boolean} h gets the link header
|
||||
* @returns {array} List of objects
|
||||
*/
|
||||
async #fetchData(u, h = false) {
|
||||
const response = await fetch(u);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`
|
||||
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)
|
||||
*/
|
||||
async #buildTimeline(t) {
|
||||
await this.#getTimelineData();
|
||||
|
||||
// console.log("Mastodon timeline data fetched: ", this.fetchedData);
|
||||
|
||||
const posts = this.fetchedData.timeline;
|
||||
let nbPostToShow = 0;
|
||||
|
||||
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.#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,16 +633,11 @@ 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 (this.mtSettings.txtMaxLines !== "0") {
|
||||
const txtCss =
|
||||
this.mtSettings.txtMaxLines.length !== 0 ? " truncate" : "";
|
||||
|
||||
if (c.spoiler_text !== "") {
|
||||
content =
|
||||
'<div class="mt-post-txt">' +
|
||||
@ -570,6 +687,7 @@ export class Init {
|
||||
"</div>" +
|
||||
"</div>";
|
||||
}
|
||||
}
|
||||
|
||||
// Media attachments
|
||||
let media = [];
|
||||
@ -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,15 +1393,8 @@ 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>'
|
||||
);
|
||||
|
||||
const containerFooter =
|
||||
this.mtContainerNode.getElementsByClassName("mt-footer")[0];
|
||||
let btnSeeMoreHTML = "";
|
||||
let btnReloadHTML = "";
|
||||
|
||||
// Create button to open Mastodon page
|
||||
if (this.mtSettings.btnSeeMore) {
|
||||
@ -1290,7 +1413,7 @@ export class Init {
|
||||
} else if (this.mtSettings.timelineType === "local") {
|
||||
btnSeeMorePath = "public/local";
|
||||
}
|
||||
const btnSeeMoreHTML = `
|
||||
btnSeeMoreHTML = `
|
||||
<a class="mt-btn-violet btn-see-more" href="${
|
||||
this.mtSettings.instanceUrl
|
||||
}/${this.#escapeHTML(
|
||||
@ -1298,21 +1421,24 @@ export class Init {
|
||||
)}" 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 = `
|
||||
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>"
|
||||
);
|
||||
|
||||
// Add event listener to the button "Refresh"
|
||||
const reloadBtn =
|
||||
this.mtContainerNode.getElementsByClassName("btn-refresh")[0];
|
||||
reloadBtn.addEventListener("click", () => {
|
||||
@ -1320,12 +1446,11 @@ export class Init {
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
Loading…
x
Reference in New Issue
Block a user