import { ComponentMetaspaceScope } from "../../component-types.mjs"; import thisCss from "./identity-header.css"; import thisHtml from "./identity-header.html"; import commonComponentStylesCss from "../../common-component-styles.css"; import { extractErrorMsg } 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 { getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs"; import { KeystoneService_V1 } from "@ibgib/core-gib/dist/keystone/keystone-service-v1.mjs"; import { KeystoneIbGib_V1 } from "@ibgib/core-gib/dist/keystone/keystone-types.mjs"; import { IbGibDynamicComponentMetaBase, IbGibDynamicComponentInstanceBase, } from "../../ibgib-dynamic-component-bases.mjs"; import { ElementsBase, IbGibDynamicComponentInstance, IbGibDynamicComponentInstanceInitOpts, } from "../../component-types.mjs"; import { SettingsType } from "../../../../common/settings/settings-constants.mjs"; import { Settings_General } from "../../../../common/settings/settings-types.mjs"; import { getComponentCtorArg } from "../../../../app-bootstrap/init-orchestration.mjs"; import { getDeterministicColorInfo } from "../../../../helpers.mjs"; import { getContextModeService } from "../../../../context/context-mode-service.mjs"; import { EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, EVENT_IBGIB_IDENTITY_CHANGED, EVENT_IBGIB_UI_MESSAGE, EVENT_IBGIB_IDENTITY_UPDATE_AVAILABLE, UI_ACTION_SHOW_IDENTITY_MANAGER, UI_ACTION_SHOW_KEYSTONE_CREATOR, UI_ACTION_SHOW_ADD_DEVICE } from "../../../ui-constants.mjs"; import { EventIbGibIdentityUpdateAvailableDetail } from "../../../ui-types.mjs"; import { getKeystoneSyncService } from "../../../../identity/keystone-sync-service.mjs"; export const IDENTITY_HEADER_COMPONENT_NAME = 'ibgib-identity-header'; /** * Metadata for the Identity Header component. */ export class IdentityHeaderComponentMeta extends IbGibDynamicComponentMetaBase { protected lc: string = `[IdentityHeaderComponentMeta]`; routeRegExp?: RegExp = new RegExp(`^${IDENTITY_HEADER_COMPONENT_NAME}$`); componentName = IDENTITY_HEADER_COMPONENT_NAME; constructor() { super(getComponentCtorArg()); if (!customElements.get(this.componentName)) { customElements.define(this.componentName, IdentityHeaderComponentInstance); } } async createInstance({ path, ibGibAddr }: { path: string; ibGibAddr: IbGibAddr; }): Promise { const component = document.createElement(this.componentName) as IdentityHeaderComponentInstance; await component.initialize({ ibGibAddr, meta: this, html: thisHtml, css: [commonComponentStylesCss, thisCss], }); return component; } } /** * References to DOM elements within the component's shadow root. */ interface IdentityHeaderElements extends ElementsBase { containerEl: HTMLDivElement; btnIdentityPillEl: HTMLButtonElement; pillAuthContentEl: HTMLDivElement; pillUnauthContentEl: HTMLDivElement; identityAvatarEl: HTMLDivElement; identityUsernameEl: HTMLSpanElement; updateBadgeEl: HTMLSpanElement; dropdownMenuEl: HTMLDivElement; sectionUpdateEl: HTMLDivElement; btnMenuUpdateIdentityEl: HTMLButtonElement; updateNTextEl: HTMLSpanElement; sectionIdentitiesEl: HTMLDivElement; identitiesListEl: HTMLDivElement; sectionActionsEl: HTMLDivElement; btnMenuCreateAccountEl: HTMLButtonElement; btnMenuSignInEl: HTMLButtonElement; btnMenuGoogleSsoEl: HTMLButtonElement; btnMenuGithubSsoEl: HTMLButtonElement; sectionSettingsEl: HTMLDivElement; btnMenuManageEl: HTMLButtonElement; syncProgressModalEl: HTMLDivElement; syncModalSpinnerEl: HTMLDivElement; syncModalTitleEl: HTMLHeadingElement; syncModalMsgEl: HTMLParagraphElement; syncModalStatusEl: HTMLDivElement; syncModalNTextEl: HTMLSpanElement; btnSyncModalCloseEl: HTMLButtonElement; } /** * Concrete implementation of the Identity Header component. */ export class IdentityHeaderComponentInstance extends IbGibDynamicComponentInstanceBase implements IbGibDynamicComponentInstance { protected lc: string = `[IdentityHeaderComponentInstance]`; /** Identity is device-scoped and mode-agnostic: always use the persistent metaspace. */ protected override metaspaceScope: ComponentMetaspaceScope = ComponentMetaspaceScope.persistent; private activeAddr: string | null = null; private _onRequestIdentityChange: ((e: any) => void) | null = null; private _onIdentityUpdateAvailable: ((e: any) => void) | null = null; private _onIdentityChanged: ((e: any) => void) | null = null; private _onDocumentClick: ((e: MouseEvent) => void) | null = null; private pendingUpdateDetail: EventIbGibIdentityUpdateAvailableDetail | null = null; private keystoneCache: Map = new Map(); private isDropdownLocked = false; private dropdownHideTimer: any = null; constructor() { super(); } private async getKeystoneDetails(addr: string): Promise<{ username: string, description: string, isPrimary: boolean }> { const cached = this.keystoneCache.get(addr); if (cached) return cached; const metaspace = await this.getMetaspace(); if (!metaspace) return { username: '', description: '', isPrimary: false }; const space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) return { username: '', description: '', isPrimary: false }; try { const keystoneSvc = new KeystoneService_V1(); const info = await keystoneSvc.getKeystoneCommonInfo({ addr, metaspace, space, }); const details = { username: info?.name ?? '', description: info?.description ?? '', isPrimary: info?.isPrimary ?? false }; this.keystoneCache.set(addr, details); return details; } catch (error) { console.error(`${this.lc}[getKeystoneDetails] Registered keystone address ${addr} not found in local browser space: ${extractErrorMsg(error)} (E: genuuid)`); return { username: '', description: '', isPrimary: false }; } } override async initialize(opts: IbGibDynamicComponentInstanceInitOpts): Promise { await super.initialize(opts); } override async created(): Promise { this.elements = {} as IdentityHeaderElements; this.elements.containerEl = this.shadowRoot!.getElementById('container') as HTMLDivElement; this.elements.btnIdentityPillEl = this.shadowRoot!.getElementById('btn-identity-pill') as HTMLButtonElement; this.elements.pillAuthContentEl = this.shadowRoot!.getElementById('pill-auth-content') as HTMLDivElement; this.elements.pillUnauthContentEl = this.shadowRoot!.getElementById('pill-unauth-content') as HTMLDivElement; this.elements.identityAvatarEl = this.shadowRoot!.getElementById('identity-avatar') as HTMLDivElement; this.elements.identityUsernameEl = this.shadowRoot!.getElementById('identity-username') as HTMLSpanElement; this.elements.updateBadgeEl = this.shadowRoot!.getElementById('update-badge') as HTMLSpanElement; this.elements.dropdownMenuEl = this.shadowRoot!.getElementById('dropdown-menu') as HTMLDivElement; this.elements.sectionUpdateEl = this.shadowRoot!.getElementById('section-update') as HTMLDivElement; this.elements.btnMenuUpdateIdentityEl = this.shadowRoot!.getElementById('btn-menu-update-identity') as HTMLButtonElement; this.elements.updateNTextEl = this.shadowRoot!.getElementById('update-n-text') as HTMLSpanElement; this.elements.sectionIdentitiesEl = this.shadowRoot!.getElementById('section-identities') as HTMLDivElement; this.elements.identitiesListEl = this.shadowRoot!.getElementById('identities-list') as HTMLDivElement; this.elements.sectionActionsEl = this.shadowRoot!.getElementById('section-actions') as HTMLDivElement; this.elements.btnMenuCreateAccountEl = this.shadowRoot!.getElementById('btn-menu-create-account') as HTMLButtonElement; this.elements.btnMenuSignInEl = this.shadowRoot!.getElementById('btn-menu-sign-in') as HTMLButtonElement; this.elements.btnMenuGoogleSsoEl = this.shadowRoot!.getElementById('btn-menu-google-sso') as HTMLButtonElement; this.elements.btnMenuGithubSsoEl = this.shadowRoot!.getElementById('btn-menu-github-sso') as HTMLButtonElement; this.elements.sectionSettingsEl = this.shadowRoot!.getElementById('section-settings') as HTMLDivElement; this.elements.btnMenuManageEl = this.shadowRoot!.getElementById('btn-menu-manage') as HTMLButtonElement; this.elements.syncProgressModalEl = this.shadowRoot!.getElementById('sync-progress-modal') as HTMLDivElement; this.elements.syncModalSpinnerEl = this.shadowRoot!.getElementById('sync-modal-spinner') as HTMLDivElement; this.elements.syncModalTitleEl = this.shadowRoot!.getElementById('sync-modal-title') as HTMLHeadingElement; this.elements.syncModalMsgEl = this.shadowRoot!.getElementById('sync-modal-msg') as HTMLParagraphElement; this.elements.syncModalStatusEl = this.shadowRoot!.getElementById('sync-modal-status') as HTMLDivElement; this.elements.syncModalNTextEl = this.shadowRoot!.getElementById('sync-modal-n-text') as HTMLSpanElement; this.elements.btnSyncModalCloseEl = this.shadowRoot!.getElementById('btn-sync-modal-close') as HTMLButtonElement; this.elements.contentEl = this.elements.containerEl; this.initHandlers(); await this.loadKeystonesIndex(); this._onRequestIdentityChange = (e: any) => this.handleRequestIdentityChange(e); this._onIdentityUpdateAvailable = (e: any) => this.handleIdentityUpdateAvailable(e); this._onIdentityChanged = (e: any) => this.handleIdentityChanged(e); this._onDocumentClick = (e: MouseEvent) => { const path = e.composedPath ? e.composedPath() : []; if (this.elements?.containerEl && !path.includes(this.elements.containerEl)) { this.closeDropdown(); } }; window.addEventListener(EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, this._onRequestIdentityChange); window.addEventListener(EVENT_IBGIB_IDENTITY_UPDATE_AVAILABLE, this._onIdentityUpdateAvailable); window.addEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged); document.addEventListener('click', this._onDocumentClick); await this.resolveActiveIdentity(); await this.renderUI(); } private async loadKeystonesIndex(): Promise { const lc = `${this.lc}[loadKeystonesIndex]`; try { const metaspace = await this.getMetaspace(); 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(); await this.initSettings(); } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); } } override async disconnected(): Promise { if (this._onRequestIdentityChange) { window.removeEventListener(EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, this._onRequestIdentityChange); } if (this._onIdentityUpdateAvailable) { window.removeEventListener(EVENT_IBGIB_IDENTITY_UPDATE_AVAILABLE, this._onIdentityUpdateAvailable); } if (this._onIdentityChanged) { window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged); } if (this._onDocumentClick) { document.removeEventListener('click', this._onDocumentClick); } } private closeDropdown() { if (this.dropdownHideTimer) { clearTimeout(this.dropdownHideTimer); this.dropdownHideTimer = null; } this.isDropdownLocked = false; this.elements?.btnIdentityPillEl.classList.remove('locked'); this.elements?.dropdownMenuEl.classList.add('hidden'); } private initHandlers() { // Hover mechanics for header widget container this.elements!.containerEl.addEventListener('mouseenter', () => { if (this.dropdownHideTimer) { clearTimeout(this.dropdownHideTimer); this.dropdownHideTimer = null; } this.elements!.dropdownMenuEl.classList.remove('hidden'); }); this.elements!.containerEl.addEventListener('mouseleave', () => { if (!this.isDropdownLocked) { this.dropdownHideTimer = setTimeout(() => { this.elements?.dropdownMenuEl.classList.add('hidden'); this.dropdownHideTimer = null; }, 300); } }); // Click on top control pill button ONLY toggles dropdown menu. NEVER executes identity update directly! this.elements!.btnIdentityPillEl.addEventListener('click', (e) => { e.stopPropagation(); if (this.dropdownHideTimer) { clearTimeout(this.dropdownHideTimer); this.dropdownHideTimer = null; } this.isDropdownLocked = !this.isDropdownLocked; if (this.isDropdownLocked) { this.elements!.btnIdentityPillEl.classList.add('locked'); this.elements!.dropdownMenuEl.classList.remove('hidden'); } else { this.elements!.btnIdentityPillEl.classList.remove('locked'); this.elements!.dropdownMenuEl.classList.add('hidden'); } }); // Modal close button handler this.elements!.btnSyncModalCloseEl.addEventListener('click', (e) => { e.stopPropagation(); this.elements?.syncProgressModalEl.classList.add('hidden'); }); // Section 1: Update Identity Button Handler this.elements!.btnMenuUpdateIdentityEl.addEventListener('click', async (e) => { e.stopPropagation(); if (!this.activeAddr) return; const lc = `${this.lc}[btnMenuUpdateIdentityEl.click]`; // 1. Immediately call closeDropdown() this.closeDropdown(); // 2. Open #sync-progress-modal displaying spinner 🔄, title, message, status n: localN ➔ serverN const localN = this.pendingUpdateDetail?.localN ?? 0; const serverN = this.pendingUpdateDetail?.serverN ?? 0; if (this.elements) { this.elements.syncModalSpinnerEl.textContent = '🔄'; this.elements.syncModalSpinnerEl.style.animation = 'spin-pulse 2s infinite ease-in-out'; this.elements.syncModalTitleEl.textContent = 'Updating Identity Keystone...'; this.elements.syncModalMsgEl.textContent = 'Fetching delta graph from server and fast-forwarding local space...'; this.elements.syncModalNTextEl.textContent = `n: ${localN} ➔ ${serverN}`; this.elements.btnSyncModalCloseEl.classList.add('hidden'); this.elements.syncProgressModalEl.classList.remove('hidden'); } try { const syncSvc = getKeystoneSyncService(); // 3. Await syncSvc.updateActiveKeystone await syncSvc.updateActiveKeystone({ activePrimaryAddr: this.activeAddr }); // 4. On Success: if (this.elements) { this.elements.syncModalSpinnerEl.textContent = '✅'; this.elements.syncModalSpinnerEl.style.animation = 'none'; this.elements.syncModalTitleEl.textContent = 'Identity Updated!'; this.elements.syncModalMsgEl.textContent = 'Successfully fast-forwarded identity keystone.'; this.elements.btnSyncModalCloseEl.textContent = 'Done'; this.elements.btnSyncModalCloseEl.classList.remove('hidden'); } this.pendingUpdateDetail = null; if (this.elements) { this.elements.updateBadgeEl.classList.add('hidden'); this.elements.sectionUpdateEl.classList.add('hidden'); } setTimeout(() => { this.elements?.syncProgressModalEl.classList.add('hidden'); }, 1500); window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_CHANGED, { detail: { activeIdentityAddr: this.activeAddr }, bubbles: true, composed: true })); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); // 5. On Error: if (this.elements) { this.elements.syncModalSpinnerEl.textContent = '❌'; this.elements.syncModalSpinnerEl.style.animation = 'none'; this.elements.syncModalTitleEl.textContent = 'Update Failed'; this.elements.syncModalMsgEl.textContent = extractErrorMsg(error); this.elements.btnSyncModalCloseEl.textContent = 'Close'; this.elements.btnSyncModalCloseEl.classList.remove('hidden'); } } }); // Section 3 Actions this.elements!.btnMenuCreateAccountEl.addEventListener('click', (e) => { e.stopPropagation(); this.closeDropdown(); window.dispatchEvent(new CustomEvent(EVENT_IBGIB_UI_MESSAGE, { detail: { action: UI_ACTION_SHOW_KEYSTONE_CREATOR }, bubbles: true, composed: true })); }); this.elements!.btnMenuSignInEl.addEventListener('click', (e) => { e.stopPropagation(); this.closeDropdown(); window.dispatchEvent(new CustomEvent(EVENT_IBGIB_UI_MESSAGE, { detail: { action: UI_ACTION_SHOW_ADD_DEVICE }, bubbles: true, composed: true })); }); this.elements!.btnMenuGoogleSsoEl.addEventListener('click', (e) => { e.stopPropagation(); this.closeDropdown(); }); this.elements!.btnMenuGithubSsoEl.addEventListener('click', (e) => { e.stopPropagation(); this.closeDropdown(); }); // Section 4 Settings: Identity Manager this.elements!.btnMenuManageEl.addEventListener('click', (e) => { e.stopPropagation(); this.closeDropdown(); this.navigateToManager(); }); } private navigateToManager() { window.dispatchEvent(new CustomEvent(EVENT_IBGIB_UI_MESSAGE, { detail: { action: UI_ACTION_SHOW_IDENTITY_MANAGER }, bubbles: true, composed: true })); } /** * Resolves the active identity from settings and verifies it exists in the keystones index. */ private async resolveActiveIdentity() { const lc = `${this.lc}[resolveActiveIdentity]`; try { const globalState = (globalThis as any).ibgib ?? {}; if (!this.settings) { this.activeAddr = null; getContextModeService().registerPersistentIdentity(undefined); globalState.identity = undefined; return; } const generalSettings = await this.getSettings({ settingsType: SettingsType.general, useCase: 'current', }); const activeAddrInSettings = (generalSettings as any)?.activeIdentityAddr; // Verify it is actually one of the registered keystones in the index relations let registeredKeystones = this.ibGib?.rel8ns?.keystone ?? []; if (activeAddrInSettings && !registeredKeystones.includes(activeAddrInSettings)) { await this.loadKeystonesIndex(); 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.activeAddr = activeAddrInSettings; } else if (primaryKeystones.length > 0) { this.activeAddr = primaryKeystones[0]; } else { this.activeAddr = null; } // Sync with globalState.identity if (this.activeAddr) { const metaspace = await this.getMetaspace(); if (metaspace) { const space = await metaspace.getLocalUserSpace({ lock: false }); if (space) { const res = await metaspace.get({ addr: this.activeAddr, space }); if (res.success && res.ibGibs && res.ibGibs.length > 0) { const domainIdentity = res.ibGibs[0] as KeystoneIbGib_V1; // Find delegate with the sync allowed verb let syncDelegateIdentity: KeystoneIbGib_V1 | undefined; const delegates = domainIdentity.data?.delegates; if (delegates) { for (const tjpAddr of Object.keys(delegates)) { const info = delegates[tjpAddr]; if (info.allowedVerbs?.includes('sync')) { const delRes = await metaspace.get({ addr: info.delegateAddr, space }); if (delRes.success && delRes.ibGibs && delRes.ibGibs.length > 0) { syncDelegateIdentity = delRes.ibGibs[0] as KeystoneIbGib_V1; break; } } } } const details = await this.getKeystoneDetails(this.activeAddr); const rawSiteUsername = (generalSettings as any)?.siteUsernames?.[this.activeAddr] || (generalSettings as any)?.siteUsername || domainIdentity.data?.siteUsername; const siteUsername = rawSiteUsername ? rawSiteUsername.replace(/^@/, '') : undefined; const identityInfo = { domainIdentity, syncDelegateIdentity, tjpAddr: this.activeAddr, username: details.username, siteUsername, aggregateDetails: details, authenticated: true, resolved: true, }; getContextModeService().registerPersistentIdentity(identityInfo); // legacy global, to be removed once all readers use getPersistentIdentity globalState.identity = identityInfo; } } } } else { getContextModeService().registerPersistentIdentity(undefined); globalState.identity = undefined; } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); this.activeAddr = null; } } protected override async renderUI() { if (!this.elements) return; const registeredKeystones = this.ibGib?.rel8ns?.keystone ?? []; if (this.activeAddr) { const addr = this.activeAddr; this.elements.btnIdentityPillEl.classList.remove('unauth'); this.elements.pillAuthContentEl.classList.remove('hidden'); this.elements.pillUnauthContentEl.classList.add('hidden'); // Render active avatar & username const details = await this.getKeystoneDetails(addr); const username = details.username || addr.substring(0, 10) + '...'; this.elements.identityUsernameEl.textContent = username; const letter = details.username ? details.username.charAt(0).toUpperCase() : 'I'; this.elements.identityAvatarEl.textContent = letter; const colorInfo = getDeterministicColorInfo({ ibGibAddr: addr }); this.elements.identityAvatarEl.style.backgroundColor = colorInfo.tjpColor || colorInfo.punctiliarColor; this.elements.identityAvatarEl.style.color = colorInfo.tjpColorContrast || colorInfo.punctiliarColorContrast; // Rich desktop tooltip showing handle, description, and address const persistentIdentity = getContextModeService().getPersistentIdentity(); const siteUsername = persistentIdentity?.siteUsername; const tooltipParts = [username]; if (siteUsername) { tooltipParts.push(`(@${siteUsername.replace(/^@/, '')})`); } if (details.description) { tooltipParts.push(`\n${details.description}`); } const emails = (details as any).emails; if (emails && emails.length > 0) { tooltipParts.push(`\nEmail: ${emails.join(', ')}`); } tooltipParts.push(`\nAddr: ${addr}`); this.elements.btnIdentityPillEl.title = tooltipParts.join(' '); } else { this.elements.btnIdentityPillEl.classList.add('unauth'); this.elements.pillAuthContentEl.classList.add('hidden'); this.elements.pillUnauthContentEl.classList.remove('hidden'); this.elements.btnIdentityPillEl.title = 'Sign In or Create Account'; } // Section 1: Render Update Affordance if update is available if (this.pendingUpdateDetail && this.pendingUpdateDetail.hasUpdate && (!this.pendingUpdateDetail.activePrimaryAddr || this.pendingUpdateDetail.activePrimaryAddr === this.activeAddr)) { this.elements.updateBadgeEl.classList.remove('hidden'); this.elements.sectionUpdateEl.classList.remove('hidden'); if (this.elements.updateNTextEl) { this.elements.updateNTextEl.textContent = `n: ${this.pendingUpdateDetail.localN} ➔ ${this.pendingUpdateDetail.serverN}`; } } else { this.elements.updateBadgeEl.classList.add('hidden'); this.elements.sectionUpdateEl.classList.add('hidden'); } // Section 2: Render Local Identities List this.elements.identitiesListEl.innerHTML = ''; const generalSettings = this.settings ? await this.getSettings({ settingsType: SettingsType.general, useCase: 'current', }) : undefined; const primaryKeystones: { addr: string; username: string; siteUsername?: string }[] = []; for (const kAddr of registeredKeystones) { const details = await this.getKeystoneDetails(kAddr); if (details.isPrimary) { const rawSiteUsername = (generalSettings as any)?.siteUsernames?.[kAddr] || (kAddr === this.activeAddr ? getContextModeService().getPersistentIdentity()?.siteUsername : undefined); const siteUsername = rawSiteUsername ? rawSiteUsername.replace(/^@/, '') : undefined; primaryKeystones.push({ addr: kAddr, username: details.username || kAddr.substring(0, 10) + '...', siteUsername, }); } } if (primaryKeystones.length > 0) { this.elements.sectionIdentitiesEl.classList.remove('hidden'); for (const item of primaryKeystones) { const isActive = item.addr === this.activeAddr; const btn = document.createElement('button'); btn.className = `dropdown-item identity-item ${isActive ? 'active-identity' : ''}`; btn.innerHTML = ` ${isActive ? '🟢' : '⚪'}
${item.username} ${item.siteUsername ? `@${item.siteUsername}` : ''}
${isActive ? 'Active' : ''} `; btn.addEventListener('click', (e) => { e.stopPropagation(); this.closeDropdown(); if (!isActive) { window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, { detail: { activeIdentityAddr: item.addr }, bubbles: true, composed: true })); } }); this.elements.identitiesListEl.appendChild(btn); } } else { this.elements.sectionIdentitiesEl.classList.add('hidden'); } } private handleIdentityUpdateAvailable(e: any) { const detail = e.detail as EventIbGibIdentityUpdateAvailableDetail; if (!detail) return; if (detail.hasUpdate && (!detail.activePrimaryAddr || detail.activePrimaryAddr === this.activeAddr)) { this.pendingUpdateDetail = detail; if (this.elements) { this.elements.updateBadgeEl?.classList.remove('hidden'); this.elements.sectionUpdateEl?.classList.remove('hidden'); if (this.elements.updateNTextEl) { this.elements.updateNTextEl.textContent = `n: ${detail.localN} ➔ ${detail.serverN}`; } } } else { this.pendingUpdateDetail = null; if (this.elements) { this.elements.updateBadgeEl?.classList.add('hidden'); this.elements.sectionUpdateEl?.classList.add('hidden'); } } } private async handleIdentityChanged(e: any) { this.pendingUpdateDetail = null; if (this.elements) { this.elements.updateBadgeEl?.classList.add('hidden'); this.elements.sectionUpdateEl?.classList.add('hidden'); } this.keystoneCache.clear(); await this.resolveActiveIdentity(); await this.renderUI(); } private async handleRequestIdentityChange(e: any) { const lc = `${this.lc}[handleRequestIdentityChange]`; const addr = e.detail?.activeIdentityAddr; if (!addr) { return; } try { if (!this.settings) { return; } const generalSettings = await this.getSettings({ settingsType: SettingsType.general, useCase: 'current', }) ?? { type: 'general' }; (generalSettings as any).activeIdentityAddr = addr; const requestedSiteUsername = e.detail?.siteUsername; if (requestedSiteUsername) { (generalSettings as any).siteUsernames = (generalSettings as any).siteUsernames || {}; (generalSettings as any).siteUsernames[addr] = String(requestedSiteUsername).replace(/^@/, ''); } await this.updateSettings({ settingsType: SettingsType.general, useCase: 'current', newSectionInfo: generalSettings, }); await this.resolveActiveIdentity(); await this.renderUI(); window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_CHANGED, { detail: { activeIdentityAddr: addr }, bubbles: true, composed: true })); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); } } override async handleContextUpdated(): Promise { await this.renderUI(); } }