From 517b558e9241a66c4cf8a5a55305aeb53d7db34c Mon Sep 17 00:00:00 2001 From: "i.j" Date: Thu, 29 Feb 2024 10:00:33 +0000 Subject: [PATCH] Show pinned posts --- .gitlab-ci.yml | 2 + CHANGELOG | 9 + README.md | 140 +- dist/mastodon-timeline.esm.js | 16 +- dist/mastodon-timeline.min.css | 2 +- dist/mastodon-timeline.umd.js | 16 +- examples/hashtag-timeline.html | 2 +- examples/local-timeline-customized.html | 169 ++ package-lock.json | 4 +- package.json | 2 +- src/mastodon-timeline.css | 36 +- src/mastodon-timeline.js | 2303 ++++++++++++----------- 12 files changed, 1487 insertions(+), 1214 deletions(-) create mode 100644 examples/local-timeline-customized.html diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7953637..0b2ef2c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,4 +9,6 @@ deploy: - echo "@scope:registry=https://${CI_SERVER_HOST}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/" > .npmrc - echo "//${CI_SERVER_HOST}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=${CI_JOB_TOKEN}" >> .npmrc - npm publish + only: + - master environment: production diff --git a/CHANGELOG b/CHANGELOG index 30bbaed..ac629f8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,12 @@ +v4.3.1 - 01/03/2024 +- Show pinned posts +- Add icon to pinned posts +- Add '*' character to edited posts +- Use Intl.DateTimeFormat for date formatting +- Allow to customize date by locale/options parameters +- Show user account under user name +- Added a new customized HTML example + v4.2.1 - 26/02/2024 - Changed project name: mastodon-embed-feed-timeline -> mastodon-embed-timeline - Improved DOM load for module purposes diff --git a/README.md b/README.md index 937736c..83ca367 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 🐘 Mastodon embed timeline (new v4.2) +# 🐘 Mastodon embed timeline ![Mastodon timeline widget screenshot](screenshot-light-dark.jpg "Mastodon timeline widget screenshot") @@ -24,27 +24,29 @@ Demo running: ## Installation +You have three different ways to install it in your project, choose the one that best suits your needs: + ### Download -Ready-to-use compiled and minified files to easily start. +Download into your project the following compiled and minified files: -- Download into your project the following files: - - `dist/mastodon-timeline.min.css` - - `dist/mastodon-timeline.umd.js` +- `dist/mastodon-timeline.min.css` +- `dist/mastodon-timeline.umd.js` Now call the CSS and JS files in your HTML page using the `` and ` + ``` ### Package manager -Install your Mastodon timeline using npm or yarn: +A quick way to get it installed using **npm** or **yarn**: ```terminal npm install @idotj/mastodon-embed-timeline @@ -84,19 +86,19 @@ or yarn add @idotj/mastodon-embed-timeline ``` -After installation, you can import the widget as follows: +After installation, you can import the Javascript as follows: ```js import * as MastodonTimeline from "@idotj/mastodon-embed-timeline"; ``` -Make sure to import also the `@idotj/mastodon-embed-timeline/dist/mastodon-timeline.min.css` file in your project. +Make sure to import also the file `mastodon-timeline.min.css` into your project. ## Setup ### Initialize -The first step to get your timeline up is to add the following HTML structure in your page: +To get your timeline up add the following HTML structure in your page: ```html
@@ -124,11 +126,11 @@ window.addEventListener("load", () => { }); ``` -The next step is to configure the options/values of your timeline according to the type you need. There are three types, **Local**, **Profile** and **Hashtag**: +The next step is to configure the options/values of your timeline according to the type you prefer. There are three types, **Local**, **Profile** and **Hashtag**. Here you have an example of each one to see how it works: #### Local timeline -Add the following option/value when initializing the timeline: +To show a timeline with posts from the instance [mastodon.online](https://mastodon.online/public/local) add the following option/value when initializing the timeline: ```js const myTimeline = new MastodonTimeline.Init({ @@ -136,11 +138,9 @@ const myTimeline = new MastodonTimeline.Init({ }); ``` -It will show a timeline with posts from the instance [mastodon.online](https://mastodon.online/public/local) - #### Profile timeline -Add the following options/values when initializing the timeline: +To show a timeline with posts from my Mastodon profile [@idotj](https://mastodon.online/@idotj) add the following options/values when initializing the timeline: ```js const myTimeline = new MastodonTimeline.Init({ @@ -151,8 +151,6 @@ const myTimeline = new MastodonTimeline.Init({ }); ``` -It will show a timeline with posts from my Mastodon profile [@idotj](https://mastodon.online/@idotj) - â„šī¸ If you don't know your `userId` you have two ways to get it: - Copy the url below and paste it in a new tab. Remember to replace the words `INSTANCE` and `USERNAME` with your current values in the url: @@ -164,7 +162,7 @@ It will show a timeline with posts from my Mastodon profile [@idotj](https://mas #### Hashtag timeline -Add the following options/values when initializing the timeline: +To show a timeline with posts containing the hashtag [#fediverse](https://mastodon.online/tags/fediverse) add the following options/values when initializing the timeline: ```js const myTimeline = new MastodonTimeline.Init({ @@ -174,68 +172,106 @@ const myTimeline = new MastodonTimeline.Init({ }); ``` -It will show a timeline with posts containing the hashtag [#fediverse](https://mastodon.online/tags/fediverse) - ### Customize -You can pass more options/values to personalize your timeline. Here you have all the available options: +In the `examples/` folder there is an HTML file `local-timeline-customized.html` where you can see how to customize your timeline by overwriting the CSS styles and using various JS options when initializing the timeline. + +If you need to change something in the core files (`src/` folder), I recommend you to read the document [CONTRIBUTING.md](https://gitlab.com/idotj/mastodon-embed-timeline/-/blob/master/CONTRIBUTING.md#testing) to see how to compile and test your changes. + +Here you have all the options available to quickly setup and customize your timeline: ```js // Id of the
containing the timeline mtContainerId: "mt-container", - // Mastodon instance Url (including https://) + // Mastodon instance Url including https:// instanceUrl: "https://mastodon.social", - // Choose type of posts to show in the timeline: 'local', 'profile', 'hashtag'. Default: local + // Choose type of posts to show in the timeline: 'local', 'profile', 'hashtag' + // Default: local timelineType: "local", - // Your user ID number on Mastodon instance. Leave it empty if you didn't choose 'profile' as type of timeline + // Your user ID number on Mastodon instance + // Leave it empty if you didn't choose 'profile' as type of timeline 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 + // 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 profileName: "", - // The name of the hashtag (not including the # symbol). Leave it empty if you didn't choose 'hashtag' as type of timeline + // The name of the hashtag (not including the # symbol) + // Leave it empty if you didn't choose 'hashtag' as type of timeline hashtagName: "", // Class name for the loading spinner (also used in CSS file) 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 + // Maximum number of posts to request to the server + // Default: 20 maxNbPostFetch: "20", - // Maximum number of posts to show in the timeline. Default: 20 + // Maximum number of posts to show in the timeline + // Default: 20 maxNbPostShow: "20", - // Hide unlisted posts. Default: don't hide + // Specifies the format of the date according to the chosen language/country + // Default: British English (day-month-year order) + dateLocale: "en-GB", + + // Customize the date format using the options + // Default: DD MMM YYYY + dateOptions: { + day: "2-digit", + month: "short", + year: "numeric", + }, + + // Hide unlisted posts + // Default: don't hide hideUnlisted: false, - // Hide boosted posts. Default: don't hide + // Hide boosted posts + // Default: don't hide hideReblog: false, - // Hide replies posts. Default: don't hide + // Hide replies posts + // Default: don't hide hideReplies: false, - // Hide video image preview and load video player instead. Default: don't hide - hideVideoPreview: false, + // Hide pinned posts from the profile timeline + // Default: don't hide + hidePinnedPosts: false, - // Hide preview card if post contains a link, photo or video from a Url. Default: don't hide - hidePreviewLink: false, + // Hide user account under the user name + // Default: don't hide + hideUserAccount: false, - // Hide custom emojis available on the server. Default: don't hide + // Hide custom emojis available on the server + // Default: don't hide hideEmojos: false, - // Converts Markdown symbol ">" at the beginning of a paragraph into a blockquote HTML tag. Default: don't apply - markdownBlockquote: false, + // Hide video image preview and load video player instead + // Default: don't hide + hideVideoPreview: false, - // Hide replies, boosts and favourites posts counter. Default: don't hide + // Hide preview card if post contains a link, photo or video from a Url + // Default: don't hide + hidePreviewLink: false, + + // Hide replies, boosts and favourites posts counter + // Default: don't hide hideCounterBar: false, - // Limit the text content to a maximum number of lines. Default: 0 (unlimited) + // Converts Markdown symbol ">" at the beginning of a paragraph into a blockquote HTML tag + // Default: don't apply + markdownBlockquote: false, + + // 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/spolier text @@ -245,16 +281,20 @@ You can pass more options/values to personalize your timeline. Here you have all // Customize the text of the button used for showing sensitive/spolier 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 + // 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", - // Customize the text of the button reloading the list of posts placed at the end of the timeline. Leave the value empty to hide it + // Customize the text of the button reloading the list of posts placed at the end of the timeline + // Leave the value empty to hide it btnReload: "Refresh", - // Keep searching for the main
container before building the timeline. Useful in some cases where extra time is needed to render the page. Default: false + // Keep searching for the main
container before building the timeline. Useful in some cases where extra time is needed to render the page + // Default: don't apply insistSearchContainer: false, - // Defines the maximum time to continue searching for the main
container. Default: 3 seconds + // Defines the maximum time to continue searching for the main
container + // Default: 3 seconds insistSearchContainerTime: "3000", ``` @@ -270,7 +310,7 @@ You can pass more options/values to personalize your timeline. Here you have all The folder `examples/` contains several demos in HTML to play with. Download the full project and open each HTML file in your favorite browser. -Also, you have other alternatives to run these examples locally. Consult the document [CONTRIBUTING.md](https://gitlab.com/idotj/mastodon-embed-timeline/-/blob/master/CONTRIBUTING.md#testing) to use other options like Docker or Http-server. +Also, you have other alternatives to run these examples locally. Consult the document [CONTRIBUTING.md](https://gitlab.com/idotj/mastodon-embed-timeline/-/blob/master/CONTRIBUTING.md#testing) to use options such as Docker or Http-server. ## 🌐 Browser support diff --git a/dist/mastodon-timeline.esm.js b/dist/mastodon-timeline.esm.js index c554b54..c11751f 100644 --- a/dist/mastodon-timeline.esm.js +++ b/dist/mastodon-timeline.esm.js @@ -1,8 +1,8 @@ -/** - * Mastodon embed timeline - * @author idotj - * @version 4.2.1 - * @url https://gitlab.com/idotj/mastodon-embed-timeline - * @license GNU AGPLv3 - */ -class t{constructor(t={}){this.defaultSettings={mtContainerId:"mt-container",instanceUrl:"https://mastodon.social",timelineType:"local",userId:"",profileName:"",hashtagName:"",spinnerClass:"mt-loading-spinner",defaultTheme:"auto",maxNbPostFetch:"20",maxNbPostShow:"20",hideUnlisted:!1,hideReblog:!1,hideReplies:!1,hideVideoPreview:!1,hidePreviewLink:!1,hideEmojos:!1,markdownBlockquote:!1,hideCounterBar:!1,txtMaxLines:"0",btnShowMore:"SHOW MORE",btnShowLess:"SHOW LESS",btnShowContent:"SHOW CONTENT",btnSeeMore:"See more posts at Mastodon",btnReload:"Refresh",insistSearchContainer:!1,insistSearchContainerTime:"3000"},this.mtSettings={...this.defaultSettings,...t},this.mtContainerNode="",this.mtBodyNode="",this.fetchedData={},this.#t((()=>{this.#e()}))}#t(t){"undefined"!=typeof document&&"complete"===document.readyState?t():"undefined"!=typeof document&&"complete"!==document.readyState&&document.addEventListener("DOMContentLoaded",t())}#e(){const t=()=>{this.mtContainerNode=document.getElementById(this.mtSettings.mtContainerId),this.mtBodyNode=this.mtContainerNode.getElementsByClassName("mt-body")[0],this.#s(),this.#i("newTimeline")};if(this.mtSettings.insistSearchContainer){const e=performance.now(),s=()=>{if(document.getElementById(this.mtSettings.mtContainerId))t();else{performance.now()-e container with id: "${this.mtSettings.mtContainerId}" after several attempts for ${this.mtSettings.insistSearchContainerTime/1e3} seconds`)}};s()}else document.getElementById(this.mtSettings.mtContainerId)?t():console.error(`Impossible to find the
container with id: "${this.mtSettings.mtContainerId}". Please try to add the option 'insistSearchContainer: true' when initializing the script`)}mtUpdate(){this.#t((()=>{this.mtBodyNode.replaceChildren(),this.mtBodyNode.insertAdjacentHTML("afterbegin",'
'),this.#i("updateTimeline")}))}mtColorTheme(t){this.#t((()=>{this.mtContainerNode.setAttribute("data-theme",t)}))}#s(){if("auto"===this.mtSettings.defaultTheme){let t=window.matchMedia("(prefers-color-scheme: dark)");t.matches?this.mtColorTheme("dark"):this.mtColorTheme("light"),t.addEventListener("change",(t=>{t.matches?this.mtColorTheme("dark"):this.mtColorTheme("light")}))}else this.mtColorTheme(this.mtSettings.defaultTheme)}#a(){return new Promise(((t,e)=>{let s={};this.mtSettings.instanceUrl?"profile"===this.mtSettings.timelineType?this.mtSettings.userId?s.timeline=`${this.mtSettings.instanceUrl}/api/v1/accounts/${this.mtSettings.userId}/statuses?limit=${this.mtSettings.maxNbPostFetch}`:this.#o("Please check your userId value","âš ī¸"):"hashtag"===this.mtSettings.timelineType?this.mtSettings.hashtagName?s.timeline=`${this.mtSettings.instanceUrl}/api/v1/timelines/tag/${this.mtSettings.hashtagName}?limit=${this.mtSettings.maxNbPostFetch}`:this.#o("Please check your hashtagName value","âš ī¸"):"local"===this.mtSettings.timelineType?s.timeline=`${this.mtSettings.instanceUrl}/api/v1/timelines/public?local=true&limit=${this.mtSettings.maxNbPostFetch}`:this.#o("Please check your timelineType value","âš ī¸"):this.#o("Please check your instanceUrl value","âš ī¸"),this.mtSettings.hideEmojos||(s.emojos=this.mtSettings.instanceUrl+"/api/v1/custom_emojis");const i=Object.entries(s).map((([t,s])=>async function(t){const e=await fetch(t);if(!e.ok)throw new Error("Failed to fetch the following Url:
"+t+"
Error status: "+e.status+"
Error message: "+e.statusText);return await e.json()}(s).then((e=>({[t]:e}))).catch((i=>(e(new Error("Something went wrong fetching data from: "+s)),this.#o(i.message),{[t]:[]})))));Promise.all(i).then((e=>{this.mtSettings.fetchedData=e.reduce(((t,e)=>({...t,...e})),{}),t()}))}))}async#i(t){await this.#a(),this.mtBodyNode.replaceChildren();let e=0;for(let t in this.mtSettings.fetchedData.timeline)("public"==this.mtSettings.fetchedData.timeline[t].visibility||!this.mtSettings.hideUnlisted&&"unlisted"==this.mtSettings.fetchedData.timeline[t].visibility)&&(this.mtSettings.hideReblog&&this.mtSettings.fetchedData.timeline[t].reblog||this.mtSettings.hideReplies&&this.mtSettings.fetchedData.timeline[t].in_reply_to_id||eThis may be due to an incorrect configuration in the parameters or to filters applied (to hide certains type of posts)";this.#o(t,"📭")}else"newTimeline"===t?(this.#r(),this.#l(),this.#m()):"updateTimeline"===t?this.#r():this.#o("The function buildTimeline() was expecting a param")}#n(t,e){this.mtBodyNode.insertAdjacentHTML("beforeend",this.#d(t,e))}#d(t,e){let s,i,a,o,n,r,l,m,d;t.reblog?(o=t.reblog.url,s='
'+this.#c(t.reblog.account.username)+' avatar
'+this.#c(t.account.username)+' avatar
',a=t.reblog.account.display_name?t.reblog.account.display_name:t.reblog.account.username,this.mtSettings.hideEmojos||(a=this.#h(a,this.mtSettings.fetchedData.emojos)),i='
'+a+' account
',n=t.reblog.created_at,d=t.reblog.replies_count,m=t.reblog.reblogs_count,l=t.reblog.favourites_count):(o=t.url,s='
'+this.#c(t.account.username)+' avatar
',a=t.account.display_name?t.account.display_name:t.account.username,this.mtSettings.hideEmojos||(a=this.#h(a,this.mtSettings.fetchedData.emojos)),i='
'+a+' account
',n=t.created_at,d=t.replies_count,m=t.reblogs_count,l=t.favourites_count),r=this.#p(n);const c='
";let h="";"0"!==this.mtSettings.txtMaxLines&&(h=" truncate",this.mtBodyNode.parentNode.style.setProperty("--mt-txt-max-lines",this.mtSettings.txtMaxLines));let p="";p=""!==t.spoiler_text?'
'+t.spoiler_text+'
'+this.#g(t.content)+"
":t.reblog&&""!==t.reblog.content&&""!==t.reblog.spoiler_text?'
'+t.reblog.spoiler_text+'
'+this.#g(t.reblog.content)+"
":t.reblog&&""!==t.reblog.content&&""===t.reblog.spoiler_text?'
'+this.#g(t.reblog.content)+"
":'
'+this.#g(t.content)+"
";let g=[];if(t.media_attachments.length>0)for(let e in t.media_attachments)g.push(this.#u(t.media_attachments[e],t.sensitive));if(t.reblog&&t.reblog.media_attachments.length>0)for(let e in t.reblog.media_attachments)g.push(this.#u(t.reblog.media_attachments[e],t.reblog.sensitive));let u="";!this.mtSettings.hidePreviewLink&&t.card&&(u=this.#v(t.card));let v="";if(t.poll){let e="";for(let s in t.poll.options)e+="
  • "+t.poll.options[s].title+"
  • ";v='
      '+e+"
    "}let b="";if(!this.mtSettings.hideCounterBar){b='
    '+('
    '+d+"
    ")+('
    '+m+"
    ")+('
    '+l+"
    ")+"
    "}return'
    '+s+i+c+"
    "+p+g.join("")+u+v+b+"
    "}#g(t){let e=t;return e=this.#b(e),this.mtSettings.hideEmojos||(e=this.#h(e,this.mtSettings.fetchedData.emojos)),this.mtSettings.markdownBlockquote&&(e=this.#S(e,"

    >","

    ","

    ","

    ")),e}#b(t){let e=t.replaceAll('rel="tag"','rel="tag" target="_blank"');return e=e.replaceAll('class="u-url mention"','class="u-url mention" target="_blank"'),e}#S(t,e,s,i,a){if(t.includes(e)){const o=new RegExp(e+"(.*?)"+s,"gi");return t.replace(o,i+"$1"+a)}return t}#c(t){return(t??"").replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}#h(t,e){if(t.includes(":")){for(const s of e){const e=new RegExp(`\\:${s.shortcode}\\:`,"g");t=t.replace(e,`Emoji ${s.shortcode}`)}return t}return t}#p(t){const e=new Date(t);return["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"][e.getMonth()]+" "+e.getDate()+", "+e.getFullYear()}#u(t,e){const s=e||!1,i=t.type;let a="";return"image"===i&&(a='
    '+(s?'":"")+''+(t.description?this.#c(t.description):
    '),"audio"===i&&(a=t.preview_url?'
    '+(s?'":"")+''+(t.description?this.#c(t.description):
    ':'
    '+(s?'":"")+'
    '),"video"!==i&&"gifv"!==i||(a=this.mtSettings.hideVideoPreview?'
    '+(s?'":"")+'
    ':'
    '+(s?'":"")+''+(t.description?this.#c(t.description):
    '),a}#f(t){const e=t.target.closest("[data-video-url]"),s=e.dataset.videoUrl;e.replaceChildren(),e.innerHTML=''}#w(t){const e=t.target.nextSibling;"img"===e.localName||"audio"===e.localName||"video"===e.localName?(t.target.parentNode.classList.remove("mt-post-media-spoiler"),t.target.style.display="none"):(e.classList.contains("spoiler-txt-hidden")||e.classList.contains("spoiler-txt-visible"))&&(t.target.textContent==this.mtSettings.btnShowMore?(e.classList.remove("spoiler-txt-hidden"),e.classList.add("spoiler-txt-visible"),t.target.setAttribute("aria-expanded","true"),t.target.textContent=this.mtSettings.btnShowLess):(e.classList.remove("spoiler-txt-visible"),e.classList.add("spoiler-txt-hidden"),t.target.setAttribute("aria-expanded","false"),t.target.textContent=this.mtSettings.btnShowMore))}#v(t){return''+(t.image?'
    '+this.#c(t.image_description)+'
    ':'
    📄
    ')+'
    '+(t.provider_name?''+this.#N(t.provider_name)+"":"")+''+t.title+""+(t.author_name?''+this.#N(t.author_name)+"":"")+"
    "}#N(t){return(new DOMParser).parseFromString(t,"text/html").body.textContent}#m(){if(this.mtSettings.btnSeeMore||this.mtSettings.btnReload){this.mtBodyNode.parentNode.insertAdjacentHTML("beforeend",'');const t=this.mtContainerNode.getElementsByClassName("mt-footer")[0];if(this.mtSettings.btnSeeMore){let e="";"profile"===this.mtSettings.timelineType?this.mtSettings.profileName?e=this.mtSettings.profileName:this.#o("Please check your profileName value","âš ī¸"):"hashtag"===this.mtSettings.timelineType?e="tags/"+this.mtSettings.hashtagName:"local"===this.mtSettings.timelineType&&(e="public/local");const s=''+this.mtSettings.btnSeeMore+"";t.insertAdjacentHTML("beforeend",s)}if(this.mtSettings.btnReload){const e='";t.insertAdjacentHTML("beforeend",e);this.mtContainerNode.getElementsByClassName("btn-refresh")[0].addEventListener("click",(()=>{this.mtUpdate()}))}}}#l(){this.mtBodyNode.addEventListener("click",(t=>{("article"==t.target.localName||"article"==t.target.offsetParent?.localName||"img"==t.target.localName&&!t.target.parentNode.getAttribute("data-video-url"))&&this.#y(t),"button"==t.target.localName&&t.target.classList.contains("mt-btn-spoiler")&&this.#w(t),("mt-post-media-play-icon"==t.target.className||"svg"==t.target.localName&&"mt-post-media-play-icon"==t.target.parentNode.className||"path"==t.target.localName&&"mt-post-media-play-icon"==t.target.parentNode.parentNode.className||"img"==t.target.localName&&t.target.parentNode.getAttribute("data-video-url"))&&this.#f(t)})),this.mtBodyNode.addEventListener("keydown",(t=>{"Enter"===t.key&&"article"==t.target.localName&&this.#y(t)}))}#y(t){const e=t.target.closest(".mt-post").dataset.location;"a"!==t.target.localName&&"span"!==t.target.localName&&"button"!==t.target.localName&&"time"!==t.target.localName&&"mt-post-preview-noImage"!==t.target.className&&"mt-post-avatar-image-big"!==t.target.parentNode.className&&"mt-post-avatar-image-small"!==t.target.parentNode.className&&"mt-post-preview-image"!==t.target.parentNode.className&&"mt-post-preview"!==t.target.parentNode.className&&e&&window.open(e,"_blank","noopener")}#r(){const t=e=>{e.target.parentNode.classList.remove(this.mtSettings.spinnerClass),e.target.removeEventListener("load",t),e.target.removeEventListener("error",t)};this.mtBodyNode.querySelectorAll(`.${this.mtSettings.spinnerClass} > img`).forEach((e=>{e.addEventListener("load",t),e.addEventListener("error",t)}))}#o(t,e){const s=e||"❌";throw this.mtBodyNode.innerHTML='
    '+s+'
    Oops, something\'s happened:
    '+t+"
    ",this.mtBodyNode.setAttribute("role","none"),new Error("Stopping the script due to an error building the timeline.")}}export{t as Init}; +/** + * Mastodon embed timeline + * @author idotj + * @version 4.3.1 + * @url https://gitlab.com/idotj/mastodon-embed-timeline + * @license GNU AGPLv3 + */ +class t{constructor(t={}){this.defaultSettings={mtContainerId:"mt-container",instanceUrl:"https://mastodon.social",timelineType:"local",userId:"",profileName:"",hashtagName:"",spinnerClass:"mt-loading-spinner",defaultTheme:"auto",maxNbPostFetch:"20",maxNbPostShow:"20",dateLocale:"en-GB",dateOptions:{day:"2-digit",month:"short",year:"numeric"},hideUnlisted:!1,hideReblog:!1,hideReplies:!1,hidePinnedPosts:!1,hideUserAccount:!1,hideEmojos:!1,hideVideoPreview:!1,hidePreviewLink:!1,hideCounterBar:!1,markdownBlockquote:!1,txtMaxLines:"0",btnShowMore:"SHOW MORE",btnShowLess:"SHOW LESS",btnShowContent:"SHOW CONTENT",btnSeeMore:"See more posts at Mastodon",btnReload:"Refresh",insistSearchContainer:!1,insistSearchContainerTime:"3000"},this.mtSettings={...this.defaultSettings,...t},this.mtContainerNode="",this.mtBodyNode="",this.fetchedData={},this.#t((()=>{this.#e()}))}#t(t){"undefined"!=typeof document&&"complete"===document.readyState?t():"undefined"!=typeof document&&"complete"!==document.readyState&&document.addEventListener("DOMContentLoaded",t())}#e(){const t=()=>{this.mtContainerNode=document.getElementById(this.mtSettings.mtContainerId),this.mtBodyNode=this.mtContainerNode.getElementsByClassName("mt-body")[0],this.#s(),this.#i("newTimeline")};if(this.mtSettings.insistSearchContainer){const e=performance.now(),s=()=>{if(document.getElementById(this.mtSettings.mtContainerId))t();else{performance.now()-e container with id: "${this.mtSettings.mtContainerId}" after several attempts for ${this.mtSettings.insistSearchContainerTime/1e3} seconds`)}};s()}else document.getElementById(this.mtSettings.mtContainerId)?t():console.error(`Impossible to find the
    container with id: "${this.mtSettings.mtContainerId}". Please try to add the option 'insistSearchContainer: true' when initializing the script`)}mtUpdate(){this.#t((()=>{this.mtBodyNode.replaceChildren(),this.mtBodyNode.insertAdjacentHTML("afterbegin",'
    '),this.#i("updateTimeline")}))}mtColorTheme(t){this.#t((()=>{this.mtContainerNode.setAttribute("data-theme",t)}))}#s(){if("auto"===this.mtSettings.defaultTheme){let t=window.matchMedia("(prefers-color-scheme: dark)");t.matches?this.mtColorTheme("dark"):this.mtColorTheme("light"),t.addEventListener("change",(t=>{t.matches?this.mtColorTheme("dark"):this.mtColorTheme("light")}))}else this.mtColorTheme(this.mtSettings.defaultTheme)}#a(){return new Promise(((t,e)=>{let s={};this.mtSettings.instanceUrl?"profile"===this.mtSettings.timelineType?this.mtSettings.userId?(s.timeline=`${this.mtSettings.instanceUrl}/api/v1/accounts/${this.mtSettings.userId}/statuses?limit=${this.mtSettings.maxNbPostFetch}`,this.mtSettings.hidePinnedPosts||(s.pinned=`${this.mtSettings.instanceUrl}/api/v1/accounts/${this.mtSettings.userId}/statuses?pinned=true`)):this.#o("Please check your userId value","âš ī¸"):"hashtag"===this.mtSettings.timelineType?this.mtSettings.hashtagName?s.timeline=`${this.mtSettings.instanceUrl}/api/v1/timelines/tag/${this.mtSettings.hashtagName}?limit=${this.mtSettings.maxNbPostFetch}`:this.#o("Please check your hashtagName value","âš ī¸"):"local"===this.mtSettings.timelineType?s.timeline=`${this.mtSettings.instanceUrl}/api/v1/timelines/public?local=true&limit=${this.mtSettings.maxNbPostFetch}`:this.#o("Please check your timelineType value","âš ī¸"):this.#o("Please check your instanceUrl value","âš ī¸"),this.mtSettings.hideEmojos||(s.emojos=this.mtSettings.instanceUrl+"/api/v1/custom_emojis");const i=Object.entries(s).map((([t,s])=>async function(t){const e=await fetch(t);if(!e.ok)throw new Error("Failed to fetch the following Url:
    "+t+"
    Error status: "+e.status+"
    Error message: "+e.statusText);return await e.json()}(s).then((e=>({[t]:e}))).catch((i=>(e(new Error("Something went wrong fetching data from: "+s)),this.#o(i.message),{[t]:[]})))));Promise.all(i).then((e=>{this.fetchedData=e.reduce(((t,e)=>({...t,...e})),{}),t()}))}))}async#i(t){let e;if(await this.#a(),this.mtSettings.hidePinnedPosts||void 0===this.fetchedData.pinned?.length||0===this.fetchedData.pinned.length)e=this.fetchedData.timeline;else{e=[...this.fetchedData.pinned.map((t=>({...t,pinned:!0}))),...this.fetchedData.timeline]}this.mtBodyNode.replaceChildren();let s=0;for(let t in e)("public"==e[t].visibility||!this.mtSettings.hideUnlisted&&"unlisted"==e[t].visibility)&&(this.mtSettings.hideReblog&&e[t].reblog||this.mtSettings.hideReplies&&e[t].in_reply_to_id||sThis may be due to an incorrect configuration in the parameters or to filters applied (to hide certains type of posts)";this.#o(t,"📭")}else"newTimeline"===t?(this.#r(),this.#l(),this.#m()):"updateTimeline"===t?this.#r():this.#o("The function buildTimeline() was expecting a param")}#n(t,e){this.mtBodyNode.insertAdjacentHTML("beforeend",this.#d(t,e))}#d(t,e){let s,i,a,o,n,r,l,m,d,h;t.reblog?(n=t.reblog.url,s='
    '+this.#h(t.reblog.account.username)+' avatar
    '+this.#h(t.account.username)+' avatar
    ',a=!this.mtSettings.hideEmojos&&t.reblog.account.display_name?this.#c(t.reblog.account.display_name,this.fetchedData.emojos):t.reblog.account.display_name,o=this.mtSettings.hideUserAccount?"":'
    ",i='
    '+a+""+o+"
    ",r=t.reblog.created_at,h=t.reblog.replies_count,d=t.reblog.reblogs_count,m=t.reblog.favourites_count):(n=t.url,s='
    '+this.#h(t.account.username)+' avatar
    ',a=!this.mtSettings.hideEmojos&&t.account.display_name?this.#c(t.account.display_name,this.fetchedData.emojos):t.account.display_name,o=this.mtSettings.hideUserAccount?"":'
    ",i='
    '+a+""+o+"
    ",r=t.created_at,h=t.replies_count,d=t.reblogs_count,m=t.favourites_count),l=this.#p(r);const c='
    '+(t.pinned?'':"")+'"+(t.edited_at?" *":"")+"
    ";let p="";"0"!==this.mtSettings.txtMaxLines&&(p=" truncate",this.mtBodyNode.parentNode.style.setProperty("--mt-txt-max-lines",this.mtSettings.txtMaxLines));let g="";g=""!==t.spoiler_text?'
    '+t.spoiler_text+'
    '+this.#g(t.content)+"
    ":t.reblog&&""!==t.reblog.content&&""!==t.reblog.spoiler_text?'
    '+t.reblog.spoiler_text+'
    '+this.#g(t.reblog.content)+"
    ":t.reblog&&""!==t.reblog.content&&""===t.reblog.spoiler_text?'
    '+this.#g(t.reblog.content)+"
    ":'
    '+this.#g(t.content)+"
    ";let u=[];if(t.media_attachments.length>0)for(let e in t.media_attachments)u.push(this.#u(t.media_attachments[e],t.sensitive));if(t.reblog&&t.reblog.media_attachments.length>0)for(let e in t.reblog.media_attachments)u.push(this.#u(t.reblog.media_attachments[e],t.reblog.sensitive));let v="";!this.mtSettings.hidePreviewLink&&t.card&&(v=this.#v(t.card));let b="";if(t.poll){let e="";for(let s in t.poll.options)e+="
  • "+t.poll.options[s].title+"
  • ";b='
      '+e+"
    "}let w="";if(!this.mtSettings.hideCounterBar){w='
    '+('
    '+h+"
    ")+('
    '+d+"
    ")+('
    '+m+"
    ")+"
    "}return'
    '+s+i+c+"
    "+g+u.join("")+v+b+w+"
    "}#g(t){let e=t;return e=this.#b(e),this.mtSettings.hideEmojos||(e=this.#c(e,this.fetchedData.emojos)),this.mtSettings.markdownBlockquote&&(e=this.#w(e,"

    >","

    ","

    ","

    ")),e}#b(t){let e=t.replaceAll('rel="tag"','rel="tag" target="_blank"');return e=e.replaceAll('class="u-url mention"','class="u-url mention" target="_blank"'),e}#w(t,e,s,i,a){if(t.includes(e)){const o=new RegExp(e+"(.*?)"+s,"gi");return t.replace(o,i+"$1"+a)}return t}#h(t){return(t??"").replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}#c(t,e){if(t.includes(":")){for(const s of e){const e=new RegExp(`\\:${s.shortcode}\\:`,"g");t=t.replace(e,`Emoji ${s.shortcode}`)}return t}return t}#p(t){const e=new Date(t);return new Intl.DateTimeFormat(this.mtSettings.dateLocale,this.mtSettings.dateOptions).format(e)}#u(t,e){const s=e||!1,i=t.type;let a="";return"image"===i&&(a='
    '+(s?'":"")+''+(t.description?this.#h(t.description):
    '),"audio"===i&&(a=t.preview_url?'
    '+(s?'":"")+''+(t.description?this.#h(t.description):
    ':'
    '+(s?'":"")+'
    '),"video"!==i&&"gifv"!==i||(a=this.mtSettings.hideVideoPreview?'
    '+(s?'":"")+'
    ':'
    '+(s?'":"")+''+(t.description?this.#h(t.description):
    '),a}#S(t){const e=t.target.closest("[data-video-url]"),s=e.dataset.videoUrl;e.replaceChildren(),e.innerHTML=''}#f(t){const e=t.target.nextSibling;"img"===e.localName||"audio"===e.localName||"video"===e.localName?(t.target.parentNode.classList.remove("mt-post-media-spoiler"),t.target.style.display="none"):(e.classList.contains("spoiler-txt-hidden")||e.classList.contains("spoiler-txt-visible"))&&(t.target.textContent==this.mtSettings.btnShowMore?(e.classList.remove("spoiler-txt-hidden"),e.classList.add("spoiler-txt-visible"),t.target.setAttribute("aria-expanded","true"),t.target.textContent=this.mtSettings.btnShowLess):(e.classList.remove("spoiler-txt-visible"),e.classList.add("spoiler-txt-hidden"),t.target.setAttribute("aria-expanded","false"),t.target.textContent=this.mtSettings.btnShowMore))}#v(t){return''+(t.image?'
    '+this.#h(t.image_description)+'
    ':'
    📄
    ')+'
    '+(t.provider_name?''+this.#y(t.provider_name)+"":"")+''+t.title+""+(t.author_name?''+this.#y(t.author_name)+"":"")+"
    "}#y(t){return(new DOMParser).parseFromString(t,"text/html").body.textContent}#m(){if(this.mtSettings.btnSeeMore||this.mtSettings.btnReload){this.mtBodyNode.parentNode.insertAdjacentHTML("beforeend",'');const t=this.mtContainerNode.getElementsByClassName("mt-footer")[0];if(this.mtSettings.btnSeeMore){let e="";"profile"===this.mtSettings.timelineType?this.mtSettings.profileName?e=this.mtSettings.profileName:this.#o("Please check your profileName value","âš ī¸"):"hashtag"===this.mtSettings.timelineType?e="tags/"+this.mtSettings.hashtagName:"local"===this.mtSettings.timelineType&&(e="public/local");const s=''+this.mtSettings.btnSeeMore+"";t.insertAdjacentHTML("beforeend",s)}if(this.mtSettings.btnReload){const e='";t.insertAdjacentHTML("beforeend",e);this.mtContainerNode.getElementsByClassName("btn-refresh")[0].addEventListener("click",(()=>{this.mtUpdate()}))}}}#l(){this.mtBodyNode.addEventListener("click",(t=>{("article"==t.target.localName||"article"==t.target.offsetParent?.localName||"img"==t.target.localName&&!t.target.parentNode.getAttribute("data-video-url"))&&this.#N(t),"button"==t.target.localName&&t.target.classList.contains("mt-btn-spoiler")&&this.#f(t),("mt-post-media-play-icon"==t.target.className||"svg"==t.target.localName&&"mt-post-media-play-icon"==t.target.parentNode.className||"path"==t.target.localName&&"mt-post-media-play-icon"==t.target.parentNode.parentNode.className||"img"==t.target.localName&&t.target.parentNode.getAttribute("data-video-url"))&&this.#S(t)})),this.mtBodyNode.addEventListener("keydown",(t=>{"Enter"===t.key&&"article"==t.target.localName&&this.#N(t)}))}#N(t){const e=t.target.closest(".mt-post").dataset.location;"a"!==t.target.localName&&"span"!==t.target.localName&&"button"!==t.target.localName&&"time"!==t.target.localName&&"mt-post-preview-noImage"!==t.target.className&&"mt-post-avatar-image-big"!==t.target.parentNode.className&&"mt-post-avatar-image-small"!==t.target.parentNode.className&&"mt-post-preview-image"!==t.target.parentNode.className&&"mt-post-preview"!==t.target.parentNode.className&&e&&window.open(e,"_blank","noopener")}#r(){const t=e=>{e.target.parentNode.classList.remove(this.mtSettings.spinnerClass),e.target.removeEventListener("load",t),e.target.removeEventListener("error",t)};this.mtBodyNode.querySelectorAll(`.${this.mtSettings.spinnerClass} > img`).forEach((e=>{e.addEventListener("load",t),e.addEventListener("error",t)}))}#o(t,e){const s=e||"❌";throw this.mtBodyNode.innerHTML='
    '+s+'
    Oops, something\'s happened:
    '+t+"
    ",this.mtBodyNode.setAttribute("role","none"),new Error("Stopping the script due to an error building the timeline.")}}export{t as Init}; diff --git a/dist/mastodon-timeline.min.css b/dist/mastodon-timeline.min.css index 4edb1b4..cc72e67 100644 --- a/dist/mastodon-timeline.min.css +++ b/dist/mastodon-timeline.min.css @@ -1 +1 @@ -.mt-container,.mt-container[data-theme=light]{--mt-txt-max-lines:none;--mt-color-bg:#fff;--mt-color-bg-hover:#d9e1e8;--mt-color-line-gray:#c0cdd9;--mt-color-contrast-gray:#606984;--mt-color-content-txt:#000;--mt-color-link:#3a3bff;--mt-color-error-txt:#8b0000;--mt-color-btn-bg:#6364ff;--mt-color-btn-bg-hover:#563acc;--mt-color-btn-txt:#fff}.mt-container[data-theme=dark]{--mt-color-bg:#282c37;--mt-color-bg-hover:#313543;--mt-color-line-gray:#393f4f;--mt-color-contrast-gray:#606984;--mt-color-content-txt:#fff;--mt-color-link:#8c8dff;--mt-color-error-txt:#fe6c6c}.mt-container button{font:inherit}.mt-container a,.mt-container button{cursor:pointer}.mt-container{display:flex;flex-direction:column;height:100%;overflow-y:auto;position:relative;background-color:var(--mt-color-bg);scrollbar-color:var(--mt-color-contrast-gray) var(--mt-color-bg);scrollbar-width:auto}.mt-container::-webkit-scrollbar{width:.25rem;height:.25rem}.mt-container::-webkit-scrollbar-thumb{background-color:var(--mt-color-contrast-gray);border:none;border-radius:3rem}.mt-container::-webkit-scrollbar-thumb:active,.mt-container::-webkit-scrollbar-thumb:hover{background-color:var(--mt-color-contrast-gray)}.mt-container::-webkit-scrollbar-track{background-color:var(--mt-color-bg);border:none;border-radius:0}.mt-container::-webkit-scrollbar-corner,.mt-container::-webkit-scrollbar-track:active,.mt-container::-webkit-scrollbar-track:hover{background-color:var(--mt-color-bg)}.mt-container a,.mt-container a:active,.mt-container a:link{text-decoration:none;color:var(--mt-color-link)}.mt-container a:not(.mt-post-preview):hover{text-decoration:underline}.mt-body{padding:1rem clamp(.25rem,4vw,1rem);white-space:pre-wrap;word-wrap:break-word;margin-bottom:1rem}.mt-body .invisible{font-size:0;line-height:0;display:inline-block;width:0;height:0;position:absolute}.mt-post{margin:.25rem;padding:1rem .5rem;position:relative;min-height:3.75rem;background-color:transparent;border-bottom:1px solid var(--mt-color-line-gray)}.mt-post:focus,.mt-post:hover{cursor:pointer;background-color:var(--mt-color-bg-hover)}.mt-post p:last-child{margin-bottom:0}.mt-post-avatar{margin-right:.75rem}.mt-post-avatar-standard{width:2.25rem;height:2.25rem}.mt-post-avatar-boosted{width:3rem;height:3rem;position:relative}.mt-post-avatar-image-big img{aspect-ratio:1/1;width:2.25rem;height:2.25rem;border-radius:.25rem;overflow:hidden}.mt-post-avatar-image-small img{aspect-ratio:1/1;width:1.5rem;height:1.5rem;top:1.5rem;left:1.5rem;position:absolute;border-radius:.25rem;overflow:hidden}.mt-post-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1rem}.mt-post-header-user{font-weight:600;margin-top:.5rem;padding-right:1rem}.mt-post-header-user>a{display:flex;align-items:flex-start;color:var(--mt-color-content-txt)!important;overflow-wrap:anywhere}.mt-post-header-date{font-size:.75rem;text-align:right;margin:.5rem 0 0 auto}.mt-post-header-date>a{color:var(--mt-color-contrast-gray)!important}.mt-post-txt{margin-bottom:1rem;color:var(--mt-color-content-txt)}.mt-post-txt .spoiler-txt-hidden{display:none}.mt-post-txt.truncate{display:-webkit-box;overflow:hidden;-webkit-line-clamp:var(--mt-txt-max-lines);-webkit-box-orient:vertical}.mt-post-txt:not(.truncate) .ellipsis::after{content:"..."}.mt-post-txt blockquote{border-left:.25rem solid var(--mt-color-line-gray);margin-left:0;padding-left:.5rem}.mt-post-header-user .mt-custom-emoji,.mt-post-txt .mt-custom-emoji{height:1.5rem;min-width:1.5rem;margin-bottom:-.25rem;width:auto}.mt-post-poll{margin-bottom:1rem;color:var(--mt-color-content-txt)}.mt-post-poll ul{list-style:none;padding:0;margin:0}.mt-post-poll ul li{font-size:.9rem;margin-bottom:.5rem}.mt-post-poll.mt-post-poll-expired ul li{color:var(--mt-color-contrast-gray)}.mt-post-poll ul li:not(:last-child){margin-bottom:.25rem}.mt-post-poll ul li:before{content:"◯";padding-right:.5rem}.mt-post-poll.mt-post-poll-expired ul li:before{content:"";padding-right:0}.mt-post-media{position:relative;overflow:hidden;margin-bottom:1rem}.mt-post-media-spoiler>.mt-post-media-play-icon,.mt-post-media-spoiler>audio,.mt-post-media-spoiler>img,.mt-post-media-spoiler>video{filter:blur(2rem);pointer-events:none}.mt-post-media>audio{width:100%;position:relative;z-index:1}.mt-post-media>img,.mt-post-media>video{width:100%;height:100%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;color:var(--mt-color-content-txt)}.mt-post-media.mt-loading-spinner .mt-post-media-play-icon{display:none}.mt-post-media-play-icon{display:flex;position:absolute;width:3rem;height:3rem;top:calc(50% - 1.5rem);left:calc(50% - 1.5rem);justify-content:center;align-items:center;background-color:transparent;border:none;cursor:pointer}.mt-post-media-play-icon>svg{width:2.5rem;height:2.5rem;fill:var(--mt-color-bg);stroke:var(--mt-color-content-txt);stroke-width:1px}.mt-post-preview{min-height:4rem;display:flex;flex-direction:row;border:1px solid var(--mt-color-line-gray);border-radius:.5rem;color:var(--mt-color-link);font-size:.8rem;margin:1rem 0;overflow:hidden}.mt-post-preview-image{width:40%;align-self:stretch}.mt-post-preview-image img{display:block;width:100%;height:100%;object-fit:cover;color:var(--mt-color-content-txt)}.mt-post-preview-noImage{width:40%;font-size:1.5rem;align-self:center;text-align:center}.mt-post-preview-content{width:60%;display:flex;align-self:center;flex-direction:column;padding:.5rem 1rem;gap:.5rem}.mt-post-preview-title{font-weight:600}.mt-post-counter-bar{display:flex;min-width:6rem;max-width:40rem;justify-content:space-between;color:var(--mt-color-contrast-gray)}.mt-post-counter-bar-favorites,.mt-post-counter-bar-reblog,.mt-post-counter-bar-replies{display:flex;font-size:.75rem;gap:.25rem;align-items:center;opacity:.5}.mt-post-counter-bar-favorites>svg,.mt-post-counter-bar-reblog>svg,.mt-post-counter-bar-replies>svg{width:1rem;fill:var(--mt-color-contrast-gray)}.mt-container .mt-btn-dark{display:flex;border-radius:.25rem;background-color:var(--mt-color-line-gray);border:0;color:var(--mt-color-content-txt);font-weight:600;font-size:.75rem;text-align:center;padding:0 .5rem;line-height:1.25rem;vertical-align:top}.mt-container .mt-btn-violet,.mt-container a.mt-btn-violet{display:flex;align-items:center;gap:.5rem;border-radius:.25rem;border:.5rem;padding:.5rem .75rem;font-size:1rem;font-weight:600;text-align:center;background-color:var(--mt-color-btn-bg);color:var(--mt-color-btn-txt)}.mt-container .mt-btn-violet:hover,.mt-container a.mt-btn-violet:hover{background-color:var(--mt-color-btn-bg-hover);text-decoration:none}.mt-post-txt .mt-btn-spoiler{display:inline-block}.mt-post-media.mt-loading-spinner>.mt-btn-spoiler{display:none}.mt-post-media>.mt-btn-spoiler{position:absolute;top:50%;left:50%;z-index:2;transform:translate(-50%,-50%)}.mt-error{position:absolute;left:50%;transform:translateX(-50%);display:flex;flex-direction:column;height:calc(100% - 3.5rem);width:calc(100% - 4.5rem);justify-content:center;align-items:center;color:var(--mt-color-error-txt);padding:.75rem;text-align:center}.mt-error-icon{font-size:2rem}.mt-error-message{width:100%;padding:1rem 0}.mt-error-message hr{color:var(--mt-color-line-gray)}.mt-body>.mt-loading-spinner{position:absolute;width:3rem;height:3rem;margin:auto;top:calc(50% - 1.5rem);right:calc(50% - 1.5rem)}.mt-loading-spinner{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 128 128'%3E%3Cg%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 64 64' to='360 64 64' dur='1000ms' repeatCount='indefinite'/%3E%3Cpath d='M64 6.69a57.3 57.3 0 1 1 0 114.61A57.3 57.3 0 0 1 6.69 64' fill='none' stroke='%23404040' stroke-width='12'/%3E%3C/g%3E%3C/svg%3E");background-repeat:no-repeat;background-position:center center;background-color:transparent;background-size:min(2.5rem,calc(100% - .5rem))}.mt-footer{display:flex;flex-flow:wrap;margin:auto auto 2rem auto;padding:0 1.5rem;gap:1.5rem;align-items:center;justify-content:center}.visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important} \ No newline at end of file +.mt-container,.mt-container[data-theme=light]{--mt-txt-max-lines:none;--mt-color-bg:#fff;--mt-color-bg-hover:#d9e1e8;--mt-color-line-gray:#c0cdd9;--mt-color-contrast-gray:#606984;--mt-color-content-txt:#000;--mt-color-link:#3a3bff;--mt-color-error-txt:#8b0000;--mt-color-btn-bg:#6364ff;--mt-color-btn-bg-hover:#563acc;--mt-color-btn-txt:#fff}.mt-container[data-theme=dark]{--mt-color-bg:#282c37;--mt-color-bg-hover:#313543;--mt-color-line-gray:#393f4f;--mt-color-contrast-gray:#606984;--mt-color-content-txt:#fff;--mt-color-link:#8c8dff;--mt-color-error-txt:#fe6c6c}.mt-container button{font:inherit}.mt-container a,.mt-container button{cursor:pointer}.mt-container{display:flex;flex-direction:column;height:100%;overflow-y:auto;position:relative;background-color:var(--mt-color-bg);scrollbar-color:var(--mt-color-contrast-gray) var(--mt-color-bg);scrollbar-width:auto}.mt-container::-webkit-scrollbar{width:.25rem;height:.25rem}.mt-container::-webkit-scrollbar-thumb{background-color:var(--mt-color-contrast-gray);border:none;border-radius:3rem}.mt-container::-webkit-scrollbar-thumb:active,.mt-container::-webkit-scrollbar-thumb:hover{background-color:var(--mt-color-contrast-gray)}.mt-container::-webkit-scrollbar-track{background-color:var(--mt-color-bg);border:none;border-radius:0}.mt-container::-webkit-scrollbar-corner,.mt-container::-webkit-scrollbar-track:active,.mt-container::-webkit-scrollbar-track:hover{background-color:var(--mt-color-bg)}.mt-container a,.mt-container a:active,.mt-container a:link{text-decoration:none;color:var(--mt-color-link)}.mt-container a:not(.mt-post-preview):hover{text-decoration:underline}.mt-body{padding:1rem clamp(.25rem,4vw,1rem);white-space:pre-wrap;word-wrap:break-word;margin-bottom:1rem}.mt-body .invisible{font-size:0;line-height:0;display:inline-block;width:0;height:0;position:absolute}.mt-post{margin:.25rem;padding:1rem .5rem;position:relative;min-height:3.75rem;background-color:transparent;border-bottom:1px solid var(--mt-color-line-gray)}.mt-post:focus,.mt-post:hover{cursor:pointer;background-color:var(--mt-color-bg-hover)}.mt-post p:last-child{margin-bottom:0}.mt-post-avatar{margin-right:.75rem}.mt-post-avatar-standard{width:2.25rem;height:2.25rem}.mt-post-avatar-boosted{width:3rem;height:3rem;position:relative}.mt-post-avatar-image-big img{aspect-ratio:1/1;width:2.25rem;height:2.25rem;border-radius:.25rem;overflow:hidden}.mt-post-avatar-image-small img{aspect-ratio:1/1;width:1.5rem;height:1.5rem;top:1.5rem;left:1.5rem;position:absolute;border-radius:.25rem;overflow:hidden}.mt-post-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1rem}.mt-post-header-user{overflow:hidden;padding-right:.75rem}.mt-post-header-user>a{color:var(--mt-color-content-txt)!important;overflow-wrap:anywhere}.mt-container .mt-post-header-user>a:hover{text-decoration:none}.mt-post-header-user-name{font-weight:600}.mt-container .mt-post-header-user:hover .mt-post-header-user-name{text-decoration:underline}.mt-post-header-user-account{display:block;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color:var(--mt-color-contrast-gray)}.mt-post-header-date{display:flex;font-size:.75rem;text-align:right;margin-left:auto}.mt-post-header-date .mt-post-pinned{width:1.25rem;margin-top:-.25rem;fill:var(--mt-color-contrast-gray)}.mt-container .mt-post-header-date>a{white-space:nowrap;color:var(--mt-color-contrast-gray)!important}.mt-post-txt{margin-bottom:1rem;color:var(--mt-color-content-txt)}.mt-post-txt .spoiler-txt-hidden{display:none}.mt-post-txt.truncate{display:-webkit-box;overflow:hidden;-webkit-line-clamp:var(--mt-txt-max-lines);-webkit-box-orient:vertical}.mt-post-txt:not(.truncate) .ellipsis::after{content:"..."}.mt-post-txt blockquote{border-left:.25rem solid var(--mt-color-line-gray);margin-left:0;padding-left:.5rem}.mt-post-header-user .mt-custom-emoji,.mt-post-txt .mt-custom-emoji{height:1.5rem;min-width:1.5rem;margin-bottom:-.25rem;width:auto}.mt-post-poll{margin-bottom:1rem;color:var(--mt-color-content-txt)}.mt-post-poll ul{list-style:none;padding:0;margin:0}.mt-post-poll ul li{font-size:.9rem;margin-bottom:.5rem}.mt-post-poll.mt-post-poll-expired ul li{color:var(--mt-color-contrast-gray)}.mt-post-poll ul li:not(:last-child){margin-bottom:.25rem}.mt-post-poll ul li:before{content:"◯";padding-right:.5rem}.mt-post-poll.mt-post-poll-expired ul li:before{content:"";padding-right:0}.mt-post-media{position:relative;overflow:hidden;margin-bottom:1rem}.mt-post-media-spoiler>.mt-post-media-play-icon,.mt-post-media-spoiler>audio,.mt-post-media-spoiler>img,.mt-post-media-spoiler>video{filter:blur(2rem);pointer-events:none}.mt-post-media>audio{width:100%;position:relative;z-index:1}.mt-post-media>img,.mt-post-media>video{width:100%;height:100%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;color:var(--mt-color-content-txt)}.mt-post-media.mt-loading-spinner .mt-post-media-play-icon{display:none}.mt-post-media-play-icon{display:flex;position:absolute;width:3rem;height:3rem;top:calc(50% - 1.5rem);left:calc(50% - 1.5rem);justify-content:center;align-items:center;background-color:transparent;border:none;cursor:pointer}.mt-post-media-play-icon>svg{width:2.5rem;height:2.5rem;fill:var(--mt-color-bg);stroke:var(--mt-color-content-txt);stroke-width:1px}.mt-post-preview{min-height:4rem;display:flex;flex-direction:row;border:1px solid var(--mt-color-line-gray);border-radius:.5rem;color:var(--mt-color-link);font-size:.8rem;margin:1rem 0;overflow:hidden}.mt-post-preview-image{width:40%;align-self:stretch}.mt-post-preview-image img{display:block;width:100%;height:100%;object-fit:cover;color:var(--mt-color-content-txt)}.mt-post-preview-noImage{width:40%;font-size:1.5rem;align-self:center;text-align:center}.mt-post-preview-content{width:60%;display:flex;align-self:center;flex-direction:column;padding:.5rem 1rem;gap:.5rem}.mt-post-preview-title{font-weight:600}.mt-post-counter-bar{display:flex;min-width:6rem;max-width:40rem;justify-content:space-between;color:var(--mt-color-contrast-gray)}.mt-post-counter-bar-favorites,.mt-post-counter-bar-reblog,.mt-post-counter-bar-replies{display:flex;font-size:.75rem;gap:.25rem;align-items:center;opacity:.5}.mt-post-counter-bar-favorites>svg,.mt-post-counter-bar-reblog>svg,.mt-post-counter-bar-replies>svg{width:1rem;fill:var(--mt-color-contrast-gray)}.mt-container .mt-btn-dark{display:flex;border-radius:.25rem;background-color:var(--mt-color-line-gray);border:0;color:var(--mt-color-content-txt);font-weight:600;font-size:.75rem;text-align:center;padding:0 .5rem;line-height:1.25rem;vertical-align:top}.mt-container .mt-btn-violet,.mt-container a.mt-btn-violet{display:flex;align-items:center;gap:.5rem;border-radius:.25rem;border:.5rem;padding:.5rem .75rem;font-size:1rem;font-weight:600;text-align:center;background-color:var(--mt-color-btn-bg);color:var(--mt-color-btn-txt)}.mt-container .mt-btn-violet:hover,.mt-container a.mt-btn-violet:hover{background-color:var(--mt-color-btn-bg-hover);text-decoration:none}.mt-post-txt .mt-btn-spoiler{display:inline-block}.mt-post-media.mt-loading-spinner>.mt-btn-spoiler{display:none}.mt-post-media>.mt-btn-spoiler{position:absolute;top:50%;left:50%;z-index:2;transform:translate(-50%,-50%)}.mt-error{position:absolute;left:50%;transform:translateX(-50%);display:flex;flex-direction:column;height:calc(100% - 3.5rem);width:calc(100% - 4.5rem);justify-content:center;align-items:center;color:var(--mt-color-error-txt);padding:.75rem;text-align:center}.mt-error-icon{font-size:2rem}.mt-error-message{width:100%;padding:1rem 0}.mt-error-message hr{color:var(--mt-color-line-gray)}.mt-body>.mt-loading-spinner{position:absolute;width:3rem;height:3rem;margin:auto;top:calc(50% - 1.5rem);right:calc(50% - 1.5rem)}.mt-loading-spinner{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 128 128'%3E%3Cg%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 64 64' to='360 64 64' dur='1000ms' repeatCount='indefinite'/%3E%3Cpath d='M64 6.69a57.3 57.3 0 1 1 0 114.61A57.3 57.3 0 0 1 6.69 64' fill='none' stroke='%23404040' stroke-width='12'/%3E%3C/g%3E%3C/svg%3E");background-repeat:no-repeat;background-position:center center;background-color:transparent;background-size:min(2.5rem,calc(100% - .5rem))}.mt-footer{display:flex;flex-flow:wrap;margin:auto auto 2rem auto;padding:0 1.5rem;gap:1.5rem;align-items:center;justify-content:center}.visually-hidden{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important} \ No newline at end of file diff --git a/dist/mastodon-timeline.umd.js b/dist/mastodon-timeline.umd.js index 1f43f1b..12db65f 100644 --- a/dist/mastodon-timeline.umd.js +++ b/dist/mastodon-timeline.umd.js @@ -1,8 +1,8 @@ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).MastodonTimeline={})}(this,(function(t){"use strict"; -/** - * Mastodon embed timeline - * @author idotj - * @version 4.2.1 - * @url https://gitlab.com/idotj/mastodon-embed-timeline - * @license GNU AGPLv3 - */t.Init=class{constructor(t={}){this.defaultSettings={mtContainerId:"mt-container",instanceUrl:"https://mastodon.social",timelineType:"local",userId:"",profileName:"",hashtagName:"",spinnerClass:"mt-loading-spinner",defaultTheme:"auto",maxNbPostFetch:"20",maxNbPostShow:"20",hideUnlisted:!1,hideReblog:!1,hideReplies:!1,hideVideoPreview:!1,hidePreviewLink:!1,hideEmojos:!1,markdownBlockquote:!1,hideCounterBar:!1,txtMaxLines:"0",btnShowMore:"SHOW MORE",btnShowLess:"SHOW LESS",btnShowContent:"SHOW CONTENT",btnSeeMore:"See more posts at Mastodon",btnReload:"Refresh",insistSearchContainer:!1,insistSearchContainerTime:"3000"},this.mtSettings={...this.defaultSettings,...t},this.mtContainerNode="",this.mtBodyNode="",this.fetchedData={},this.#t((()=>{this.#e()}))}#t(t){"undefined"!=typeof document&&"complete"===document.readyState?t():"undefined"!=typeof document&&"complete"!==document.readyState&&document.addEventListener("DOMContentLoaded",t())}#e(){const t=()=>{this.mtContainerNode=document.getElementById(this.mtSettings.mtContainerId),this.mtBodyNode=this.mtContainerNode.getElementsByClassName("mt-body")[0],this.#s(),this.#i("newTimeline")};if(this.mtSettings.insistSearchContainer){const e=performance.now(),s=()=>{if(document.getElementById(this.mtSettings.mtContainerId))t();else{performance.now()-e container with id: "${this.mtSettings.mtContainerId}" after several attempts for ${this.mtSettings.insistSearchContainerTime/1e3} seconds`)}};s()}else document.getElementById(this.mtSettings.mtContainerId)?t():console.error(`Impossible to find the
    container with id: "${this.mtSettings.mtContainerId}". Please try to add the option 'insistSearchContainer: true' when initializing the script`)}mtUpdate(){this.#t((()=>{this.mtBodyNode.replaceChildren(),this.mtBodyNode.insertAdjacentHTML("afterbegin",'
    '),this.#i("updateTimeline")}))}mtColorTheme(t){this.#t((()=>{this.mtContainerNode.setAttribute("data-theme",t)}))}#s(){if("auto"===this.mtSettings.defaultTheme){let t=window.matchMedia("(prefers-color-scheme: dark)");t.matches?this.mtColorTheme("dark"):this.mtColorTheme("light"),t.addEventListener("change",(t=>{t.matches?this.mtColorTheme("dark"):this.mtColorTheme("light")}))}else this.mtColorTheme(this.mtSettings.defaultTheme)}#a(){return new Promise(((t,e)=>{let s={};this.mtSettings.instanceUrl?"profile"===this.mtSettings.timelineType?this.mtSettings.userId?s.timeline=`${this.mtSettings.instanceUrl}/api/v1/accounts/${this.mtSettings.userId}/statuses?limit=${this.mtSettings.maxNbPostFetch}`:this.#o("Please check your userId value","âš ī¸"):"hashtag"===this.mtSettings.timelineType?this.mtSettings.hashtagName?s.timeline=`${this.mtSettings.instanceUrl}/api/v1/timelines/tag/${this.mtSettings.hashtagName}?limit=${this.mtSettings.maxNbPostFetch}`:this.#o("Please check your hashtagName value","âš ī¸"):"local"===this.mtSettings.timelineType?s.timeline=`${this.mtSettings.instanceUrl}/api/v1/timelines/public?local=true&limit=${this.mtSettings.maxNbPostFetch}`:this.#o("Please check your timelineType value","âš ī¸"):this.#o("Please check your instanceUrl value","âš ī¸"),this.mtSettings.hideEmojos||(s.emojos=this.mtSettings.instanceUrl+"/api/v1/custom_emojis");const i=Object.entries(s).map((([t,s])=>async function(t){const e=await fetch(t);if(!e.ok)throw new Error("Failed to fetch the following Url:
    "+t+"
    Error status: "+e.status+"
    Error message: "+e.statusText);return await e.json()}(s).then((e=>({[t]:e}))).catch((i=>(e(new Error("Something went wrong fetching data from: "+s)),this.#o(i.message),{[t]:[]})))));Promise.all(i).then((e=>{this.mtSettings.fetchedData=e.reduce(((t,e)=>({...t,...e})),{}),t()}))}))}async#i(t){await this.#a(),this.mtBodyNode.replaceChildren();let e=0;for(let t in this.mtSettings.fetchedData.timeline)("public"==this.mtSettings.fetchedData.timeline[t].visibility||!this.mtSettings.hideUnlisted&&"unlisted"==this.mtSettings.fetchedData.timeline[t].visibility)&&(this.mtSettings.hideReblog&&this.mtSettings.fetchedData.timeline[t].reblog||this.mtSettings.hideReplies&&this.mtSettings.fetchedData.timeline[t].in_reply_to_id||eThis may be due to an incorrect configuration in the parameters or to filters applied (to hide certains type of posts)";this.#o(t,"📭")}else"newTimeline"===t?(this.#r(),this.#l(),this.#m()):"updateTimeline"===t?this.#r():this.#o("The function buildTimeline() was expecting a param")}#n(t,e){this.mtBodyNode.insertAdjacentHTML("beforeend",this.#d(t,e))}#d(t,e){let s,i,a,o,n,r,l,m,d;t.reblog?(o=t.reblog.url,s='
    '+this.#c(t.reblog.account.username)+' avatar
    '+this.#c(t.account.username)+' avatar
    ',a=t.reblog.account.display_name?t.reblog.account.display_name:t.reblog.account.username,this.mtSettings.hideEmojos||(a=this.#h(a,this.mtSettings.fetchedData.emojos)),i='
    '+a+' account
    ',n=t.reblog.created_at,d=t.reblog.replies_count,m=t.reblog.reblogs_count,l=t.reblog.favourites_count):(o=t.url,s='
    '+this.#c(t.account.username)+' avatar
    ',a=t.account.display_name?t.account.display_name:t.account.username,this.mtSettings.hideEmojos||(a=this.#h(a,this.mtSettings.fetchedData.emojos)),i='
    '+a+' account
    ',n=t.created_at,d=t.replies_count,m=t.reblogs_count,l=t.favourites_count),r=this.#p(n);const c='
    ";let h="";"0"!==this.mtSettings.txtMaxLines&&(h=" truncate",this.mtBodyNode.parentNode.style.setProperty("--mt-txt-max-lines",this.mtSettings.txtMaxLines));let p="";p=""!==t.spoiler_text?'
    '+t.spoiler_text+'
    '+this.#g(t.content)+"
    ":t.reblog&&""!==t.reblog.content&&""!==t.reblog.spoiler_text?'
    '+t.reblog.spoiler_text+'
    '+this.#g(t.reblog.content)+"
    ":t.reblog&&""!==t.reblog.content&&""===t.reblog.spoiler_text?'
    '+this.#g(t.reblog.content)+"
    ":'
    '+this.#g(t.content)+"
    ";let g=[];if(t.media_attachments.length>0)for(let e in t.media_attachments)g.push(this.#u(t.media_attachments[e],t.sensitive));if(t.reblog&&t.reblog.media_attachments.length>0)for(let e in t.reblog.media_attachments)g.push(this.#u(t.reblog.media_attachments[e],t.reblog.sensitive));let u="";!this.mtSettings.hidePreviewLink&&t.card&&(u=this.#v(t.card));let v="";if(t.poll){let e="";for(let s in t.poll.options)e+="
  • "+t.poll.options[s].title+"
  • ";v='
      '+e+"
    "}let b="";if(!this.mtSettings.hideCounterBar){b='
    '+('
    '+d+"
    ")+('
    '+m+"
    ")+('
    '+l+"
    ")+"
    "}return'
    '+s+i+c+"
    "+p+g.join("")+u+v+b+"
    "}#g(t){let e=t;return e=this.#b(e),this.mtSettings.hideEmojos||(e=this.#h(e,this.mtSettings.fetchedData.emojos)),this.mtSettings.markdownBlockquote&&(e=this.#f(e,"

    >","

    ","

    ","

    ")),e}#b(t){let e=t.replaceAll('rel="tag"','rel="tag" target="_blank"');return e=e.replaceAll('class="u-url mention"','class="u-url mention" target="_blank"'),e}#f(t,e,s,i,a){if(t.includes(e)){const o=new RegExp(e+"(.*?)"+s,"gi");return t.replace(o,i+"$1"+a)}return t}#c(t){return(t??"").replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}#h(t,e){if(t.includes(":")){for(const s of e){const e=new RegExp(`\\:${s.shortcode}\\:`,"g");t=t.replace(e,`Emoji ${s.shortcode}`)}return t}return t}#p(t){const e=new Date(t);return["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"][e.getMonth()]+" "+e.getDate()+", "+e.getFullYear()}#u(t,e){const s=e||!1,i=t.type;let a="";return"image"===i&&(a='
    '+(s?'":"")+''+(t.description?this.#c(t.description):
    '),"audio"===i&&(a=t.preview_url?'
    '+(s?'":"")+''+(t.description?this.#c(t.description):
    ':'
    '+(s?'":"")+'
    '),"video"!==i&&"gifv"!==i||(a=this.mtSettings.hideVideoPreview?'
    '+(s?'":"")+'
    ':'
    '+(s?'":"")+''+(t.description?this.#c(t.description):
    '),a}#S(t){const e=t.target.closest("[data-video-url]"),s=e.dataset.videoUrl;e.replaceChildren(),e.innerHTML=''}#w(t){const e=t.target.nextSibling;"img"===e.localName||"audio"===e.localName||"video"===e.localName?(t.target.parentNode.classList.remove("mt-post-media-spoiler"),t.target.style.display="none"):(e.classList.contains("spoiler-txt-hidden")||e.classList.contains("spoiler-txt-visible"))&&(t.target.textContent==this.mtSettings.btnShowMore?(e.classList.remove("spoiler-txt-hidden"),e.classList.add("spoiler-txt-visible"),t.target.setAttribute("aria-expanded","true"),t.target.textContent=this.mtSettings.btnShowLess):(e.classList.remove("spoiler-txt-visible"),e.classList.add("spoiler-txt-hidden"),t.target.setAttribute("aria-expanded","false"),t.target.textContent=this.mtSettings.btnShowMore))}#v(t){return''+(t.image?'
    '+this.#c(t.image_description)+'
    ':'
    📄
    ')+'
    '+(t.provider_name?''+this.#y(t.provider_name)+"":"")+''+t.title+""+(t.author_name?''+this.#y(t.author_name)+"":"")+"
    "}#y(t){return(new DOMParser).parseFromString(t,"text/html").body.textContent}#m(){if(this.mtSettings.btnSeeMore||this.mtSettings.btnReload){this.mtBodyNode.parentNode.insertAdjacentHTML("beforeend",'');const t=this.mtContainerNode.getElementsByClassName("mt-footer")[0];if(this.mtSettings.btnSeeMore){let e="";"profile"===this.mtSettings.timelineType?this.mtSettings.profileName?e=this.mtSettings.profileName:this.#o("Please check your profileName value","âš ī¸"):"hashtag"===this.mtSettings.timelineType?e="tags/"+this.mtSettings.hashtagName:"local"===this.mtSettings.timelineType&&(e="public/local");const s=''+this.mtSettings.btnSeeMore+"";t.insertAdjacentHTML("beforeend",s)}if(this.mtSettings.btnReload){const e='";t.insertAdjacentHTML("beforeend",e);this.mtContainerNode.getElementsByClassName("btn-refresh")[0].addEventListener("click",(()=>{this.mtUpdate()}))}}}#l(){this.mtBodyNode.addEventListener("click",(t=>{("article"==t.target.localName||"article"==t.target.offsetParent?.localName||"img"==t.target.localName&&!t.target.parentNode.getAttribute("data-video-url"))&&this.#N(t),"button"==t.target.localName&&t.target.classList.contains("mt-btn-spoiler")&&this.#w(t),("mt-post-media-play-icon"==t.target.className||"svg"==t.target.localName&&"mt-post-media-play-icon"==t.target.parentNode.className||"path"==t.target.localName&&"mt-post-media-play-icon"==t.target.parentNode.parentNode.className||"img"==t.target.localName&&t.target.parentNode.getAttribute("data-video-url"))&&this.#S(t)})),this.mtBodyNode.addEventListener("keydown",(t=>{"Enter"===t.key&&"article"==t.target.localName&&this.#N(t)}))}#N(t){const e=t.target.closest(".mt-post").dataset.location;"a"!==t.target.localName&&"span"!==t.target.localName&&"button"!==t.target.localName&&"time"!==t.target.localName&&"mt-post-preview-noImage"!==t.target.className&&"mt-post-avatar-image-big"!==t.target.parentNode.className&&"mt-post-avatar-image-small"!==t.target.parentNode.className&&"mt-post-preview-image"!==t.target.parentNode.className&&"mt-post-preview"!==t.target.parentNode.className&&e&&window.open(e,"_blank","noopener")}#r(){const t=e=>{e.target.parentNode.classList.remove(this.mtSettings.spinnerClass),e.target.removeEventListener("load",t),e.target.removeEventListener("error",t)};this.mtBodyNode.querySelectorAll(`.${this.mtSettings.spinnerClass} > img`).forEach((e=>{e.addEventListener("load",t),e.addEventListener("error",t)}))}#o(t,e){const s=e||"❌";throw this.mtBodyNode.innerHTML='
    '+s+'
    Oops, something\'s happened:
    '+t+"
    ",this.mtBodyNode.setAttribute("role","none"),new Error("Stopping the script due to an error building the timeline.")}}})); +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).MastodonTimeline={})}(this,(function(t){"use strict"; +/** + * Mastodon embed timeline + * @author idotj + * @version 4.3.1 + * @url https://gitlab.com/idotj/mastodon-embed-timeline + * @license GNU AGPLv3 + */t.Init=class{constructor(t={}){this.defaultSettings={mtContainerId:"mt-container",instanceUrl:"https://mastodon.social",timelineType:"local",userId:"",profileName:"",hashtagName:"",spinnerClass:"mt-loading-spinner",defaultTheme:"auto",maxNbPostFetch:"20",maxNbPostShow:"20",dateLocale:"en-GB",dateOptions:{day:"2-digit",month:"short",year:"numeric"},hideUnlisted:!1,hideReblog:!1,hideReplies:!1,hidePinnedPosts:!1,hideUserAccount:!1,hideEmojos:!1,hideVideoPreview:!1,hidePreviewLink:!1,hideCounterBar:!1,markdownBlockquote:!1,txtMaxLines:"0",btnShowMore:"SHOW MORE",btnShowLess:"SHOW LESS",btnShowContent:"SHOW CONTENT",btnSeeMore:"See more posts at Mastodon",btnReload:"Refresh",insistSearchContainer:!1,insistSearchContainerTime:"3000"},this.mtSettings={...this.defaultSettings,...t},this.mtContainerNode="",this.mtBodyNode="",this.fetchedData={},this.#t((()=>{this.#e()}))}#t(t){"undefined"!=typeof document&&"complete"===document.readyState?t():"undefined"!=typeof document&&"complete"!==document.readyState&&document.addEventListener("DOMContentLoaded",t())}#e(){const t=()=>{this.mtContainerNode=document.getElementById(this.mtSettings.mtContainerId),this.mtBodyNode=this.mtContainerNode.getElementsByClassName("mt-body")[0],this.#s(),this.#i("newTimeline")};if(this.mtSettings.insistSearchContainer){const e=performance.now(),s=()=>{if(document.getElementById(this.mtSettings.mtContainerId))t();else{performance.now()-e container with id: "${this.mtSettings.mtContainerId}" after several attempts for ${this.mtSettings.insistSearchContainerTime/1e3} seconds`)}};s()}else document.getElementById(this.mtSettings.mtContainerId)?t():console.error(`Impossible to find the
    container with id: "${this.mtSettings.mtContainerId}". Please try to add the option 'insistSearchContainer: true' when initializing the script`)}mtUpdate(){this.#t((()=>{this.mtBodyNode.replaceChildren(),this.mtBodyNode.insertAdjacentHTML("afterbegin",'
    '),this.#i("updateTimeline")}))}mtColorTheme(t){this.#t((()=>{this.mtContainerNode.setAttribute("data-theme",t)}))}#s(){if("auto"===this.mtSettings.defaultTheme){let t=window.matchMedia("(prefers-color-scheme: dark)");t.matches?this.mtColorTheme("dark"):this.mtColorTheme("light"),t.addEventListener("change",(t=>{t.matches?this.mtColorTheme("dark"):this.mtColorTheme("light")}))}else this.mtColorTheme(this.mtSettings.defaultTheme)}#a(){return new Promise(((t,e)=>{let s={};this.mtSettings.instanceUrl?"profile"===this.mtSettings.timelineType?this.mtSettings.userId?(s.timeline=`${this.mtSettings.instanceUrl}/api/v1/accounts/${this.mtSettings.userId}/statuses?limit=${this.mtSettings.maxNbPostFetch}`,this.mtSettings.hidePinnedPosts||(s.pinned=`${this.mtSettings.instanceUrl}/api/v1/accounts/${this.mtSettings.userId}/statuses?pinned=true`)):this.#o("Please check your userId value","âš ī¸"):"hashtag"===this.mtSettings.timelineType?this.mtSettings.hashtagName?s.timeline=`${this.mtSettings.instanceUrl}/api/v1/timelines/tag/${this.mtSettings.hashtagName}?limit=${this.mtSettings.maxNbPostFetch}`:this.#o("Please check your hashtagName value","âš ī¸"):"local"===this.mtSettings.timelineType?s.timeline=`${this.mtSettings.instanceUrl}/api/v1/timelines/public?local=true&limit=${this.mtSettings.maxNbPostFetch}`:this.#o("Please check your timelineType value","âš ī¸"):this.#o("Please check your instanceUrl value","âš ī¸"),this.mtSettings.hideEmojos||(s.emojos=this.mtSettings.instanceUrl+"/api/v1/custom_emojis");const i=Object.entries(s).map((([t,s])=>async function(t){const e=await fetch(t);if(!e.ok)throw new Error("Failed to fetch the following Url:
    "+t+"
    Error status: "+e.status+"
    Error message: "+e.statusText);return await e.json()}(s).then((e=>({[t]:e}))).catch((i=>(e(new Error("Something went wrong fetching data from: "+s)),this.#o(i.message),{[t]:[]})))));Promise.all(i).then((e=>{this.fetchedData=e.reduce(((t,e)=>({...t,...e})),{}),t()}))}))}async#i(t){let e;if(await this.#a(),this.mtSettings.hidePinnedPosts||void 0===this.fetchedData.pinned?.length||0===this.fetchedData.pinned.length)e=this.fetchedData.timeline;else{e=[...this.fetchedData.pinned.map((t=>({...t,pinned:!0}))),...this.fetchedData.timeline]}this.mtBodyNode.replaceChildren();let s=0;for(let t in e)("public"==e[t].visibility||!this.mtSettings.hideUnlisted&&"unlisted"==e[t].visibility)&&(this.mtSettings.hideReblog&&e[t].reblog||this.mtSettings.hideReplies&&e[t].in_reply_to_id||sThis may be due to an incorrect configuration in the parameters or to filters applied (to hide certains type of posts)";this.#o(t,"📭")}else"newTimeline"===t?(this.#r(),this.#l(),this.#m()):"updateTimeline"===t?this.#r():this.#o("The function buildTimeline() was expecting a param")}#n(t,e){this.mtBodyNode.insertAdjacentHTML("beforeend",this.#d(t,e))}#d(t,e){let s,i,a,o,n,r,l,m,d,h;t.reblog?(n=t.reblog.url,s='
    '+this.#h(t.reblog.account.username)+' avatar
    '+this.#h(t.account.username)+' avatar
    ',a=!this.mtSettings.hideEmojos&&t.reblog.account.display_name?this.#c(t.reblog.account.display_name,this.fetchedData.emojos):t.reblog.account.display_name,o=this.mtSettings.hideUserAccount?"":'
    ",i='
    '+a+""+o+"
    ",r=t.reblog.created_at,h=t.reblog.replies_count,d=t.reblog.reblogs_count,m=t.reblog.favourites_count):(n=t.url,s='
    '+this.#h(t.account.username)+' avatar
    ',a=!this.mtSettings.hideEmojos&&t.account.display_name?this.#c(t.account.display_name,this.fetchedData.emojos):t.account.display_name,o=this.mtSettings.hideUserAccount?"":'
    ",i='
    '+a+""+o+"
    ",r=t.created_at,h=t.replies_count,d=t.reblogs_count,m=t.favourites_count),l=this.#p(r);const c='
    '+(t.pinned?'':"")+'"+(t.edited_at?" *":"")+"
    ";let p="";"0"!==this.mtSettings.txtMaxLines&&(p=" truncate",this.mtBodyNode.parentNode.style.setProperty("--mt-txt-max-lines",this.mtSettings.txtMaxLines));let g="";g=""!==t.spoiler_text?'
    '+t.spoiler_text+'
    '+this.#g(t.content)+"
    ":t.reblog&&""!==t.reblog.content&&""!==t.reblog.spoiler_text?'
    '+t.reblog.spoiler_text+'
    '+this.#g(t.reblog.content)+"
    ":t.reblog&&""!==t.reblog.content&&""===t.reblog.spoiler_text?'
    '+this.#g(t.reblog.content)+"
    ":'
    '+this.#g(t.content)+"
    ";let u=[];if(t.media_attachments.length>0)for(let e in t.media_attachments)u.push(this.#u(t.media_attachments[e],t.sensitive));if(t.reblog&&t.reblog.media_attachments.length>0)for(let e in t.reblog.media_attachments)u.push(this.#u(t.reblog.media_attachments[e],t.reblog.sensitive));let v="";!this.mtSettings.hidePreviewLink&&t.card&&(v=this.#v(t.card));let b="";if(t.poll){let e="";for(let s in t.poll.options)e+="
  • "+t.poll.options[s].title+"
  • ";b='
      '+e+"
    "}let f="";if(!this.mtSettings.hideCounterBar){f='
    '+('
    '+h+"
    ")+('
    '+d+"
    ")+('
    '+m+"
    ")+"
    "}return'
    '+s+i+c+"
    "+g+u.join("")+v+b+f+"
    "}#g(t){let e=t;return e=this.#b(e),this.mtSettings.hideEmojos||(e=this.#c(e,this.fetchedData.emojos)),this.mtSettings.markdownBlockquote&&(e=this.#f(e,"

    >","

    ","

    ","

    ")),e}#b(t){let e=t.replaceAll('rel="tag"','rel="tag" target="_blank"');return e=e.replaceAll('class="u-url mention"','class="u-url mention" target="_blank"'),e}#f(t,e,s,i,a){if(t.includes(e)){const o=new RegExp(e+"(.*?)"+s,"gi");return t.replace(o,i+"$1"+a)}return t}#h(t){return(t??"").replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}#c(t,e){if(t.includes(":")){for(const s of e){const e=new RegExp(`\\:${s.shortcode}\\:`,"g");t=t.replace(e,`Emoji ${s.shortcode}`)}return t}return t}#p(t){const e=new Date(t);return new Intl.DateTimeFormat(this.mtSettings.dateLocale,this.mtSettings.dateOptions).format(e)}#u(t,e){const s=e||!1,i=t.type;let a="";return"image"===i&&(a='
    '+(s?'":"")+''+(t.description?this.#h(t.description):
    '),"audio"===i&&(a=t.preview_url?'
    '+(s?'":"")+''+(t.description?this.#h(t.description):
    ':'
    '+(s?'":"")+'
    '),"video"!==i&&"gifv"!==i||(a=this.mtSettings.hideVideoPreview?'
    '+(s?'":"")+'
    ':'
    '+(s?'":"")+''+(t.description?this.#h(t.description):
    '),a}#w(t){const e=t.target.closest("[data-video-url]"),s=e.dataset.videoUrl;e.replaceChildren(),e.innerHTML=''}#S(t){const e=t.target.nextSibling;"img"===e.localName||"audio"===e.localName||"video"===e.localName?(t.target.parentNode.classList.remove("mt-post-media-spoiler"),t.target.style.display="none"):(e.classList.contains("spoiler-txt-hidden")||e.classList.contains("spoiler-txt-visible"))&&(t.target.textContent==this.mtSettings.btnShowMore?(e.classList.remove("spoiler-txt-hidden"),e.classList.add("spoiler-txt-visible"),t.target.setAttribute("aria-expanded","true"),t.target.textContent=this.mtSettings.btnShowLess):(e.classList.remove("spoiler-txt-visible"),e.classList.add("spoiler-txt-hidden"),t.target.setAttribute("aria-expanded","false"),t.target.textContent=this.mtSettings.btnShowMore))}#v(t){return''+(t.image?'
    '+this.#h(t.image_description)+'
    ':'
    📄
    ')+'
    '+(t.provider_name?''+this.#y(t.provider_name)+"":"")+''+t.title+""+(t.author_name?''+this.#y(t.author_name)+"":"")+"
    "}#y(t){return(new DOMParser).parseFromString(t,"text/html").body.textContent}#m(){if(this.mtSettings.btnSeeMore||this.mtSettings.btnReload){this.mtBodyNode.parentNode.insertAdjacentHTML("beforeend",'');const t=this.mtContainerNode.getElementsByClassName("mt-footer")[0];if(this.mtSettings.btnSeeMore){let e="";"profile"===this.mtSettings.timelineType?this.mtSettings.profileName?e=this.mtSettings.profileName:this.#o("Please check your profileName value","âš ī¸"):"hashtag"===this.mtSettings.timelineType?e="tags/"+this.mtSettings.hashtagName:"local"===this.mtSettings.timelineType&&(e="public/local");const s=''+this.mtSettings.btnSeeMore+"";t.insertAdjacentHTML("beforeend",s)}if(this.mtSettings.btnReload){const e='";t.insertAdjacentHTML("beforeend",e);this.mtContainerNode.getElementsByClassName("btn-refresh")[0].addEventListener("click",(()=>{this.mtUpdate()}))}}}#l(){this.mtBodyNode.addEventListener("click",(t=>{("article"==t.target.localName||"article"==t.target.offsetParent?.localName||"img"==t.target.localName&&!t.target.parentNode.getAttribute("data-video-url"))&&this.#N(t),"button"==t.target.localName&&t.target.classList.contains("mt-btn-spoiler")&&this.#S(t),("mt-post-media-play-icon"==t.target.className||"svg"==t.target.localName&&"mt-post-media-play-icon"==t.target.parentNode.className||"path"==t.target.localName&&"mt-post-media-play-icon"==t.target.parentNode.parentNode.className||"img"==t.target.localName&&t.target.parentNode.getAttribute("data-video-url"))&&this.#w(t)})),this.mtBodyNode.addEventListener("keydown",(t=>{"Enter"===t.key&&"article"==t.target.localName&&this.#N(t)}))}#N(t){const e=t.target.closest(".mt-post").dataset.location;"a"!==t.target.localName&&"span"!==t.target.localName&&"button"!==t.target.localName&&"time"!==t.target.localName&&"mt-post-preview-noImage"!==t.target.className&&"mt-post-avatar-image-big"!==t.target.parentNode.className&&"mt-post-avatar-image-small"!==t.target.parentNode.className&&"mt-post-preview-image"!==t.target.parentNode.className&&"mt-post-preview"!==t.target.parentNode.className&&e&&window.open(e,"_blank","noopener")}#r(){const t=e=>{e.target.parentNode.classList.remove(this.mtSettings.spinnerClass),e.target.removeEventListener("load",t),e.target.removeEventListener("error",t)};this.mtBodyNode.querySelectorAll(`.${this.mtSettings.spinnerClass} > img`).forEach((e=>{e.addEventListener("load",t),e.addEventListener("error",t)}))}#o(t,e){const s=e||"❌";throw this.mtBodyNode.innerHTML='
    '+s+'
    Oops, something\'s happened:
    '+t+"
    ",this.mtBodyNode.setAttribute("role","none"),new Error("Stopping the script due to an error building the timeline.")}}})); diff --git a/examples/hashtag-timeline.html b/examples/hashtag-timeline.html index 7c5aa03..83511fe 100644 --- a/examples/hashtag-timeline.html +++ b/examples/hashtag-timeline.html @@ -67,7 +67,7 @@

    🐘 Mastodon embed timeline

    -

    Profile timeline

    +

    Hashtag timeline

    This example shows posts containing the hashtag
    diff --git a/examples/local-timeline-customized.html b/examples/local-timeline-customized.html new file mode 100644 index 0000000..d9f8f43 --- /dev/null +++ b/examples/local-timeline-customized.html @@ -0,0 +1,169 @@ + + + + + Mastodon embed timeline + + + + + + + + + + +

    + +
    +

    🐘 Mastodon embed timeline

    +

    Local timeline (customized)

    +

    + This example shows 10 posts from the following instance: +
    + mastodon.social +

    +

    + Contains several CSS styles that change its appearance without the + need to modify the original CSS file (inspect the code at the + beginning of the HTML file to see the changes). +

    +

    + At JS level, it defaults to the light theme and the date is displayed + in US format using digits only. In order to achieve a minimalist + style, the following options have been changed at its + initialization: +

    +
    +        
    +  <script>
    +    const myTimeline = new MastodonTimeline.Init({
    +      instanceUrl: "https://mastodon.online",
    +      defaultTheme: "light",
    +      dateLocale: "en-CA",
    +      dateOptions: {
    +        day: "2-digit",
    +        month: "2-digit",
    +        year: "numeric",
    +      },
    +      hideReplies: true,
    +      hideUserAccount: true,
    +      hidePreviewLink: true,
    +      hideCounterBar: true,
    +      txtMaxLines: "3",
    +      btnSeeMore: "",
    +      btnReload: ""
    +    });
    +  </script>
    +          
    +        
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    + + + + + + diff --git a/package-lock.json b/package-lock.json index bc6d080..435131c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@idotj/mastodon-embed-timeline", - "version": "4.2.1", + "version": "4.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@idotj/mastodon-embed-timeline", - "version": "4.2.1", + "version": "4.3.1", "license": "GNU", "devDependencies": { "@rollup/plugin-terser": "^0.4.4", diff --git a/package.json b/package.json index 21a026f..d28a622 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@idotj/mastodon-embed-timeline", - "version": "4.2.1", + "version": "4.3.1", "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": { diff --git a/src/mastodon-timeline.css b/src/mastodon-timeline.css index e02a3b2..06844da 100644 --- a/src/mastodon-timeline.css +++ b/src/mastodon-timeline.css @@ -1,4 +1,4 @@ -/* Mastodon embed timeline v4.2.1 */ +/* Mastodon embed timeline v4.3.1 */ /* More info at: */ /* https://gitlab.com/idotj/mastodon-embed-timeline */ @@ -151,22 +151,42 @@ margin-bottom: 1rem; } .mt-post-header-user { - font-weight: 600; - margin-top: 0.5rem; - padding-right: 1rem; + overflow: hidden; + padding-right: 0.75rem; } .mt-post-header-user > a { - display: flex; - align-items: flex-start; color: var(--mt-color-content-txt) !important; overflow-wrap: anywhere; } +.mt-container .mt-post-header-user > a:hover { + text-decoration: none; +} +.mt-post-header-user-name { + font-weight: 600; +} +.mt-container .mt-post-header-user:hover .mt-post-header-user-name { + text-decoration: underline; +} +.mt-post-header-user-account { + display: block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + color: var(--mt-color-contrast-gray); +} .mt-post-header-date { + display: flex; font-size: 0.75rem; text-align: right; - margin: 0.5rem 0 0 auto; + margin-left: auto; } -.mt-post-header-date > a { +.mt-post-header-date .mt-post-pinned { + width: 1.25rem; + margin-top: -0.25rem; + fill: var(--mt-color-contrast-gray); +} +.mt-container .mt-post-header-date > a { + white-space: nowrap; color: var(--mt-color-contrast-gray) !important; } diff --git a/src/mastodon-timeline.js b/src/mastodon-timeline.js index ce2d69b..69ea34d 100644 --- a/src/mastodon-timeline.js +++ b/src/mastodon-timeline.js @@ -1,1135 +1,1168 @@ -/** - * Mastodon embed timeline - * @author idotj - * @version 4.2.1 - * @url https://gitlab.com/idotj/mastodon-embed-timeline - * @license GNU AGPLv3 - */ -"use strict"; - -export class Init { - constructor(customSettings = {}) { - this.defaultSettings = { - mtContainerId: "mt-container", - instanceUrl: "https://mastodon.social", - timelineType: "local", - userId: "", - profileName: "", - hashtagName: "", - spinnerClass: "mt-loading-spinner", - defaultTheme: "auto", - maxNbPostFetch: "20", - maxNbPostShow: "20", - hideUnlisted: false, - hideReblog: false, - hideReplies: false, - hideVideoPreview: false, - hidePreviewLink: false, - hideEmojos: false, - markdownBlockquote: false, - hideCounterBar: false, - txtMaxLines: "0", - btnShowMore: "SHOW MORE", - btnShowLess: "SHOW LESS", - btnShowContent: "SHOW CONTENT", - btnSeeMore: "See more posts at Mastodon", - btnReload: "Refresh", - insistSearchContainer: false, - insistSearchContainerTime: "3000", - }; - - this.mtSettings = { ...this.defaultSettings, ...customSettings }; - - this.mtContainerNode = ""; - this.mtBodyNode = ""; - this.fetchedData = {}; - - this.#onDOMContentLoaded(() => { - this.#getContainerNode(); - }); - } - - /** - * Trigger callback when DOM loaded or completed - * @param {function} c Callback executed - */ - #onDOMContentLoaded(c) { - if (typeof document !== "undefined" && document.readyState === "complete") { - c(); - } else if ( - typeof document !== "undefined" && - document.readyState !== "complete" - ) { - document.addEventListener("DOMContentLoaded", c()); - } - } - - /** - * Find main container in DOM before building the timeline - */ - #getContainerNode() { - // console.log("Initializing Mastodon timeline with settings: ", this.mtSettings); - - const assignContainerNode = () => { - this.mtContainerNode = document.getElementById( - this.mtSettings.mtContainerId - ); - this.mtBodyNode = - this.mtContainerNode.getElementsByClassName("mt-body")[0]; - this.#loadColorTheme(); - this.#buildTimeline("newTimeline"); - }; - - // Some frameworks render the DOM in deferred way and need some extra time - if (this.mtSettings.insistSearchContainer) { - const startCheck = performance.now(); - const findContainerNode = () => { - // Check if the container is ready in DOM - if (document.getElementById(this.mtSettings.mtContainerId)) { - assignContainerNode(); - } else { - // If the container is not found, check again - const currentCheck = performance.now(); - if ( - currentCheck - startCheck < - this.mtSettings.insistSearchContainerTime - ) { - requestAnimationFrame(findContainerNode); - } else { - console.error( - `Impossible to find the
    container with id: "${ - this.mtSettings.mtContainerId - }" after several attempts for ${ - this.mtSettings.insistSearchContainerTime / 1000 - } seconds` - ); - } - } - }; - findContainerNode(); - } else { - if (document.getElementById(this.mtSettings.mtContainerId)) { - assignContainerNode(); - } else { - console.error( - `Impossible to find the
    container with id: "${this.mtSettings.mtContainerId}". Please try to add the option 'insistSearchContainer: true' when initializing the script` - ); - } - } - } - - /** - * Reload the timeline by fetching the lastest posts - */ - mtUpdate() { - this.#onDOMContentLoaded(() => { - this.mtBodyNode.replaceChildren(); - this.mtBodyNode.insertAdjacentHTML( - "afterbegin", - '
    ' - ); - this.#buildTimeline("updateTimeline"); - }); - } - - /** - * Apply the color theme in the timeline - * @param {string} themeType Type of color theme - */ - mtColorTheme(themeType) { - this.#onDOMContentLoaded(() => { - this.mtContainerNode.setAttribute("data-theme", themeType); - }); - } - - /** - * Get the theme style chosen by the user or by the browser/OS - */ - #loadColorTheme() { - if (this.mtSettings.defaultTheme === "auto") { - let systemTheme = window.matchMedia("(prefers-color-scheme: dark)"); - systemTheme.matches - ? this.mtColorTheme("dark") - : this.mtColorTheme("light"); - // Update the theme if user change browser/OS preference - systemTheme.addEventListener("change", (e) => { - e.matches ? this.mtColorTheme("dark") : this.mtColorTheme("light"); - }); - } else { - this.mtColorTheme(this.mtSettings.defaultTheme); - } - } - - /** - * Requests to the server to collect all the data - * @returns {object} Data container - */ - #fetchTimelineData() { - return new Promise((resolve, reject) => { - /** - * Fetch data from server - * @param {string} url address to fetch - * @returns {array} 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 + - "
    " + - "Error status: " + - response.status + - "
    " + - "Error message: " + - response.statusText - ); - } - - const data = await response.json(); - return data; - } - - // Urls to fetch - let urls = {}; - - if (this.mtSettings.instanceUrl) { - if (this.mtSettings.timelineType === "profile") { - if (this.mtSettings.userId) { - urls.timeline = `${this.mtSettings.instanceUrl}/api/v1/accounts/${this.mtSettings.userId}/statuses?limit=${this.mtSettings.maxNbPostFetch}`; - } else { - this.#showError( - "Please check your userId value", - "âš ī¸" - ); - } - } else if (this.mtSettings.timelineType === "hashtag") { - if (this.mtSettings.hashtagName) { - urls.timeline = `${this.mtSettings.instanceUrl}/api/v1/timelines/tag/${this.mtSettings.hashtagName}?limit=${this.mtSettings.maxNbPostFetch}`; - } else { - this.#showError( - "Please check your hashtagName value", - "âš ī¸" - ); - } - } else if (this.mtSettings.timelineType === "local") { - urls.timeline = `${this.mtSettings.instanceUrl}/api/v1/timelines/public?local=true&limit=${this.mtSettings.maxNbPostFetch}`; - } else { - this.#showError( - "Please check your timelineType value", - "âš ī¸" - ); - } - } else { - this.#showError( - "Please check your instanceUrl value", - "âš ī¸" - ); - } - if (!this.mtSettings.hideEmojos) { - urls.emojos = this.mtSettings.instanceUrl + "/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 from: " + url) - ); - this.#showError(error.message); - return { [key]: [] }; - }); - }); - - // Fetch all urls simultaneously - Promise.all(urlsPromises).then((dataObjects) => { - this.mtSettings.fetchedData = dataObjects.reduce((result, dataItem) => { - return { ...result, ...dataItem }; - }, {}); - - // console.log("Mastodon timeline data fetched: ", this.mtSettings.fetchedData); - resolve(); - }); - }); - } - - /** - * Filter all fetched posts and append them on the timeline - * @param {string} t Type of build (new or reload) - */ - async #buildTimeline(t) { - await this.#fetchTimelineData(); - - // Empty container body - this.mtBodyNode.replaceChildren(); - - // Set posts counter to 0 - let nbPostShow = 0; - - for (let i in this.mtSettings.fetchedData.timeline) { - // First filter (Public / Unlisted) - if ( - this.mtSettings.fetchedData.timeline[i].visibility == "public" || - (!this.mtSettings.hideUnlisted && - this.mtSettings.fetchedData.timeline[i].visibility == "unlisted") - ) { - // Second filter (Reblog / Replies) - if ( - (this.mtSettings.hideReblog && - this.mtSettings.fetchedData.timeline[i].reblog) || - (this.mtSettings.hideReplies && - this.mtSettings.fetchedData.timeline[i].in_reply_to_id) - ) { - // Nothing here (Don't append posts) - } else { - if (nbPostShow < this.mtSettings.maxNbPostShow) { - this.#appendPost( - this.mtSettings.fetchedData.timeline[i], - Number(i) - ); - nbPostShow++; - } else { - // Nothing here (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
    " + - (this.mtSettings.fetchedData.timeline?.length || 0) + - " posts have been fetched from the server
    This may be due to an incorrect configuration in the parameters or to filters applied (to hide certains type of posts)"; - this.#showError(errorMessage, "📭"); - } else { - if (t === "newTimeline") { - this.#manageSpinner(); - this.#setPostsInteracion(); - this.#buildFooter(); - } else if (t === "updateTimeline") { - this.#manageSpinner(); - } else { - this.#showError("The function buildTimeline() was expecting a param"); - } - } - } - - /** - * Add each post in the timeline container - * @param {object} c Post content - * @param {number} i Index of post - */ - #appendPost(c, i) { - this.mtBodyNode.insertAdjacentHTML("beforeend", this.#assamblePost(c, i)); - } - - /** - * Build post structure - * @param {object} c Post content - * @param {number} i Index of post - */ - #assamblePost(c, i) { - let avatar, - user, - userName, - url, - date, - formattedDate, - favoritesCount, - reblogCount, - repliesCount; - - if (c.reblog) { - // BOOSTED post - // Post url - url = c.reblog.url; - - // Boosted avatar - avatar = - '' + - '
    ' + - '
    ' + - '' +
-        this.#escapeHtml(c.reblog.account.username) +
-        ' avatar' + - "
    " + - '
    ' + - '' +
-        this.#escapeHtml(c.account.username) +
-        ' avatar' + - "
    " + - "
    " + - "
    "; - - // User name and url - userName = c.reblog.account.display_name - ? c.reblog.account.display_name - : c.reblog.account.username; - if (!this.mtSettings.hideEmojos) { - userName = this.#createEmoji( - userName, - this.mtSettings.fetchedData.emojos - ); - } - user = - '
    ' + - '' + - userName + - ' account' + - "" + - "
    "; - - // Date - date = c.reblog.created_at; - - // Counter bar - repliesCount = c.reblog.replies_count; - reblogCount = c.reblog.reblogs_count; - favoritesCount = c.reblog.favourites_count; - } else { - // STANDARD post - // Post url - url = c.url; - - // Avatar - avatar = - '' + - '
    ' + - '
    ' + - '' +
-        this.#escapeHtml(c.account.username) +
-        ' avatar' + - "
    " + - "
    " + - "
    "; - - // User name and url - userName = c.account.display_name - ? c.account.display_name - : c.account.username; - if (!this.mtSettings.hideEmojos) { - userName = this.#createEmoji( - userName, - this.mtSettings.fetchedData.emojos - ); - } - user = - '
    ' + - '' + - userName + - ' account' + - "" + - "
    "; - - // Date - date = c.created_at; - - // Counter bar - repliesCount = c.replies_count; - reblogCount = c.reblogs_count; - favoritesCount = c.favourites_count; - } - - // Date - formattedDate = this.#formatDate(date); - const timestamp = - '
    ' + - '' + - '" + - "" + - "
    "; - - // 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 (c.spoiler_text !== "") { - content = - '
    ' + - c.spoiler_text + - ' " + - '
    ' + - this.#formatPostText(c.content) + - "
    " + - "
    "; - } else if ( - c.reblog && - c.reblog.content !== "" && - c.reblog.spoiler_text !== "" - ) { - content = - '
    ' + - c.reblog.spoiler_text + - ' " + - '
    ' + - this.#formatPostText(c.reblog.content) + - "
    " + - "
    "; - } else if ( - c.reblog && - c.reblog.content !== "" && - c.reblog.spoiler_text === "" - ) { - content = - '
    ' + - '
    ' + - this.#formatPostText(c.reblog.content) + - "
    " + - "
    "; - } else { - content = - '
    ' + - '
    ' + - this.#formatPostText(c.content) + - "
    " + - "
    "; - } - - // Media attachments - let media = []; - if (c.media_attachments.length > 0) { - for (let i in c.media_attachments) { - media.push(this.#createMedia(c.media_attachments[i], c.sensitive)); - } - } - if (c.reblog && c.reblog.media_attachments.length > 0) { - for (let i in c.reblog.media_attachments) { - media.push( - this.#createMedia(c.reblog.media_attachments[i], c.reblog.sensitive) - ); - } - } - - // Preview link - let previewLink = ""; - if (!this.mtSettings.hidePreviewLink && c.card) { - previewLink = this.#createPreviewLink(c.card); - } - - // Poll - let poll = ""; - if (c.poll) { - let pollOption = ""; - for (let i in c.poll.options) { - pollOption += "
  • " + c.poll.options[i].title + "
  • "; - } - poll = - '
    ' + - "
      " + - pollOption + - "
    " + - "
    "; - } - - // Counter bar - let counterBar = ""; - if (!this.mtSettings.hideCounterBar) { - const repliesTag = - '
    ' + - '' + - repliesCount + - "
    "; - - const reblogTag = - '
    ' + - '' + - reblogCount + - "
    "; - - const favoritesTag = - '
    ' + - '' + - favoritesCount + - "
    "; - - counterBar = - '
    ' + - repliesTag + - reblogTag + - favoritesTag + - "
    "; - } - - // Add all to main post container - const post = - '
    ' + - '
    ' + - avatar + - user + - timestamp + - "
    " + - content + - media.join("") + - previewLink + - poll + - counterBar + - "
    "; - - return post; - } - - /** - * Handle text changes made to posts - * @param {string} c Text content - * @returns {string} Text content modified - */ - #formatPostText(c) { - let content = c; - - // Format hashtags and mentions - content = this.#addTarget2hashtagMention(content); - - // Convert emojos shortcode into images - if (!this.mtSettings.hideEmojos) { - content = this.#createEmoji(content, this.mtSettings.fetchedData.emojos); - } - - // Convert markdown styles into HTML - if (this.mtSettings.markdownBlockquote) { - content = this.#replaceHTMLtag( - content, - "

    >", - "

    ", - "

    ", - "

    " - ); - } - - return content; - } - - /** - * Add target="_blank" to all #hashtags and @mentions in the post - * @param {string} c Text content - * @returns {string} Text content modified - */ - #addTarget2hashtagMention(c) { - let content = c.replaceAll('rel="tag"', 'rel="tag" target="_blank"'); - content = content.replaceAll( - 'class="u-url mention"', - 'class="u-url mention" target="_blank"' - ); - - return content; - } - - /** - * Find all start/end and replace them by another start/end - * @param {string} c Text content - * @param {string} initialTagOpen Start HTML tag to replace - * @param {string} initialTagClose End HTML tag to replace - * @param {string} replacedTagOpen New start HTML tag - * @param {string} replacedTagClose New end HTML tag - * @returns {string} Text in HTML format - */ - #replaceHTMLtag( - c, - initialTagOpen, - initialTagClose, - replacedTagOpen, - replacedTagClose - ) { - if (c.includes(initialTagOpen)) { - const regex = new RegExp( - initialTagOpen + "(.*?)" + initialTagClose, - "gi" - ); - - return c.replace(regex, replacedTagOpen + "$1" + replacedTagClose); - } else { - return c; - } - } - - /** - * Escape quotes and other special characters, to make them safe to add - * to HTML content and attributes as plain text - * @param {string} s String - * @returns {string} String - */ - #escapeHtml(s) { - return (s ?? "") - .replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">") - .replaceAll('"', """) - .replaceAll("'", "'"); - } - - /** - * 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 - */ - #createEmoji(c, e) { - if (c.includes(":")) { - for (const emojo of e) { - const regex = new RegExp(`\\:${emojo.shortcode}\\:`, "g"); - c = c.replace( - regex, - `Emoji ${emojo.shortcode}` - ); - } - - return c; - } else { - return c; - } - } - - /** - * Format date - * @param {string} d Date in ISO format (YYYY-MM-DDTHH:mm:ss.sssZ) - * @returns {string} Date formated (MM DD, YYYY) - */ - #formatDate(d) { - const monthNames = [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec", - ]; - - const date = new Date(d); - - const displayDate = - monthNames[date.getMonth()] + - " " + - date.getDate() + - ", " + - date.getFullYear(); - - return displayDate; - } - - /** - * Create media element - * @param {object} m Media content - * @param {boolean} s Spoiler/Sensitive status - * @returns {string} Media in HTML format - */ - #createMedia(m, s) { - const spoiler = s || false; - const type = m.type; - let media = ""; - - if (type === "image") { - media = - '
    ' + - (spoiler - ? '" - : "") + - '' +
-        (m.description ? this.#escapeHtml(m.description) : ' + - "
    "; - } - - if (type === "audio") { - if (m.preview_url) { - media = - '
    ' + - (spoiler - ? '" - : "") + - '' + - '' +
-          (m.description ? this.#escapeHtml(m.description) : ' + - "
    "; - } else { - media = - '
    ' + - (spoiler - ? '" - : "") + - '' + - "
    "; - } - } - - if (type === "video" || type === "gifv") { - if (!this.mtSettings.hideVideoPreview) { - media = - '
    ' + - (spoiler - ? '" - : "") + - '' +
-          (m.description ? this.#escapeHtml(m.description) : ' + - '' + - "
    "; - } else { - media = - '
    ' + - (spoiler - ? '" - : "") + - '' + - "
    "; - } - } - - return media; - } - - /** - * Replace the video preview image by the video player - * @param {event} e User interaction trigger - */ - #loadPostVideo(e) { - const parentNode = e.target.closest("[data-video-url]"); - const videoUrl = parentNode.dataset.videoUrl; - parentNode.replaceChildren(); - parentNode.innerHTML = - ''; - } - - /** - * Spoiler button - * @param {event} e User interaction trigger - */ - #toogleSpoiler(e) { - const nextSibling = e.target.nextSibling; - if ( - nextSibling.localName === "img" || - nextSibling.localName === "audio" || - nextSibling.localName === "video" - ) { - e.target.parentNode.classList.remove("mt-post-media-spoiler"); - e.target.style.display = "none"; - } else if ( - nextSibling.classList.contains("spoiler-txt-hidden") || - nextSibling.classList.contains("spoiler-txt-visible") - ) { - if (e.target.textContent == this.mtSettings.btnShowMore) { - nextSibling.classList.remove("spoiler-txt-hidden"); - nextSibling.classList.add("spoiler-txt-visible"); - e.target.setAttribute("aria-expanded", "true"); - e.target.textContent = this.mtSettings.btnShowLess; - } else { - nextSibling.classList.remove("spoiler-txt-visible"); - nextSibling.classList.add("spoiler-txt-hidden"); - e.target.setAttribute("aria-expanded", "false"); - e.target.textContent = this.mtSettings.btnShowMore; - } - } - } - - /** - * Create preview link - * @param {object} c Preview link content - * @returns {string} Preview link in HTML format - */ - #createPreviewLink(c) { - const card = - '' + - (c.image - ? '
    ' +
-          this.#escapeHtml(c.image_description) +
-          '
    ' - : '
    📄
    ') + - "
    " + - '
    ' + - (c.provider_name - ? '' + - this.#parseHTMLstring(c.provider_name) + - "" - : "") + - '' + - c.title + - "" + - (c.author_name - ? '' + - this.#parseHTMLstring(c.author_name) + - "" - : "") + - "
    " + - "
    "; - - return card; - } - - /** - * Parse HTML string - * @param {string} s HTML string - * @returns {string} Plain text - */ - #parseHTMLstring(s) { - const parser = new DOMParser(); - const txt = parser.parseFromString(s, "text/html"); - return txt.body.textContent; - } - - /** - * Build footer after last post - */ - #buildFooter() { - if (this.mtSettings.btnSeeMore || this.mtSettings.btnReload) { - // Add footer container - this.mtBodyNode.parentNode.insertAdjacentHTML( - "beforeend", - '' - ); - - const containerFooter = - this.mtContainerNode.getElementsByClassName("mt-footer")[0]; - - // Create button to open Mastodon page - if (this.mtSettings.btnSeeMore) { - let btnSeeMorePath = ""; - if (this.mtSettings.timelineType === "profile") { - if (this.mtSettings.profileName) { - btnSeeMorePath = this.mtSettings.profileName; - } else { - this.#showError( - "Please check your profileName value", - "âš ī¸" - ); - } - } else if (this.mtSettings.timelineType === "hashtag") { - btnSeeMorePath = "tags/" + this.mtSettings.hashtagName; - } else if (this.mtSettings.timelineType === "local") { - btnSeeMorePath = "public/local"; - } - const btnSeeMoreHTML = - '' + - this.mtSettings.btnSeeMore + - ""; - - containerFooter.insertAdjacentHTML("beforeend", btnSeeMoreHTML); - } - - // Create button to refresh the timeline - if (this.mtSettings.btnReload) { - const btnReloadHTML = - '"; - - containerFooter.insertAdjacentHTML("beforeend", btnReloadHTML); - - const reloadBtn = - this.mtContainerNode.getElementsByClassName("btn-refresh")[0]; - reloadBtn.addEventListener("click", () => { - this.mtUpdate(); - }); - } - } - } - - /** - * Add EventListeners for timeline interactions and trigger functions - */ - #setPostsInteracion() { - this.mtBodyNode.addEventListener("click", (e) => { - // Check if post cointainer was clicked - if ( - e.target.localName == "article" || - e.target.offsetParent?.localName == "article" || - (e.target.localName == "img" && - !e.target.parentNode.getAttribute("data-video-url")) - ) { - this.#openPostUrl(e); - } - // Check if Show More/Less button was clicked - if ( - e.target.localName == "button" && - e.target.classList.contains("mt-btn-spoiler") - ) { - this.#toogleSpoiler(e); - } - // Check if video preview image or play icon/button was clicked - if ( - e.target.className == "mt-post-media-play-icon" || - (e.target.localName == "svg" && - e.target.parentNode.className == "mt-post-media-play-icon") || - (e.target.localName == "path" && - e.target.parentNode.parentNode.className == - "mt-post-media-play-icon") || - (e.target.localName == "img" && - e.target.parentNode.getAttribute("data-video-url")) - ) { - this.#loadPostVideo(e); - } - }); - this.mtBodyNode.addEventListener("keydown", (e) => { - // Check if Enter key was pressed with focus in an article - if (e.key === "Enter" && e.target.localName == "article") { - this.#openPostUrl(e); - } - }); - } - - /** - * Open post in a new tab/page avoiding any other natural link - * @param {event} e User interaction trigger - */ - #openPostUrl(e) { - const urlPost = e.target.closest(".mt-post").dataset.location; - if ( - e.target.localName !== "a" && - e.target.localName !== "span" && - e.target.localName !== "button" && - e.target.localName !== "time" && - 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-preview-image" && - e.target.parentNode.className !== "mt-post-preview" && - urlPost - ) { - window.open(urlPost, "_blank", "noopener"); - } - } - - /** - * Add/Remove EventListeners for loading spinner - */ - #manageSpinner() { - // Remove EventListener and CSS class to container - const removeSpinner = (e) => { - e.target.parentNode.classList.remove(this.mtSettings.spinnerClass); - e.target.removeEventListener("load", removeSpinner); - e.target.removeEventListener("error", removeSpinner); - }; - // Add EventListener to images - this.mtBodyNode - .querySelectorAll(`.${this.mtSettings.spinnerClass} > img`) - .forEach((e) => { - e.addEventListener("load", removeSpinner); - e.addEventListener("error", removeSpinner); - }); - } - - /** - * Show an error on the timeline - * @param {string} e Error message - * @param {string} i Icon - */ - #showError(t, i) { - const icon = i || "❌"; - this.mtBodyNode.innerHTML = - '
    ' + - icon + - '
    Oops, something\'s happened:
    ' + - t + - "
    "; - this.mtBodyNode.setAttribute("role", "none"); - throw new Error( - "Stopping the script due to an error building the timeline." - ); - } -} +/** + * Mastodon embed timeline + * @author idotj + * @version 4.3.1 + * @url https://gitlab.com/idotj/mastodon-embed-timeline + * @license GNU AGPLv3 + */ +"use strict"; + +export class Init { + constructor(customSettings = {}) { + this.defaultSettings = { + mtContainerId: "mt-container", + instanceUrl: "https://mastodon.social", + timelineType: "local", + userId: "", + profileName: "", + hashtagName: "", + spinnerClass: "mt-loading-spinner", + defaultTheme: "auto", + maxNbPostFetch: "20", + maxNbPostShow: "20", + dateLocale: "en-GB", + dateOptions: { + day: "2-digit", + month: "short", + year: "numeric", + }, + hideUnlisted: false, + hideReblog: false, + hideReplies: false, + hidePinnedPosts: false, + hideUserAccount: false, + hideEmojos: false, + hideVideoPreview: false, + hidePreviewLink: false, + hideCounterBar: false, + markdownBlockquote: false, + txtMaxLines: "0", + btnShowMore: "SHOW MORE", + btnShowLess: "SHOW LESS", + btnShowContent: "SHOW CONTENT", + btnSeeMore: "See more posts at Mastodon", + btnReload: "Refresh", + insistSearchContainer: false, + insistSearchContainerTime: "3000", + }; + + this.mtSettings = { ...this.defaultSettings, ...customSettings }; + + this.mtContainerNode = ""; + this.mtBodyNode = ""; + this.fetchedData = {}; + + this.#onDOMContentLoaded(() => { + this.#getContainerNode(); + }); + } + + /** + * Trigger callback when DOM loaded or completed + * @param {function} c Callback executed + */ + #onDOMContentLoaded(c) { + if (typeof document !== "undefined" && document.readyState === "complete") { + c(); + } else if ( + typeof document !== "undefined" && + document.readyState !== "complete" + ) { + document.addEventListener("DOMContentLoaded", c()); + } + } + + /** + * Find main container in DOM before building the timeline + */ + #getContainerNode() { + // console.log("Initializing Mastodon timeline with settings: ", this.mtSettings); + + const assignContainerNode = () => { + this.mtContainerNode = document.getElementById( + this.mtSettings.mtContainerId + ); + this.mtBodyNode = + this.mtContainerNode.getElementsByClassName("mt-body")[0]; + this.#loadColorTheme(); + this.#buildTimeline("newTimeline"); + }; + + // Some frameworks render the DOM in deferred way and need some extra time + if (this.mtSettings.insistSearchContainer) { + const startCheck = performance.now(); + const findContainerNode = () => { + // Check if the container is ready in DOM + if (document.getElementById(this.mtSettings.mtContainerId)) { + assignContainerNode(); + } else { + // If the container is not found, check again + const currentCheck = performance.now(); + if ( + currentCheck - startCheck < + this.mtSettings.insistSearchContainerTime + ) { + requestAnimationFrame(findContainerNode); + } else { + console.error( + `Impossible to find the
    container with id: "${ + this.mtSettings.mtContainerId + }" after several attempts for ${ + this.mtSettings.insistSearchContainerTime / 1000 + } seconds` + ); + } + } + }; + findContainerNode(); + } else { + if (document.getElementById(this.mtSettings.mtContainerId)) { + assignContainerNode(); + } else { + console.error( + `Impossible to find the
    container with id: "${this.mtSettings.mtContainerId}". Please try to add the option 'insistSearchContainer: true' when initializing the script` + ); + } + } + } + + /** + * Reload the timeline by fetching the lastest posts + */ + mtUpdate() { + this.#onDOMContentLoaded(() => { + this.mtBodyNode.replaceChildren(); + this.mtBodyNode.insertAdjacentHTML( + "afterbegin", + '
    ' + ); + this.#buildTimeline("updateTimeline"); + }); + } + + /** + * Apply the color theme in the timeline + * @param {string} themeType Type of color theme + */ + mtColorTheme(themeType) { + this.#onDOMContentLoaded(() => { + this.mtContainerNode.setAttribute("data-theme", themeType); + }); + } + + /** + * Get the theme style chosen by the user or by the browser/OS + */ + #loadColorTheme() { + if (this.mtSettings.defaultTheme === "auto") { + let systemTheme = window.matchMedia("(prefers-color-scheme: dark)"); + systemTheme.matches + ? this.mtColorTheme("dark") + : this.mtColorTheme("light"); + // Update the theme if user change browser/OS preference + systemTheme.addEventListener("change", (e) => { + e.matches ? this.mtColorTheme("dark") : this.mtColorTheme("light"); + }); + } else { + this.mtColorTheme(this.mtSettings.defaultTheme); + } + } + + /** + * Requests to the server to collect all the data + * @returns {object} Data container + */ + #fetchTimelineData() { + return new Promise((resolve, reject) => { + /** + * Fetch data from server + * @param {string} url address to fetch + * @returns {array} 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 + + "
    " + + "Error status: " + + response.status + + "
    " + + "Error message: " + + response.statusText + ); + } + + const data = await response.json(); + return data; + } + + // Urls to fetch + let urls = {}; + + if (this.mtSettings.instanceUrl) { + if (this.mtSettings.timelineType === "profile") { + if (this.mtSettings.userId) { + urls.timeline = `${this.mtSettings.instanceUrl}/api/v1/accounts/${this.mtSettings.userId}/statuses?limit=${this.mtSettings.maxNbPostFetch}`; + if (!this.mtSettings.hidePinnedPosts) { + urls.pinned = `${this.mtSettings.instanceUrl}/api/v1/accounts/${this.mtSettings.userId}/statuses?pinned=true`; + } + } else { + this.#showError( + "Please check your userId value", + "âš ī¸" + ); + } + } else if (this.mtSettings.timelineType === "hashtag") { + if (this.mtSettings.hashtagName) { + urls.timeline = `${this.mtSettings.instanceUrl}/api/v1/timelines/tag/${this.mtSettings.hashtagName}?limit=${this.mtSettings.maxNbPostFetch}`; + } else { + this.#showError( + "Please check your hashtagName value", + "âš ī¸" + ); + } + } else if (this.mtSettings.timelineType === "local") { + urls.timeline = `${this.mtSettings.instanceUrl}/api/v1/timelines/public?local=true&limit=${this.mtSettings.maxNbPostFetch}`; + } else { + this.#showError( + "Please check your timelineType value", + "âš ī¸" + ); + } + } else { + this.#showError( + "Please check your instanceUrl value", + "âš ī¸" + ); + } + if (!this.mtSettings.hideEmojos) { + urls.emojos = this.mtSettings.instanceUrl + "/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 from: " + url) + ); + this.#showError(error.message); + return { [key]: [] }; + }); + }); + + // Fetch all urls simultaneously + Promise.all(urlsPromises).then((dataObjects) => { + this.fetchedData = dataObjects.reduce((result, dataItem) => { + return { ...result, ...dataItem }; + }, {}); + + // console.log("Mastodon timeline data fetched: ", this.fetchedData); + resolve(); + }); + }); + } + + /** + * Filter all fetched posts and append them on the timeline + * @param {string} t Type of build (new or reload) + */ + async #buildTimeline(t) { + await this.#fetchTimelineData(); + + // Merge pinned posts with timeline posts + let posts; + if ( + !this.mtSettings.hidePinnedPosts && + this.fetchedData.pinned?.length !== undefined && + this.fetchedData.pinned.length !== 0 + ) { + const pinnedPosts = this.fetchedData.pinned.map((obj) => ({ + ...obj, + pinned: true, + })); + posts = [...pinnedPosts, ...this.fetchedData.timeline]; + } else { + posts = this.fetchedData.timeline; + } + + // Empty container body + this.mtBodyNode.replaceChildren(); + + // Set posts counter to 0 + let nbPostShow = 0; + + 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) + } 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) + } + } + } + } + + // If there are no posts to display, show an error message + if (this.mtBodyNode.innerHTML === "") { + const errorMessage = + "No posts to show
    " + + (posts?.length || 0) + + " posts have been fetched from the server
    This may be due to an incorrect configuration in the parameters or to filters applied (to hide certains type of posts)"; + this.#showError(errorMessage, "📭"); + } else { + if (t === "newTimeline") { + this.#manageSpinner(); + this.#setPostsInteracion(); + this.#buildFooter(); + } else if (t === "updateTimeline") { + this.#manageSpinner(); + } else { + this.#showError("The function buildTimeline() was expecting a param"); + } + } + } + + /** + * Add each post in the timeline container + * @param {object} c Post content + * @param {number} i Index of post + */ + #appendPost(c, i) { + this.mtBodyNode.insertAdjacentHTML("beforeend", this.#assamblePost(c, i)); + } + + /** + * Build post structure + * @param {object} c Post content + * @param {number} i Index of post + */ + #assamblePost(c, i) { + let avatar, + user, + userName, + accountName, + url, + date, + formattedDate, + favoritesCount, + reblogCount, + repliesCount; + + if (c.reblog) { + // BOOSTED post + // Post url + url = c.reblog.url; + + // Boosted avatar + avatar = + '' + + '
    ' + + '
    ' + + '' +
+        this.#escapeHtml(c.reblog.account.username) +
+        ' avatar' + + "
    " + + '
    ' + + '' +
+        this.#escapeHtml(c.account.username) +
+        ' avatar' + + "
    " + + "
    " + + "
    "; + + // User name and url + if (!this.mtSettings.hideEmojos && c.reblog.account.display_name) { + userName = this.#createEmoji( + c.reblog.account.display_name, + this.fetchedData.emojos + ); + } else { + userName = c.reblog.account.display_name; + } + + if (!this.mtSettings.hideUserAccount) { + accountName = + '
    "; + } else { + accountName = ""; + } + + user = + '
    ' + + '' + + userName + + "" + + accountName + + "" + + "
    "; + + // Date + date = c.reblog.created_at; + + // Counter bar + repliesCount = c.reblog.replies_count; + reblogCount = c.reblog.reblogs_count; + favoritesCount = c.reblog.favourites_count; + } else { + // STANDARD post + // Post url + url = c.url; + + // Avatar + avatar = + '' + + '
    ' + + '
    ' + + '' +
+        this.#escapeHtml(c.account.username) +
+        ' avatar' + + "
    " + + "
    " + + "
    "; + + // User name and url + if (!this.mtSettings.hideEmojos && c.account.display_name) { + userName = this.#createEmoji( + c.account.display_name, + this.fetchedData.emojos + ); + } else { + userName = c.account.display_name; + } + + if (!this.mtSettings.hideUserAccount) { + accountName = + '
    "; + } else { + accountName = ""; + } + + user = + '
    ' + + '' + + userName + + "" + + accountName + + "" + + "
    "; + + // Date + date = c.created_at; + + // Counter bar + repliesCount = c.replies_count; + reblogCount = c.reblogs_count; + favoritesCount = c.favourites_count; + } + + // Date + formattedDate = this.#formatDate(date); + const timestamp = + '
    ' + + (c.pinned + ? '' + : "") + + '' + + '" + + (c.edited_at ? " *" : "") + + "" + + "
    "; + + // 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 (c.spoiler_text !== "") { + content = + '
    ' + + c.spoiler_text + + ' " + + '
    ' + + this.#formatPostText(c.content) + + "
    " + + "
    "; + } else if ( + c.reblog && + c.reblog.content !== "" && + c.reblog.spoiler_text !== "" + ) { + content = + '
    ' + + c.reblog.spoiler_text + + ' " + + '
    ' + + this.#formatPostText(c.reblog.content) + + "
    " + + "
    "; + } else if ( + c.reblog && + c.reblog.content !== "" && + c.reblog.spoiler_text === "" + ) { + content = + '
    ' + + '
    ' + + this.#formatPostText(c.reblog.content) + + "
    " + + "
    "; + } else { + content = + '
    ' + + '
    ' + + this.#formatPostText(c.content) + + "
    " + + "
    "; + } + + // Media attachments + let media = []; + if (c.media_attachments.length > 0) { + for (let i in c.media_attachments) { + media.push(this.#createMedia(c.media_attachments[i], c.sensitive)); + } + } + if (c.reblog && c.reblog.media_attachments.length > 0) { + for (let i in c.reblog.media_attachments) { + media.push( + this.#createMedia(c.reblog.media_attachments[i], c.reblog.sensitive) + ); + } + } + + // Preview link + let previewLink = ""; + if (!this.mtSettings.hidePreviewLink && c.card) { + previewLink = this.#createPreviewLink(c.card); + } + + // Poll + let poll = ""; + if (c.poll) { + let pollOption = ""; + for (let i in c.poll.options) { + pollOption += "
  • " + c.poll.options[i].title + "
  • "; + } + poll = + '
    ' + + "
      " + + pollOption + + "
    " + + "
    "; + } + + // Counter bar + let counterBar = ""; + if (!this.mtSettings.hideCounterBar) { + const repliesTag = + '
    ' + + '' + + repliesCount + + "
    "; + + const reblogTag = + '
    ' + + '' + + reblogCount + + "
    "; + + const favoritesTag = + '
    ' + + '' + + favoritesCount + + "
    "; + + counterBar = + '
    ' + + repliesTag + + reblogTag + + favoritesTag + + "
    "; + } + + // Add all to main post container + const post = + '
    ' + + '
    ' + + avatar + + user + + timestamp + + "
    " + + content + + media.join("") + + previewLink + + poll + + counterBar + + "
    "; + + return post; + } + + /** + * Handle text changes made to posts + * @param {string} c Text content + * @returns {string} Text content modified + */ + #formatPostText(c) { + let content = c; + + // Format hashtags and mentions + content = this.#addTarget2hashtagMention(content); + + // Convert emojos shortcode into images + if (!this.mtSettings.hideEmojos) { + content = this.#createEmoji(content, this.fetchedData.emojos); + } + + // Convert markdown styles into HTML + if (this.mtSettings.markdownBlockquote) { + content = this.#replaceHTMLtag( + content, + "

    >", + "

    ", + "

    ", + "

    " + ); + } + + return content; + } + + /** + * Add target="_blank" to all #hashtags and @mentions in the post + * @param {string} c Text content + * @returns {string} Text content modified + */ + #addTarget2hashtagMention(c) { + let content = c.replaceAll('rel="tag"', 'rel="tag" target="_blank"'); + content = content.replaceAll( + 'class="u-url mention"', + 'class="u-url mention" target="_blank"' + ); + + return content; + } + + /** + * Find all start/end and replace them by another start/end + * @param {string} c Text content + * @param {string} initialTagOpen Start HTML tag to replace + * @param {string} initialTagClose End HTML tag to replace + * @param {string} replacedTagOpen New start HTML tag + * @param {string} replacedTagClose New end HTML tag + * @returns {string} Text in HTML format + */ + #replaceHTMLtag( + c, + initialTagOpen, + initialTagClose, + replacedTagOpen, + replacedTagClose + ) { + if (c.includes(initialTagOpen)) { + const regex = new RegExp( + initialTagOpen + "(.*?)" + initialTagClose, + "gi" + ); + + return c.replace(regex, replacedTagOpen + "$1" + replacedTagClose); + } else { + return c; + } + } + + /** + * Escape quotes and other special characters, to make them safe to add + * to HTML content and attributes as plain text + * @param {string} s String + * @returns {string} String + */ + #escapeHtml(s) { + return (s ?? "") + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); + } + + /** + * 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 + */ + #createEmoji(c, e) { + if (c.includes(":")) { + for (const emojo of e) { + const regex = new RegExp(`\\:${emojo.shortcode}\\:`, "g"); + c = c.replace( + regex, + `Emoji ${emojo.shortcode}` + ); + } + + return c; + } else { + return c; + } + } + + /** + * Format date + * @param {string} d Date in ISO format (YYYY-MM-DDTHH:mm:ss.sssZ) + * @returns {string} Date formated + */ + #formatDate(d) { + const originalDate = new Date(d); + + const formattedDate = new Intl.DateTimeFormat( + this.mtSettings.dateLocale, + this.mtSettings.dateOptions + ).format(originalDate); + + return formattedDate; + } + + /** + * Create media element + * @param {object} m Media content + * @param {boolean} s Spoiler/Sensitive status + * @returns {string} Media in HTML format + */ + #createMedia(m, s) { + const spoiler = s || false; + const type = m.type; + let media = ""; + + if (type === "image") { + media = + '
    ' + + (spoiler + ? '" + : "") + + '' +
+        (m.description ? this.#escapeHtml(m.description) : ' + + "
    "; + } + + if (type === "audio") { + if (m.preview_url) { + media = + '
    ' + + (spoiler + ? '" + : "") + + '' + + '' +
+          (m.description ? this.#escapeHtml(m.description) : ' + + "
    "; + } else { + media = + '
    ' + + (spoiler + ? '" + : "") + + '' + + "
    "; + } + } + + if (type === "video" || type === "gifv") { + if (!this.mtSettings.hideVideoPreview) { + media = + '
    ' + + (spoiler + ? '" + : "") + + '' +
+          (m.description ? this.#escapeHtml(m.description) : ' + + '' + + "
    "; + } else { + media = + '
    ' + + (spoiler + ? '" + : "") + + '' + + "
    "; + } + } + + return media; + } + + /** + * Replace the video preview image by the video player + * @param {event} e User interaction trigger + */ + #loadPostVideo(e) { + const parentNode = e.target.closest("[data-video-url]"); + const videoUrl = parentNode.dataset.videoUrl; + parentNode.replaceChildren(); + parentNode.innerHTML = + ''; + } + + /** + * Spoiler button + * @param {event} e User interaction trigger + */ + #toogleSpoiler(e) { + const nextSibling = e.target.nextSibling; + if ( + nextSibling.localName === "img" || + nextSibling.localName === "audio" || + nextSibling.localName === "video" + ) { + e.target.parentNode.classList.remove("mt-post-media-spoiler"); + e.target.style.display = "none"; + } else if ( + nextSibling.classList.contains("spoiler-txt-hidden") || + nextSibling.classList.contains("spoiler-txt-visible") + ) { + if (e.target.textContent == this.mtSettings.btnShowMore) { + nextSibling.classList.remove("spoiler-txt-hidden"); + nextSibling.classList.add("spoiler-txt-visible"); + e.target.setAttribute("aria-expanded", "true"); + e.target.textContent = this.mtSettings.btnShowLess; + } else { + nextSibling.classList.remove("spoiler-txt-visible"); + nextSibling.classList.add("spoiler-txt-hidden"); + e.target.setAttribute("aria-expanded", "false"); + e.target.textContent = this.mtSettings.btnShowMore; + } + } + } + + /** + * Create preview link + * @param {object} c Preview link content + * @returns {string} Preview link in HTML format + */ + #createPreviewLink(c) { + const card = + '' + + (c.image + ? '
    ' +
+          this.#escapeHtml(c.image_description) +
+          '
    ' + : '
    📄
    ') + + "
    " + + '
    ' + + (c.provider_name + ? '' + + this.#parseHTMLstring(c.provider_name) + + "" + : "") + + '' + + c.title + + "" + + (c.author_name + ? '' + + this.#parseHTMLstring(c.author_name) + + "" + : "") + + "
    " + + "
    "; + + return card; + } + + /** + * Parse HTML string + * @param {string} s HTML string + * @returns {string} Plain text + */ + #parseHTMLstring(s) { + const parser = new DOMParser(); + const txt = parser.parseFromString(s, "text/html"); + return txt.body.textContent; + } + + /** + * Build footer after last post + */ + #buildFooter() { + if (this.mtSettings.btnSeeMore || this.mtSettings.btnReload) { + // Add footer container + this.mtBodyNode.parentNode.insertAdjacentHTML( + "beforeend", + '' + ); + + const containerFooter = + this.mtContainerNode.getElementsByClassName("mt-footer")[0]; + + // Create button to open Mastodon page + if (this.mtSettings.btnSeeMore) { + let btnSeeMorePath = ""; + if (this.mtSettings.timelineType === "profile") { + if (this.mtSettings.profileName) { + btnSeeMorePath = this.mtSettings.profileName; + } else { + this.#showError( + "Please check your profileName value", + "âš ī¸" + ); + } + } else if (this.mtSettings.timelineType === "hashtag") { + btnSeeMorePath = "tags/" + this.mtSettings.hashtagName; + } else if (this.mtSettings.timelineType === "local") { + btnSeeMorePath = "public/local"; + } + const btnSeeMoreHTML = + '' + + this.mtSettings.btnSeeMore + + ""; + + containerFooter.insertAdjacentHTML("beforeend", btnSeeMoreHTML); + } + + // Create button to refresh the timeline + if (this.mtSettings.btnReload) { + const btnReloadHTML = + '"; + + containerFooter.insertAdjacentHTML("beforeend", btnReloadHTML); + + const reloadBtn = + this.mtContainerNode.getElementsByClassName("btn-refresh")[0]; + reloadBtn.addEventListener("click", () => { + this.mtUpdate(); + }); + } + } + } + + /** + * Add EventListeners for timeline interactions and trigger functions + */ + #setPostsInteracion() { + this.mtBodyNode.addEventListener("click", (e) => { + // Check if post cointainer was clicked + if ( + e.target.localName == "article" || + e.target.offsetParent?.localName == "article" || + (e.target.localName == "img" && + !e.target.parentNode.getAttribute("data-video-url")) + ) { + this.#openPostUrl(e); + } + // Check if Show More/Less button was clicked + if ( + e.target.localName == "button" && + e.target.classList.contains("mt-btn-spoiler") + ) { + this.#toogleSpoiler(e); + } + // Check if video preview image or play icon/button was clicked + if ( + e.target.className == "mt-post-media-play-icon" || + (e.target.localName == "svg" && + e.target.parentNode.className == "mt-post-media-play-icon") || + (e.target.localName == "path" && + e.target.parentNode.parentNode.className == + "mt-post-media-play-icon") || + (e.target.localName == "img" && + e.target.parentNode.getAttribute("data-video-url")) + ) { + this.#loadPostVideo(e); + } + }); + this.mtBodyNode.addEventListener("keydown", (e) => { + // Check if Enter key was pressed with focus in an article + if (e.key === "Enter" && e.target.localName == "article") { + this.#openPostUrl(e); + } + }); + } + + /** + * Open post in a new tab/page avoiding any other natural link + * @param {event} e User interaction trigger + */ + #openPostUrl(e) { + const urlPost = e.target.closest(".mt-post").dataset.location; + if ( + e.target.localName !== "a" && + e.target.localName !== "span" && + e.target.localName !== "button" && + e.target.localName !== "time" && + 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-preview-image" && + e.target.parentNode.className !== "mt-post-preview" && + urlPost + ) { + window.open(urlPost, "_blank", "noopener"); + } + } + + /** + * Add/Remove EventListeners for loading spinner + */ + #manageSpinner() { + // Remove EventListener and CSS class to container + const removeSpinner = (e) => { + e.target.parentNode.classList.remove(this.mtSettings.spinnerClass); + e.target.removeEventListener("load", removeSpinner); + e.target.removeEventListener("error", removeSpinner); + }; + // Add EventListener to images + this.mtBodyNode + .querySelectorAll(`.${this.mtSettings.spinnerClass} > img`) + .forEach((e) => { + e.addEventListener("load", removeSpinner); + e.addEventListener("error", removeSpinner); + }); + } + + /** + * Show an error on the timeline + * @param {string} e Error message + * @param {string} i Icon + */ + #showError(t, i) { + const icon = i || "❌"; + this.mtBodyNode.innerHTML = + '
    ' + + icon + + '
    Oops, something\'s happened:
    ' + + t + + "
    "; + this.mtBodyNode.setAttribute("role", "none"); + throw new Error( + "Stopping the script due to an error building the timeline." + ); + } +}