mirror of
				https://gitlab.com/idotj/mastodon-embed-timeline.git
				synced 2025-10-29 22:22:23 +00:00 
			
		
		
		
	Feature/emojo support
This commit is contained in:
		
							parent
							
								
									526d01f797
								
							
						
					
					
						commit
						da04faa369
					
				| @ -1,3 +1,7 @@ | ||||
| v3.8.2 - xx/08/2023 | ||||
| - Add support to customized emojis | ||||
| - Javascript refactoring to allow multiple requests | ||||
| 
 | ||||
| v3.8.1 - 14/08/2023 | ||||
| - Show preview card from link, photo or video URL | ||||
| - Add description for the ALT attribute in images | ||||
|  | ||||
| @ -83,7 +83,10 @@ hide_replies: false, | ||||
| // Hide preview for links. Default: don't hide | ||||
| hide_preview_link: false, | ||||
| 
 | ||||
| // Converts Markdown symbol ">" at the beginning of a paragraph into a blockquote HTML tag (default: don't apply) | ||||
| // Show custom emojis available on the server. Default: show them | ||||
| show_emojos: true, | ||||
| 
 | ||||
| // Converts Markdown symbol ">" at the beginning of a paragraph into a blockquote HTML tag. Default: don't apply | ||||
| markdown_blockquote: false, | ||||
| 
 | ||||
| // Limit the text content to a maximum number of lines. Default: 0 (unlimited) | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| /* Mastodon embed feed timeline v3.8.1 */ | ||||
| /* Mastodon embed feed timeline v3.8.2 */ | ||||
| /* More info at: */ | ||||
| /* https://gitlab.com/idotj/mastodon-embed-feed-timeline */ | ||||
| 
 | ||||
| @ -97,14 +97,14 @@ html[data-theme="dark"] { | ||||
| .mt-avatar { | ||||
|   position: absolute; | ||||
|   top: 1rem; | ||||
|   left: 5px; | ||||
|   left: 0.25rem; | ||||
|   width: 3rem; | ||||
|   height: 3rem; | ||||
|   background-repeat: no-repeat; | ||||
|   background-position: 50% 50%; | ||||
|   background-size: contain; | ||||
|   background-color: var(--bg-color); | ||||
|   border-radius: 5px; | ||||
|   border-radius: 0.25rem; | ||||
| } | ||||
| .mt-avatar-boosted { | ||||
|   width: 2.5rem; | ||||
| @ -145,11 +145,16 @@ html[data-theme="dark"] { | ||||
| .toot-text:not(.truncate) .ellipsis::after { | ||||
|   content: "..."; | ||||
| } | ||||
| 
 | ||||
| .toot-text blockquote { | ||||
|   border-left: 4px solid var(--line-gray-color); | ||||
|   border-left: 0.25rem solid var(--line-gray-color); | ||||
|   margin-left: 0; | ||||
|   padding-left: 8px; | ||||
|   padding-left: 0.5rem; | ||||
| } | ||||
| .toot-text .custom-emoji { | ||||
|   height: 1.5rem; | ||||
|   min-width: 1.5rem; | ||||
|   margin-bottom: -0.25rem; | ||||
|   width: auto; | ||||
| } | ||||
| 
 | ||||
| .mt-error { | ||||
| @ -164,13 +169,15 @@ html[data-theme="dark"] { | ||||
|   padding: 0.75rem; | ||||
|   text-align: center; | ||||
| } | ||||
| 
 | ||||
| .mt-error-icon { | ||||
|   font-size: 2rem; | ||||
| } | ||||
| .mt-error-message { | ||||
|   padding: 1rem 0; | ||||
| } | ||||
| .mt-error-message hr { | ||||
|   color: var(--line-gray-color); | ||||
| } | ||||
| 
 | ||||
| /* Poll */ | ||||
| .toot-poll { | ||||
| @ -244,7 +251,7 @@ html[data-theme="dark"] { | ||||
| .toot-preview-link { | ||||
|   min-height: 4rem; | ||||
|   display: flex; | ||||
|   flex-direction: row;   | ||||
|   flex-direction: row; | ||||
| 
 | ||||
|   border: 1px solid var(--line-gray-color); | ||||
|   border-radius: 0.5rem; | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| /** | ||||
|  * Mastodon embed feed timeline v3.8.1 | ||||
|  * Mastodon embed feed timeline v3.8.2 | ||||
|  * More info at: | ||||
|  * https://gitlab.com/idotj/mastodon-embed-feed-timeline
 | ||||
|  */ | ||||
| @ -9,7 +9,7 @@ | ||||
|  * Adjust these parameters to customize your timeline | ||||
|  */ | ||||
| window.addEventListener("load", () => { | ||||
|   let mapi = new MastodonApi({ | ||||
|   const customParams = new MastodonApi({ | ||||
|     // Id of the <div> containing the timeline
 | ||||
|     container_body_id: "mt-body", | ||||
| 
 | ||||
| @ -46,7 +46,10 @@ window.addEventListener("load", () => { | ||||
|     // Hide preview card if toot contains a link, photo or video from a URL. Default: don't hide
 | ||||
|     hide_preview_link: false, | ||||
| 
 | ||||
|     // Converts Markdown symbol ">" at the beginning of a paragraph into a blockquote HTML tag (default: don't apply)
 | ||||
|     // Show custom emojis available on the server. Default: show them
 | ||||
|     show_emojos: true, | ||||
| 
 | ||||
|     // Converts Markdown symbol ">" at the beginning of a paragraph into a blockquote HTML tag. Ddefault: don't apply
 | ||||
|     markdown_blockquote: false, | ||||
| 
 | ||||
|     // Limit the text content to a maximum number of lines. Default: 0 (unlimited)
 | ||||
| @ -60,7 +63,6 @@ window.addEventListener("load", () => { | ||||
| /** | ||||
|  * Set all variables with customized values or use default ones | ||||
|  * @param {object} params_ User customized values | ||||
|  * Trigger color theme function | ||||
|  * Trigger main function to build the timeline | ||||
|  */ | ||||
| const MastodonApi = function (params_) { | ||||
| @ -83,149 +85,86 @@ const MastodonApi = function (params_) { | ||||
|     typeof params_.hide_preview_link !== "undefined" | ||||
|       ? params_.hide_preview_link | ||||
|       : false; | ||||
|   this.SHOW_EMOJOS = | ||||
|     typeof params_.show_emojos !== "undefined" ? params_.show_emojos : true; | ||||
|   this.MARKDOWN_BLOCKQUOTE = | ||||
|     typeof params_.markdown_blockquote !== "undefined" | ||||
|       ? params_.markdown_blockquote | ||||
|       : false; | ||||
|   this.TEXT_MAX_LINES = params_.text_max_lines || "0"; | ||||
|   this.LINK_SEE_MORE = params_.link_see_more; | ||||
|   this.FETCHED_DATA = {}; | ||||
| 
 | ||||
|   this.mtBodyContainer = document.getElementById(params_.container_body_id); | ||||
| 
 | ||||
|   this.setTheme(); | ||||
| 
 | ||||
|   this.buildTimeline(); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Set the theme style choosen by user or browser/OS | ||||
|  * Trigger functions and construct timeline | ||||
|  */ | ||||
| MastodonApi.prototype.setTheme = function () { | ||||
|   /** | ||||
|    * Set the theme value in the <html> tag using the attribute "data-theme" | ||||
|    * @param {string} theme Type of theme to apply: dark or light | ||||
|    */ | ||||
|   const setTheme = function (theme) { | ||||
|     document.documentElement.setAttribute("data-theme", theme); | ||||
|   }; | ||||
| MastodonApi.prototype.buildTimeline = async function () { | ||||
|   // Apply color theme
 | ||||
|   this.setTheme(); | ||||
| 
 | ||||
|   if (this.DEFAULT_THEME === "auto") { | ||||
|     let systemTheme = window.matchMedia("(prefers-color-scheme: dark)"); | ||||
|     systemTheme.matches ? setTheme("dark") : setTheme("light"); | ||||
|     // Update the theme if user change browser/OS preference
 | ||||
|     systemTheme.addEventListener("change", (e) => { | ||||
|       e.matches ? setTheme("dark") : setTheme("light"); | ||||
|     }); | ||||
|   // Get server data
 | ||||
|   await this.getTimelineData(); | ||||
| 
 | ||||
|   // Empty the <div> container
 | ||||
|   this.mtBodyContainer.innerHTML = ""; | ||||
| 
 | ||||
|   for (let i in this.FETCHED_DATA.timeline) { | ||||
|     // First filter (Public / Unlisted)
 | ||||
|     if ( | ||||
|       this.FETCHED_DATA.timeline[i].visibility == "public" || | ||||
|       (!this.HIDE_UNLISTED && | ||||
|         this.FETCHED_DATA.timeline[i].visibility == "unlisted") | ||||
|     ) { | ||||
|       // Second filter (Reblog / Replies)
 | ||||
|       if ( | ||||
|         (this.HIDE_REBLOG && this.FETCHED_DATA.timeline[i].reblog) || | ||||
|         (this.HIDE_REPLIES && this.FETCHED_DATA.timeline[i].in_reply_to_id) | ||||
|       ) { | ||||
|         // Nothing here (Don't append toots)
 | ||||
|       } else { | ||||
|         // Append toots
 | ||||
|         this.appendToot(this.FETCHED_DATA.timeline[i], Number(i)); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   // Check if there are toots in the container (due to filters applied)
 | ||||
|   if (this.mtBodyContainer.innerHTML === "") { | ||||
|     this.mtBodyContainer.setAttribute("role", "none"); | ||||
|     this.mtBodyContainer.innerHTML = | ||||
|       '<div class="mt-error"><span class="mt-error-icon">📭</span><br/><strong>Sorry, no toots to show</strong><br/><div class="mt-error-message">Got ' + | ||||
|       this.FETCHED_DATA.timeline.length + | ||||
|       ' toots from the server but due to the "hide filters" applied, no toot is shown</div></div>'; | ||||
|   } else { | ||||
|     setTheme(this.DEFAULT_THEME); | ||||
|     // Insert link after last toot to visit Mastodon page
 | ||||
|     if (this.LINK_SEE_MORE) { | ||||
|       let linkSeeMorePath = ""; | ||||
|       if (this.TIMELINE_TYPE === "profile") { | ||||
|         linkSeeMorePath = this.PROFILE_NAME; | ||||
|       } else if (this.TIMELINE_TYPE === "hashtag") { | ||||
|         linkSeeMorePath = "tags/" + this.HASHTAG_NAME; | ||||
|       } else if (this.TIMELINE_TYPE === "local") { | ||||
|         linkSeeMorePath = "public/local"; | ||||
|       } | ||||
|       let linkSeeMore = | ||||
|         '<div class="mt-footer"><a href="' + | ||||
|         this.INSTANCE_URL + | ||||
|         "/" + | ||||
|         linkSeeMorePath + | ||||
|         '" class="btn" target="_blank" rel="nofollow noopener noreferrer">' + | ||||
|         this.LINK_SEE_MORE + | ||||
|         "</a></div>"; | ||||
|       this.mtBodyContainer.parentNode.insertAdjacentHTML( | ||||
|         "beforeend", | ||||
|         linkSeeMore | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Listing toots function | ||||
|  */ | ||||
| MastodonApi.prototype.buildTimeline = function () { | ||||
|   let mapi = this; | ||||
|   let requestURL = ""; | ||||
| 
 | ||||
|   // Get request
 | ||||
|   if (this.TIMELINE_TYPE === "profile") { | ||||
|     requestURL = `${this.INSTANCE_URL}/api/v1/accounts/${this.USER_ID}/statuses?limit=${this.TOOTS_LIMIT}`; | ||||
|   } else if (this.TIMELINE_TYPE === "hashtag") { | ||||
|     requestURL = `${this.INSTANCE_URL}/api/v1/timelines/tag/${this.HASHTAG_NAME}?limit=${this.TOOTS_LIMIT}`; | ||||
|   } else if (this.TIMELINE_TYPE === "local") { | ||||
|     requestURL = `${this.INSTANCE_URL}/api/v1/timelines/public?local=true&limit=${this.TOOTS_LIMIT}`; | ||||
|   } | ||||
| 
 | ||||
|   fetch(requestURL, { | ||||
|     method: "get", | ||||
|   }) | ||||
|     .then((response) => { | ||||
|       if (response.ok) { | ||||
|         return response.json(); | ||||
|       } else if (response.status === 404) { | ||||
|         throw new Error("404 Not found", { cause: response }); | ||||
|       } else { | ||||
|         throw new Error(response.status); | ||||
|       } | ||||
|     }) | ||||
|     .then((jsonData) => { | ||||
|       // console.log("jsonData: ", jsonData);
 | ||||
| 
 | ||||
|       // Empty the <div> container
 | ||||
|       this.mtBodyContainer.innerHTML = ""; | ||||
| 
 | ||||
|       for (let i in jsonData) { | ||||
|         // First filter (Public / Unlisted)
 | ||||
|         if ( | ||||
|           jsonData[i].visibility == "public" || | ||||
|           (!this.HIDE_UNLISTED && jsonData[i].visibility == "unlisted") | ||||
|         ) { | ||||
|           // Second filter (Reblog / Replies)
 | ||||
|           if ( | ||||
|             (mapi.HIDE_REBLOG && jsonData[i].reblog) || | ||||
|             (mapi.HIDE_REPLIES && jsonData[i].in_reply_to_id) | ||||
|           ) { | ||||
|             // Nothing here (Don't append toots)
 | ||||
|           } else { | ||||
|             // Format and append toots
 | ||||
|             appendToot.call(mapi, jsonData[i], Number(i)); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       // Check if there are toots in the container (due to filters applied)
 | ||||
|       if (this.mtBodyContainer.innerHTML === "") { | ||||
|         this.mtBodyContainer.setAttribute("role", "none"); | ||||
|         this.mtBodyContainer.innerHTML = | ||||
|           '<div class="mt-error"><span class="mt-error-icon">📭</span><br/><strong>Sorry, no toots to show</strong><br/><div class="mt-error-message">Got ' + | ||||
|           jsonData.length + | ||||
|           ' toots from the server but due to the "hide filters" applied, no toot is shown</div></div>'; | ||||
|       } else { | ||||
|         // Insert link after last toot to visit Mastodon page
 | ||||
|         if (mapi.LINK_SEE_MORE) { | ||||
|           let linkSeeMorePath = ""; | ||||
|           if (this.TIMELINE_TYPE === "profile") { | ||||
|             linkSeeMorePath = mapi.PROFILE_NAME; | ||||
|           } else if (this.TIMELINE_TYPE === "hashtag") { | ||||
|             linkSeeMorePath = "tags/" + this.HASHTAG_NAME; | ||||
|           } else if (this.TIMELINE_TYPE === "local") { | ||||
|             linkSeeMorePath = "public/local"; | ||||
|           } | ||||
|           let linkSeeMore = | ||||
|             '<div class="mt-footer"><a href="' + | ||||
|             mapi.INSTANCE_URL + | ||||
|             "/" + | ||||
|             linkSeeMorePath + | ||||
|             '" class="btn" target="_blank" rel="nofollow noopener noreferrer">' + | ||||
|             mapi.LINK_SEE_MORE + | ||||
|             "</a></div>"; | ||||
|           this.mtBodyContainer.parentNode.insertAdjacentHTML( | ||||
|             "beforeend", | ||||
|             linkSeeMore | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|     .catch((err) => { | ||||
|       this.mtBodyContainer.innerHTML = | ||||
|         '<div class="mt-error"><span class="mt-error-icon">❌</span><br/><strong>Sorry, request failed:</strong><br/><div class="mt-error-message">' + | ||||
|         err + | ||||
|         "</div></div>"; | ||||
|       this.mtBodyContainer.setAttribute("role", "none"); | ||||
|     }); | ||||
| 
 | ||||
|   /** | ||||
|    * Inner function to add each toot in timeline container | ||||
|    * @param {object} c Toot content | ||||
|    * @param {number} i Index of toot | ||||
|    */ | ||||
|   const appendToot = function (c, i) { | ||||
|     this.mtBodyContainer.insertAdjacentHTML( | ||||
|       "beforeend", | ||||
|       this.assambleToot(c, i) | ||||
|     ); | ||||
|   }; | ||||
| 
 | ||||
|   // Toot interactions
 | ||||
|   this.mtBodyContainer.addEventListener("click", function (e) { | ||||
| @ -297,6 +236,108 @@ MastodonApi.prototype.buildTimeline = function () { | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Set the theme style chosen by the user or by the browser/OS | ||||
|  */ | ||||
| MastodonApi.prototype.setTheme = function () { | ||||
|   /** | ||||
|    * Set the theme value in the <html> tag using the attribute "data-theme" | ||||
|    * @param {string} theme Type of theme to apply: dark or light | ||||
|    */ | ||||
|   const setTheme = function (theme) { | ||||
|     document.documentElement.setAttribute("data-theme", theme); | ||||
|   }; | ||||
| 
 | ||||
|   if (this.DEFAULT_THEME === "auto") { | ||||
|     let systemTheme = window.matchMedia("(prefers-color-scheme: dark)"); | ||||
|     systemTheme.matches ? setTheme("dark") : setTheme("light"); | ||||
|     // Update the theme if user change browser/OS preference
 | ||||
|     systemTheme.addEventListener("change", (e) => { | ||||
|       e.matches ? setTheme("dark") : setTheme("light"); | ||||
|     }); | ||||
|   } else { | ||||
|     setTheme(this.DEFAULT_THEME); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Requests to the server to get all the data | ||||
|  */ | ||||
| MastodonApi.prototype.getTimelineData = async function () { | ||||
|   return new Promise((resolve, reject) => { | ||||
|     /** | ||||
|      * Fetch data from server | ||||
|      * @param {string} url address to fetch | ||||
|      * @returns {object} List of objects | ||||
|      */ | ||||
|     async function fetchData(url) { | ||||
|       const response = await fetch(url); | ||||
| 
 | ||||
|       if (!response.ok) { | ||||
|         throw new Error( | ||||
|           "Failed to fetch the following URL: " + | ||||
|             url + | ||||
|             "<hr>" + | ||||
|             "Error status: " + | ||||
|             response.status + | ||||
|             "<hr>" + | ||||
|             "Error message: " + | ||||
|             response.statusText | ||||
|         ); | ||||
|       } | ||||
| 
 | ||||
|       const data = await response.json(); | ||||
|       return data; | ||||
|     } | ||||
| 
 | ||||
|     // URLs to fetch
 | ||||
|     let urls = {}; | ||||
|     if (this.TIMELINE_TYPE === "profile") { | ||||
|       urls.timeline = `${this.INSTANCE_URL}/api/v1/accounts/${this.USER_ID}/statuses?limit=${this.TOOTS_LIMIT}`; | ||||
|     } else if (this.TIMELINE_TYPE === "hashtag") { | ||||
|       urls.timeline = `${this.INSTANCE_URL}/api/v1/timelines/tag/${this.HASHTAG_NAME}?limit=${this.TOOTS_LIMIT}`; | ||||
|     } else if (this.TIMELINE_TYPE === "local") { | ||||
|       urls.timeline = `${this.INSTANCE_URL}/api/v1/timelines/public?local=true&limit=${this.TOOTS_LIMIT}`; | ||||
|     } | ||||
|     if (this.SHOW_EMOJOS) { | ||||
|       urls.emojos = this.INSTANCE_URL + "/api/v1/custom_emojis"; | ||||
|     } | ||||
| 
 | ||||
|     const urlsPromises = Object.entries(urls).map(([key, url]) => { | ||||
|       return fetchData(url) | ||||
|         .then((data) => ({ [key]: data })) | ||||
|         .catch((error) => { | ||||
|           reject(new Error("Something went wrong fetching data")); | ||||
|           this.mtBodyContainer.innerHTML = | ||||
|             '<div class="mt-error"><span class="mt-error-icon">❌</span><br/><strong>Sorry, request failed:</strong><br/><div class="mt-error-message">' + | ||||
|             error.message + | ||||
|             "</div></div>"; | ||||
|           this.mtBodyContainer.setAttribute("role", "none"); | ||||
|           return { [key]: [] }; | ||||
|         }); | ||||
|     }); | ||||
| 
 | ||||
|     // Fetch all urls simultaneously
 | ||||
|     Promise.all(urlsPromises).then((dataObjects) => { | ||||
|       this.FETCHED_DATA = dataObjects.reduce((result, dataItem) => { | ||||
|         return { ...result, ...dataItem }; | ||||
|       }, {}); | ||||
| 
 | ||||
|       // console.log("Timeline data: ", this.FETCHED_DATA);
 | ||||
|       resolve(); | ||||
|     }); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Inner function to add each toot in timeline container | ||||
|  * @param {object} c Toot content | ||||
|  * @param {number} i Index of toot | ||||
|  */ | ||||
| MastodonApi.prototype.appendToot = function (c, i) { | ||||
|   this.mtBodyContainer.insertAdjacentHTML("beforeend", this.assambleToot(c, i)); | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Build toot structure | ||||
|  * @param {object} c Toot content | ||||
| @ -503,6 +544,11 @@ MastodonApi.prototype.formatTootText = function (c) { | ||||
|   // Format hashtags and mentions
 | ||||
|   content = this.addTarget2hashtagMention(content); | ||||
| 
 | ||||
|   // Convert emojos shortcode into images
 | ||||
|   if (this.SHOW_EMOJOS) { | ||||
|     content = this.showEmojos(content, this.FETCHED_DATA.emojos); | ||||
|   } | ||||
| 
 | ||||
|   // Convert markdown styles into HTML
 | ||||
|   if (this.MARKDOWN_BLOCKQUOTE) { | ||||
|     content = this.replaceHTMLtag( | ||||
| @ -532,6 +578,28 @@ MastodonApi.prototype.addTarget2hashtagMention = function (c) { | ||||
|   return content; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Find all custom emojis shortcode and replace by image | ||||
|  * @param {string} c Text content | ||||
|  * @param {array} e List with all custom emojis | ||||
|  * @returns {string} Text content modified | ||||
|  */ | ||||
| MastodonApi.prototype.showEmojos = function (c, e) { | ||||
|   if (c.includes(":")) { | ||||
|     for (const emojo of e) { | ||||
|       const regex = new RegExp(`\\:${emojo.shortcode}\\:`, "g"); | ||||
|       c = c.replace( | ||||
|         regex, | ||||
|         `<img src="${emojo.url}" class="custom-emoji" alt="Emoji ${emojo.shortcode}" />` | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     return c; | ||||
|   } else { | ||||
|     return c; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Find all start/end <tags> and replace them by another start/end <tags> | ||||
|  * @param {string} c Text content | ||||
|  | ||||
							
								
								
									
										1
									
								
								src/mastodon-timeline.min.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								src/mastodon-timeline.min.css
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										1
									
								
								src/mastodon-timeline.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								src/mastodon-timeline.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 i.j
						i.j