Mirror of https://github.com/improsocial/impro-sample-plugin
0

Configure Feed

Select the types of activity you want to include in your feed.

Use hosted package

Grace Kind (May 20, 2026, 11:08 PM -0500) f311399e 16c9a64d

+24 -563
+1
.npmrc
··· 1 + @atpkgs:registry = https://registry.atpkgs.easrng.net/
+10
package-lock.json
··· 7 7 "": { 8 8 "name": "impro-sample-plugin", 9 9 "version": "0.1.1", 10 + "dependencies": { 11 + "@impro.social/impro-plugin": "npm:@atpkgs/didplc__bpyevhddfoledxzsxracdlkg__impro-plugin@latest" 12 + }, 10 13 "devDependencies": { 11 14 "esbuild": "^0.25.10" 12 15 } ··· 452 455 "engines": { 453 456 "node": ">=18" 454 457 } 458 + }, 459 + "node_modules/@impro.social/impro-plugin": { 460 + "name": "@atpkgs/didplc__bpyevhddfoledxzsxracdlkg__impro-plugin", 461 + "version": "0.0.3", 462 + "resolved": "https://hebeloma.us-west.host.bsky.network/xrpc/com.atproto.sync.getBlob?did=did:plc:bpyevhddfoledxzsxracdlkg&cid=bafkreihsbioguk2jqs5pzcj52x3wbxj73svyyytth7npan25hgo6wc4v5u", 463 + "integrity": "sha256-8gocaitJhLr8iT3V92DdP9yrjGJzP9rwN105nesLle0=", 464 + "license": "0BSD" 455 465 }, 456 466 "node_modules/esbuild": { 457 467 "version": "0.25.12",
+6 -3
package.json
··· 1 1 { 2 2 "name": "impro-sample-plugin", 3 - "version": "0.1.1", 3 + "version": "0.1.2", 4 4 "private": true, 5 5 "type": "module", 6 6 "scripts": { 7 7 "start": "npm run watch", 8 - "build": "esbuild src/main.js --bundle --format=esm --outfile=main.js", 9 - "watch": "esbuild src/main.js --bundle --format=esm --outfile=main.js --watch" 8 + "build": "esbuild src/main.js --bundle --format=cjs --outfile=main.js", 9 + "watch": "esbuild src/main.js --bundle --format=cjs --outfile=main.js --watch" 10 10 }, 11 11 "devDependencies": { 12 12 "esbuild": "^0.25.10" 13 + }, 14 + "dependencies": { 15 + "@impro.social/impro-plugin": "npm:@atpkgs/didplc__bpyevhddfoledxzsxracdlkg__impro-plugin@latest" 13 16 } 14 17 }
+7 -4
src/main.js
··· 1 - import { Modal, Plugin, PluginSettingTab, Setting } from "./pluginWorker.js"; 1 + import { 2 + Modal, 3 + Plugin, 4 + PluginSettingTab, 5 + Setting, 6 + } from "@impro.social/impro-plugin"; 2 7 3 8 const DEFAULT_SETTINGS = { 4 9 greeting: "Hello", ··· 92 97 } 93 98 } 94 99 95 - class ImproSamplePlugin extends Plugin { 100 + export default class ImproSamplePlugin extends Plugin { 96 101 async onload() { 97 102 const saved = await this.loadData(); 98 103 this.settings = { ...DEFAULT_SETTINGS, ...(saved ?? {}) }; ··· 114 119 }); 115 120 } 116 121 } 117 - 118 - ImproSamplePlugin.register();
-556
src/pluginWorker.js
··· 1 - // v0.0.2 2 - export class SimpleUUID { 3 - constructor() { 4 - this._id = 0; 5 - } 6 - create() { 7 - return this._id++; 8 - } 9 - } 10 - 11 - const uuid = new SimpleUUID(); 12 - 13 - const callHandlers = new Map(); 14 - 15 - const pendingHostCalls = new Map(); 16 - 17 - function hostCall(method, ...args) { 18 - const hostCallId = uuid.create(); 19 - return new Promise((resolve, reject) => { 20 - pendingHostCalls.set(hostCallId, { resolve, reject }); 21 - self.postMessage({ type: "hostCall", method, hostCallId, args }); 22 - }); 23 - } 24 - 25 - const eventListeners = new Map(); 26 - 27 - function addEventListener(event, listener) { 28 - let listeners = eventListeners.get(event); 29 - if (!listeners) { 30 - listeners = new Set(); 31 - eventListeners.set(event, listeners); 32 - const handlerId = uuid.create(); 33 - callHandlers.set(handlerId, async (...args) => { 34 - const menu = new Menu(); 35 - for (const eventListener of listeners) { 36 - try { 37 - await eventListener(menu, ...args); 38 - } catch (error) { 39 - console.error(`"${event}" listener threw:`, error); 40 - } 41 - } 42 - return menu._serialize(); 43 - }); 44 - self.postMessage({ 45 - type: "register", 46 - target: "eventListener", 47 - event, 48 - handlerId, 49 - }); 50 - } 51 - listeners.add(listener); 52 - } 53 - 54 - export class MenuItem { 55 - constructor() { 56 - this.title = ""; 57 - this.icon = null; 58 - this._callback = () => {}; 59 - } 60 - setTitle(title) { 61 - this.title = title; 62 - return this; 63 - } 64 - setIcon(icon) { 65 - this.icon = icon; 66 - return this; 67 - } 68 - onClick(callback) { 69 - this._callback = callback; 70 - return this; 71 - } 72 - } 73 - 74 - export class Menu { 75 - constructor() { 76 - this.items = []; 77 - } 78 - addItem(builder) { 79 - const item = new MenuItem(); 80 - builder(item); 81 - this.items.push(item); 82 - return this; 83 - } 84 - _serialize() { 85 - return this.items.map((item) => { 86 - const handlerId = uuid.create(); 87 - callHandlers.set(handlerId, item._callback); 88 - return { title: item.title, icon: item.icon, handlerId }; 89 - }); 90 - } 91 - } 92 - 93 - class App { 94 - constructor() { 95 - this.currentUser = null; 96 - } 97 - on(event, listener) { 98 - addEventListener(event, listener); 99 - } 100 - 101 - refreshFeedFilters(feedURI = null) { 102 - return hostCall("refreshFeedFilters", feedURI); 103 - } 104 - } 105 - 106 - export class Notice { 107 - constructor(message, timeout = 0) { 108 - this._toastId = uuid.create(); 109 - this._timeout = timeout; 110 - this._hidden = false; 111 - this.noticeEl = new VirtualEl("div"); 112 - this.noticeEl.addClass("toast"); 113 - this.noticeEl.setText(message); 114 - queueMicrotask(() => { 115 - if (this._hidden) return; 116 - hostCall("showToast", { 117 - toastId: this._toastId, 118 - element: this.noticeEl._serialize(), 119 - timeout: this._timeout, 120 - }); 121 - }); 122 - } 123 - setMessage(message) { 124 - this.noticeEl.setText(message); 125 - return this; 126 - } 127 - hide() { 128 - if (this._hidden) return; 129 - this._hidden = true; 130 - hostCall("hideToast", { toastId: this._toastId }); 131 - } 132 - } 133 - 134 - export class StyleSnippet { 135 - constructor(cssText) { 136 - this._snippetId = uuid.create(); 137 - this._removed = false; 138 - this.ready = new Promise((resolve, reject) => { 139 - queueMicrotask(() => { 140 - if (this._removed) return resolve(); 141 - hostCall("applyStyleSnippet", { 142 - snippetId: this._snippetId, 143 - cssText, 144 - }).then(resolve, reject); 145 - }); 146 - }); 147 - } 148 - remove() { 149 - if (this._removed) return; 150 - this._removed = true; 151 - hostCall("removeStyleSnippet", { snippetId: this._snippetId }); 152 - } 153 - } 154 - 155 - let registered = false; 156 - 157 - export class Plugin { 158 - constructor() { 159 - this.app = new App(); 160 - } 161 - 162 - addSidebarItem(icon, title, callback = () => {}) { 163 - const handlerId = uuid.create(); 164 - callHandlers.set(handlerId, callback); 165 - self.postMessage({ 166 - type: "register", 167 - target: "sidebarItem", 168 - icon, 169 - title, 170 - handlerId, 171 - }); 172 - } 173 - 174 - async loadData() { 175 - return hostCall("loadData"); 176 - } 177 - 178 - async saveData(data) { 179 - await hostCall("saveData", { data }); 180 - } 181 - 182 - addSettingTab(tab) { 183 - tab.plugin = this; 184 - const displayHandlerId = uuid.create(); 185 - callHandlers.set(displayHandlerId, () => { 186 - tab.containerEl = new VirtualEl("div"); 187 - tab.display(); 188 - return tab.containerEl._serialize(); 189 - }); 190 - self.postMessage({ 191 - type: "register", 192 - target: "settingTab", 193 - name: tab.name ?? null, 194 - displayHandlerId, 195 - }); 196 - this._settingTab = tab; 197 - } 198 - 199 - addFeedFilter(callback = () => {}) { 200 - const handlerId = uuid.create(); 201 - callHandlers.set(handlerId, callback); 202 - self.postMessage({ 203 - type: "register", 204 - target: "feedFilter", 205 - handlerId, 206 - }); 207 - } 208 - 209 - onload() {} 210 - onunload() {} 211 - 212 - static register() { 213 - if (registered) return; 214 - registered = true; 215 - const instance = new this(); 216 - hostCall("getCurrentUser") 217 - .then((user) => { 218 - instance.app.currentUser = user; 219 - return instance.onload(); 220 - }) 221 - .then( 222 - () => self.postMessage({ type: "ready" }), 223 - (error) => 224 - self.postMessage({ 225 - type: "ready", 226 - error: error?.message ?? String(error), 227 - }), 228 - ); 229 - } 230 - } 231 - 232 - const openModals = new Map(); 233 - 234 - export class Modal { 235 - constructor() { 236 - this._modalId = uuid.create(); 237 - this.contentEl = new VirtualEl("div"); 238 - this.titleEl = new VirtualEl("h2"); 239 - } 240 - 241 - open() { 242 - if (openModals.has(this._modalId)) return; 243 - openModals.set(this._modalId, this); 244 - this.onOpen(); 245 - self.postMessage({ 246 - type: "hostCall", 247 - method: "openModal", 248 - args: [ 249 - { 250 - modalId: this._modalId, 251 - title: this.titleEl._serialize(), 252 - content: this.contentEl._serialize(), 253 - }, 254 - ], 255 - }); 256 - } 257 - 258 - close() { 259 - if (!openModals.has(this._modalId)) return; 260 - openModals.delete(this._modalId); 261 - self.postMessage({ 262 - type: "hostCall", 263 - method: "closeModal", 264 - args: [{ modalId: this._modalId }], 265 - }); 266 - this.onClose(); 267 - } 268 - 269 - onOpen() {} 270 - onClose() {} 271 - } 272 - 273 - export class PluginSettingTab { 274 - constructor() { 275 - this.containerEl = new VirtualEl("div"); 276 - this.name = null; 277 - } 278 - setName(name) { 279 - this.name = name; 280 - return this; 281 - } 282 - display() {} 283 - refresh() { 284 - return hostCall("refreshSettingTab"); 285 - } 286 - } 287 - 288 - export class Setting { 289 - constructor(containerEl) { 290 - this.settingEl = containerEl.createDiv({ cls: "plugin-setting-item" }); 291 - this.infoEl = this.settingEl.createDiv({ cls: "plugin-setting-item-info" }); 292 - this.nameEl = this.infoEl.createDiv({ cls: "plugin-setting-item-name" }); 293 - this.descEl = this.infoEl.createDiv({ cls: "plugin-setting-item-desc" }); 294 - this.controlEl = this.settingEl.createDiv({ 295 - cls: "plugin-setting-item-control", 296 - }); 297 - } 298 - setName(text) { 299 - this.nameEl.setText(text); 300 - return this; 301 - } 302 - setDesc(text) { 303 - this.descEl.setText(text); 304 - return this; 305 - } 306 - addText(callback) { 307 - const component = new TextComponent(this.controlEl); 308 - callback(component); 309 - return this; 310 - } 311 - addToggle(callback) { 312 - const component = new ToggleComponent(this.controlEl); 313 - callback(component); 314 - return this; 315 - } 316 - addDropdown(callback) { 317 - const component = new DropdownComponent(this.controlEl); 318 - callback(component); 319 - return this; 320 - } 321 - addButton(callback) { 322 - const component = new ButtonComponent(this.controlEl); 323 - callback(component); 324 - return this; 325 - } 326 - } 327 - 328 - class TextComponent { 329 - constructor(containerEl) { 330 - this.el = containerEl.createEl("input", { 331 - attr: { type: "text" }, 332 - cls: "plugin-setting-text-input", 333 - }); 334 - } 335 - setValue(value) { 336 - this.el.setAttr("value", value == null ? "" : String(value)); 337 - return this; 338 - } 339 - setPlaceholder(value) { 340 - this.el.setAttr("placeholder", value); 341 - return this; 342 - } 343 - onChange(callback) { 344 - this.el.onChange((event) => callback(event.target.value)); 345 - return this; 346 - } 347 - } 348 - 349 - class ToggleComponent { 350 - constructor(containerEl) { 351 - this.el = containerEl.createEl("input", { 352 - attr: { type: "checkbox" }, 353 - cls: "plugin-setting-toggle", 354 - }); 355 - } 356 - setValue(value) { 357 - if (value) this.el.setAttr("checked", ""); 358 - else delete this.el.attrs.checked; 359 - return this; 360 - } 361 - onChange(callback) { 362 - this.el.onChange((event) => callback(event.target.checked)); 363 - return this; 364 - } 365 - } 366 - 367 - class DropdownComponent { 368 - constructor(containerEl) { 369 - this.el = containerEl.createEl("select", { 370 - cls: "plugin-setting-dropdown", 371 - }); 372 - } 373 - addOption(value, label) { 374 - this.el.createEl("option", { text: label, attr: { value } }); 375 - return this; 376 - } 377 - addOptions(map) { 378 - for (const [value, label] of Object.entries(map)) { 379 - this.addOption(value, label); 380 - } 381 - return this; 382 - } 383 - setValue(value) { 384 - for (const child of this.el.children) { 385 - if (child.attrs?.value === value) { 386 - child.attrs.selected = ""; 387 - } else if (child.attrs) { 388 - delete child.attrs.selected; 389 - } 390 - } 391 - return this; 392 - } 393 - onChange(callback) { 394 - this.el.onChange((event) => callback(event.target.value)); 395 - return this; 396 - } 397 - } 398 - 399 - class ButtonComponent { 400 - constructor(containerEl) { 401 - this.el = containerEl.createEl("button", { 402 - cls: "plugin-setting-button", 403 - }); 404 - } 405 - setButtonText(text) { 406 - this.el.setText(text); 407 - return this; 408 - } 409 - setCta() { 410 - this.el.addClass("primary-button"); 411 - return this; 412 - } 413 - onClick(callback) { 414 - this.el.onClick(callback); 415 - return this; 416 - } 417 - } 418 - 419 - class VirtualEl { 420 - constructor(tag) { 421 - this.tag = tag; 422 - this.attrs = {}; 423 - this.text = null; 424 - this.children = []; 425 - this.events = {}; 426 - } 427 - 428 - onClick(fn) { 429 - const handlerId = uuid.create(); 430 - callHandlers.set(handlerId, fn); 431 - this.events.click = handlerId; 432 - return this; 433 - } 434 - 435 - onChange(fn) { 436 - const handlerId = uuid.create(); 437 - callHandlers.set(handlerId, fn); 438 - this.events.change = handlerId; 439 - return this; 440 - } 441 - 442 - onInput(fn) { 443 - const handlerId = uuid.create(); 444 - callHandlers.set(handlerId, fn); 445 - this.events.input = handlerId; 446 - return this; 447 - } 448 - 449 - setText(text) { 450 - this.text = text; 451 - this.children = []; 452 - return this; 453 - } 454 - 455 - empty() { 456 - this.text = null; 457 - this.children = []; 458 - return this; 459 - } 460 - 461 - addClass(cls) { 462 - this.attrs.class = this.attrs.class ? `${this.attrs.class} ${cls}` : cls; 463 - return this; 464 - } 465 - 466 - setAttr(name, value) { 467 - this.attrs[name] = value === undefined ? "" : value; 468 - return this; 469 - } 470 - 471 - createEl(tag, options = {}, callback) { 472 - const child = new VirtualEl(tag); 473 - if (options.text != null) child.text = options.text; 474 - if (options.cls) { 475 - child.attrs.class = Array.isArray(options.cls) 476 - ? options.cls.join(" ") 477 - : options.cls; 478 - } 479 - if (options.attr) Object.assign(child.attrs, options.attr); 480 - this.children.push(child); 481 - if (typeof callback === "function") callback(child); 482 - return child; 483 - } 484 - 485 - createDiv(options = {}, callback) { 486 - return this.createEl("div", options, callback); 487 - } 488 - 489 - createSpan(options = {}, callback) { 490 - return this.createEl("span", options, callback); 491 - } 492 - 493 - _serialize() { 494 - return { 495 - tag: this.tag, 496 - attrs: this.attrs, 497 - text: this.text, 498 - children: this.children.map((child) => child._serialize()), 499 - events: this.events, 500 - }; 501 - } 502 - } 503 - 504 - self.onmessage = async (event) => { 505 - const message = event.data; 506 - if (!message || typeof message !== "object") return; 507 - 508 - // RPC calls 509 - if (message.type === "call") { 510 - const fn = callHandlers.get(message.handlerId); 511 - if (!fn) { 512 - self.postMessage({ 513 - type: "result", 514 - callId: message.callId, 515 - error: `unknown handler ${message.handlerId}`, 516 - }); 517 - return; 518 - } 519 - try { 520 - const value = await fn(...message.args); 521 - self.postMessage({ type: "result", callId: message.callId, value }); 522 - } catch (error) { 523 - self.postMessage({ 524 - type: "result", 525 - callId: message.callId, 526 - error: error.message ?? String(error), 527 - }); 528 - } 529 - return; 530 - } 531 - 532 - // Host call results 533 - if (message.type === "hostResult") { 534 - const pending = pendingHostCalls.get(message.hostCallId); 535 - if (!pending) return; 536 - pendingHostCalls.delete(message.hostCallId); 537 - if (message.error) pending.reject(new Error(message.error)); 538 - else pending.resolve(message.value); 539 - return; 540 - } 541 - 542 - // Events 543 - if (message.type === "event") { 544 - switch (message.event) { 545 - case "modalDismissed": { 546 - const modal = openModals.get(message.data.modalId); 547 - if (modal) { 548 - openModals.delete(message.data.modalId); 549 - modal.onClose(); 550 - } 551 - return; 552 - } 553 - } 554 - return; 555 - } 556 - };