theseus/bundled-addons/hermes/index.js
Local Dev b5b3e5c1b2 hermes: Nostr kind:0 profiles — display name, picture, about
Peers' chat-list labels now cascade: contact nickname → kind:0 display name →
BNS handle → pk preview. Avatars render as a URL picture when kind:0 carries
one, with initials as fallback. Profiles are fetched on demand (background,
non-blocking) when a new peer pk shows up in the inbox or contact list, and
cached on disk so a cold start renders instantly.

- lib/hermes.js: buildProfileEvent({sk, displayName, picture, about}) emits
  a signed kind:0 with `name` + `display_name` set together (clients vary
  on which they prefer). parseProfileContent(content) handles both.
- main.js: profile cache at <userData>/hermes/profiles.json; auto-fetch
  from HERMES_DEFAULT_RELAYS with a 4 s per-relay cap. hermesPublishEvent
  centralises relay publish (used by profile save; send() retains its
  inlined copy for the DRY-up later). hermesRecord() kicks off a background
  profile fetch the first time a peer is seen; result fires on the
  hermes-profile bus event.
- main.js: _hermesAPI.updateProfile / getProfile. Addon proxies both.
- panel.html: Settings → Profile editor (display name, picture URL, about);
  publishes on save, pre-fills from the cache. Picture validated as http(s).
  labelFor() + avatarFor() centralise the display cascade. Profile state
  lives in state.profiles.
2026-10-21 17:18:07 +03:00

82 lines
3.5 KiB
JavaScript

// Hermes bundled add-on — thin proxy to the host's panel API.
//
// All the real messaging code (NIP-17 wrap/unwrap, relay subscriptions,
// persistent inbox, contacts) lives in Theseus main.js. Too much raw network
// + crypto to run under the add-on sandbox, and the vault seed must never
// cross the host/add-on boundary.
//
// This file does two things:
// 1. Register the left-strip sidebar entry (so Hermes is discoverable
// without pressing Ctrl+Shift+E).
// 2. Proxy a small, allow-listed set of panel operations between the
// panel's silentmode.invoke() calls and main.js's `global._hermesAPI`.
// The panel never sees ipcRenderer — only this named handler surface.
//
// Events coming the other way (inbox updates, relay status changes) are
// fanned out via a shared ipcMain event bus `__hermes_bus`: main.js's
// hermesBusEmit publishes, this file forwards each payload to the panel.
const { ipcMain } = require("electron");
const PANEL_METHODS = [
"status", "canUseVault", "close", "init", "signOut", "getBackup", "hasBackup",
"getProfile", "updateProfile",
"inbox", "send", "markThreadRead",
"contactsList", "contactsAdd", "contactsUpdate", "contactsRemove",
];
module.exports = {
activate(api) {
api.registerSidebarPanel({
id: "main",
title: "Hermes",
page: "panel.html",
// Dock on the LEFT strip (alongside mail/pithos/telegram etc.), not
// the right-hand add-on dock. The host checks `side` on both the
// manifest's panels[] entry and the registerSidebarPanel call —
// declaring it in both keeps dormant vs active renders consistent.
side: "left",
});
// Legacy launcher behavior: panel.html can still ask to open the
// standalone Messages window. Kept because Ctrl+Shift+E users have
// muscle memory, and because a few flows (debug, multi-window)
// benefit from a free-floating window.
api.onMessage("open-messages", () => {
ipcMain.emit("hermes-open-request");
return { ok: true };
});
// Panel API proxies. Every method listed in PANEL_METHODS invokes the
// same-named function on global._hermesAPI. The main.js side defines
// that object once hermes is initialised; before that, status/canUseVault
// are the only safe methods and will return a stub-ok.
for (const name of PANEL_METHODS) {
api.onMessage(name, async (payload) => {
const hermes = global._hermesAPI;
if (!hermes || typeof hermes[name] !== "function") {
// Host not ready. Treat status/canUseVault as "no identity yet"
// so the panel can render its signin screen without a toast.
if (name === "status") return { ok: true, ready: false };
if (name === "canUseVault") return { ok: true, available: false };
return { ok: false, error: "hermes host not ready" };
}
return await hermes[name](payload);
});
}
// Main-side → panel event bus. hermesBusEmit in main.js fires events on
// ipcMain under __hermes_bus; forward each to the panel via api.emit so
// panel-side `silentmode.on("hermes-message", cb)` works identically to
// the ipcRenderer path used by the standalone window.
const busForward = (channel, payload) => {
try { api.emit(channel, payload); } catch {}
};
ipcMain.on("__hermes_bus", busForward);
api.onDeactivate && api.onDeactivate(() => {
try { ipcMain.off("__hermes_bus", busForward); } catch {}
});
api.log("hermes sidebar panel registered (proxy mode)");
},
};