diff --git a/docs/discovery.md b/docs/discovery.md index 4c471ba..3793dd8 100644 --- a/docs/discovery.md +++ b/docs/discovery.md @@ -2,6 +2,8 @@ Discover is the packaged napplet catalog. Open it from the workspace overview or **Settings → Discover napplets**. Opening Discover again focuses its existing workspace card. The guest reads signed publications from the selected Network relays. Hypergolic resolves an Open request separately across the selected Lookup and Network relays and presents native review for the exact target. [Published opening contract](published-open.md) +The selected Open button shows **Opening…** until Hypergolic returns a result. Discover pauses other Open actions during that request and shows the outcome beside the selected action. The bridge reports the final outcome, so the spinner stays indeterminate through lookup, download and verification. + ## Build and import The portable guest lives in `apps/discovery` in the separate `napplets` monorepo. Build it with Node.js 24.20.0 and pnpm 10.34.5. From the Hypergolic checkout, set `NAPPLETS_CHECKOUT` to the guest monorepo checkout. diff --git a/runtime/fixtures/napplet-discovery-manifest.json b/runtime/fixtures/napplet-discovery-manifest.json index 8c6750b..06588e5 100644 --- a/runtime/fixtures/napplet-discovery-manifest.json +++ b/runtime/fixtures/napplet-discovery-manifest.json @@ -13,11 +13,11 @@ [ "path", "/index.html", - "74051ed5602ab68d07d88b32ab8e0cbbf5623e1749a393d03e3730e132e7a8ec" + "32f90ddc47d2c1a92ef89399737fd7c5f3fde4087707a075923586e81d2268a7" ], [ "x", - "ac40811acb447f037d84830c5a6da6a39212d8fc38385adeec03aec6aa876ccb", + "862cd3c6e674353dd53fb76801140f4e315776810497faf2079e5095c91bf25e", "aggregate" ], [ @@ -29,5 +29,5 @@ "resource" ] ], - "aggregateHash": "ac40811acb447f037d84830c5a6da6a39212d8fc38385adeec03aec6aa876ccb" + "aggregateHash": "862cd3c6e674353dd53fb76801140f4e315776810497faf2079e5095c91bf25e" } diff --git a/runtime/fixtures/napplet-discovery.html b/runtime/fixtures/napplet-discovery.html index 8632f09..e2b45c9 100644 --- a/runtime/fixtures/napplet-discovery.html +++ b/runtime/fixtures/napplet-discovery.html @@ -203,6 +203,23 @@ .primary, .secondary { padding: .45rem .85rem; border-radius: .65rem; font-weight: 700; } .primary { color: var(--action-ink); background: var(--ink); } .row-open { flex: 0 0 auto; min-width: 4.25rem; font-size: .91rem; } +.open-action { display: inline-flex; align-items: center; justify-content: center; gap: .5em; } +.open-action[data-opening] { opacity: 1; } +.open-spinner { display: none; } +.open-action[data-opening] .open-spinner { + display: block; + flex: 0 0 auto; + width: 1em; + height: 1em; + border: .13em solid currentColor; + border-inline-end-color: transparent; + border-radius: 50%; + animation: open-spin .8s linear infinite; +} +@keyframes open-spin { to { transform: rotate(1turn); } } +@media (prefers-reduced-motion: reduce) { + .open-action[data-opening] .open-spinner { animation: none; } +} .secondary { border: 1px solid var(--well-line); background: var(--well); } .quiet { padding: .45rem .7rem; color: var(--muted); font-weight: 560; } .quiet:hover { text-decoration: underline; } @@ -292,7 +309,9 @@

Recently published

- +
+ +

    @@ -303,7 +322,8 @@

    Recently published

    - `;var listing=find("listing"),detail=find("detail"),heading=find("heading"),search=find("query"),back=find("search-back"),clear=find("clear"),filter=find("compatibility"),browseControls=find("browse-controls"),feedHeading=find("feed-heading"),results=find("results"),more=find("more"),refresh=find("refresh"),scope=find("scope"),notice=find("notice"),empty=find("empty"),coverage=find("coverage"),rowOpenStatus=find("row-open-status"),categories=["All","Games","Tools","Creative"],profiles=new Map,attemptedProfiles=new Set,history=new RevisionHistory,blockedVersions=new Set,selectedCategory="All",homeFilters,selected,detailBack,pager,retryReset,listMedia=new ViewMedia,detailMedia=new ViewMedia,loading=!0,relayAccess=!1,readFailed=!1,stale=!1,profilePending=!1,openPending,rowFeedbackContext=0,visibleEntries=[];back.prepend(glyph("back"));clear.append(glyph("close"));find("search-icon").append(glyph("search"));find("filter-icon").append(glyph("filter"));for(let category of categories){let control=button(category,"category",()=>{selectedCategory=category,updateResults()});control.dataset.category=category,find("categories").append(control)}function focus(element){element.focus({preventScroll:!0})}function enterSearch(){homeFilters||(homeFilters={category:selectedCategory,filter:filter.value,scroll:window.scrollY},heading.replaceChildren(document.createTextNode("Search results"),node("span","accent",".")),heading.lastElementChild?.setAttribute("aria-hidden","true"),find("eyebrow").textContent="Search results",back.hidden=!1,listing.classList.add("search-mode"))}function leaveSearch(){if(!homeFilters)return;clearRowFeedback();let previous=homeFilters;homeFilters=void 0,selectedCategory=previous.category,filter.value=previous.filter,search.value="",heading.replaceChildren(document.createTextNode("Find a Napplet to try"),node("span","accent",".")),heading.lastElementChild?.setAttribute("aria-hidden","true"),find("eyebrow").textContent="Explore Napplets",back.hidden=!0,listing.classList.remove("search-mode"),renderList(),window.scrollTo(0,previous.scroll),focus(search)}function clearRowFeedback(){rowFeedbackContext+=1,rowOpenStatus.replaceChildren(),rowOpenStatus.hidden=!0}function updateResults(){clearRowFeedback(),renderList();let headingTop=feedHeading.getBoundingClientRect().top,controlsBottom=browseControls.getBoundingClientRect().bottom;headingTop{search.value.trim()&&enterSearch(),updateResults()});clear.addEventListener("click",()=>{search.value="",updateResults(),focus(search)});back.addEventListener("click",leaveSearch);filter.addEventListener("change",updateResults);find("search-form").addEventListener("submit",event=>{event.preventDefault(),enterSearch(),updateResults(),focus(feedHeading)});more.addEventListener("click",()=>{clearRowFeedback(),load(!1)});refresh.addEventListener("click",()=>{clearRowFeedback(),load(!0)});function listRow(entry,showPreview){let app=entry.publication,row=node("li","row"),cell=node("div","row-cell"),info=button("","row-info",event=>showDetail(entry,event.currentTarget));info.setAttribute("aria-label",`Details for ${app.title}`),info.dataset.row=app.address;let summary=node("span","row-summary");if(summary.append(node("strong","row-title",app.title)),app.description?summary.append(node("span","row-description",app.description)):summary.append(node("span","row-description","No description provided")),info.append(artwork(app,listMedia),summary),showPreview){let preview=node("div","row-art");screenshot(preview,app.screenshots[0],listMedia,!0),row.append(preview)}if(cell.append(info),openMode(entry)==="direct"&&!stale){let open=button("Open","primary row-open",event=>{requestOpen(entry,event.currentTarget,rowOpenStatus)});open.setAttribute("aria-label",`Open ${app.title}`),open.dataset.open=app.address,blockedVersions.has(app.signed.event.id)&&open.setAttribute("aria-disabled","true"),cell.append(open)}return row.append(cell),row}function renderList(){let active=document.activeElement,prior=active?.dataset.row??active?.dataset.open,wasOpen=!!active?.dataset.open,needle=search.value.trim();for(let tab of find("categories").querySelectorAll("button"))tab.setAttribute("aria-pressed",String(tab.dataset.category===selectedCategory));if(clear.hidden=!search.value,filter.dataset.active=filter.value,feedHeading.textContent=homeFilters?needle?`Results for \u201C${needle}\u201D`:"Search results":"Recently published",scope.hidden=!homeFilters,scope.textContent="Search covers loaded publications and fetched publisher profiles.",visibleEntries=(pager?.entries??[]).filter(({publication})=>(selectedCategory==="All"||publication.categories.includes(selectedCategory))&&(filter.value==="all"||compatibility(publication).requirementsAvailable)&&matchesSearch(publication,needle,profiles.get(publication.publisher))),listMedia.clearTargets(),results.replaceChildren(...visibleEntries.map((entry,index)=>listRow(entry,index<2))),prior&&!listing.hidden){let replacement=[...results.querySelectorAll(wasOpen?"[data-open]":"[data-row]")].find(control=>(wasOpen?control.dataset.open:control.dataset.row)===prior);focus(replacement||feedHeading)}let count=visibleEntries.length,resultStatus=find("result-status"),resultMessage=`${count} matching publication${count===1?"":"s"} in loaded results.`;!loading&&resultStatus.textContent!==resultMessage&&(resultStatus.textContent=resultMessage),notice.textContent=loading?pager?.entries.length?"Checking the relays\u2026":"Reading signed publications\u2026":relayAccess?readFailed?stale?"Showing saved results from this session. The relays could not refresh the directory.":pager?"Could not load older publications. Your loaded results are still available.":"The directory could not load. The Network relays did not return a usable response.":pager?.gap?"Some publications share a timestamp. Relay limits left a gap in the results.":pager?.partial?pager.done?"Some Network relay pages remain unavailable. Refresh to try again.":"Some Network relays could not be read. Loading more retries their missing pages.":pager?.withheld?"Some relays returned older versions of releases seen in this session.":pager?.skipped||pager?.reduced?"Relay limits or validation may have left out some publications.":"":"Directory access is unavailable. This Napplet needs relay access from the shell.",notice.hidden=!notice.textContent,empty.hidden=!relayAccess||loading||readFailed&&!pager||!!count,empty.replaceChildren(),empty.hidden||(needle||selectedCategory!=="All"||filter.value!=="all"?empty.append(node("p","",filter.value!=="all"?"No loaded results meet this compatibility filter.":"No matches in the loaded results."),button("Clear filters","secondary",()=>{search.value="",selectedCategory="All",filter.value="all",updateResults(),focus(search)})):empty.append(node("p","",pager?.done?"No eligible named Napplets in the queried window.":"No eligible Napplets on this page."))),more.hidden=!pager||pager.done||stale,more.setAttribute("aria-disabled",String(loading)),more.textContent=readFailed&&!stale?"Retry older publications":"Load more",refresh.hidden=!relayAccess||!pager&&!readFailed,refresh.setAttribute("aria-disabled",String(loading)),refresh.textContent=readFailed&&(!pager||stale)?"Retry":"Refresh",coverage.hidden=!pager,coverage.textContent=pager?`${pager.entries.length} verified publication${pager.entries.length===1?"":"s"} \xB7 Reachable Network relays \xB7 ${pager.pagesChecked} page${pager.pagesChecked===1?"":"s"} checked${pager.bounded?" \xB7 Directory limit reached":""}`:"",enrich()}async function load(reset){if(loading||!canRead())return;let next=reset||!pager?retryReset??new CatalogPager(query,void 0,history,catalogPaging()):pager;loading=!0,readFailed=!1,renderList();try{await next.more(),pager=next,retryReset=void 0,stale=!1}catch{readFailed=!0,(reset||!pager)&&(retryReset=retryReset===next?void 0:next),reset&&pager?.entries.length&&(stale=!0)}finally{loading=!1,renderList()}}async function enrich(){if(!relayAccess||loading||profilePending||attemptedProfiles.size>=48)return;let candidates=[...selected?[selected]:[],...visibleEntries.slice(0,12),...homeFilters&&search.value.trim()?pager?.entries.slice(0,24)??[]:[]].map(({publication})=>publication.publisher),authors=[...new Set(candidates)].filter(author=>!attemptedProfiles.has(author)).slice(0,Math.min(4,48-attemptedProfiles.size));if(authors.length){authors.forEach(author=>attemptedProfiles.add(author)),profilePending=!0;try{let now=Math.floor(Date.now()/1e3),response=await query(authors.map(author=>({kinds:[0],authors:[author],until:now,limit:2})));if(!Array.isArray(response)||response.length>32)return;let changed=!1;for(let row of response){let verified=verifySignedEvent(row?.event,{kinds:[0],authors,until:now},now);if(!verified)continue;let profile=parseProfile(verified);if(!profile)continue;let author=verified.event.pubkey,current=profiles.get(author);(!current||current.timestamp{enrich()})}}}function detailSection(title){let section=node("section","detail-section");return section.append(node("h2","",title)),section}var capNames={relay:["Relays","Read or publish Nostr events through the shell."],resource:["Resources","Load resources through the shell."],theme:["Theme","Follow the shell\u2019s appearance."],storage:["Storage","Save this Napplet\u2019s data."],identity:["Identity","Read public account information."],outbox:["Outbox","Read or publish using the shell\u2019s Outbox routing."],link:["Links","Ask the shell to open a web link."],upload:["Upload","Ask to send selected files to Blossom."],intent:["Intents","Request an action from a compatible Napplet."],inc:["Messages","Exchange messages with compatible Napplets."],notify:["Notifications","Show notifications within the shell."],config:["Configuration","Use settings maintained by the shell."]};function capabilities(section,label,names){if(!names.length)return;section.append(node("h3","cap-group",label));let list=node("ul","capabilities");list.setAttribute("role","list");for(let name of names){let supported=availability(name),[title,description]=capNames[name]??[name,"Declared by the publisher."],row=node("li","cap-row"),icon=node("span","cap-icon");icon.append(glyph("domain"));let copy=node("div");copy.append(node("strong","",title),node("p","muted",description)),supported.status!=="available"&©.append(node("p","cap-state",supported.status==="unavailable"?"Unavailable on this shell":"Availability unknown")),row.append(icon,copy),list.append(row)}section.append(list)}function reference(label,value){let field=node("div","reference");return field.append(node("span","muted",label),node("code","",value)),field}function updatePublisher(){if(!selected)return;let profile=profiles.get(selected.publication.publisher),publisher=find("publisher-name");publisher.textContent=profile?.name||"View publisher key";let about=find("publisher-about");about.textContent=profile?.about||(profile?.name?"":"No verified publisher profile is available."),about.hidden=!about.textContent}function settingsInstructions(){return node("p","settings-instructions","Select the publication address below, then go to Hypergolic Settings \u2192 Open a napplet. Paste the address and choose Verify and open. Settings looks up the latest eligible release, which may differ from this one.")}function showDetail(entry,returnFocus){openPending||(clearRowFeedback(),detailBack={scroll:window.scrollY,focus:returnFocus},selected=entry,listing.hidden=!0,detail.hidden=!1,listMedia.close(),listMedia=new ViewMedia,renderDetail(),window.scrollTo(0,0),focus(find("detail-heading")),enrich())}function closeDetail(){if(!detailBack||openPending)return;let previous=detailBack;selected=detailBack=void 0,detailMedia.close(),detailMedia=new ViewMedia,detail.replaceChildren(),detail.hidden=!0,listing.hidden=!1;let returnKey=previous.focus.dataset.row;renderList(),window.scrollTo(0,previous.scroll);let restored=[...results.querySelectorAll("[data-row]")].find(control=>control.dataset.row===returnKey);focus(restored??heading)}function renderDetail(){if(!selected)return;let entry=selected,app=entry.publication,state=compatibility(app),header=node("header"),goBack=button(homeFilters?"Search results":"Discover","back",closeDetail);goBack.prepend(glyph("back")),goBack.setAttribute("aria-label",homeFilters?"Back to search results":"Back to Discover");let title=node("h1","","Napplet details");title.id="detail-heading",title.tabIndex=-1,header.append(goBack,node("p","eyebrow","Publication"),title);let identity=node("div","identity"),names=node("div");names.append(node("h2","app-name",app.title)),app.categories.length&&names.append(node("p","muted",app.categories.join(" \xB7 "))),identity.append(artwork(app,detailMedia,!0),names);let actions=node("div","detail-actions"),feedback=node("p","feedback");feedback.setAttribute("role","status"),feedback.hidden=!0;let mode=openMode(entry),label=state.label,explanation=state.detail;if(entry.conflict)label="Conflicting releases",explanation="This publisher signed different releases at the same timestamp. Open is unavailable until the conflict is resolved.";else if(mode==="direct"&&!stale&&!blockedVersions.has(app.signed.event.id))actions.append(button("Open Napplet","primary detail-open",event=>{requestOpen(entry,event.currentTarget,feedback)}));else if(mode==="settings"&&!stale){let manual=button("Open via Settings","primary detail-open",()=>{manualInstructions.hidden=!1,manual.setAttribute("aria-expanded","true"),find("publication-reference").open=!0});manual.setAttribute("aria-expanded","false"),manual.setAttribute("aria-controls","manual-open"),actions.append(manual)}else mode==="unknown"?explanation+=" This shell does not offer opening from the directory.":stale&&(explanation="Refresh the directory before opening a saved release.");let manualInstructions=settingsInstructions();manualInstructions.id="manual-open",manualInstructions.hidden=!0;let stateText=node("p",`compat-state ${entry.conflict?"warn":state.tone}`,label);actions.append(stateText,node("p","compat-detail",explanation),manualInstructions,feedback);let preview=node("div","detail-art");screenshot(preview,app.screenshots[0],detailMedia,!1);let about=detailSection("About");about.append(node("p","description",app.description||"The publisher did not provide a description."));let publisher=detailSection("Publisher"),profile=node("details","publisher"),summary=node("summary"),profileLabel=node("span");profileLabel.id="publisher-name",summary.append(glyph("profile"),profileLabel,glyph("chevron"));let profileAbout=node("p","description");profileAbout.id="publisher-about",profile.append(summary,profileAbout,reference("Signing key",app.npub)),publisher.append(profile);let caps=detailSection("Capabilities");capabilities(caps,"Required",app.required),capabilities(caps,"Optional",app.optional),!app.required.length&&!app.optional.length&&caps.append(node("p","muted","The publisher did not declare any capabilities."));let publication=node("details","publication");publication.id="publication-reference";let referenceTitle=node("summary","","View publication");referenceTitle.append(glyph("down")),publication.append(referenceTitle,reference("Publication address",app.address),reference("Signed event ID",app.signed.event.id)),detail.replaceChildren(header,identity,actions,preview,about,publisher,caps,publication),updatePublisher()}var openFailureMessages={"lookup-unavailable":"Hypergolic could not complete its relay lookup. Try again.","download-unavailable":"Hypergolic could not download this release from its published servers. Try again.","hash-mismatch":"The downloaded file did not match the signed release.","invalid-html":"The signed release did not meet Hypergolic\u2019s napplet HTML requirements.","stage-unavailable":"Hypergolic could not prepare this verified release. Try again.","workspace-full":"Your workspace is full. Close a napplet and try again."};async function requestOpen(entry,control,feedback){let version=entry.publication.signed.event.id;if(openPending||blockedVersions.has(version)||stale||openMode(entry)!=="direct")return;openPending=version;let context=feedback===rowOpenStatus?rowFeedbackContext:void 0,currentFeedback=()=>context===void 0||context===rowFeedbackContext;control.setAttribute("aria-disabled","true"),feedback.hidden=!1,feedback.textContent="Waiting for Hypergolic to verify and open this release\u2026";let{status,reason}=await openPublication(entry.publication);openPending=void 0;let blocked=status==="refresh-required"||status==="conflict";if(blocked&&blockedVersions.add(version),!currentFeedback()){blocked?renderList():control.removeAttribute("aria-disabled");return}let messages={opened:"Opened in Hypergolic.",busy:"Hypergolic is handling another open request. Try again when it finishes.",cancelled:"Opening cancelled.",rejected:"Opening was declined in Hypergolic.","not-found":"Hypergolic could not find this release on its configured relays. Refresh the directory and try again.",unsupported:"Hypergolic could not open this publication with its current support profile.",conflict:"Hypergolic found conflicting revisions. Refresh the directory or check the saved Napplet for an update.","refresh-required":"The release changed. Refresh the directory and review the new details before opening.",failed:"Hypergolic could not verify or load this release. Try again.",unavailable:"Opening from the directory is unavailable in this session."};if(feedback.textContent=reason?openFailureMessages[reason]:messages[status],blocked){feedback.append(button("Refresh directory","quiet",async()=>{if(!(loading||(feedback.querySelector("button")?.setAttribute("aria-disabled","true"),await load(!0),!currentFeedback()))){if(readFailed){feedback.textContent="Could not refresh the directory. Try refreshing from the list.";return}if(selected){let updated=pager?.entries.find(item=>item.publication.address===entry.publication.address);updated&&updated.publication.signed.event.id!==version?(selected=updated,detailMedia.close(),detailMedia=new ViewMedia,renderDetail(),window.scrollTo(0,0),focus(find("detail-heading"))):feedback.textContent="The selected release is still unavailable. Look further back in the refreshed directory."}else feedback.textContent="Directory refreshed. Review the listing before trying again.",focus(find("feed-heading"))}})),control.isConnected&&focus(control);return}status==="unavailable"&&!reason&&selected===entry&&compatibility(entry.publication).requirementsAvailable&&(feedback.append(settingsInstructions()),find("publication-reference").open=!0),control.removeAttribute("aria-disabled"),status!=="opened"&&control.isConnected&&focus(control)}window.addEventListener("pagehide",event=>{event.persisted||(listMedia.close(),detailMedia.close())});connectTheme();ready().then(available=>{relayAccess=available,loading=!1,available?load(!0):renderList()});})(); + +

    `;var listing=find("listing"),detail=find("detail"),heading=find("heading"),search=find("query"),back=find("search-back"),clear=find("clear"),filter=find("compatibility"),browseControls=find("browse-controls"),feedHeading=find("feed-heading"),results=find("results"),more=find("more"),refresh=find("refresh"),scope=find("scope"),notice=find("notice"),empty=find("empty"),coverage=find("coverage"),rowOpenStatus=find("row-open-status"),rowOpenAnnouncement=find("row-open-announcement"),openingStatus=find("opening-status"),categories=["All","Games","Tools","Creative"],profiles=new Map,attemptedProfiles=new Set,history=new RevisionHistory,blockedVersions=new Set,selectedCategory="All",homeFilters,selected,detailBack,pager,retryReset,listMedia=new ViewMedia,detailMedia=new ViewMedia,loading=!0,relayAccess=!1,readFailed=!1,stale=!1,profilePending=!1,openPending,rowFeedbackContext=0,rowFeedbackVersion,visibleEntries=[];back.prepend(glyph("back"));clear.append(glyph("close"));find("search-icon").append(glyph("search"));find("filter-icon").append(glyph("filter"));for(let category of categories){let control=button(category,"category",()=>{selectedCategory=category,updateResults()});control.dataset.category=category,find("categories").append(control)}function focus(element){element.focus({preventScroll:!0})}function enterSearch(){homeFilters||(homeFilters={category:selectedCategory,filter:filter.value,scroll:window.scrollY},heading.replaceChildren(document.createTextNode("Search results"),node("span","accent",".")),heading.lastElementChild?.setAttribute("aria-hidden","true"),find("eyebrow").textContent="Search results",back.hidden=!1,listing.classList.add("search-mode"))}function leaveSearch(){if(!homeFilters)return;clearRowFeedback();let previous=homeFilters;homeFilters=void 0,selectedCategory=previous.category,filter.value=previous.filter,search.value="",heading.replaceChildren(document.createTextNode("Find a Napplet to try"),node("span","accent",".")),heading.lastElementChild?.setAttribute("aria-hidden","true"),find("eyebrow").textContent="Explore Napplets",back.hidden=!0,listing.classList.remove("search-mode"),renderList(),window.scrollTo(0,previous.scroll),focus(search)}function clearRowFeedback(){rowFeedbackContext+=1,rowFeedbackVersion=void 0,rowOpenStatus.replaceChildren(),rowOpenStatus.hidden=!0}function placeRowFeedback(){let row=(rowFeedbackVersion?results.querySelector(`[data-open-version="${rowFeedbackVersion}"]`):null)?.closest(".row");row?row.append(rowOpenAnnouncement):results.before(rowOpenAnnouncement)}function updateResults(){clearRowFeedback(),renderList();let headingTop=feedHeading.getBoundingClientRect().top,controlsBottom=browseControls.getBoundingClientRect().bottom;headingTop{search.value.trim()&&enterSearch(),updateResults()});clear.addEventListener("click",()=>{search.value="",updateResults(),focus(search)});back.addEventListener("click",leaveSearch);filter.addEventListener("change",updateResults);find("search-form").addEventListener("submit",event=>{event.preventDefault(),enterSearch(),updateResults(),focus(feedHeading)});more.addEventListener("click",()=>{clearRowFeedback(),load(!1)});refresh.addEventListener("click",()=>{clearRowFeedback(),load(!0)});function listRow(entry,showPreview){let app=entry.publication,row=node("li","row"),cell=node("div","row-cell"),info=button("","row-info",event=>showDetail(entry,event.currentTarget));info.setAttribute("aria-label",`Details for ${app.title}`),info.dataset.row=app.address;let summary=node("span","row-summary");if(summary.append(node("strong","row-title",app.title)),app.description?summary.append(node("span","row-description",app.description)):summary.append(node("span","row-description","No description provided")),info.append(artwork(app,listMedia),summary),showPreview){let preview=node("div","row-art");screenshot(preview,app.screenshots[0],listMedia,!0),row.append(preview)}if(cell.append(info),openMode(entry)==="direct"&&!stale){let open=openButton(entry,"row",rowOpenStatus);open.dataset.open=app.address,cell.append(open)}return row.append(cell),row}function renderList(){let active=document.activeElement,prior=active?.dataset.row??active?.dataset.open,wasOpen=!!active?.dataset.open,needle=search.value.trim();for(let tab of find("categories").querySelectorAll("button"))tab.setAttribute("aria-pressed",String(tab.dataset.category===selectedCategory));if(clear.hidden=!search.value,filter.dataset.active=filter.value,feedHeading.textContent=homeFilters?needle?`Results for \u201C${needle}\u201D`:"Search results":"Recently published",scope.hidden=!homeFilters,scope.textContent="Search covers loaded publications and fetched publisher profiles.",visibleEntries=(pager?.entries??[]).filter(({publication})=>(selectedCategory==="All"||publication.categories.includes(selectedCategory))&&(filter.value==="all"||compatibility(publication).requirementsAvailable)&&matchesSearch(publication,needle,profiles.get(publication.publisher))),listMedia.clearTargets(),results.replaceChildren(...visibleEntries.map((entry,index)=>listRow(entry,index<2))),placeRowFeedback(),prior&&!listing.hidden){let replacement=[...results.querySelectorAll(wasOpen?"[data-open]":"[data-row]")].find(control=>(wasOpen?control.dataset.open:control.dataset.row)===prior);focus(replacement||feedHeading)}let count=visibleEntries.length,resultStatus=find("result-status"),resultMessage=`${count} matching publication${count===1?"":"s"} in loaded results.`;!loading&&resultStatus.textContent!==resultMessage&&(resultStatus.textContent=resultMessage),notice.textContent=loading?pager?.entries.length?"Checking the relays\u2026":"Reading signed publications\u2026":relayAccess?readFailed?stale?"Showing saved results from this session. The relays could not refresh the directory.":pager?"Could not load older publications. Your loaded results are still available.":"The directory could not load. The Network relays did not return a usable response.":pager?.gap?"Some publications share a timestamp. Relay limits left a gap in the results.":pager?.partial?pager.done?"Some Network relay pages remain unavailable. Refresh to try again.":"Some Network relays could not be read. Loading more retries their missing pages.":pager?.withheld?"Some relays returned older versions of releases seen in this session.":pager?.skipped||pager?.reduced?"Relay limits or validation may have left out some publications.":"":"Directory access is unavailable. This Napplet needs relay access from the shell.",notice.hidden=!notice.textContent,empty.hidden=!relayAccess||loading||readFailed&&!pager||!!count,empty.replaceChildren(),empty.hidden||(needle||selectedCategory!=="All"||filter.value!=="all"?empty.append(node("p","",filter.value!=="all"?"No loaded results meet this compatibility filter.":"No matches in the loaded results."),button("Clear filters","secondary",()=>{search.value="",selectedCategory="All",filter.value="all",updateResults(),focus(search)})):empty.append(node("p","",pager?.done?"No eligible named Napplets in the queried window.":"No eligible Napplets on this page."))),more.hidden=!pager||pager.done||stale,more.setAttribute("aria-disabled",String(loading)),more.textContent=readFailed&&!stale?"Retry older publications":"Load more",refresh.hidden=!relayAccess||!pager&&!readFailed,refresh.setAttribute("aria-disabled",String(loading)),refresh.textContent=readFailed&&(!pager||stale)?"Retry":"Refresh",coverage.hidden=!pager,coverage.textContent=pager?`${pager.entries.length} verified publication${pager.entries.length===1?"":"s"} \xB7 Reachable Network relays \xB7 ${pager.pagesChecked} page${pager.pagesChecked===1?"":"s"} checked${pager.bounded?" \xB7 Directory limit reached":""}`:"",enrich()}async function load(reset){if(loading||!canRead())return;let next=reset||!pager?retryReset??new CatalogPager(query,void 0,history,catalogPaging()):pager;loading=!0,readFailed=!1,renderList();try{await next.more(),pager=next,retryReset=void 0,stale=!1}catch{readFailed=!0,(reset||!pager)&&(retryReset=retryReset===next?void 0:next),reset&&pager?.entries.length&&(stale=!0)}finally{loading=!1,renderList()}}async function enrich(){if(!relayAccess||loading||profilePending||attemptedProfiles.size>=48)return;let candidates=[...selected?[selected]:[],...visibleEntries.slice(0,12),...homeFilters&&search.value.trim()?pager?.entries.slice(0,24)??[]:[]].map(({publication})=>publication.publisher),authors=[...new Set(candidates)].filter(author=>!attemptedProfiles.has(author)).slice(0,Math.min(4,48-attemptedProfiles.size));if(authors.length){authors.forEach(author=>attemptedProfiles.add(author)),profilePending=!0;try{let now=Math.floor(Date.now()/1e3),response=await query(authors.map(author=>({kinds:[0],authors:[author],until:now,limit:2})));if(!Array.isArray(response)||response.length>32)return;let changed=!1;for(let row of response){let verified=verifySignedEvent(row?.event,{kinds:[0],authors,until:now},now);if(!verified)continue;let profile=parseProfile(verified);if(!profile)continue;let author=verified.event.pubkey,current=profiles.get(author);(!current||current.timestamp{enrich()})}}}function detailSection(title){let section=node("section","detail-section");return section.append(node("h2","",title)),section}var capNames={relay:["Relays","Read or publish Nostr events through the shell."],resource:["Resources","Load resources through the shell."],theme:["Theme","Follow the shell\u2019s appearance."],storage:["Storage","Save this Napplet\u2019s data."],identity:["Identity","Read public account information."],outbox:["Outbox","Read or publish using the shell\u2019s Outbox routing."],link:["Links","Ask the shell to open a web link."],upload:["Upload","Ask to send selected files to Blossom."],intent:["Intents","Request an action from a compatible Napplet."],inc:["Messages","Exchange messages with compatible Napplets."],notify:["Notifications","Show notifications within the shell."],config:["Configuration","Use settings maintained by the shell."]};function capabilities(section,label,names){if(!names.length)return;section.append(node("h3","cap-group",label));let list=node("ul","capabilities");list.setAttribute("role","list");for(let name of names){let supported=availability(name),[title,description]=capNames[name]??[name,"Declared by the publisher."],row=node("li","cap-row"),icon=node("span","cap-icon");icon.append(glyph("domain"));let copy=node("div");copy.append(node("strong","",title),node("p","muted",description)),supported.status!=="available"&©.append(node("p","cap-state",supported.status==="unavailable"?"Unavailable on this shell":"Availability unknown")),row.append(icon,copy),list.append(row)}section.append(list)}function reference(label,value){let field=node("div","reference");return field.append(node("span","muted",label),node("code","",value)),field}function updatePublisher(){if(!selected)return;let profile=profiles.get(selected.publication.publisher),publisher=find("publisher-name");publisher.textContent=profile?.name||"View publisher key";let about=find("publisher-about");about.textContent=profile?.about||(profile?.name?"":"No verified publisher profile is available."),about.hidden=!about.textContent}function settingsInstructions(){return node("p","settings-instructions","Select the publication address below, then go to Hypergolic Settings \u2192 Open a napplet. Paste the address and choose Verify and open. Settings looks up the latest eligible release, which may differ from this one.")}function showDetail(entry,returnFocus){openPending||(clearRowFeedback(),detailBack={scroll:window.scrollY,focus:returnFocus},selected=entry,listing.hidden=!0,detail.hidden=!1,listMedia.close(),listMedia=new ViewMedia,renderDetail(),window.scrollTo(0,0),focus(find("detail-heading")),enrich())}function closeDetail(){if(!detailBack||openPending)return;let previous=detailBack;selected=detailBack=void 0,detailMedia.close(),detailMedia=new ViewMedia,detail.replaceChildren(),detail.hidden=!0,listing.hidden=!1;let returnKey=previous.focus.dataset.row;renderList(),window.scrollTo(0,previous.scroll);let restored=[...results.querySelectorAll("[data-row]")].find(control=>control.dataset.row===returnKey);focus(restored??heading)}function renderDetail(){if(!selected)return;let entry=selected,app=entry.publication,state=compatibility(app),header=node("header"),goBack=button(homeFilters?"Search results":"Discover","back",closeDetail);goBack.prepend(glyph("back")),goBack.setAttribute("aria-label",homeFilters?"Back to search results":"Back to Discover");let title=node("h1","","Napplet details");title.id="detail-heading",title.tabIndex=-1,header.append(goBack,node("p","eyebrow","Publication"),title);let identity=node("div","identity"),names=node("div");names.append(node("h2","app-name",app.title)),app.categories.length&&names.append(node("p","muted",app.categories.join(" \xB7 "))),identity.append(artwork(app,detailMedia,!0),names);let actions=node("div","detail-actions"),announcement=node("div");announcement.setAttribute("role","status"),announcement.setAttribute("aria-atomic","true");let feedback=node("div","feedback");feedback.hidden=!0,announcement.append(feedback);let mode=openMode(entry),label=state.label,explanation=state.detail;if(entry.conflict)label="Conflicting releases",explanation="This publisher signed different releases at the same timestamp. Open is unavailable until the conflict is resolved.";else if(mode==="direct"&&!stale&&!blockedVersions.has(app.signed.event.id))actions.append(openButton(entry,"detail",feedback));else if(mode==="settings"&&!stale){let manual=button("Open via Settings","primary detail-open",()=>{manualInstructions.hidden=!1,manual.setAttribute("aria-expanded","true"),find("publication-reference").open=!0});manual.setAttribute("aria-expanded","false"),manual.setAttribute("aria-controls","manual-open"),actions.append(manual)}else mode==="unknown"?explanation+=" This shell does not offer opening from the directory.":stale&&(explanation="Refresh the directory before opening a saved release.");let manualInstructions=settingsInstructions();manualInstructions.id="manual-open",manualInstructions.hidden=!0;let stateText=node("p",`compat-state ${entry.conflict?"warn":state.tone}`,label);actions.append(announcement,stateText,node("p","compat-detail",explanation),manualInstructions);let preview=node("div","detail-art");screenshot(preview,app.screenshots[0],detailMedia,!1);let about=detailSection("About");about.append(node("p","description",app.description||"The publisher did not provide a description."));let publisher=detailSection("Publisher"),profile=node("details","publisher"),summary=node("summary"),profileLabel=node("span");profileLabel.id="publisher-name",summary.append(glyph("profile"),profileLabel,glyph("chevron"));let profileAbout=node("p","description");profileAbout.id="publisher-about",profile.append(summary,profileAbout,reference("Signing key",app.npub)),publisher.append(profile);let caps=detailSection("Capabilities");capabilities(caps,"Required",app.required),capabilities(caps,"Optional",app.optional),!app.required.length&&!app.optional.length&&caps.append(node("p","muted","The publisher did not declare any capabilities."));let publication=node("details","publication");publication.id="publication-reference";let referenceTitle=node("summary","","View publication");referenceTitle.append(glyph("down")),publication.append(referenceTitle,reference("Publication address",app.address),reference("Signed event ID",app.signed.event.id)),detail.replaceChildren(header,identity,actions,preview,about,publisher,caps,publication),updatePublisher()}var openFailureMessages={"lookup-unavailable":"Hypergolic could not complete its relay lookup. Try again.","download-unavailable":"Hypergolic could not download this release from its published servers. Try again.","hash-mismatch":"The downloaded file did not match the signed release.","invalid-html":"The signed release did not meet Hypergolic\u2019s napplet HTML requirements.","stage-unavailable":"Hypergolic could not prepare this verified release. Try again.","workspace-full":"Your workspace is full. Close a napplet and try again."};function openButton(entry,placement,feedback){let control=button("",`primary ${placement}-open open-action`,event=>{requestOpen(entry,event.currentTarget,feedback)});control.dataset.openVersion=entry.publication.signed.event.id,control.dataset.openTitle=entry.publication.title,control.dataset.openLabel=placement==="row"?"Open":"Open Napplet";let spinner=node("span","open-spinner");return spinner.setAttribute("aria-hidden","true"),control.append(spinner,node("span","open-label")),updateOpenControl(control),control}function updateOpenControl(control){let version=control.dataset.openVersion,title=control.dataset.openTitle,pending=openPending===version;control.toggleAttribute("data-opening",pending),openPending||blockedVersions.has(version)?control.setAttribute("aria-disabled","true"):control.removeAttribute("aria-disabled");let label=control.dataset.openLabel;control.setAttribute("aria-label",pending?`Opening ${title}\u2026`:label==="Open"?`Open ${title}`:label),control.querySelector(".open-label").textContent=pending?"Opening\u2026":label}function updateOpenControls(){document.querySelectorAll("[data-open-version]").forEach(updateOpenControl)}async function requestOpen(entry,control,feedback){let version=entry.publication.signed.event.id;if(openPending||blockedVersions.has(version)||stale||openMode(entry)!=="direct")return;openPending=version;let context=feedback===rowOpenStatus?rowFeedbackContext:void 0,currentFeedback=()=>context===void 0||context===rowFeedbackContext;context!==void 0&&(rowFeedbackVersion=version,placeRowFeedback()),feedback.hidden=!0,feedback.replaceChildren(),focus(control),updateOpenControls(),openingStatus.textContent=`Opening ${entry.publication.title}\u2026`;let{status,reason}=await openPublication(entry.publication).then(result=>((result.status==="refresh-required"||result.status==="conflict")&&blockedVersions.add(version),result)).finally(()=>{openPending=void 0,openingStatus.textContent="",updateOpenControls()});if(!currentFeedback())return;let messages={opened:"Opened in Hypergolic.",busy:"Hypergolic is handling another open request. Try again when it finishes.",cancelled:"Opening cancelled.",rejected:"Opening was declined in Hypergolic.","not-found":"Hypergolic could not find this release on its configured relays. Refresh the directory and try again.",unsupported:"Hypergolic could not open this publication with its current support profile.",conflict:"Hypergolic found conflicting revisions. Refresh the directory or check the saved Napplet for an update.","refresh-required":"The release changed. Refresh the directory and review the new details before opening.",failed:"Hypergolic could not verify or load this release. Try again.",unavailable:"Opening from the directory is unavailable in this session."};if(feedback.hidden=!1,feedback.textContent=reason?openFailureMessages[reason]:messages[status],blockedVersions.has(version)){feedback.append(button("Refresh directory","quiet",async()=>{if(!(loading||(feedback.querySelector("button")?.setAttribute("aria-disabled","true"),await load(!0),!currentFeedback()))){if(readFailed){feedback.textContent="Could not refresh the directory. Try refreshing from the list.";return}if(selected){let updated=pager?.entries.find(item=>item.publication.address===entry.publication.address);updated&&updated.publication.signed.event.id!==version?(selected=updated,detailMedia.close(),detailMedia=new ViewMedia,renderDetail(),window.scrollTo(0,0),focus(find("detail-heading"))):feedback.textContent="The selected release is still unavailable. Look further back in the refreshed directory."}else feedback.textContent="Directory refreshed. Review the listing before trying again.",focus(find("feed-heading"))}}));return}status==="unavailable"&&!reason&&selected===entry&&compatibility(entry.publication).requirementsAvailable&&(feedback.append(settingsInstructions()),find("publication-reference").open=!0)}window.addEventListener("pagehide",event=>{event.persisted||(listMedia.close(),detailMedia.close())});connectTheme();ready().then(available=>{relayAccess=available,loading=!1,available?load(!0):renderList()});})(); diff --git a/src/runtime/discovery-bundle.ts b/src/runtime/discovery-bundle.ts index ee1bc92..ba4ecfe 100644 --- a/src/runtime/discovery-bundle.ts +++ b/src/runtime/discovery-bundle.ts @@ -2,8 +2,8 @@ export const DISCOVERY_BUNDLE = Object.freeze({ "title": "Discover", "appId": "napplet-discovery", - "sha256": "74051ed5602ab68d07d88b32ab8e0cbbf5623e1749a393d03e3730e132e7a8ec", - "aggregateHash": "ac40811acb447f037d84830c5a6da6a39212d8fc38385adeec03aec6aa876ccb", + "sha256": "32f90ddc47d2c1a92ef89399737fd7c5f3fde4087707a075923586e81d2268a7", + "aggregateHash": "862cd3c6e674353dd53fb76801140f4e315776810497faf2079e5095c91bf25e", "domains": Object.freeze([ "relay", "resource",