1// Mastodon embed feed timeline v3.5.1 2// More info at: 3// https://gitlab.com/idotj/mastodon-embed-feed-timeline 4 5// Timeline settings 6document.addEventListener("DOMContentLoaded", () => { 7 let mapi = new MastodonApi({ 8 // Id of the <div> containing the timeline. 9 container_body_id: "mt-body", 10 11 // Preferred color theme: 'light', 'dark' or 'auto' (default: auto). 12 default_theme: "auto", 13 14 // Your Mastodon instance. 15 instance_url: "https://fosstodon.org", 16 17 // Choose type of toots to show in the timeline: 'local', 'profile', 'hashtag' (default: local). 18 timeline_type: "profile", 19 20 // Your user ID on Mastodon instance. Leave empty if you didn't choose 'profile' as type of timeline. 21 user_id: "109319087453855702", 22 23 // Your user name on Mastodon instance. Leave empty if you didn't choose 'profile' as type of timeline. 24 profile_name: "@R_Foundation", 25 26 // The name of the hashtag. Leave empty if you didn't choose 'hashtag' as type of timeline. 27 hashtag_name: "", 28 29 // Maximum amount of toots to get (default: 20). 30 toots_limit: "3", 31 32 // Hide unlisted toots (default: don't hide). 33 hide_unlisted: false, 34 35 // Hide boosted toots (default: don't hide). 36 hide_reblog: false, 37 38 // Hide replies toots (default: don't hide). 39 hide_replies: false, 40 41 // Limit the text content to a maximum number of lines (default: unlimited). 42 text_max_lines: "0", 43 44 // Customize the text of the link pointing to the Mastodon page (appears after the last toot). 45 link_see_more: "See more posts at fosstodon.org", 46 }); 47}); 48 49let MastodonApi = function (params_) { 50 // Endpoint access settings / default values 51 this.DEFAULT_THEME = params_.default_theme || "auto"; 52 this.INSTANCE_URL = params_.instance_url; 53 this.USER_ID = params_.user_id || "";
54 this.PROFILE_NAME = this.USER_ID ? params_.profile_name : ""; 55 this.TIMELINE_TYPE = params_.timeline_type || "local"; 56 this.HASHTAG_NAME = params_.hashtag_name || ""; 57 this.TOOTS_LIMIT = params_.toots_limit || "20"; 58 this.HIDE_UNLISTED = 59 typeof params_.hide_unlisted !== "undefined" 60 ? params_.hide_unlisted 61 : false; 62 this.HIDE_REBLOG = 63 typeof params_.hide_reblog !== "undefined" ? params_.hide_reblog : false; 64 this.HIDE_REPLIES = 65 typeof params_.hide_replies !== "undefined" ? params_.hide_replies : false; 66 this.TEXT_MAX_LINES = params_.text_max_lines || "0"; 67 this.LINK_SEE_MORE = params_.link_see_more; 68 69 // Target selector 70 this.mtBodyContainer = document.getElementById(params_.container_body_id); 71 72 // Apply selected appearance 73 this.applyTheme(); 74 75 // Get the toots 76 this.getToots(); 77}; 78 79// Theme style 80MastodonApi.prototype.applyTheme = function () { 81 const setTheme = function (theme) { 82 document.documentElement.setAttribute("data-theme", theme); 83 }; 84 if (this.DEFAULT_THEME === "auto") { 85 let systemTheme = window.matchMedia("(prefers-color-scheme: dark)"); 86 systemTheme.matches ? setTheme("dark") : setTheme("light"); 87 systemTheme.addEventListener("change", (event) => { 88 event.matches ? setTheme("dark") : setTheme("light"); 89 }); 90 } else { 91 setTheme(this.DEFAULT_THEME); 92 } 93}; 94 95// Listing toots function 96MastodonApi.prototype.getToots = function () { 97 let mapi = this; 98 let requestURL = ""; 99 100 // Get request 101 if (this.TIMELINE_TYPE === "profile") { 102 requestURL = 103 this.INSTANCE_URL + 104 "/api/v1/accounts/" + 105 this.USER_ID + 106 "/statuses?limit=" + 107 this.TOOTS_LIMIT; 108 } else if (this.TIMELINE_TYPE === "hashtag") { 109 requestURL = 110 this.INSTANCE_URL + 111 "/api/v1/timelines/tag/" + 112 this.HASHTAG_NAME + 113 "?limit=" + 114 this.TOOTS_LIMIT; 115 } else if (this.TIMELINE_TYPE === "local") { 116 requestURL = 117 this.INSTANCE_URL + 118 "/api/v1/timelines/public?local=true&limit=" + 119 this.TOOTS_LIMIT; 120 } 121 122 fetch(requestURL, { 123 method: "get", 124 }) 125 .then((response) => { 126 if (response.ok) { 127 return response.json(); 128 } else if (response.status === 404) { 129 throw new Error("404 Not found", { cause: response }); 130 } else { 131 throw new Error(response.status); 132 } 133 }) 134 .then((jsonData) => { 135 // console.log('jsonData: ', jsonData); 136 137 // Empty the <div> container 138 this.mtBodyContainer.innerHTML = ""; 139 140 // Add toots 141 for (let i in jsonData) { 142 // First filter (Public / Unlisted) 143 if ( 144 jsonData[i].visibility == "public" || 145 (!this.HIDE_UNLISTED && jsonData[i].visibility == "unlisted") 146 ) { 147 // Second filter (Reblog / Replies) 148 if ( 149 (mapi.HIDE_REBLOG && jsonData[i].reblog) || 150 (mapi.HIDE_REPLIES && jsonData[i].in_reply_to_id) 151 ) { 152 // Nothing here (Don't append toots) 153 } else { 154 appendToot.call(mapi, jsonData[i], i); 155 } 156 } 157 } 158 159 // Check if there are toots in the container (due to filters applied) 160 if (this.mtBodyContainer.innerHTML === "") { 161 this.mtBodyContainer.setAttribute("role", "none"); 162 this.mtBodyContainer.innerHTML = 163 '<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 ' + 164 jsonData.length + 165 ' toots from the server but due to the "hide filters" applied, no toot is shown</div></div>'; 166 } else { 167 // Add target="_blank" to all #hashtags and @mentions 168 let linksToBlank = document.querySelectorAll( 169 "#mt-body .hashtag, .u-url.mention" 170 ); 171 for (let j = 0; j < linksToBlank.length; j++) { 172 linksToBlank[j].target = "_blank"; 173 linksToBlank[j].hasAttribute("rel", "tag") 174 ? linksToBlank[j].setAttribute( 175 "rel", 176 "tag nofollow noopener noreferrer" 177 ) 178 : linksToBlank[j].setAttribute( 179 "rel", 180 "nofollow noopener noreferrer" 181 ); 182 } 183 184 // Insert link after last toot to visit Mastodon page 185 if (mapi.LINK_SEE_MORE) { 186 let linkHtml = ""; 187 if (this.TIMELINE_TYPE === "profile") { 188 linkHtml = 189 '<div class="mt-footer"><a href="' + 190 mapi.INSTANCE_URL + 191 "/" + 192 mapi.PROFILE_NAME + 193 '" class="btn" target="_blank" rel="nofollow noopener noreferrer">' + 194 mapi.LINK_SEE_MORE + 195 "</a></div>"; 196 } else if (this.TIMELINE_TYPE === "hashtag") { 197 linkHtml = 198 '<div class="mt-footer"><a href="' + 199 mapi.INSTANCE_URL + 200 "/tags/" + 201 this.HASHTAG_NAME + 202 '" class="btn" target="_blank" rel="nofollow noopener noreferrer">' + 203 mapi.LINK_SEE_MORE + 204 "</a></div>"; 205 } else if (this.TIMELINE_TYPE === "local") { 206 linkHtml = 207 '<div class="mt-footer"><a href="' + 208 mapi.INSTANCE_URL + 209 "/public/local" + 210 '" class="btn" target="_blank" rel="nofollow noopener noreferrer">' + 211 mapi.LINK_SEE_MORE + 212 "</a></div>"; 213 } 214 this.mtBodyContainer.parentNode.insertAdjacentHTML( 215 "beforeend", 216 linkHtml 217 ); 218 } 219 } 220 }) 221 .catch((err) => { 222 this.mtBodyContainer.innerHTML = 223 '<div class="mt-error"><span class="mt-error-icon">â</span><br/><strong>Sorry, request failed:</strong><br/><div class="mt-error-message">' + 224 err + 225 "</div></div>"; 226 this.mtBodyContainer.setAttribute("role", "none"); 227 }); 228 229 // Inner function to add each toot content in container 230 let appendToot = function (status_, index) { 231 let avatar, user, content, url, date; 232 233 if (status_.reblog) { 234 // BOOSTED toot 235 // Toot url 236 url = status_.reblog.url; 237 238 // Boosted avatar 239 avatar = 240 '<a href="' + 241 status_.reblog.account.url + 242 '" class="mt-avatar mt-avatar-boosted" style="background-image:url(' + 243 status_.reblog.account.avatar + 244 ');" rel="nofollow noopener noreferrer" target="_blank">' + 245 '<div class="mt-avatar mt-avatar-booster" style="background-image:url(' + 246 status_.account.avatar + 247 ');">' + 248 "</div>" + 249 '<span class="visually-hidden">' + 250 status_.account.username + 251 " avatar" + 252 "</span>" + 253 "</a>"; 254 255 // User name and url 256 user = 257 '<div class="mt-user">' + 258 '<a href="' + 259 status_.reblog.account.url + 260 '" rel="nofollow noopener noreferrer" target="_blank">' + 261 status_.reblog.account.username + 262 '<span class="visually-hidden"> post</span>' + 263 "</a>" + 264 "</div>"; 265 266 // Date 267 date = this.formatDate(status_.reblog.created_at); 268 } else { 269 // STANDARD toot 270 // Toot url 271 url = status_.url; 272 273 // Avatar 274 avatar = 275 '<a href="' + 276 status_.account.url + 277 '" class="mt-avatar" style="background-image:url(' + 278 status_.account.avatar + 279 ');" rel="nofollow noopener noreferrer" target="_blank">' + 280 '<span class="visually-hidden">' + 281 status_.account.username + 282 " avatar" + 283 "</span>" + 284 "</a>"; 285 286 // User name and url 287 user = 288 '<div class="mt-user">' + 289 '<a href="' + 290 status_.account.url + 291 '" rel="nofollow noopener noreferrer" target="_blank">' + 292 status_.account.username + 293 '<span class="visually-hidden"> post</span>' + 294 "</a>" + 295 "</div>"; 296 297 // Date 298 date = this.formatDate(status_.created_at); 299 } 300 301 // Main text 302 let text_css = ""; 303 if (this.TEXT_MAX_LINES !== "0") { 304 text_css = "truncate"; 305 document.documentElement.style.setProperty( 306 "--text-max-lines", 307 this.TEXT_MAX_LINES 308 ); 309 } 310 311 if (status_.spoiler_text !== "") { 312 content = 313 '<div class="toot-text">' + 314 status_.spoiler_text + 315 ' <button type="button" class="spoiler-link" aria-expanded="false">Show more</button>' + 316 '<div class="spoiler-text">' + 317 status_.content + 318 "</div>" + 319 "</div>"; 320 } else if (status_.reblog && status_.reblog.content !== "") { 321 content = 322 '<div class="toot-text' + 323 " " + 324 text_css + 325 '">' + 326 "<div>" + 327 status_.reblog.content + 328 "</div>" + 329 "</div>"; 330 } else { 331 content = 332 '<div class="toot-text' + 333 " " + 334 text_css + 335 '">' + 336 "<div>" + 337 status_.content + 338 "</div>" + 339 "</div>"; 340 } 341 342 // Media attachments 343 let media = ""; 344 if (status_.media_attachments.length > 0) { 345 for (let picid in status_.media_attachments) { 346 media = this.replaceMedias( 347 status_.media_attachments[picid], 348 status_.sensitive 349 ); 350 } 351 } 352 if (status_.reblog && status_.reblog.media_attachments.length > 0) { 353 for (let picid in status_.reblog.media_attachments) { 354 media = this.replaceMedias( 355 status_.reblog.media_attachments[picid], 356 status_.sensitive 357 ); 358 } 359 } 360 361 // Poll 362 let poll = ""; 363 let pollOption = ""; 364 if (status_.poll) { 365 for (let i in status_.poll.options) { 366 pollOption += "<li>" + status_.poll.options[i].title + "</li>"; 367 } 368 poll = 369 '<div class="toot-poll">' + "<ul>" + pollOption + "</ul>" + "</div>"; 370 } 371 372 // Date 373 let timestamp = 374 '<div class="toot-date">' + 375 '<a href="' + 376 url + 377 '" rel="nofollow noopener noreferrer" tabindex="-1" target="_blank">' + 378 date + 379 "</a>" + 380 "</div>"; 381 382 // Add all to main toot container 383 let toot = 384 '<article class="mt-toot" aria-posinset="' + 385 (Number(index) + 1) + 386 '" aria-setsize="' + 387 this.TOOTS_LIMIT + 388 '" data-location="' + 389 url + 390 '" tabindex="0">' + 391 avatar + 392 user + 393 content + 394 media + 395 poll + 396 timestamp + 397 "</article>"; 398 399 this.mtBodyContainer.insertAdjacentHTML("beforeend", toot); 400 }; 401 402 // Toot interactions 403 this.mtBodyContainer.addEventListener("click", function (event) { 404 // Check if clicked in a toot 405 if ( 406 event.target.localName == "article" || 407 event.target.offsetParent.localName == "article" || 408 event.target.localName == "img" 409 ) { 410 openTootURL(event); 411 } 412 // Check if clicked in Show More/Less button 413 if ( 414 event.target.localName == "button" && 415 event.target.className == "spoiler-link" 416 ) { 417 toogleSpoiler(event); 418 } 419 }); 420 this.mtBodyContainer.addEventListener("keydown", function (event) { 421 // Check if Enter key pressed with focus in an article 422 if (event.code === "Enter" && event.target.localName == "article") { 423 openTootURL(event); 424 } 425 }); 426 427 // Open Toot in a new page avoiding any other natural link 428 openTootURL = function (event_) { 429 let urlToot = event_.target.closest(".mt-toot").dataset.location; 430 if ( 431 event_.target.localName !== "a" && 432 event_.target.localName !== "span" && 433 event_.target.localName !== "button" && 434 urlToot 435 ) { 436 window.open(urlToot, "_blank"); 437 } 438 }; 439 440 // Spoiler button 441 toogleSpoiler = function (event_) { 442 let spoilerText = event_.target.nextSibling; 443 let spoilerBtnText = event_.target.textContent; 444 spoilerText.classList.toggle("spoiler-text"); 445 if (spoilerBtnText == "Show more") { 446 spoilerBtnText = "Show less"; 447 event_.target.setAttribute("aria-expanded", "true"); 448 } else { 449 spoilerBtnText = "Show more"; 450 event_.target.setAttribute("aria-expanded", "false"); 451 } 452 }; 453}; 454 455// Place media 456MastodonApi.prototype.replaceMedias = function (media_, spoiler_) { 457 let spoiler = spoiler_ || false; 458 let pic = 459 '<div class="toot-media ' + 460 (spoiler ? "toot-media-spoiler" : "") + 461 ' img-ratio14_7 loading-spinner">' + 462 '<img onload="removeSpinner(this)" onerror="removeSpinner(this)" src="' + 463 media_.preview_url + 464 '" alt="" loading="lazy" />' + 465 "</div>"; 466 467 return pic; 468}; 469 470// Format date 471MastodonApi.prototype.formatDate = function (date_) { 472 const monthNames = [ 473 "Jan", 474 "Feb", 475 "Mar", 476 "Apr", 477 "May", 478 "Jun", 479 "Jul", 480 "Aug", 481 "Sep", 482 "Oct", 483 "Nov", 484 "Dec", 485 ]; 486 487 let date = new Date(date_); 488 489 let displayDate = 490 monthNames[date.getMonth()] + 491 " " + 492 date.getDate() + 493 ", " + 494 date.getFullYear(); 495
496 return displayDate; 497}; 498 499// Loading spinner 500removeSpinner = function (element) { 501 const spinnerCSS = "loading-spinner"; 502 // Find closest parent container (1st, 2nd or 3rd level) 503 let spinnerContainer = element.closest("." + spinnerCSS); 504 if (spinnerContainer) { 505 spinnerContainer.classList.remove(spinnerCSS); 506 } 507};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.