2026-03-12 23:41:07 +00:00
|
|
|
import theme from "tailwindcss/defaultTheme"
|
2026-09-08 17:23:22 +00:00
|
|
|
import {get, writable} from "svelte/store"
|
2025-06-05 20:37:50 +00:00
|
|
|
import * as nip19 from "nostr-tools/nip19"
|
2026-02-16 23:28:25 +00:00
|
|
|
import {page} from "$app/stores"
|
2025-02-06 17:29:30 +00:00
|
|
|
import {identity} from "@welshman/lib"
|
2026-07-28 16:16:26 +00:00
|
|
|
import type {TrustedEvent} from "@welshman/util"
|
2025-06-05 17:55:16 +00:00
|
|
|
import {
|
2026-02-03 19:00:13 +00:00
|
|
|
CLASSIFIED,
|
2025-06-05 17:55:16 +00:00
|
|
|
EVENT_TIME,
|
2026-08-17 15:42:14 +00:00
|
|
|
LONG_FORM,
|
2026-07-28 16:16:26 +00:00
|
|
|
MESSAGE,
|
|
|
|
|
PINBOARD,
|
2026-04-16 18:30:59 +00:00
|
|
|
POLL,
|
2026-07-28 16:16:26 +00:00
|
|
|
THREAD,
|
|
|
|
|
ZAP_GOAL,
|
2026-08-14 15:37:27 +00:00
|
|
|
getIdOrAddress,
|
2026-07-28 16:16:26 +00:00
|
|
|
hexTags,
|
|
|
|
|
tagSpec,
|
|
|
|
|
tagValue,
|
|
|
|
|
tagValues,
|
2025-06-05 17:55:16 +00:00
|
|
|
} from "@welshman/util"
|
2026-07-28 16:16:26 +00:00
|
|
|
import {app, messagingRelayLists, relays, user} from "@app/core"
|
2026-06-08 22:33:38 +00:00
|
|
|
import {makeChatId} from "@app/chats"
|
2026-07-04 00:01:37 +00:00
|
|
|
import {entityLink, PLATFORM_URL, PLATFORM_RELAYS} from "@app/env"
|
2026-09-08 17:23:22 +00:00
|
|
|
import {decodeRelay, encodeRelay} from "@app/relays"
|
2026-06-08 22:33:38 +00:00
|
|
|
import {DM_KINDS} from "@app/content"
|
2026-07-28 16:16:26 +00:00
|
|
|
import {ROOM} from "@app/rooms"
|
2026-09-08 17:23:22 +00:00
|
|
|
import {navigate, pushModal} from "@app/modal"
|
2026-03-23 21:21:04 +00:00
|
|
|
import ChatEnable from "@app/components/ChatEnable.svelte"
|
|
|
|
|
|
2026-06-08 21:14:40 +00:00
|
|
|
// State
|
|
|
|
|
|
|
|
|
|
export let lastChatUrl: string | undefined = undefined
|
|
|
|
|
|
|
|
|
|
export const lastPageBySpaceUrl = new Map<string, string>()
|
|
|
|
|
|
2026-09-08 17:23:22 +00:00
|
|
|
// The space the user was in most recently, so the space menu can be opened from a page that isn't
|
|
|
|
|
// in a space. A store because it's read from markup, unlike lastChatUrl.
|
|
|
|
|
export const lastSpaceUrl = writable<string | undefined>(undefined)
|
|
|
|
|
|
2026-06-08 21:14:40 +00:00
|
|
|
export const setupHistory = () =>
|
|
|
|
|
page.subscribe($page => {
|
|
|
|
|
if ($page.params.relay) {
|
|
|
|
|
lastPageBySpaceUrl.set($page.params.relay, $page.url.pathname)
|
2026-09-08 17:23:22 +00:00
|
|
|
lastSpaceUrl.set(decodeRelay($page.params.relay))
|
2026-06-08 21:14:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($page.params.chat) {
|
|
|
|
|
lastChatUrl = $page.url.pathname
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-06-18 10:52:04 +00:00
|
|
|
// Profiles
|
|
|
|
|
|
|
|
|
|
export const makeProfilePath = (pubkey: string) => `/people/${nip19.npubEncode(pubkey)}`
|
|
|
|
|
|
2026-03-23 21:21:04 +00:00
|
|
|
// Chat
|
|
|
|
|
|
|
|
|
|
export const makeChatPath = (pubkeys: string[]) => `/chat/${makeChatId(pubkeys)}`
|
|
|
|
|
|
|
|
|
|
export const makeRoomPath = (url: string, h: string) => `/spaces/${encodeRelay(url)}/${h}`
|
|
|
|
|
|
|
|
|
|
export const makeSpaceChatPath = (url: string) => makeRoomPath(url, "chat")
|
|
|
|
|
|
2026-08-07 21:05:17 +00:00
|
|
|
export const goToChat = (pubkeys: string[] = [], options: {replaceState?: boolean} = {}) => {
|
2026-07-28 16:16:26 +00:00
|
|
|
if (messagingRelayLists.get().urls(user.get().pubkey).get().length === 0) {
|
2026-08-07 21:05:17 +00:00
|
|
|
pushModal(ChatEnable, {next: () => goToChat(pubkeys, options)})
|
2026-03-23 21:21:04 +00:00
|
|
|
} else if (pubkeys.length === 0) {
|
2026-09-08 17:23:22 +00:00
|
|
|
navigate(lastChatUrl ?? "/chat", options)
|
2026-03-23 21:21:04 +00:00
|
|
|
} else {
|
2026-09-08 17:23:22 +00:00
|
|
|
navigate(makeChatPath(pubkeys), options)
|
2026-03-23 21:21:04 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Spaces
|
2024-08-14 20:38:55 +00:00
|
|
|
|
2025-02-06 17:29:30 +00:00
|
|
|
export const makeSpacePath = (url: string, ...extra: (string | undefined)[]) => {
|
2024-10-21 17:30:12 +00:00
|
|
|
let path = `/spaces/${encodeRelay(url)}`
|
2024-09-11 17:48:34 +00:00
|
|
|
|
2024-10-31 19:24:40 +00:00
|
|
|
if (extra.length > 0) {
|
2025-02-06 17:29:30 +00:00
|
|
|
path +=
|
|
|
|
|
"/" +
|
|
|
|
|
extra
|
|
|
|
|
.filter(identity)
|
|
|
|
|
.map(s => encodeURIComponent(s as string))
|
|
|
|
|
.join("/")
|
2024-09-11 17:48:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return path
|
|
|
|
|
}
|
2024-08-15 19:08:07 +00:00
|
|
|
|
2026-09-02 23:42:29 +00:00
|
|
|
export const makeSpaceEntryPath = (url: string) => {
|
2025-11-05 00:14:32 +00:00
|
|
|
const prevPath = lastPageBySpaceUrl.get(encodeRelay(url))
|
|
|
|
|
|
2026-09-10 15:44:21 +00:00
|
|
|
if (prevPath) {
|
2026-09-02 23:42:29 +00:00
|
|
|
return prevPath
|
2026-06-24 18:09:44 +00:00
|
|
|
}
|
|
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
if (!relays.get().get(url)?.hasNip(29)) {
|
2026-09-02 23:42:29 +00:00
|
|
|
return makeSpaceChatPath(url)
|
2025-11-05 00:14:32 +00:00
|
|
|
}
|
2026-06-24 18:09:44 +00:00
|
|
|
|
2026-09-10 15:44:21 +00:00
|
|
|
return makeSpacePath(url, "about")
|
2025-11-05 00:14:32 +00:00
|
|
|
}
|
|
|
|
|
|
2026-09-02 23:42:29 +00:00
|
|
|
export const goToSpace = (url: string, options: {replaceState?: boolean} = {}) =>
|
2026-09-08 17:23:22 +00:00
|
|
|
navigate(makeSpaceEntryPath(url), options)
|
2026-09-02 23:42:29 +00:00
|
|
|
|
2026-07-15 16:30:17 +00:00
|
|
|
export const goToMovedSpace = (oldUrl: string, newUrl: string) =>
|
2026-09-08 17:23:22 +00:00
|
|
|
navigate(get(page).url.pathname.replace(encodeRelay(oldUrl), encodeRelay(newUrl)))
|
2026-07-15 16:30:17 +00:00
|
|
|
|
2026-07-04 00:01:37 +00:00
|
|
|
export const goToHome = () => {
|
|
|
|
|
if (PLATFORM_RELAYS.length > 0) {
|
2026-09-08 17:23:22 +00:00
|
|
|
return navigate(makeSpaceEntryPath(PLATFORM_RELAYS[0]) + get(page).url.hash, {
|
|
|
|
|
replaceState: true,
|
|
|
|
|
})
|
2026-07-04 00:01:37 +00:00
|
|
|
}
|
2026-08-11 21:54:05 +00:00
|
|
|
|
2026-09-08 17:23:22 +00:00
|
|
|
return navigate("/home" + get(page).url.hash)
|
2026-07-04 00:01:37 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-23 21:21:04 +00:00
|
|
|
// Content types, events
|
2025-06-25 00:36:14 +00:00
|
|
|
|
2026-02-16 22:45:54 +00:00
|
|
|
export const makeMessagePath = (url: string, event: TrustedEvent) => {
|
2026-07-28 16:16:26 +00:00
|
|
|
const h = tagValue(tagSpec(ROOM), event.tags)
|
2026-02-16 22:45:54 +00:00
|
|
|
const path = h ? makeRoomPath(url, h) : makeSpaceChatPath(url)
|
|
|
|
|
const qp = new URLSearchParams({at: String(event.created_at)})
|
|
|
|
|
|
|
|
|
|
return path + "?" + qp.toString()
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 00:19:14 +00:00
|
|
|
export const makeGoalPath = (url: string, id?: string) => makeSpacePath(url, "goals", id)
|
2025-07-07 22:28:36 +00:00
|
|
|
|
2026-02-04 00:19:14 +00:00
|
|
|
export const makeThreadPath = (url: string, id?: string) => makeSpacePath(url, "threads", id)
|
2024-12-16 18:30:42 +00:00
|
|
|
|
2026-02-04 01:09:30 +00:00
|
|
|
export const makeClassifiedPath = (url: string, address?: string) =>
|
|
|
|
|
makeSpacePath(url, "classifieds", address)
|
2026-02-03 19:00:13 +00:00
|
|
|
|
2026-08-17 15:42:14 +00:00
|
|
|
export const makeArticlePath = (url: string, address?: string) =>
|
|
|
|
|
makeSpacePath(url, "articles", address)
|
|
|
|
|
|
2026-08-18 21:18:10 +00:00
|
|
|
// shareToChat is separate from h because the space-level chat has no room to scope an article to
|
|
|
|
|
export const makeArticleCreatePath = (
|
|
|
|
|
url: string,
|
|
|
|
|
{h, shareToChat}: {h?: string; shareToChat?: boolean} = {},
|
|
|
|
|
) => {
|
|
|
|
|
const params = new URLSearchParams()
|
|
|
|
|
|
|
|
|
|
if (h) {
|
|
|
|
|
params.set("h", h)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (shareToChat) {
|
|
|
|
|
params.set("shareToChat", "1")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const query = params.toString()
|
|
|
|
|
|
|
|
|
|
return makeSpacePath(url, "articles", "create") + (query ? "?" + query : "")
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 01:09:30 +00:00
|
|
|
export const makeCalendarPath = (url: string, address?: string) =>
|
|
|
|
|
makeSpacePath(url, "calendar", address)
|
2024-10-23 21:20:24 +00:00
|
|
|
|
2026-04-03 17:54:50 +00:00
|
|
|
export const makePollPath = (url: string, id?: string) => makeSpacePath(url, "polls", id)
|
|
|
|
|
|
2026-08-14 23:06:17 +00:00
|
|
|
// Shelves are selected in place on the library page rather than having their own
|
|
|
|
|
// route, so the address goes in a query param.
|
|
|
|
|
export const makeLibraryPath = (url: string, address?: string) => {
|
|
|
|
|
const path = makeSpacePath(url, "library")
|
|
|
|
|
|
|
|
|
|
if (address) {
|
|
|
|
|
return path + "?" + new URLSearchParams({board: address}).toString()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return path
|
|
|
|
|
}
|
2026-07-08 17:29:56 +00:00
|
|
|
|
2026-08-14 15:47:36 +00:00
|
|
|
export const makeContentPath = (url: string, kind: number, idOrAddress?: string) => {
|
2026-08-14 15:37:27 +00:00
|
|
|
switch (kind) {
|
|
|
|
|
case ZAP_GOAL:
|
|
|
|
|
return makeGoalPath(url, idOrAddress)
|
|
|
|
|
case THREAD:
|
|
|
|
|
return makeThreadPath(url, idOrAddress)
|
|
|
|
|
case CLASSIFIED:
|
|
|
|
|
return makeClassifiedPath(url, idOrAddress)
|
2026-08-17 15:42:14 +00:00
|
|
|
case LONG_FORM:
|
|
|
|
|
return makeArticlePath(url, idOrAddress)
|
2026-08-14 15:37:27 +00:00
|
|
|
case EVENT_TIME:
|
|
|
|
|
return makeCalendarPath(url, idOrAddress)
|
|
|
|
|
case PINBOARD:
|
|
|
|
|
return makeLibraryPath(url, idOrAddress)
|
|
|
|
|
case POLL:
|
|
|
|
|
return makePollPath(url, idOrAddress)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-14 15:47:36 +00:00
|
|
|
export const makeEventPath = (event: TrustedEvent, urls: string[]) => {
|
2026-02-13 23:37:17 +00:00
|
|
|
if (DM_KINDS.includes(event.kind)) {
|
2026-07-28 16:16:26 +00:00
|
|
|
return makeChatPath([event.pubkey, ...tagValues(hexTags("p"), event.tags)])
|
2025-09-09 16:54:08 +00:00
|
|
|
}
|
|
|
|
|
|
2025-06-05 20:37:50 +00:00
|
|
|
if (urls.length > 0) {
|
|
|
|
|
const url = urls[0]
|
|
|
|
|
|
2025-06-05 17:55:16 +00:00
|
|
|
if (event.kind === MESSAGE) {
|
2026-02-16 22:45:54 +00:00
|
|
|
return makeMessagePath(url, event)
|
2025-06-05 17:55:16 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-14 15:47:36 +00:00
|
|
|
const path = makeContentPath(url, event.kind, getIdOrAddress(event))
|
2025-06-05 17:55:16 +00:00
|
|
|
|
2026-08-14 15:47:36 +00:00
|
|
|
if (path) {
|
|
|
|
|
return path
|
|
|
|
|
}
|
2025-07-07 22:28:36 +00:00
|
|
|
|
2026-08-14 15:47:36 +00:00
|
|
|
const parentKind = tagValue(tagSpec("K"), event.tags)
|
|
|
|
|
const parentIdOrAddress =
|
|
|
|
|
tagValue(tagSpec("A"), event.tags) ?? tagValue(tagSpec("E"), event.tags)
|
2025-06-05 17:55:16 +00:00
|
|
|
|
2026-08-14 15:47:36 +00:00
|
|
|
if (parentKind && parentIdOrAddress) {
|
|
|
|
|
if (parseInt(parentKind) === MESSAGE) {
|
2026-02-16 22:45:54 +00:00
|
|
|
return makeMessagePath(url, event)
|
2026-02-04 00:19:14 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-14 15:47:36 +00:00
|
|
|
const parentPath = makeContentPath(url, parseInt(parentKind), parentIdOrAddress)
|
2026-02-03 19:00:13 +00:00
|
|
|
|
2026-08-14 15:47:36 +00:00
|
|
|
if (parentPath) {
|
|
|
|
|
return parentPath
|
2025-06-05 17:55:16 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-05 20:37:50 +00:00
|
|
|
|
2025-06-19 17:22:17 +00:00
|
|
|
return entityLink(nip19.neventEncode({id: event.id, relays: urls}))
|
2025-06-05 17:55:16 +00:00
|
|
|
}
|
2025-10-06 18:23:19 +00:00
|
|
|
|
2026-06-17 16:08:58 +00:00
|
|
|
export const makeEventPermalink = (event: TrustedEvent, url?: string) => {
|
2026-07-28 16:16:26 +00:00
|
|
|
const urls = url ? [url] : Array.from(app.get().tracker.getRelays(event.id))
|
2026-08-14 15:47:36 +00:00
|
|
|
const path = makeEventPath(event, urls)
|
2026-06-17 16:08:58 +00:00
|
|
|
|
|
|
|
|
if (path.includes("://")) {
|
|
|
|
|
return path
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `${PLATFORM_URL}${path}#${nip19.neventEncode({id: event.id, relays: urls})}`
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-14 15:47:36 +00:00
|
|
|
export const scrollToEvent = (id: string) => {
|
2026-08-31 15:47:55 +00:00
|
|
|
const element = document.querySelector(`[data-event="${id}"]`)
|
2026-08-14 15:47:36 +00:00
|
|
|
|
2026-08-31 15:47:55 +00:00
|
|
|
if (element instanceof HTMLElement) {
|
2026-08-14 15:47:36 +00:00
|
|
|
element.scrollIntoView({behavior: "smooth", block: "center"})
|
2026-08-31 15:47:55 +00:00
|
|
|
element.classList.add("highlight-target")
|
2026-08-14 15:47:36 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-31 15:47:55 +00:00
|
|
|
return element instanceof HTMLElement
|
2026-08-14 15:47:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const goToEvent = (event: TrustedEvent, options: Record<string, any> = {}) => {
|
|
|
|
|
const urls = Array.from(app.get().tracker.getRelays(event.id))
|
|
|
|
|
const path = makeEventPath(event, urls)
|
|
|
|
|
|
|
|
|
|
if (path.includes("://")) {
|
|
|
|
|
window.open(path)
|
|
|
|
|
} else if (!scrollToEvent(event.id)) {
|
|
|
|
|
const replaceState = path.replace(/\?.*$/, "") === get(page).url.pathname
|
|
|
|
|
|
2026-09-08 17:23:22 +00:00
|
|
|
navigate(path, {replaceState, ...options})
|
2026-08-14 15:47:36 +00:00
|
|
|
}
|
|
|
|
|
}
|