import { ComponentMetaspaceScope } from "../../component-types.mjs"; import thisCss from "./identity-manager.css"; import thisHtml from "./identity-manager.html"; import commonComponentStylesCss from "../../common-component-styles.css"; import { delay, extractErrorMsg, pretty } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs"; import { IbGibAddr } from "@ibgib/ts-gib/dist/types.mjs"; import { IbGib_V1 } from "@ibgib/ts-gib/dist/V1/types.mjs"; import { getIbAndGib, getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs"; import { ROOT_ADDR } from "@ibgib/ts-gib/dist/V1/constants.mjs"; import { getGibInfo } from "@ibgib/ts-gib/dist/V1/transforms/transform-helper.mjs"; import { KeystoneService_V1 } from "@ibgib/core-gib/dist/keystone/keystone-service-v1.mjs"; import { getTjpAddr } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs"; import { mut8Timeline } from "@ibgib/core-gib/dist/timeline/timeline-api.mjs"; import { MetaspaceService } from "@ibgib/core-gib/dist/witness/space/metaspace/metaspace-types.mjs"; import { IbGibDynamicComponentMetaBase, IbGibDynamicComponentInstanceBase_ParentOfTabs, } from "../../ibgib-dynamic-component-bases.mjs"; import { ElementsBase, ChildInfoBase, IbGibDynamicComponentInstance, IbGibDynamicComponentInstanceInitOpts, } from "../../component-types.mjs"; import { getDeterministicColorInfo } from "../../../../helpers.mjs"; import { getContextModeService } from "../../../../context/context-mode-service.mjs"; import { SettingsType } from "../../../../common/settings/settings-constants.mjs"; import { Settings_General, Settings_IdentityManager } from "../../../../common/settings/settings-types.mjs"; import { getSectionName } from "../../../../common/settings/settings-helpers.mjs"; import { getComponentSvc } from "../../ibgib-component-service.mjs"; import { EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, EVENT_IBGIB_IDENTITY_CHANGED, EVENT_IBGIB_UI_MESSAGE } from "../../../ui-constants.mjs"; import { getComponentCtorArg } from "../../../../app-bootstrap/init-orchestration.mjs"; import { KEYSTONE_ATOM } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs"; export const IDENTITY_MANAGER_COMPONENT_NAME = 'ibgib-identity-manager'; /** * Metadata for the Identity Manager component. */ export class IdentityManagerComponentMeta extends IbGibDynamicComponentMetaBase { protected lc: string = `[IdentityManagerComponentMeta]`; routeRegExp?: RegExp = new RegExp(`^${IDENTITY_MANAGER_COMPONENT_NAME}$`); componentName = IDENTITY_MANAGER_COMPONENT_NAME; constructor() { super(getComponentCtorArg()); if (!customElements.get(this.componentName)) { customElements.define(this.componentName, IdentityManagerComponentInstance); } } async createInstance({ path, ibGibAddr }: { path: string; ibGibAddr: IbGibAddr; }): Promise { const lc = `${this.lc}[${this.createInstance.name}]`; const component = document.createElement(this.componentName) as IdentityManagerComponentInstance; await component.initialize({ ibGibAddr, meta: this, html: thisHtml, css: [commonComponentStylesCss, thisCss], }); return component; } } interface IdentityTabInfo extends ChildInfoBase> { } /** * References to DOM elements within the component's shadow root. */ interface IdentityManagerElements extends ElementsBase { containerEl: HTMLDivElement; titleEl: HTMLHeadingElement; tabsListEl: HTMLDivElement; btnAddIdentityEl: HTMLButtonElement; noIdentityPlaceholderEl: HTMLDivElement; btnCreatePlaceholderEl: HTMLButtonElement; } /** * Concrete implementation of the Identity Manager component. */ export class IdentityManagerComponentInstance extends IbGibDynamicComponentInstanceBase_ParentOfTabs implements IbGibDynamicComponentInstance { /** Identity is device-scoped and mode-agnostic: always use the persistent metaspace. */ protected override metaspaceScope: ComponentMetaspaceScope = ComponentMetaspaceScope.persistent; protected override lc: string = `[IdentityManagerComponentInstance]`; private globalActiveAddr: string | null = null; private _onIdentityChanged: ((e: any) => void) | null = null; private _onUiMessage: ((e: any) => void) | null = null; private keystoneCache: Map = new Map(); metaspace: MetaspaceService | undefined; protected get settingsType(): SettingsType { return SettingsType.identityManager; } constructor() { super(); } private async getKeystoneDetails(addr: string): Promise<{ username: string, description: string, isPrimary: boolean, n: number }> { const cached = this.keystoneCache.get(addr); if (cached) return cached; const metaspace = getContextModeService().getPersistentMetaspace(); if (!metaspace) return { username: '', description: '', isPrimary: false, n: 0 }; const space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) return { username: '', description: '', isPrimary: false, n: 0 }; try { const keystoneSvc = new KeystoneService_V1(); const info = await keystoneSvc.getKeystoneCommonInfo({ addr, metaspace, space }); if (info) { const details = { username: info.name, description: info.description, isPrimary: info.isPrimary, n: info.n }; this.keystoneCache.set(addr, details); return details; } } catch (err) { console.warn(`Error getting details for keystone ${addr}: ${extractErrorMsg(err)}`); } return { username: '', description: '', isPrimary: false, n: 0 }; } override async initialize(opts: IbGibDynamicComponentInstanceInitOpts): Promise { if (!this.metaspace) { this.metaspace = getContextModeService().getPersistentMetaspace(); } opts.ibGibAddr = await this.metaspace.getLatestAddr({ addr: opts.ibGibAddr }) ?? opts.ibGibAddr; await super.initialize(opts); } override async created(): Promise { this.elements = {} as IdentityManagerElements; const shadow = this.shadowRoot!; this.elements.containerEl = shadow.getElementById('container') as HTMLDivElement; this.elements.titleEl = shadow.getElementById('title') as HTMLHeadingElement; this.elements.tabsListEl = shadow.getElementById('identity-tabs') as HTMLDivElement; this.elements.btnAddIdentityEl = shadow.getElementById('btn-add-identity') as HTMLButtonElement; this.elements.noIdentityPlaceholderEl = shadow.getElementById('no-identity-placeholder') as HTMLDivElement; this.elements.btnCreatePlaceholderEl = shadow.getElementById('btn-create-placeholder') as HTMLButtonElement; // ElementBase requires contentEl this.elements.contentEl = shadow.getElementById('identity-manager-content') as HTMLDivElement; this.initHandlers(); // Subscribe to global active identity changes this._onIdentityChanged = async (e: any) => { this.globalActiveAddr = e.detail?.activeIdentityAddr || null; this.keystoneCache.clear(); await this.loadKeystonesIndex(); if (this.childInfos) { for (const childInfo of this.childInfos) { if (childInfo.component) { try { const comp = childInfo.component as any; if (typeof comp.loadIbGib === 'function') { await comp.loadIbGib({ getLatest: true }); } } catch (err) { console.warn(`[IdentityManager] Error reloading child component: ${extractErrorMsg(err)}`); } } } } await this.renderUI(); }; window.addEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged); // Listen for requests to view details of a delegate (from child details component) this.addEventListener('ibgib-view-keystone-details', async (e: any) => { const addr = e.detail?.addr; if (addr) { await this.activateIbGib({ ibGibAddr: addr }); } }); // Listen for requests to switch to keystone-creator or add-device if (this._onUiMessage) { window.removeEventListener(EVENT_IBGIB_UI_MESSAGE, this._onUiMessage); } this._onUiMessage = (e: any) => { const action = e.detail?.action; const subAction = e.detail?.subAction; if (action === 'show-keystone-creator' || subAction === 'show-keystone-creator') { this.handleAddNewIdentityFlow('ibgib-keystone-creator'); } else if (action === 'show-add-device' || subAction === 'show-add-device') { this.handleAddNewIdentityFlow('ibgib-add-device'); } }; window.addEventListener(EVENT_IBGIB_UI_MESSAGE, this._onUiMessage); if (this.ibGibAddr === ROOT_ADDR || !this.ibGib) { await this.loadKeystonesIndex(); } else { await this.initSettings(); } await this.renderUI(); // Spin off tab restore or defaults to allow createdPromise to resolve first this.restoreOrInitTabs(); } private async restoreOrInitTabs(): Promise { const settings = await this.getSettings({ settingsType: this.settingsType, useCase: 'current', }); if (settings) { const validAddrs = (settings.openChildTjpAddrs ?? []).filter(addr => { if (!addr || addr === '^' || addr === 'gib') return false; const { gib } = getIbAndGib({ ibGibAddr: addr }); return !!gib && gib !== 'gib' && gib !== '^'; }); if (validAddrs.length !== (settings.openChildTjpAddrs ?? []).length) { settings.openChildTjpAddrs = validAddrs; const metaspace = getContextModeService().getPersistentMetaspace(); if (metaspace) { const sectionName_current = await getSectionName({ settingsType: this.settingsType, useCase: 'current', }); await mut8Timeline({ timeline: this.settings!.ibGib!, metaspace, mut8Opts: { dataToAddOrPatch: { sections: { [sectionName_current]: settings, } }, }, }); } } } if (!settings || (settings.openChildTjpAddrs ?? []).length === 0) { await this.loadDefaultPrimaryIdentityTabs(); } else { await this.reopenOldTabs(); } } override async disconnected(): Promise { if (this._onIdentityChanged) { window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged); this._onIdentityChanged = null; } if (this._onUiMessage) { window.removeEventListener(EVENT_IBGIB_UI_MESSAGE, this._onUiMessage); this._onUiMessage = null; } } private async loadKeystonesIndex(): Promise { const lc = `${this.lc}[loadKeystonesIndex]`; try { const metaspace = getContextModeService().getPersistentMetaspace(); if (!metaspace) { return; } const space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) { return; } const keystonesIndex = await metaspace.getSpecialIbGib({ type: "keystones", space, initialize: true, }); if (keystonesIndex) { this.ibGibAddr = getIbGibAddr({ ibGib: keystonesIndex }); await this.loadIbGib({ getLatest: true }); await this.initSettings(); } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); } } private initHandlers() { // Buttons this.elements!.btnAddIdentityEl.addEventListener('click', () => this.handleAddNewIdentityFlow('ibgib-keystone-creator')); this.elements!.btnCreatePlaceholderEl.addEventListener('click', () => this.handleAddNewIdentityFlow('ibgib-keystone-creator')); // Convert vertical mouse wheel scrolling into horizontal scroll for tabs this.elements!.tabsListEl.addEventListener('wheel', (e: WheelEvent) => { if (e.deltaY !== 0) { e.preventDefault(); this.elements!.tabsListEl.scrollBy({ left: e.deltaY, behavior: 'smooth' }); } }, { passive: false }); } protected async handleAddNewIdentityFlow(componentPath: 'ibgib-keystone-creator' | 'ibgib-add-device' = 'ibgib-keystone-creator') { const lc = `${this.lc}[${this.handleAddNewIdentityFlow.name}]`; console.log(`${lc} Triggering identity creation/import flow (${componentPath})...`); try { const componentSvc = await getComponentSvc(); const childComp = await componentSvc.getComponentInstance({ path: componentPath, ibGibAddr: ROOT_ADDR, useRegExpPrefilter: true, }); if (childComp) { // Hide placeholder this.elements!.noIdentityPlaceholderEl.classList.add('hidden'); // Clear any previous creator/details/add-device const existingCreator = this.elements!.contentEl.querySelector('ibgib-keystone-creator'); if (existingCreator) { existingCreator.remove(); } const existingAddDevice = this.elements!.contentEl.querySelector('ibgib-add-device'); if (existingAddDevice) { existingAddDevice.remove(); } // Clear active child component inside ParentOfTabs to prevent conflict const active = this.activeChildInfo; if (active) { active.childBtnEl.classList.remove('active'); active.active = false; } this.elements!.contentEl.innerHTML = ''; this.elements!.contentEl.appendChild(childComp as any); console.log(`${lc} Mounted ${componentPath} successfully.`); } else { throw new Error(`Could not find meta for ${componentPath}`); } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); } } private async resolveActiveIdentity() { const lc = `${this.lc}[resolveActiveIdentity]`; try { if (!this.settings) { this.globalActiveAddr = null; return; } const generalSettings = await this.getSettings({ settingsType: SettingsType.general, useCase: 'current', }); const activeAddrInSettings = (generalSettings as any)?.activeIdentityAddr; const registeredKeystones = this.ibGib?.rel8ns?.keystone ?? []; const detailsPromises = registeredKeystones.map(async addr => { const details = await this.getKeystoneDetails(addr); return { addr, isPrimary: details?.isPrimary ?? false }; }); const resolvedDetails = await Promise.all(detailsPromises); const primaryKeystones = resolvedDetails .filter(item => item.isPrimary) .map(item => item.addr); if (activeAddrInSettings && primaryKeystones.includes(activeAddrInSettings)) { this.globalActiveAddr = activeAddrInSettings; } else if (primaryKeystones.length > 0) { this.globalActiveAddr = primaryKeystones[0]; } else { this.globalActiveAddr = null; } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); this.globalActiveAddr = null; } } protected override async renderUI(): Promise { const lc = `${this.lc}[${this.renderUI.name}]`; if (!this.elements) { return; } await this.resolveActiveIdentity(); const keystones = this.ibGib?.rel8ns?.keystone ?? []; if (keystones.length === 0 && this.childInfos.length === 0) { this.elements.noIdentityPlaceholderEl.classList.remove('hidden'); this.elements.contentEl.innerHTML = ''; return; } this.elements.noIdentityPlaceholderEl.classList.add('hidden'); // Render tab highlights for (const info of this.childInfos) { const btn = info.childBtnEl; btn.className = `tab-btn ${info.active ? 'active' : ''} ${info.addr === this.globalActiveAddr ? 'global-active' : ''}`; } } private async loadDefaultPrimaryIdentityTabs() { const keystones = (this.ibGib?.rel8ns?.keystone ?? []).filter(addr => { if (!addr || addr === '^' || addr === 'gib') return false; const { gib } = getIbAndGib({ ibGibAddr: addr }); return !!gib && gib !== 'gib' && gib !== '^'; }); const detailsPromises = keystones.map(async addr => { const details = await this.getKeystoneDetails(addr); return { addr, isPrimary: details?.isPrimary ?? false }; }); const resolvedDetails = await Promise.all(detailsPromises); const primaryKeystones = resolvedDetails .filter(item => item.isPrimary) .map(item => item.addr); if (primaryKeystones.length === 0) { this.elements!.noIdentityPlaceholderEl.classList.remove('hidden'); return; } this.elements!.noIdentityPlaceholderEl.classList.add('hidden'); this.reloadingTabs = true; try { for (const addr of primaryKeystones) { await this.activateIbGib({ ibGibAddr: addr }); } } finally { this.reloadingTabs = false; } // Activate the global active identity (if open), or the first primary one const activeToOpen = primaryKeystones.includes(this.globalActiveAddr || '') ? this.globalActiveAddr! : primaryKeystones[0]; await this.activateIbGib({ ibGibAddr: activeToOpen }); } public override async activateIbGib({ ibGibAddr, ibGib }: { ibGibAddr?: IbGibAddr; ibGib?: IbGib_V1; }): Promise { const lc = `${this.lc}[activateIbGib]`; try { if (!this.metaspace) { throw new Error(`(UNEXPECTED) this.metaspace falsy? (E: f9e0e9a1427899eba8e469c18d7ae826)`); } await super.activateIbGib({ ibGibAddr: ibGibAddr, ibGib }); if (!this.activeChildInfo) { throw new Error(`(UNEXPECTED) activeChildInfo falsy after activateIbGib? (E: d76bd8bc300f3fdc486d50c8b2644826)`); } ibGib ??= this.activeChildInfo.component?.ibGib; if (!ibGib) { throw new Error(`(UNEXPECTED) ibGib falsy? (E: 5b6ec745dd32fd2128635728e3f35e26)`); } if (!ibGib.ib.startsWith(KEYSTONE_ATOM)) { throw new Error(`ibGib.ib (${ibGib.ib}) does not start with keystone atom (${KEYSTONE_ATOM}) (E: e0df410381c8e569eb6da996e79b6826)`); } this.elements!.noIdentityPlaceholderEl.classList.add('hidden'); if (!this.reloadingTabs) { const settings_current = await this.getSettings({ settingsType: this.settingsType, useCase: 'current', }); if (settings_current) { let modified = false; const tjpAddr = getTjpAddr({ ibGib: this.activeChildInfo.component?.ibGib || ibGib!, defaultIfNone: 'incomingAddr' }); if (tjpAddr) { if (!settings_current.openChildTjpAddrs.includes(tjpAddr)) { settings_current.openChildTjpAddrs.push(tjpAddr); modified = true; } if (settings_current.activeChildTjpAddr !== tjpAddr) { settings_current.activeChildTjpAddr = tjpAddr; modified = true; } } if (modified) { const metaspace = getContextModeService().getPersistentMetaspace(); if (metaspace) { const sectionName_current = await getSectionName({ settingsType: this.settingsType, useCase: 'current', }); await mut8Timeline({ timeline: this.settings!.ibGib!, metaspace, mut8Opts: { dataToAddOrPatch: { sections: { [sectionName_current]: settings_current, } }, }, }); } } } } await this.renderUI(); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); } } protected async getLoadedChildInfo({ addr, ibGib, }: { addr: IbGibAddr, ibGib?: IbGib_V1, }): Promise { const lc = `${this.lc}[getLoadedChildInfo]`; try { if (!addr) { throw new Error(`(UNEXPECTED) addr falsy?`); } if (addr === '^' || addr === 'gib' || getIbAndGib({ ibGibAddr: addr }).gib === '^') { throw new Error(`Cannot load child info for primitive address ${addr}`); } const tjpGib = getGibInfo({ ibGibAddr: addr }).tjpGib ?? getIbAndGib({ ibGibAddr: addr }).gib; const fnCreateAndLoadChildComponent = async () => { const componentSvc = await getComponentSvc(); const detailsComponent = await componentSvc.getComponentInstance({ path: 'ibgib-keystone-scrubber', ibGibAddr: addr, useRegExpPrefilter: true, }); if (!detailsComponent) { throw new Error(`(UNEXPECTED) detailsComponent falsy?`); } return detailsComponent; } let tabInfo: IdentityTabInfo; const filtered = this.childInfos.filter(x => { const tabGibInfo = getGibInfo({ ibGibAddr: x.addr }); const tabTjpGib = tabGibInfo.tjpGib ?? getIbAndGib({ ibGibAddr: x.addr }).gib; return tabTjpGib === tjpGib; }); if (filtered.length > 0) { if (filtered.length !== 1) { throw new Error(`(UNEXPECTED) filtered.length !== 1?`); } tabInfo = filtered[0]; if (!tabInfo.component) { tabInfo.component = await fnCreateAndLoadChildComponent(); } } else { const detailsComponent = await fnCreateAndLoadChildComponent(); let i = 0; while (!detailsComponent.ibGib!) { await delay(100); i++; if (i > 50) { // 5 seconds debugger; // error loading details component ibgib throw new Error(`(UNEXPECTED) detailsComponent.ibGib falsy? didn't load? (E: de50689834124914f770520f1eccb826)`); } } ibGib = detailsComponent.ibGib!; addr = getIbGibAddr({ ibGib }); const tabBtnEl = await this.addChild({ addr, ibGib: ibGib! }); tabInfo = { addr, childBtnEl: tabBtnEl, component: detailsComponent, active: false, }; this.childInfos.push(tabInfo); } return tabInfo; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } } protected override async addChild({ addr, ibGib, }: { ibGib: IbGib_V1, addr?: IbGibAddr, }): Promise { const lc = `${this.lc}[addChild]`; try { addr ??= getIbGibAddr({ ibGib }); if (addr === '^' || addr === 'gib' || getIbAndGib({ ibGibAddr: addr }).gib === '^') { throw new Error(`Cannot add child tab for primitive address ${addr}`); } if (!this.elements) { throw new Error(`(UNEXPECTED) this.elements falsy?`); } const { tabsListEl } = this.elements; addr ??= getIbGibAddr({ ibGib }); const childBtnEl = document.createElement('button'); childBtnEl.className = 'tab-btn'; childBtnEl.id = `identity-tab-button-${addr}`; await this.updateTabTitleAndText({ span: childBtnEl, ibGib, addr }); const details = await this.getKeystoneDetails(addr); if (!details.isPrimary) { const closeBtnEl = document.createElement('span'); closeBtnEl.className = 'close-tab-button'; closeBtnEl.textContent = ' ×'; closeBtnEl.style.marginLeft = '0.5rem'; closeBtnEl.style.cursor = 'pointer'; childBtnEl.appendChild(closeBtnEl); closeBtnEl.addEventListener('click', async (event) => { event.stopPropagation(); // Prevent activating the tab await this.closeTab({ ibGib }); }); } tabsListEl.appendChild(childBtnEl); childBtnEl.addEventListener('click', async (event) => { await this.activateIbGib({ ibGibAddr: addr }); }); return childBtnEl; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } } private async updateTabTitleAndText({ span, ibGib, addr, }: { span: HTMLElement, ibGib: IbGib_V1, addr: string, }): Promise { const details = await this.getKeystoneDetails(addr); let name = details.username; const maxTabLen = 16; if (name) { if (name.length > maxTabLen) { name = name.substring(0, maxTabLen - 3) + '...'; } span.textContent = name; } else { let displayTab = addr; if (displayTab.length > 12) { displayTab = displayTab.substring(0, 5) + '...' + displayTab.substring(displayTab.length - 4); } span.textContent = displayTab; } const tooltipParts: string[] = []; if (details.username) { tooltipParts.push(`User: ${details.username}`); } if (details.description) { const desc = details.description.length > 100 ? details.description.substring(0, 97) + '...' : details.description; tooltipParts.push(`Desc: ${desc}`); } tooltipParts.push(`Addr: ${addr}`); span.title = tooltipParts.join('\n'); } protected async removeTabBtn({ tabInfo }: { tabInfo: IdentityTabInfo }): Promise { this.elements!.tabsListEl.removeChild(tabInfo.childBtnEl); } }