import { apiInitializer } from "discourse/lib/api"; import { debounce } from "@ember/runloop"; import { ajax } from "discourse/lib/ajax"; import I18n from "I18n"; const URL_REGEX = /^(https?:\/\/[^\s/$.?#][^\s]*)$/i; const DEBOUNCE_MS = 600; export default apiInitializer("1.8.0", (api) => { if (!api.container.lookup("site-settings:main").url_to_article_enabled) { return; } // ----------------------------------------------------------------------- // Inject a helper button + status banner into the composer // ----------------------------------------------------------------------- api.modifyClass("component:composer-editor", { pluginId: "url-to-article", didInsertElement() { this._super(...arguments); this._setupUrlToArticle(); }, willDestroyElement() { this._super(...arguments); this._teardownUrlToArticle(); }, _setupUrlToArticle() { // Watch the title field — it lives outside the composer-editor DOM, // so we observe via the composer model's `title` property. const composer = this.get("composer"); if (!composer) return; this._titleObserver = () => this._onTitleChanged(); composer.addObserver("model.title", this, "_titleObserver"); }, _teardownUrlToArticle() { const composer = this.get("composer"); if (!composer) return; composer.removeObserver("model.title", this, "_titleObserver"); this._restoreNativeTitleLookup(); }, _onTitleChanged() { const title = this.get("composer.model.title") || ""; const match = title.trim().match(URL_REGEX); if (!match) { this._restoreNativeTitleLookup(); this._hideArticleBar(); return; } const url = match[1]; if (this._lastDetectedUrl === url) return; // Same URL — no-op this._lastDetectedUrl = url; // Suppress Discourse's native title lookup so it doesn't race our bar this._suppressNativeTitleLookup(); const autoPopulate = api.container .lookup("site-settings:main") .url_to_article_auto_populate; if (autoPopulate) { debounce(this, "_fetchAndPopulate", url, DEBOUNCE_MS); } else { this._showArticleBar(url); } }, // ---- Native title-lookup suppression ------------------------------ _suppressNativeTitleLookup() { const model = this.get("composer.model"); if (!model || this._originalTitleLookup) return; if (typeof model._titleLookup === "function") { this._originalTitleLookup = model._titleLookup.bind(model); model._titleLookup = () => {}; } }, _restoreNativeTitleLookup() { const model = this.get("composer.model"); if (!model || !this._originalTitleLookup) return; model._titleLookup = this._originalTitleLookup; this._originalTitleLookup = null; }, _triggerNativeTitleLookup() { this._restoreNativeTitleLookup(); const model = this.get("composer.model"); if (model && typeof model._titleLookup === "function") { model._titleLookup(); } }, // ---- Bar UI ------------------------------------------------------- _showArticleBar(url) { this._hideArticleBar(); // remove any existing bar first const bar = document.createElement("div"); bar.className = "url-to-article-bar"; bar.dataset.url = url; bar.innerHTML = ` 📄 ${I18n.t("url_to_article.bar_label")} `; bar.querySelector(".url-to-article-btn").addEventListener("click", () => { this._fetchAndPopulate(url); }); bar.querySelector(".url-to-article-onebox-btn").addEventListener("click", () => { this._hideArticleBar(); this._lastDetectedUrl = null; this._triggerNativeTitleLookup(); }); bar.querySelector(".url-to-article-dismiss").addEventListener("click", () => { this._hideArticleBar(); this._restoreNativeTitleLookup(); this._lastDetectedUrl = null; // Allow re-detection if title changes }); const toolbarEl = this.element.querySelector(".d-editor-container"); if (toolbarEl) { toolbarEl.insertAdjacentElement("afterbegin", bar); } }, _hideArticleBar() { this.element?.querySelectorAll(".url-to-article-bar").forEach((el) => el.remove()); }, _setStatus(message, type = "info") { const bar = this.element?.querySelector(".url-to-article-bar"); if (!bar) return; let status = bar.querySelector(".url-to-article-status"); if (!status) { status = document.createElement("span"); status.className = "url-to-article-status"; bar.appendChild(status); } status.textContent = message; status.className = `url-to-article-status url-to-article-status--${type}`; }, // ---- Fetch & populate --------------------------------------------- async _fetchAndPopulate(url) { const bar = this.element?.querySelector(".url-to-article-bar"); const btn = bar?.querySelector(".url-to-article-btn"); if (btn) { btn.disabled = true; btn.textContent = I18n.t("url_to_article.fetching"); } this._setStatus(I18n.t("url_to_article.fetching"), "info"); try { const data = await ajax("/url-to-article/extract", { type: "POST", data: { url }, }); if (data.error) { throw new Error(data.error); } this._populateComposer(data); this._restoreNativeTitleLookup(); this._setStatus(I18n.t("url_to_article.success"), "success"); // Auto-hide bar after 3 seconds on success setTimeout(() => this._hideArticleBar(), 3000); } catch (err) { const msg = err.jqXHR?.responseJSON?.error || err.message || I18n.t("url_to_article.error_generic"); this._setStatus(`${I18n.t("url_to_article.error_prefix")} ${msg}`, "error"); if (btn) { btn.disabled = false; btn.textContent = I18n.t("url_to_article.retry_button"); } } }, _populateComposer(data) { const composerModel = this.get("composer.model"); if (!composerModel) return; // Build the article body in Markdown const lines = []; // Attribution header const siteName = data.site_name ? `**${data.site_name}**` : ""; const byline = data.byline ? ` — *${data.byline}*` : ""; if (siteName || byline) { lines.push(`> ${siteName}${byline}`); lines.push(`> ${I18n.t("url_to_article.source_label")}: <${data.url}>`); lines.push(""); } else { lines.push(`> ${I18n.t("url_to_article.source_label")}: <${data.url}>`); lines.push(""); } if (data.description) { lines.push(`*${data.description}*`); lines.push(""); lines.push("---"); lines.push(""); } lines.push(data.markdown || ""); const body = lines.join("\n"); // Only set title if it's still the raw URL (avoid overwriting edited titles) const currentTitle = composerModel.get("title") || ""; if (currentTitle.trim() === data.url || currentTitle.trim() === "") { composerModel.set("title", data.title || data.url); } composerModel.set("reply", body); }, }); });