"
if pic, ok2 := authorPics[ev.PubKey]; ok2 && pic != "" {
headerHTML = headerHTML | "

"
}
headerHTML = headerHTML | "
" | escapeHTML(name) | " "
capturedEv := ev
for _, elemID := range elemIDs {
el := dom.GetElementById(elemID)
if el == 0 {
continue
}
clearChildren(el)
dom.SetStyle(el, "opacity", "1")
dom.SetStyle(el, "cursor", "pointer")
hdr := dom.CreateElement("div")
dom.SetInnerHTML(hdr, headerHTML)
dom.AppendChild(el, hdr)
body := dom.CreateElement("div")
dom.SetStyle(body, "fontSize", "13px")
dom.SetStyle(body, "lineHeight", "1.4")
dom.SetInnerHTML(body, renderEmbedText(text))
dom.AppendChild(el, body)
thisEl := el
dom.AddEventListener(thisEl, "click", dom.RegisterCallback(func() {
showNoteThread(capturedEv.ID, capturedEv.ID)
}))
}
}
// embedCoordEOSECleanup marks unfound coord embeds as not found.
func embedCoordEOSECleanup(subID string) {
coord, ok := embedCoordSubIDs[subID]
if !ok {
return
}
delete(embedCoordSubIDs, subID)
// Check if any other coord sub is still pending for this coord.
for _, other := range embedCoordSubIDs {
if other == coord {
return
}
}
elemIDs, ok := embedCoordCBs[coord]
if !ok {
return
}
delete(embedCoordCBs, coord)
for _, elemID := range elemIDs {
el := dom.GetElementById(elemID)
if el == 0 {
continue
}
dom.SetInnerHTML(el, ""
if pic, ok := authorPics[ev.PubKey]; ok && pic != "" {
headerHTML = headerHTML | "

"
}
headerHTML = headerHTML | "
" | escapeHTML(name) | " "
firstNL := -1
for i := 0; i < len(ev.Content); i++ {
if ev.Content[i] == '\n' {
firstNL = i
break
}
}
truncated := firstNL >= 0 && firstNL < len(ev.Content)-1
text := ev.Content
if truncated {
text = text[:firstNL]
}
embedEvID := ev.ID
embedRootID := getRootID(ev)
if embedRootID == "" {
embedRootID = embedEvID
}
for _, elemID := range elemIDs {
el := dom.GetElementById(elemID)
if el == 0 {
continue
}
clearChildren(el)
dom.SetStyle(el, "opacity", "1")
dom.SetStyle(el, "cursor", "pointer")
hdr := dom.CreateElement("div")
dom.SetInnerHTML(hdr, headerHTML)
dom.AppendChild(el, hdr)
body := dom.CreateElement("div")
dom.SetStyle(body, "fontSize", "13px")
dom.SetStyle(body, "lineHeight", "1.4")
dom.SetInnerHTML(body, renderEmbedText(text))
dom.AppendChild(el, body)
thisEl := el
dom.AddEventListener(thisEl, "click", dom.RegisterCallback(func() {
showNoteThread(embedRootID, embedEvID)
}))
}
if _, ok := authorNames[ev.PubKey]; !ok {
if !lookupFetchedK0(ev.PubKey) {
profile.Send(`["P_RESOLVE",` | jstr(ev.PubKey) | `]`)
}
}
}
// jsonGetNum extracts a numeric value for a given key from a JSON object.
func jsonGetNum(s, key string) (n int64) {
needle := "\"" | key | "\":"
idx := strIndex(s, needle)
if idx < 0 {
return 0
}
idx += len(needle)
// Skip whitespace.
for idx < len(s) && (s[idx] == ' ' || s[idx] == '\t') {
idx++
}
if idx >= len(s) {
return 0
}
for idx < len(s) && s[idx] >= '0' && s[idx] <= '9' {
n = n*10 + int64(s[idx]-'0')
idx++
}
return n
}
// jsonEsc escapes a string for embedding in a JSON value.
// defaultServiceURL derives a service subdomain URL from the current page origin.
// wss://smesh.lol + "invidious" → https://invidious.smesh.lol
func defaultServiceURL(sub string) (s string) {
local := localRelayURL()
var host string
if len(local) >= 6 && local[:6] == "wss://" {
host = local[6:]
} else if len(local) >= 5 && local[:5] == "ws://" {
host = local[5:]
} else {
return ""
}
// Only derive for non-localhost origins.
if host == "localhost" || len(host) > 9 && host[:9] == "127.0.0.1" {
return ""
}
return "https://" | sub | "." | host
}
// defaultBlossomURL derives the blossom server URL from the current page origin.
// wss://smesh.lol → https://smesh.lol/blossom
// --- Search ---
func activateSearchBar(btn dom.Element) {
if !searchBarActive {
searchReturnPage = activePage // remember where to return on manual close
}
searchBarActive = true
dom.SetStyle(btn, "background", "var(--accent)")
dom.SetStyle(btn, "color", "#fff")
dom.SetStyle(btn, "borderRadius", "4px")
dom.SetStyle(feedSelectEl, "display", "none")
dom.SetStyle(pageTitleEl, "display", "none")
dom.SetStyle(notifBarGroup, "display", "none")
dom.SetStyle(topBarRight, "display", "none")
dom.SetStyle(searchInputEl, "display", "block")
dom.SetStyle(searchInputEl, "flex", "1")
dom.SetStyle(searchSubmitBtn, "display", "block")
dom.Focus(searchInputEl)
}
// deactivateSearchBar restores the bar appearance only.
// Callers are responsible for any page navigation.
func deactivateSearchBar(btn dom.Element) {
searchBarActive = false
dom.SetStyle(btn, "background", "transparent")
dom.SetStyle(btn, "color", "var(--muted)")
dom.SetStyle(btn, "borderRadius", "4px")
dom.SetStyle(topBarRight, "display", "flex")
dom.SetStyle(searchInputEl, "display", "none")
dom.SetStyle(searchInputEl, "flex", "")
dom.SetStyle(searchSubmitBtn, "display", "none")
}
// closeSearch is called when the user explicitly closes search (clicks 🔍 again).
// Deactivates the bar and returns to the previous page.
func closeSearch(btn dom.Element) {
deactivateSearchBar(btn)
ret := searchReturnPage
if ret == "" || ret == "search" {
ret = "feed"
}
searchReturnPage = ""
activePage = "" // force switchPage to run
switchPage(ret)
}
// executeSearch submits a search query: #tag searches by t-tag, other text
// runs NIP-50 full-text search. Switches to the search page and subscribes.
func executeSearch(q string) {
q = trimStr(q)
if q == "" {
return
}
searchCurrentQ = q
searchResults = []*nostr.Event{:0}
searchSeen = map[string]bool{}
switchPage("search")
// URL reflects the current query.
dom.ReplaceState("/search?q=" | dom.EncodeURIComponent(q))
// Build filter and choose target relays.
var filter string
var searchRelayList []string
if len(q) > 1 && q[0] == '#' {
// Tag search works on all relays; cap at 5 to limit event burst.
tag := q[1:]
filter = "{\"kinds\":[1,1111],\"#t\":[" | jstr(tag) | "],\"limit\":10}"
searchRelayList = relayURLs
if len(searchRelayList) > 5 {
searchRelayList = searchRelayList[:5]
}
} else {
// Full-text search: only NIP-50 relays, capped at 3 to bound
// the event burst through the relay-proxy WASM.
filter = "{\"kinds\":[1,1111],\"search\":" | jstr(q) | ",\"limit\":10}"
searchRelayList = nip50Relays()
if len(searchRelayList) > 3 {
searchRelayList = searchRelayList[:3]
}
}
// Build filter bar (sticky, above results) then show loading.
clearChildren(searchContainer)
buildSearchFilterBar()
loading := dom.CreateElement("div")
dom.SetStyle(loading, "color", "var(--muted)")
dom.SetStyle(loading, "fontSize", "14px")
dom.SetStyle(loading, "padding", "16px")
dom.SetStyle(loading, "fontFamily", "'Fira Code', monospace")
if len(searchRelayList) == 0 {
dom.SetTextContent(loading, "no full-text search relays available \xe2\x80\x94 try #hashtag")
dom.AppendChild(searchContainer, loading)
return
}
relayWord := "relays"
if len(searchRelayList) == 1 {
relayWord = "relay"
}
dom.SetTextContent(loading, "searching " | itoa(len(searchRelayList)) | " " | relayWord | "...")
dom.AppendChild(searchContainer, loading)
routeMsg("[\"CLOSE\",\"search\"]")
routeMsg(buildProxyMsg("search", filter, searchRelayList))
}
func buildSearchFilterBar() {
// Rebuild the filter bar (recreate buttons so state labels are fresh).
if searchFilterBarEl != 0 {
dom.RemoveChild(searchPage, searchFilterBarEl)
}
bar := dom.CreateElement("div")
searchFilterBarEl = bar
dom.SetAttribute(bar, "class", "search-filter-bar")
dom.SetStyle(bar, "display", "flex")
dom.SetStyle(bar, "gap", "8px")
dom.SetStyle(bar, "padding", "8px 12px")
dom.SetStyle(bar, "borderBottom", "1px solid var(--border)")
dom.SetStyle(bar, "flexWrap", "wrap")
dom.SetStyle(bar, "alignItems", "center")
dom.SetStyle(bar, "position", "sticky")
dom.SetStyle(bar, "top", "0")
dom.SetStyle(bar, "background", "var(--bg2)")
dom.SetStyle(bar, "zIndex", "10")
followsBtn := searchFilterBtn("follows only", searchFollowsOnly)
dom.AddEventListener(followsBtn, "click", dom.RegisterCallback(func() {
searchFollowsOnly = !searchFollowsOnly
applySearchFilterBtn(followsBtn, searchFollowsOnly)
renderSearchResults()
}))
dom.AppendChild(bar, followsBtn)
sortBtn := searchFilterBtn("newest first", searchSortDesc)
dom.SetAttribute(sortBtn, "data-sort", "1")
dom.AddEventListener(sortBtn, "click", dom.RegisterCallback(func() {
searchSortDesc = !searchSortDesc
if searchSortDesc {
dom.SetTextContent(sortBtn, "newest first")
} else {
dom.SetTextContent(sortBtn, "oldest first")
}
applySearchFilterBtn(sortBtn, true)
renderSearchResults()
}))
dom.AppendChild(bar, sortBtn)
nsfwBtn := searchFilterBtn("nsfw", searchNSFW)
dom.AddEventListener(nsfwBtn, "click", dom.RegisterCallback(func() {
searchNSFW = !searchNSFW
applySearchFilterBtn(nsfwBtn, searchNSFW)
renderSearchResults()
}))
dom.AppendChild(bar, nsfwBtn)
// Insert filter bar BEFORE searchContainer in searchPage.
dom.InsertBefore(searchPage, bar, searchContainer)
}
func searchFilterBtn(label string, active bool) (e dom.Element) {
btn := dom.CreateElement("button")
dom.SetTextContent(btn, label)
dom.SetStyle(btn, "padding", "4px 12px")
dom.SetStyle(btn, "borderRadius", "4px")
dom.SetStyle(btn, "fontSize", "12px")
dom.SetStyle(btn, "cursor", "pointer")
dom.SetStyle(btn, "fontFamily", "'Fira Code', monospace")
applySearchFilterBtn(btn, active)
return btn
}
func applySearchFilterBtn(btn dom.Element, active bool) {
if active {
dom.SetStyle(btn, "background", "var(--accent)")
dom.SetStyle(btn, "color", "#fff")
dom.SetStyle(btn, "border", "none")
} else {
dom.SetStyle(btn, "background", "transparent")
dom.SetStyle(btn, "color", "var(--muted)")
dom.SetStyle(btn, "border", "1px solid var(--border)")
}
}
func handleSearchEvent(ev *nostr.Event) {
if ev.Kind != 1 && ev.Kind != 1111 {
return
}
if searchSeen[ev.ID] {
return
}
searchSeen[ev.ID] = true
if isMuted(ev.PubKey) {
return
}
if looksLikeJSONSpam(ev.Content) {
return
}
searchResults = append(searchResults, ev)
setEvent(ev.ID, ev)
}
const searchPageSize = 25 // results rendered per page / scroll load
// filteredSearchResults returns searchResults with current filters applied and sorted.
func filteredSearchResults() (ss []*nostr.Event) {
var filtered []*nostr.Event
for _, ev := range searchResults {
if searchFollowsOnly && !isFollowing(ev.PubKey) {
continue
}
if !searchNSFW {
if ev.Tags.GetFirst([]byte("content-warning")) != nil {
continue
}
}
filtered = append(filtered, ev)
}
for i := 0; i < len(filtered); i++ {
for j := i + 1; j < len(filtered); j++ {
var swap bool
if searchSortDesc {
swap = filtered[j].CreatedAt > filtered[i].CreatedAt
} else {
swap = filtered[j].CreatedAt < filtered[i].CreatedAt
}
if swap {
filtered[i], filtered[j] = filtered[j], filtered[i]
}
}
}
return filtered
}
// renderSearchResults rebuilds the result list from scratch, rendering only
// the first searchPageSize results. Remaining results load on scroll.
func renderSearchResults() {
searchRendered = 0
clearChildren(searchContainer)
filtered := filteredSearchResults()
if len(filtered) == 0 {
empty := dom.CreateElement("div")
dom.SetTextContent(empty, "no results")
dom.SetStyle(empty, "color", "var(--muted)")
dom.SetStyle(empty, "fontSize", "14px")
dom.SetStyle(empty, "padding", "16px")
dom.SetStyle(empty, "fontFamily", "'Fira Code', monospace")
dom.AppendChild(searchContainer, empty)
return
}
end := len(filtered)
if end > searchPageSize {
end = searchPageSize
}
for i := 0; i < end; i++ {
dom.AppendChild(searchContainer, renderSearchResult(filtered[i]))
}
searchRendered = end
}
// appendSearchResults renders the next batch of results (called on scroll).
func appendSearchResults() {
filtered := filteredSearchResults()
if searchRendered >= len(filtered) {
return
}
end := searchRendered + searchPageSize
if end > len(filtered) {
end = len(filtered)
}
for i := searchRendered; i < end; i++ {
dom.AppendChild(searchContainer, renderSearchResult(filtered[i]))
}
searchRendered = end
}
func renderSearchResult(ev *nostr.Event) (e dom.Element) {
row := dom.CreateElement("div")
dom.SetStyle(row, "display", "flex")
dom.SetStyle(row, "flexDirection", "column")
dom.SetStyle(row, "padding", "10px 12px")
dom.SetStyle(row, "borderBottom", "1px solid var(--border)")
dom.SetStyle(row, "cursor", "pointer")
dom.SetStyle(row, "gap", "4px")
// Header: avatar + name + timestamp.
header := dom.CreateElement("div")
dom.SetStyle(header, "display", "flex")
dom.SetStyle(header, "alignItems", "center")
dom.SetStyle(header, "gap", "8px")
avatar := dom.CreateElement("img")
dom.SetAttribute(avatar, "width", "20")
dom.SetAttribute(avatar, "height", "20")
dom.SetAttribute(avatar, "referrerpolicy", "no-referrer")
dom.SetStyle(avatar, "borderRadius", "50%")
dom.SetStyle(avatar, "objectFit", "cover")
dom.SetStyle(avatar, "flexShrink", "0")
pk := ev.PubKey
if pic, ok := authorPics[pk]; ok && pic != "" {
setMediaSrc(avatar, pic)
dom.SetAttribute(avatar, "onerror", "this.style.display='none'")
} else {
dom.SetStyle(avatar, "display", "none")
}
dom.AppendChild(header, avatar)
name := dom.CreateElement("span")
dom.SetStyle(name, "fontSize", "14px")
dom.SetStyle(name, "fontWeight", "bold")
dom.SetStyle(name, "color", "var(--fg)")
dom.SetStyle(name, "flex", "1")
dom.SetStyle(name, "overflow", "hidden")
dom.SetStyle(name, "textOverflow", "ellipsis")
dom.SetStyle(name, "whiteSpace", "nowrap")
if n, ok := authorNames[pk]; ok && n != "" {
dom.SetTextContent(name, n)
} else {
npub := helpers.EncodeNpub(helpers.HexDecode(pk))
if len(npub) > 20 {
dom.SetTextContent(name, npub[:12] | "..." | npub[len(npub)-4:])
}
}
dom.AppendChild(header, name)
ts := dom.CreateElement("span")
dom.SetTextContent(ts, formatTime(ev.CreatedAt))
dom.SetStyle(ts, "fontSize", "11px")
dom.SetStyle(ts, "color", "var(--muted)")
dom.SetStyle(ts, "flexShrink", "0")
dom.AppendChild(header, ts)
dom.AppendChild(row, header)
// Two-line content preview (plain text, no markdown rendering).
preview := dom.CreateElement("div")
dom.SetStyle(preview, "fontSize", "13px")
dom.SetStyle(preview, "color", "var(--fg)")
dom.SetStyle(preview, "lineHeight", "1.4")
dom.SetStyle(preview, "overflow", "hidden")
dom.SetStyle(preview, "display", "-webkit-box")
dom.SetStyle(preview, "-webkit-line-clamp", "2")
dom.SetStyle(preview, "-webkit-box-orient", "vertical")
dom.SetStyle(preview, "wordBreak", "break-word")
content := ev.Content
if len(content) > 300 {
content = content[:300] | "..."
}
dom.SetTextContent(preview, content)
dom.AppendChild(row, preview)
// Click opens thread.
evID := ev.ID
evRootID := getRootID(ev)
if evRootID == "" {
evRootID = evID
}
dom.AddEventListener(row, "click", dom.RegisterCallback(func() {
showNoteThread(evRootID, evID)
}))
if _, cached := authorNames[pk]; !cached {
pendingNotes[pk] = append(pendingNotes[pk], header)
if !lookupFetchedK0(pk) {
profile.Send(`["P_RESOLVE",` | jstr(pk) | `]`)
}
}
return row
}
// extractHashtags replaces #tag tokens with placeholders so markdown-it
// preserves them. Restored after rendering as clickable search spans.
func extractHashtags(s string, slots *[]string) (sv string) {
out := ""
i := 0
for i < len(s) {
if s[i] != '#' {
out = out | string([]byte{s[i]})
i++
continue
}
// Must be preceded by whitespace/start/newline to be a hashtag.
if i > 0 {
prev := s[i-1]
if prev != ' ' && prev != '\n' && prev != '\t' && prev != '\r' {
out = out | "#"
i++
continue
}
}
// Collect tag characters: letters, digits, underscores.
j := i + 1
for j < len(s) {
c := s[j]
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' {
j++
} else {
break
}
}
if j == i+1 {
// No tag characters after #.
out = out | "#"
i++
continue
}
tag := s[i+1 : j]
idx := len(*slots)
span := "