diff --git a/main.js b/main.js index 6c234b3..a1d6572 100644 --- a/main.js +++ b/main.js @@ -22,872 +22,7 @@ __export(main_exports, { default: () => TagsPlugin }); module.exports = __toCommonJS(main_exports); - -// ../impro/impro-plugin/main.js -var SimpleUUID = class { - constructor() { - this._id = 0; - } - create() { - return this._id++; - } -}; -var uuid = new SimpleUUID(); -var callHandlers = /* @__PURE__ */ new Map(); -var pendingHostCalls = /* @__PURE__ */ new Map(); -function hostCall(method, ...args) { - const hostCallId = uuid.create(); - return new Promise((resolve, reject) => { - pendingHostCalls.set(hostCallId, { resolve, reject }); - self.postMessage({ type: "hostCall", method, hostCallId, args }); - }); -} -var eventListeners = /* @__PURE__ */ new Map(); -var registeredEvents = /* @__PURE__ */ new Set(); -async function invokeListeners(listeners, event, args) { - for (const listener of listeners) { - try { - await listener(...args); - } catch (error) { - console.error(`"${event}" listener threw:`, error); - } - } -} -async function dispatchEvent(event, args) { - const listeners = eventListeners.get(event) ?? /* @__PURE__ */ new Set(); - switch (event) { - case "post-context-menu": - case "profile-context-menu": { - const menu = new Menu(); - await invokeListeners(listeners, event, [menu, ...args]); - return menu._serialize(); - } - case "post-composer-open": { - const composer = new Composer(); - await invokeListeners(listeners, event, [composer, ...args]); - return composer._serialize(); - } - default: - console.warn(`No dispatch case for plugin event "${event}".`); - return null; - } -} -function addEventListener(event, listener) { - let listeners = eventListeners.get(event); - if (!listeners) { - listeners = /* @__PURE__ */ new Set(); - eventListeners.set(event, listeners); - } - listeners.add(listener); - if (!registeredEvents.has(event)) { - registeredEvents.add(event); - const handlerId = uuid.create(); - callHandlers.set(handlerId, (...args) => dispatchEvent(event, args)); - self.postMessage({ - type: "register", - target: "eventListener", - event, - handlerId - }); - } -} -var MenuItem = class { - constructor() { - this.title = ""; - this.icon = null; - this._callback = () => { - }; - } - setTitle(title) { - this.title = title; - return this; - } - setIcon(icon) { - this.icon = icon; - return this; - } - onClick(callback) { - this._callback = callback; - return this; - } -}; -var Menu = class { - constructor() { - this.items = []; - } - addItem(builder) { - const item = new MenuItem(); - builder(item); - this.items.push(item); - return this; - } - _serialize() { - return this.items.map((item) => { - const handlerId = uuid.create(); - callHandlers.set(handlerId, item._callback); - const icon = item.icon instanceof VirtualEl ? item.icon._serialize() : item.icon; - return { title: item.title, icon, handlerId }; - }); - } -}; -var Composer = class { - constructor() { - this._ops = []; - this._cursor = null; - } - setText(text) { - this._ops.push({ op: "set", text: String(text) }); - return this; - } - appendText(text) { - this._ops.push({ op: "append", text: String(text) }); - return this; - } - prependText(text) { - this._ops.push({ op: "prepend", text: String(text) }); - return this; - } - setCursor(index) { - this._cursor = index; - return this; - } - _serialize() { - return { ops: this._ops, cursor: this._cursor }; - } -}; -var PluginData = class { - getPost(uri) { - return hostCall("getPost", { uri }); - } - getProfile(did) { - return hostCall("getProfile", { did }); - } - // Like getProfile, but includes viewer relationship details not present - // on the basic profile view: viewer.following, viewer.followedBy, and - // viewer.knownFollowers (a summary of mutual followers). - getDetailedProfile(did) { - return hostCall("getDetailedProfile", { did }); - } - // The full known-followers list for did (the summary on - // getDetailedProfile's viewer.knownFollowers is capped to a handful). - getKnownFollowers(did) { - return hostCall("getKnownFollowers", { did }); - } - getRecord(repo, collection, rkey) { - return hostCall("getRecord", { repo, collection, rkey }); - } -}; -var App = class { - constructor() { - this.currentUser = null; - this.data = new PluginData(); - } - on(event, listener) { - addEventListener(event, listener); - } - refreshFeedFilters(feedURI = null) { - return hostCall("refreshFeedFilters", feedURI); - } - // Actions on behalf of the signed-in user. Each method requires the - // corresponding scope ("mute", "block", "feedFeedback") to be declared in - // the plugin manifest's `permissions.actions` array, which the user must - // grant at install time. - muteActor(did) { - return hostCall("muteActor", { did, mute: true }); - } - unmuteActor(did) { - return hostCall("muteActor", { did, mute: false }); - } - blockActor(did) { - return hostCall("blockActor", { did, block: true }); - } - unblockActor(did) { - return hostCall("blockActor", { did, block: false }); - } - // Acts like the user clicking "Show less like this": sends the requestLess - // feedback signal to the feed that served the post and collapses the post - // behind a feedback message in feeds. - showLessLikeThis(postUri, feedUri) { - return hostCall("showLessLikeThis", { postUri, feedUri }); - } - showMoreLikeThis(postUri, feedUri) { - return hostCall("showMoreLikeThis", { postUri, feedUri }); - } -}; -async function fetch(url, init = {}) { - const result = await hostCall("fetch", { - url, - init: serializeFetchInit(init) - }); - return new PluginResponse(result); -} -function serializeFetchInit(init) { - const serialized = {}; - if (init.method != null) serialized.method = String(init.method); - if (init.headers != null) { - const headers = {}; - if (typeof init.headers.forEach === "function") { - init.headers.forEach((value, name) => { - headers[name] = value; - }); - } else if (typeof init.headers[Symbol.iterator] === "function") { - for (const [name, value] of init.headers) headers[name] = value; - } else { - Object.assign(headers, init.headers); - } - serialized.headers = headers; - } - if (init.body != null) serialized.body = init.body; - return serialized; -} -var PluginResponse = class { - constructor({ status, ok, headers, body }) { - this.status = status; - this.ok = ok; - this.headers = new Map(Object.entries(headers ?? {})); - this._body = body; - } - async text() { - return this._body; - } - async json() { - return JSON.parse(this._body); - } -}; -var Notice = class { - constructor(message, timeout = 0) { - this._toastId = uuid.create(); - this._timeout = timeout; - this._hidden = false; - this.noticeEl = new VirtualEl("div"); - this.noticeEl.addClass("toast"); - this.noticeEl.setText(message); - queueMicrotask(() => { - if (this._hidden) return; - hostCall("showToast", { - toastId: this._toastId, - element: this.noticeEl._serialize(), - timeout: this._timeout - }); - }); - } - setMessage(message) { - this.noticeEl.setText(message); - return this; - } - hide() { - if (this._hidden) return; - this._hidden = true; - hostCall("hideToast", { toastId: this._toastId }); - } -}; -var StyleSnippet = class { - constructor(cssText) { - this._snippetId = uuid.create(); - this._removed = false; - this.ready = new Promise((resolve, reject) => { - queueMicrotask(() => { - if (this._removed) return resolve(); - hostCall("applyStyleSnippet", { - snippetId: this._snippetId, - cssText - }).then(resolve, reject); - }); - }); - } - remove() { - if (this._removed) return; - this._removed = true; - hostCall("removeStyleSnippet", { snippetId: this._snippetId }); - } -}; -var registered = false; -var Plugin = class { - constructor() { - this.app = new App(); - } - addSidebarItem(icon, title, callback = () => { - }) { - const handlerId = uuid.create(); - callHandlers.set(handlerId, callback); - self.postMessage({ - type: "register", - target: "sidebarItem", - icon: icon instanceof VirtualEl ? icon._serialize() : icon, - title, - handlerId - }); - } - async loadData() { - return hostCall("loadData"); - } - async saveData(data) { - await hostCall("saveData", { data }); - } - // Device-local counterpart to loadData/saveData: never synced through the - // user's account preferences, so it's the right place for anything that - // shouldn't silently follow the plugin to another device (e.g. a locally - // held secret key). Cleared on uninstall, same as loadData/saveData. - async loadLocalData() { - return hostCall("loadLocalData"); - } - async saveLocalData(data) { - await hostCall("saveLocalData", { data }); - } - addSettingTab(tab) { - tab.plugin = this; - const displayHandlerId = uuid.create(); - callHandlers.set(displayHandlerId, () => { - tab.containerEl = new VirtualEl("div"); - tab.display(); - return tab.containerEl._serialize(); - }); - self.postMessage({ - type: "register", - target: "settingTab", - name: tab.name ?? null, - displayHandlerId - }); - this._settingTab = tab; - } - addFeedFilter(callback = () => { - }) { - const handlerId = uuid.create(); - callHandlers.set(handlerId, callback); - self.postMessage({ - type: "register", - target: "feedFilter", - handlerId - }); - } - // callback(tokens, context) receives the rich-text token stream for one - // post and returns a new token array (or the input unchanged). The host - // batches all posts of a render into one call per plugin. - // - // options.handlesFacetTypes: array of facet feature $type strings this - // transform owns, to prevent render flash of fallback text - registerRichTextTransform(callback = (tokens) => tokens, options = {}) { - const handlerId = uuid.create(); - callHandlers.set(handlerId, async (batch) => { - const results = []; - for (const { tokens, context } of batch) { - try { - const value = await callback(tokens, context); - results.push({ value: serializeTransformTokens(value) }); - } catch (error) { - results.push({ error: error?.message ?? String(error) }); - } - } - return results; - }); - const handlesFacetTypes = Array.isArray(options.handlesFacetTypes) ? options.handlesFacetTypes.filter((type) => typeof type === "string") : []; - self.postMessage({ - type: "register", - target: "richTextTransform", - handlerId, - handlesFacetTypes - }); - } - registerSlot(name, callback = () => null) { - const handlerId = uuid.create(); - callHandlers.set(handlerId, async (context) => { - const result = await callback(context); - if (result == null) return null; - if (!(result instanceof VirtualEl)) { - const description = result?.constructor?.name ?? typeof result; - throw new Error( - `Slot "${name}" must return a VirtualEl (or null), got ${description}` - ); - } - return result._serialize(); - }); - self.postMessage({ - type: "register", - target: "slot", - name, - handlerId - }); - } - // Makes every mounted re-invoke this plugin's - // registered callbacks for that slot. Useful when a slot's content depends - // on data that resolves asynchronously after the initial render. - refreshSlot(name) { - return hostCall("refreshSlot", { name }); - } - onload() { - } - onunload() { - } - static register() { - if (registered) return; - registered = true; - const instance = new this(); - hostCall("getCurrentUser").then((user) => { - instance.app.currentUser = user; - return instance.onload(); - }).then( - () => self.postMessage({ type: "ready" }), - (error) => self.postMessage({ - type: "ready", - error: error?.message ?? String(error) - }) - ); - } -}; -function serializeTransformTokens(tokens) { - if (!Array.isArray(tokens)) return tokens; - return tokens.map((token) => { - if ((token?.type === "inline" || token?.type === "block") && token.node instanceof VirtualEl) { - return { ...token, node: token.node._serialize() }; - } - return token; - }); -} -var openModals = /* @__PURE__ */ new Map(); -var Modal = class { - constructor() { - this._modalId = uuid.create(); - this.contentEl = new VirtualEl("div"); - this.titleEl = new VirtualEl("h2"); - } - open() { - if (openModals.has(this._modalId)) return; - openModals.set(this._modalId, this); - this.onOpen(); - self.postMessage({ - type: "hostCall", - method: "openModal", - args: [ - { - modalId: this._modalId, - title: this.titleEl._serialize(), - content: this.contentEl._serialize() - } - ] - }); - } - update() { - if (!openModals.has(this._modalId)) return; - self.postMessage({ - type: "hostCall", - method: "updateModal", - args: [ - { - modalId: this._modalId, - title: this.titleEl._serialize(), - content: this.contentEl._serialize() - } - ] - }); - } - close() { - if (!openModals.has(this._modalId)) return; - openModals.delete(this._modalId); - self.postMessage({ - type: "hostCall", - method: "closeModal", - args: [{ modalId: this._modalId }] - }); - this.onClose(); - } - onOpen() { - } - onClose() { - } -}; -var PluginSettingTab = class { - constructor() { - this.containerEl = new VirtualEl("div"); - this.name = null; - } - setName(name) { - this.name = name; - return this; - } - display() { - } - refresh({ reset = false } = {}) { - return hostCall("refreshSettingTab", { reset }); - } -}; -var Setting = class { - constructor(containerEl) { - this.settingEl = containerEl.createDiv({ cls: "setting-item" }); - this.infoEl = this.settingEl.createDiv({ cls: "setting-item-info" }); - this.nameEl = this.infoEl.createEl("h2", { cls: "setting-item-name" }); - this.descEl = this.infoEl.createEl("p", { cls: "setting-item-desc" }); - this.controlEl = this.settingEl.createDiv({ - cls: "setting-item-control" - }); - } - setName(text) { - this.nameEl.setText(text); - return this; - } - setDesc(text) { - this.descEl.setText(text); - return this; - } - addText(callback) { - const component = new TextComponent(this.controlEl); - callback(component); - return this; - } - addTextArea(callback) { - const component = new TextAreaComponent(this.controlEl); - callback(component); - return this; - } - addToggle(callback) { - const component = new ToggleComponent(this.controlEl); - callback(component); - return this; - } - addDropdown(callback) { - const component = new DropdownComponent(this.controlEl); - callback(component); - return this; - } - addButton(callback) { - const component = new ButtonComponent(this.controlEl); - callback(component); - return this; - } -}; -var TextComponent = class { - constructor(containerEl) { - this.el = containerEl.createEl("input", { - attr: { type: "text" }, - cls: "setting-item-text-input" - }); - } - setValue(value) { - this.el.setAttr("value", value == null ? "" : String(value)); - return this; - } - setPlaceholder(value) { - this.el.setAttr("placeholder", value); - return this; - } - onChange(callback) { - this.el.onChange((event) => callback(event.target.value)); - return this; - } -}; -var TextAreaComponent = class { - constructor(containerEl) { - this.el = containerEl.createEl("textarea", { - cls: "setting-item-textarea" - }); - } - setValue(value) { - this.el.setText(value == null ? "" : String(value)); - return this; - } - setPlaceholder(value) { - this.el.setAttr("placeholder", value); - return this; - } - onChange(callback) { - this.el.onChange((event) => callback(event.target.value)); - return this; - } -}; -var ToggleComponent = class { - constructor(containerEl) { - this.el = containerEl.createEl("toggle-switch", { - cls: "setting-item-toggle" - }); - } - setValue(value) { - if (value) this.el.setAttr("checked", ""); - else delete this.el.attrs.checked; - return this; - } - onChange(callback) { - this.el.onChange((event) => callback(event.target.checked)); - return this; - } -}; -var DropdownComponent = class { - constructor(containerEl) { - this.el = containerEl.createEl("select", { - cls: "setting-item-dropdown" - }); - } - addOption(value, label) { - this.el.createEl("option", { text: label, attr: { value } }); - return this; - } - addOptions(map) { - for (const [value, label] of Object.entries(map)) { - this.addOption(value, label); - } - return this; - } - setValue(value) { - for (const child of this.el.children) { - if (child.attrs?.value === value) { - child.attrs.selected = ""; - } else if (child.attrs) { - delete child.attrs.selected; - } - } - return this; - } - onChange(callback) { - this.el.onChange((event) => callback(event.target.value)); - return this; - } -}; -var ButtonComponent = class { - constructor(containerEl) { - this.el = containerEl.createEl("button", { - cls: "rounded-button" - }); - } - setButtonText(text) { - this.el.setText(text); - return this; - } - setCta() { - this.el.addClass("rounded-button-primary"); - return this; - } - onClick(callback) { - this.el.onClick(callback); - return this; - } -}; -var IconComponent = class { - constructor(containerEl) { - this.el = containerEl.createEl("plugin-icon"); - } - setIcon(name) { - this.el.setAttr("icon", name); - return this; - } -}; -var BlobImageComponent = class { - constructor(containerEl) { - this.el = containerEl.createEl("plugin-blob-image"); - } - setDid(did) { - this.el.setAttr("did", did); - return this; - } - setCid(cid) { - this.el.setAttr("cid", cid); - return this; - } - setAlt(alt) { - this.el.setAttr("alt", alt); - return this; - } - setCdnPrefix(prefix) { - this.el.setAttr("cdn-prefix", prefix); - return this; - } -}; -var ProfilesListComponent = class { - constructor(containerEl) { - this.el = containerEl.createEl("plugin-profiles-list"); - } - setDids(dids) { - const value = Array.isArray(dids) ? dids.join(",") : String(dids ?? ""); - this.el.setAttr("dids", value); - return this; - } - setEmptyMessage(message) { - this.el.setAttr("empty-message", message); - return this; - } -}; -var PostsFeedComponent = class { - constructor(containerEl) { - this.el = containerEl.createEl("plugin-posts-feed"); - } - setUris(uris) { - const value = Array.isArray(uris) ? uris.join(",") : String(uris ?? ""); - this.el.setAttr("uris", value); - return this; - } - setEmptyMessage(message) { - this.el.setAttr("empty-message", message); - return this; - } -}; -var VirtualText = class { - constructor(value) { - this.value = value == null ? "" : String(value); - } - _serialize() { - return { type: "text", value: this.value }; - } -}; -var VirtualEl = class _VirtualEl { - constructor(tag) { - this.tag = tag; - this.attrs = {}; - this.styles = {}; - this.children = []; - this.events = {}; - } - setStyle(name, value) { - this.styles[String(name)] = value == null ? "" : String(value); - return this; - } - onClick(fn) { - const handlerId = uuid.create(); - callHandlers.set(handlerId, fn); - this.events.click = handlerId; - return this; - } - onChange(fn) { - const handlerId = uuid.create(); - callHandlers.set(handlerId, fn); - this.events.change = handlerId; - return this; - } - onInput(fn) { - const handlerId = uuid.create(); - callHandlers.set(handlerId, fn); - this.events.input = handlerId; - return this; - } - setText(text) { - this.children = []; - if (text != null && text !== "") this.children.push(new VirtualText(text)); - return this; - } - empty() { - this.children = []; - return this; - } - appendChild(child) { - if (!(child instanceof _VirtualEl) && !(child instanceof VirtualText)) { - throw new TypeError( - "appendChild expects a VirtualEl or VirtualText instance" - ); - } - this.children.push(child); - return this; - } - appendText(value) { - this.children.push(new VirtualText(value)); - return this; - } - createText(value) { - const node = new VirtualText(value); - this.children.push(node); - return node; - } - addClass(cls) { - this.attrs.class = this.attrs.class ? `${this.attrs.class} ${cls}` : cls; - return this; - } - setAttr(name, value) { - this.attrs[name] = value === void 0 ? "" : value; - return this; - } - createEl(tag, options = {}, callback) { - const child = new _VirtualEl(tag); - if (options.text != null) child.setText(options.text); - if (options.cls) { - child.attrs.class = Array.isArray(options.cls) ? options.cls.join(" ") : options.cls; - } - if (options.attr) Object.assign(child.attrs, options.attr); - this.children.push(child); - if (typeof callback === "function") callback(child); - return child; - } - createDiv(options = {}, callback) { - return this.createEl("div", options, callback); - } - createSpan(options = {}, callback) { - return this.createEl("span", options, callback); - } - createProfilesList(callback) { - const component = new ProfilesListComponent(this); - if (typeof callback === "function") callback(component); - return component; - } - createPostsFeed(callback) { - const component = new PostsFeedComponent(this); - if (typeof callback === "function") callback(component); - return component; - } - createIcon(callback) { - const component = new IconComponent(this); - if (typeof callback === "function") callback(component); - return component; - } - createBlobImage(callback) { - const component = new BlobImageComponent(this); - if (typeof callback === "function") callback(component); - return component; - } - _serialize() { - const serialized = { - type: "element", - tag: this.tag, - attrs: this.attrs, - events: this.events, - children: this.children.map((child) => child._serialize()) - }; - if (Object.keys(this.styles).length > 0) serialized.styles = this.styles; - return serialized; - } -}; -self.onmessage = async (event) => { - const message = event.data; - if (!message || typeof message !== "object") return; - if (message.type === "call") { - const fn = callHandlers.get(message.handlerId); - if (!fn) { - self.postMessage({ - type: "result", - callId: message.callId, - error: `unknown handler ${message.handlerId}` - }); - return; - } - try { - const value = await fn(...message.args); - self.postMessage({ type: "result", callId: message.callId, value }); - } catch (error) { - self.postMessage({ - type: "result", - callId: message.callId, - error: error.message ?? String(error) - }); - } - return; - } - if (message.type === "hostResult") { - const pending = pendingHostCalls.get(message.hostCallId); - if (!pending) return; - pendingHostCalls.delete(message.hostCallId); - if (message.error) pending.reject(new Error(message.error)); - else pending.resolve(message.value); - return; - } - if (message.type === "event") { - switch (message.event) { - case "modalDismissed": { - const modal = openModals.get(message.data.modalId); - if (modal) { - openModals.delete(message.data.modalId); - modal.onClose(); - } - return; - } - } - return; - } -}; - -// src/main.js +var import_impro_plugin = require("@impro.social/impro-plugin"); var DEFAULT_DATA = { // { [did]: { tags: string[], updatedAt: string } } tags: {}, @@ -1698,7 +833,7 @@ var emojiNames = null; var emojiNamesPromise = null; function ensureEmojiNames() { if (!emojiNamesPromise) { - emojiNamesPromise = fetch(EMOJI_NAMES_URL).then((res) => res.ok ? res.json() : {}).then((data) => { + emojiNamesPromise = (0, import_impro_plugin.fetch)(EMOJI_NAMES_URL).then((res) => res.ok ? res.json() : {}).then((data) => { emojiNames = data; }).catch(() => { emojiNames = {}; @@ -1779,16 +914,7 @@ function renderIconPicker(containerEl, { var APPVIEW_BASE = "https://public.api.bsky.app"; async function appViewXrpc(path, query) { const qs = new URLSearchParams(query).toString(); - const res = await fetch(`${APPVIEW_BASE}/xrpc/${path}?${qs}`); - if (!res.ok) { - throw new Error(`${path} failed: ${res.status}`); - } - return res.json(); -} -var CONSTELLATION_BASE = "https://constellation.microcosm.blue"; -async function constellationXrpc(path, query) { - const qs = new URLSearchParams(query).toString(); - const res = await fetch(`${CONSTELLATION_BASE}/xrpc/${path}?${qs}`); + const res = await (0, import_impro_plugin.fetch)(`${APPVIEW_BASE}/xrpc/${path}?${qs}`); if (!res.ok) { throw new Error(`${path} failed: ${res.status}`); } @@ -1816,7 +942,7 @@ async function describeProfile(app, did) { } return did; } -var EditTagsModal = class extends Modal { +var EditTagsModal = class extends import_impro_plugin.Modal { constructor(plugin, did, tags) { super(); this.plugin = plugin; @@ -1892,7 +1018,7 @@ var EditTagsModal = class extends Modal { this.update(); }); } - new Setting(this.contentEl).setName("Add tag").addText((text) => { + new import_impro_plugin.Setting(this.contentEl).setName("Add tag").addText((text) => { text.setPlaceholder("friend").setValue(this._newTagText); text.el.onInput((event) => { this._newTagText = event.target.value; @@ -1926,7 +1052,7 @@ var EditTagsModal = class extends Modal { ); this.close(); } catch (error) { - new Notice(`Couldn't save tags: ${error.message ?? error}`, 4e3); + new import_impro_plugin.Notice(`Couldn't save tags: ${error.message ?? error}`, 4e3); } }); if (this.rows.length > 0) { @@ -1938,7 +1064,7 @@ var EditTagsModal = class extends Modal { await this.plugin.removeTags(this.did); this.close(); } catch (error) { - new Notice(`Couldn't remove tags: ${error.message ?? error}`, 4e3); + new import_impro_plugin.Notice(`Couldn't remove tags: ${error.message ?? error}`, 4e3); } }); } @@ -1949,7 +1075,7 @@ var EditTagsModal = class extends Modal { this.contentEl.empty(); } }; -var TagListModal = class extends Modal { +var TagListModal = class extends import_impro_plugin.Modal { constructor(plugin, tag, entries) { super(); this.plugin = plugin; @@ -1974,7 +1100,7 @@ var TagListModal = class extends Modal { // and reopening. render() { this.contentEl.empty(); - new Setting(this.contentEl).setName("Description").setDesc("Shown as a hover tooltip on this tag's badges.").addText((text) => { + new import_impro_plugin.Setting(this.contentEl).setName("Description").setDesc("Shown as a hover tooltip on this tag's badges.").addText((text) => { text.setPlaceholder("Optional").setValue(this._descriptionText); text.el.onInput((event) => { this._descriptionText = event.target.value; @@ -1983,9 +1109,9 @@ var TagListModal = class extends Modal { (button) => button.setButtonText("Save").onClick(async () => { try { await this.plugin.setTagDescription(this.tag, this._descriptionText); - new Notice("Description saved.", 2e3); + new import_impro_plugin.Notice("Description saved.", 2e3); } catch (error) { - new Notice(`Couldn't save description: ${error.message ?? error}`, 4e3); + new import_impro_plugin.Notice(`Couldn't save description: ${error.message ?? error}`, 4e3); } }) ); @@ -2019,7 +1145,7 @@ var TagListModal = class extends Modal { try { await this.plugin.setTagIcon(this.tag, { kind: "icon", value }); } catch (error) { - new Notice(`Couldn't save icon: ${error.message ?? error}`, 4e3); + new import_impro_plugin.Notice(`Couldn't save icon: ${error.message ?? error}`, 4e3); } this._iconPickerOpen = false; this._iconFilter = ""; @@ -2031,7 +1157,7 @@ var TagListModal = class extends Modal { try { await this.plugin.setTagIcon(this.tag, { kind: "emoji", value }); } catch (error) { - new Notice(`Couldn't save icon: ${error.message ?? error}`, 4e3); + new import_impro_plugin.Notice(`Couldn't save icon: ${error.message ?? error}`, 4e3); } this._iconPickerOpen = false; this._iconFilter = ""; @@ -2068,7 +1194,7 @@ var TagListModal = class extends Modal { this.render(); this.update(); } catch (error) { - new Notice(`Couldn't update tag: ${error.message ?? error}`, 4e3); + new import_impro_plugin.Notice(`Couldn't update tag: ${error.message ?? error}`, 4e3); } }); } @@ -2080,7 +1206,7 @@ var TagListModal = class extends Modal { this.contentEl.empty(); } }; -var MigrateListModal = class extends Modal { +var MigrateListModal = class extends import_impro_plugin.Modal { constructor(plugin, lists) { super(); this.plugin = plugin; @@ -2114,7 +1240,7 @@ var MigrateListModal = class extends Modal { buttons2.createEl("button", { text: "Close", cls: "modal-dialog-button" }).onClick(() => this.close()); return; } - new Setting(this.contentEl).setName("List").addDropdown((dropdown) => { + new import_impro_plugin.Setting(this.contentEl).setName("List").addDropdown((dropdown) => { dropdown.addOptions( Object.fromEntries(this.lists.map((list) => [list.uri, list.name])) ); @@ -2127,7 +1253,7 @@ var MigrateListModal = class extends Modal { this.update(); }); }); - new Setting(this.contentEl).setName("Tag").setDesc( + new import_impro_plugin.Setting(this.contentEl).setName("Tag").setDesc( "Applied to every member of the list, merged into any tags they already have. Auto-abbreviated from the list name \u2014 edit freely." ).addText((text) => { text.setValue(this.tagText); @@ -2193,7 +1319,7 @@ var MigrateListModal = class extends Modal { this._migrating = false; this.render(); this.update(); - new Notice( + new import_impro_plugin.Notice( `Couldn't migrate list: ${error.message ?? error}`, 4e3 ); @@ -2207,7 +1333,7 @@ var MigrateListModal = class extends Modal { } }; var TAGS_EXPORT_TYPE = "impro-tags-export"; -var ExportImportModal = class extends Modal { +var ExportImportModal = class extends import_impro_plugin.Modal { constructor(plugin) { super(); this.plugin = plugin; @@ -2224,7 +1350,7 @@ var ExportImportModal = class extends Modal { text: "Select all and copy \u2014 to back up your tags, or to move them to another account.", cls: "setting-item-desc" }); - new Setting(this.contentEl).addTextArea( + new import_impro_plugin.Setting(this.contentEl).addTextArea( (textarea) => textarea.setValue(this.plugin.exportTagsData()) ); this.contentEl.createEl("h3", { text: "Import", cls: "setting-item-name" }); @@ -2232,7 +1358,7 @@ var ExportImportModal = class extends Modal { text: "Paste an export from this plugin (from this account or another one).", cls: "setting-item-desc" }); - new Setting(this.contentEl).addTextArea((textarea) => { + new import_impro_plugin.Setting(this.contentEl).addTextArea((textarea) => { textarea.setPlaceholder("Paste exported JSON here"); textarea.el.onInput((event) => { this._importText = event.target.value; @@ -2254,13 +1380,13 @@ var ExportImportModal = class extends Modal { if (!text) return; try { const { accountCount } = await this.plugin.importTagsData(text, mode); - new Notice( + new import_impro_plugin.Notice( mode === "replace" ? `Replaced all tags with the import (${accountCount} account(s)).` : `Merged in tags for ${accountCount} account(s).`, 4e3 ); this.close(); } catch (error) { - new Notice(`Import failed: ${error.message ?? error}`, 4e3); + new import_impro_plugin.Notice(`Import failed: ${error.message ?? error}`, 4e3); } } onClose() { @@ -2268,7 +1394,7 @@ var ExportImportModal = class extends Modal { this.contentEl.empty(); } }; -var TagsSettingTab = class extends PluginSettingTab { +var TagsSettingTab = class extends import_impro_plugin.PluginSettingTab { constructor() { super(); this.setName("Tags"); @@ -2280,17 +1406,17 @@ var TagsSettingTab = class extends PluginSettingTab { text: "Tag accounts from the post/profile menu \u2014 tags are private to you and synced across your devices.", cls: "setting-item-desc" }); - new Setting(this.containerEl).setName("Migrate a list to tags").setDesc("Bulk-tag every member of one of your Bluesky lists.").addButton( + new import_impro_plugin.Setting(this.containerEl).setName("Migrate a list to tags").setDesc("Bulk-tag every member of one of your Bluesky lists.").addButton( (button) => button.setButtonText("Choose a list").onClick(async () => { try { const lists = await this.plugin.getMyLists(); new MigrateListModal(this.plugin, lists).open(); } catch (error) { - new Notice(`Couldn't load your lists: ${error.message ?? error}`, 4e3); + new import_impro_plugin.Notice(`Couldn't load your lists: ${error.message ?? error}`, 4e3); } }) ); - new Setting(this.containerEl).setName("Export / import tags").setDesc("Back up your tags, or move them to another account.").addButton( + new import_impro_plugin.Setting(this.containerEl).setName("Export / import tags").setDesc("Back up your tags, or move them to another account.").addButton( (button) => button.setButtonText("Open").onClick(() => { new ExportImportModal(this.plugin).open(); }) @@ -2307,7 +1433,7 @@ var TagsSettingTab = class extends PluginSettingTab { cls: "setting-item-name" }); const browseTag = this._browseTag ?? tags[0]; - new Setting(this.containerEl).setName("Tag").addDropdown((dropdown) => { + new import_impro_plugin.Setting(this.containerEl).setName("Tag").addDropdown((dropdown) => { dropdown.addOptions(Object.fromEntries(tags.map((t) => [t, t]))); dropdown.setValue(browseTag); dropdown.onChange((value) => { @@ -2433,11 +1559,11 @@ var BADGE_STYLE = ` margin: 0; } `; -var TagsPlugin = class extends Plugin { +var TagsPlugin = class extends import_impro_plugin.Plugin { async onload() { const saved = await this.loadData(); this.data = { ...DEFAULT_DATA, ...saved ?? {} }; - new StyleSnippet(BADGE_STYLE); + new import_impro_plugin.StyleSnippet(BADGE_STYLE); this.settingTab = new TagsSettingTab(); this.addSettingTab(this.settingTab); this.app.on("profile-context-menu", (menu, profile) => { @@ -2560,7 +1686,7 @@ var TagsPlugin = class extends Plugin { if (!did) return null; const entry = this.data.tags[did]; if (!entry || entry.tags.length === 0) return null; - const container = new VirtualEl("div"); + const container = new import_impro_plugin.VirtualEl("div"); container.addClass("label-badges"); for (const tag of entry.tags) { const description = this.data.tagDescriptions[tag]; @@ -2597,26 +1723,19 @@ var TagsPlugin = class extends Plugin { return lists; } // Enumerates every app.bsky.graph.listitem record referencing this list - // via Constellation (raw, unfiltered — see CONSTELLATION_BASE comment - // above), then reads each one's actual subject DID via the existing - // getRecord bridge method (no extra permission needed for that part — - // it isn't gated). Listitem records only ever live in the list owner's - // own repo, so `did` here is always the current user's own DID. + // via the host's native backlink index (raw, unfiltered — unlike + // app.bsky.graph.getList, an AppView-hydrated view that can quietly + // omit list items depending on the relationship between the list owner + // and a member), then reads each one's actual subject DID via the + // existing getRecord bridge method. Listitem records only ever live in + // the list owner's own repo, so `did` here is always the current + // user's own DID. async getListMembers(listUri) { - const refs = []; - let cursor; - do { - const data = await constellationXrpc("blue.microcosm.links.getBacklinks", { - subject: listUri, - source: "app.bsky.graph.listitem:list", - limit: 100, - ...cursor ? { cursor } : {} - }); - for (const record of data.records ?? []) { - refs.push(record); - } - cursor = data.cursor ?? null; - } while (cursor); + const refs = await this.app.data.getBacklinks({ + subject: listUri, + source: "app.bsky.graph.listitem:list", + limit: 1e3 + }); const subjects = await Promise.all( refs.map(async ({ did, collection, rkey }) => { try { @@ -2652,7 +1771,7 @@ var TagsPlugin = class extends Plugin { await this.persist(); this.refreshSlot("author-badges"); this.refreshSettingsTab(); - new Notice( + new import_impro_plugin.Notice( `Tagged ${members.length} account(s) from "${list.name}" as "${tag}".`, 4e3 ); diff --git a/manifest.json b/manifest.json index 27dd9dd..189c9a7 100644 --- a/manifest.json +++ b/manifest.json @@ -7,7 +7,6 @@ "permissions": { "fetch": [ "https://public.api.bsky.app/xrpc/*", - "https://constellation.microcosm.blue/xrpc/*", "https://cdn.jsdelivr.net/npm/unicode-emoji-json@0.9.0/data-by-emoji.json" ] } diff --git a/package-lock.json b/package-lock.json index 01e04f7..35e7305 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,8 +16,12 @@ }, "../impro/impro-plugin": { "name": "@impro.social/impro-plugin", - "version": "0.0.18", - "license": "0BSD" + "version": "0.0.23", + "license": "0BSD", + "devDependencies": { + "typedoc": "^0.28.20", + "typedoc-plugin-markdown": "^4.12.0" + } }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", diff --git a/package.json b/package.json index 129e9ac..b9c28a0 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "type": "module", "scripts": { "start": "npm run watch", - "build": "esbuild src/main.js --bundle --format=cjs --outfile=main.js", - "watch": "esbuild src/main.js --bundle --format=cjs --outfile=main.js --watch" + "build": "esbuild src/main.js --bundle --external:@impro.social/impro-plugin --format=cjs --outfile=main.js", + "watch": "esbuild src/main.js --bundle --external:@impro.social/impro-plugin --format=cjs --outfile=main.js --watch" }, "devDependencies": { "esbuild": "^0.25.10" diff --git a/src/main.js b/src/main.js index a9ec41e..6850fe7 100644 --- a/src/main.js +++ b/src/main.js @@ -975,25 +975,6 @@ async function appViewXrpc(path, query) { return res.json(); } -// Constellation (https://github.com/at-microcosm/constellation) is a -// public, network-wide backlink indexer — already a trusted provider in -// this workspace (idle-radar/src/constellationClient.js). Unlike -// app.bsky.graph.getList (an AppView-hydrated view that can quietly omit -// list items depending on the relationship between the list owner and a -// member), Constellation just indexes "this record exists and references -// this URI" with no relationship-based filtering — the only reliable way -// to get a list's complete raw membership. -const CONSTELLATION_BASE = "https://constellation.microcosm.blue"; - -async function constellationXrpc(path, query) { - const qs = new URLSearchParams(query).toString(); - const res = await pluginFetch(`${CONSTELLATION_BASE}/xrpc/${path}?${qs}`); - if (!res.ok) { - throw new Error(`${path} failed: ${res.status}`); - } - return res.json(); -} - // Slugifies short list names as-is, reduces longer multi-word names to // initials, and truncates longer single-word names — a starting point for // the migration modal's tag field, not meant to be exact. Always @@ -1933,26 +1914,19 @@ export default class TagsPlugin extends Plugin { } // Enumerates every app.bsky.graph.listitem record referencing this list - // via Constellation (raw, unfiltered — see CONSTELLATION_BASE comment - // above), then reads each one's actual subject DID via the existing - // getRecord bridge method (no extra permission needed for that part — - // it isn't gated). Listitem records only ever live in the list owner's - // own repo, so `did` here is always the current user's own DID. + // via the host's native backlink index (raw, unfiltered — unlike + // app.bsky.graph.getList, an AppView-hydrated view that can quietly + // omit list items depending on the relationship between the list owner + // and a member), then reads each one's actual subject DID via the + // existing getRecord bridge method. Listitem records only ever live in + // the list owner's own repo, so `did` here is always the current + // user's own DID. async getListMembers(listUri) { - const refs = []; - let cursor; - do { - const data = await constellationXrpc("blue.microcosm.links.getBacklinks", { - subject: listUri, - source: "app.bsky.graph.listitem:list", - limit: 100, - ...(cursor ? { cursor } : {}), - }); - for (const record of data.records ?? []) { - refs.push(record); - } - cursor = data.cursor ?? null; - } while (cursor); + const refs = await this.app.data.getBacklinks({ + subject: listUri, + source: "app.bsky.graph.listitem:list", + limit: 1000, + }); const subjects = await Promise.all( refs.map(async ({ did, collection, rkey }) => {