diff --git a/public/js/widgets.js b/public/js/widgets.js index 0b897ef..5c8bc9c 100644 --- a/public/js/widgets.js +++ b/public/js/widgets.js @@ -1,135 +1,197 @@ /** * Drop-in replacement for Twitter's widgets.js - * Include this script to automatically convert twitter-tweet blockquotes to Nitter embeds + * Converts twitter-tweet/twitter-video blockquotes to Nitter embeds + * + * Usage: LibRedirect can redirect platform.twitter.com/widgets.js to nitter.net/widgets.js */ (function () { "use strict"; - // Determine the Nitter instance URL from the script src, or fall back to current origin - var widgetScripts = document.querySelectorAll('script[src*="widgets.js"]'); - var NITTER_URL = widgetScripts.length - ? new URL(widgetScripts[widgetScripts.length - 1].src).origin + // Idempotent - only run once + if (window.__nitterWidgets) return; + window.__nitterWidgets = true; + + // Determine Nitter instance URL from script src, or fall back to current origin + var scripts = document.querySelectorAll('script[src*="widgets.js"]'); + var NITTER = scripts.length + ? new URL(scripts[scripts.length - 1].src).origin : location.origin; - var TWEET_URL_PATTERN = - /^https?:\/\/(?:twitter\.com|x\.com)\/([^\/]+)\/status\/(\d+)/i; + var TWEET_PATTERN = /(?:twitter\.com|x\.com)\/([^\/]+)\/status\/(\d+)/i; + var SELECTOR = "blockquote.twitter-tweet, blockquote.twitter-video"; - // Track iframes by URL for resize messages - var iframesByUrl = {}; + // Ready callback queue + var readyCallbacks = []; + var isReady = false; - /** - * Extract tweet info (username and ID) from a blockquote's links - */ - function findTweetInfo(blockquote) { - var links = blockquote.querySelectorAll("a"); - for (var i = 0; i < links.length; i++) { - var match = TWEET_URL_PATTERN.exec(links[i].href); - if (match) { - return { username: match[1], tweetId: match[2] }; - } - } - return null; + function parseTweetUrl(url) { + if (!url) return null; + var match = TWEET_PATTERN.exec(url); + if (match) return { username: match[1], id: match[2] }; + // Fallback: extract any large number (tweet ID) + var idMatch = url.match(/(\d{15,})/); + return idMatch ? { username: null, id: idMatch[1] } : null; } - /** - * Transform all twitter-tweet blockquotes into Nitter embed iframes - */ - function transformBlockquotes() { - var blockquotes = document.querySelectorAll("blockquote.twitter-tweet"); + function createIframe(tweet, options) { + var embedUrl = tweet.username + ? NITTER + "/" + tweet.username + "/status/" + tweet.id + "/embed" + : NITTER + "/i/status/" + tweet.id + "/embed"; + + var iframe = document.createElement("iframe"); + iframe.src = embedUrl; + iframe.className = "nitter-embed-frame"; + iframe.setAttribute("allowtransparency", "true"); + iframe.setAttribute("frameborder", "0"); + iframe.setAttribute("scrolling", "no"); + iframe.loading = "lazy"; + + // Styling with data attribute support + var width = options.width || "550"; + var align = options.align || "center"; + var margin = align === "center" ? "10px auto" : + align === "right" ? "10px 0 10px auto" : "10px auto 10px 0"; + + iframe.style.cssText = "width:100%;max-width:" + width + "px;height:250px;border:none;display:block;margin:" + margin; + + return iframe; + } + + function processBlockquote(bq) { + if (bq.dataset.nitterProcessed) return false; + bq.dataset.nitterProcessed = "true"; + + // Find tweet URL in links + var tweet = null; + var links = bq.querySelectorAll("a[href]"); + for (var i = 0; i < links.length; i++) { + tweet = parseTweetUrl(links[i].href); + if (tweet) break; + } + + if (!tweet) { + console.warn("[Nitter widgets.js] No tweet URL found in blockquote"); + return false; + } + + // Read Twitter's data attributes + var options = { + width: bq.dataset.width, + align: bq.dataset.align, + theme: bq.dataset.theme + }; + + var iframe = createIframe(tweet, options); + + // Hide original (keep as fallback), insert iframe after + bq.style.display = "none"; + bq.parentNode.insertBefore(iframe, bq.nextSibling); + return true; + } + + function processEmbeds(container) { + var root = container || document; + var blockquotes = root.querySelectorAll(SELECTOR + ":not([data-nitter-processed])"); + var count = 0; for (var i = 0; i < blockquotes.length; i++) { - var blockquote = blockquotes[i]; - var tweetInfo = findTweetInfo(blockquote); - if (!tweetInfo) continue; + if (processBlockquote(blockquotes[i])) count++; + } - var embedUrl = - NITTER_URL + - "/" + - tweetInfo.username + - "/status/" + - tweetInfo.tweetId + - "/embed"; - - var iframe = document.createElement("iframe"); - iframe.src = embedUrl; - iframe.style.cssText = - "width: 100%; max-width: 550px; height: 250px; border: none; display: block;"; - iframe.loading = "lazy"; - - // Track iframe for resize messages - if (!iframesByUrl[embedUrl]) { - iframesByUrl[embedUrl] = []; - } - iframesByUrl[embedUrl].push(iframe); - - blockquote.parentNode.replaceChild(iframe, blockquote); + if (count > 0) { + console.log("[Nitter widgets.js] Processed " + count + " embed(s)"); } } - /** - * Handle resize messages from Nitter embeds - */ - function handleResizeMessage(event) { + function handleResize(event) { if (!Array.isArray(event.data) || event.data[0] !== "resizeIframe") return; + var h = event.data[1] && event.data[1].h; + if (!h || h <= 0) return; - var data = event.data[1]; - if (!data.h || data.h <= 0) return; - - var iframes = iframesByUrl[data.url]; - if (iframes) { - for (var i = 0; i < iframes.length; i++) { - iframes[i].style.height = data.h + "px"; + // Find iframe by matching contentWindow + var iframes = document.querySelectorAll("iframe.nitter-embed-frame"); + for (var i = 0; i < iframes.length; i++) { + if (iframes[i].contentWindow === event.source) { + iframes[i].style.height = h + "px"; + break; } } } - // Remove any Twitter widget scripts that might have been loaded + function observeDOM() { + if (!window.MutationObserver || !document.body) return; + + new MutationObserver(function (mutations) { + for (var i = 0; i < mutations.length; i++) { + var nodes = mutations[i].addedNodes; + for (var j = 0; j < nodes.length; j++) { + var node = nodes[j]; + if (node.nodeType !== 1) continue; + if ((node.matches && node.matches(SELECTOR)) || + (node.querySelector && node.querySelector(SELECTOR))) { + processEmbeds(); + return; + } + } + } + }).observe(document.body, { childList: true, subtree: true }); + } + + function fireReady() { + isReady = true; + for (var i = 0; i < readyCallbacks.length; i++) { + try { readyCallbacks[i](window.twttr); } catch (e) {} + } + readyCallbacks = []; + } + + // Expose twttr API for compatibility + var prevTwttr = window.twttr; + window.twttr = { + widgets: { + load: function (el) { processEmbeds(el); }, + createTweet: function (id, container, opts) { + if (!container) return Promise.reject("No container"); + var iframe = createIframe({ id: id, username: null }, opts || {}); + container.appendChild(iframe); + return Promise.resolve(iframe); + }, + loaded: true + }, + ready: function (cb) { + if (typeof cb !== "function") return; + if (isReady) cb(window.twttr); + else readyCallbacks.push(cb); + }, + _e: [] + }; + + // Process any callbacks queued before we loaded (twttr._e pattern) + if (prevTwttr && prevTwttr._e) { + for (var i = 0; i < prevTwttr._e.length; i++) { + try { prevTwttr._e[i](); } catch (e) {} + } + } + + // Remove Twitter scripts that might have snuck in var twitterScripts = document.querySelectorAll( - 'script[src*="platform.twitter.com/widgets.js"], script[src*="platform.x.com/widgets.js"]', + 'script[src*="platform.twitter.com"], script[src*="platform.x.com"]' ); for (var i = 0; i < twitterScripts.length; i++) { twitterScripts[i].remove(); } - // Listen for resize messages from embeds - window.addEventListener("message", handleResizeMessage); + // Initialize + function init() { + window.addEventListener("message", handleResize); + processEmbeds(); + observeDOM(); + fireReady(); + } - // Transform existing blockquotes if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", transformBlockquotes); + document.addEventListener("DOMContentLoaded", init); } else { - transformBlockquotes(); + init(); } - - // Watch for dynamically added blockquotes - var observer = new MutationObserver(function (mutations) { - for (var i = 0; i < mutations.length; i++) { - var addedNodes = mutations[i].addedNodes; - for (var j = 0; j < addedNodes.length; j++) { - var node = addedNodes[j]; - if (node.nodeType !== 1) continue; - - var isTwitterBlockquote = - node.matches && node.matches("blockquote.twitter-tweet"); - var containsTwitterBlockquote = - node.querySelector && node.querySelector("blockquote.twitter-tweet"); - - if (isTwitterBlockquote || containsTwitterBlockquote) { - transformBlockquotes(); - return; - } - } - } - }); - - if (document.body) { - observer.observe(document.body, { childList: true, subtree: true }); - } - - // Provide a fake twttr object for compatibility with sites that check for it - window.twttr = window.twttr || {}; - window.twttr.widgets = { - load: transformBlockquotes, - loaded: true, - }; })();