Feature/posts limit
This commit is contained in:
		
							parent
							
								
									a78de16fd9
								
							
						
					
					
						commit
						410e83bce1
					
				
							
								
								
									
										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); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
| @ -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
	 i.j
						i.j