PageSourceSearch

https://www.r-project.org/js/mastodon-timeline.js

js r-project.org collected 2026-09-24 06:48:09 UTC 15,013 bytes, 507 lines download raw bytes

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.