Name the group-space, platform-flag and active-space rules once, and keep makeEventPath to in-app paths

This commit is contained in:
Jon Staab 2026-09-26 00:00:18 -07:00
parent 0e01055855
commit 602ac09293
18 changed files with 134 additions and 135 deletions

View file

@ -185,7 +185,7 @@ One web build runs in several shells:
Platform checks call Capacitor directly. There is no wrapper module:
```ts
export const ENABLE_ZAPS = Capacitor.getPlatform() != "ios" // src/app/env.ts
export const ENABLE_ZAPS = Capacitor.getPlatform() !== "ios" // src/app/env.ts
export const HOSTING_ENABLED = Capacitor.getPlatform() !== "ios" // src/app/hosting.ts
if (!Capacitor.isPluginAvailable("Keyboard")) return noop // src/app/keyboard.ts

View file

@ -31,7 +31,8 @@ carries `["h", h]`; content with no `h` belongs to the whole space. `/spaces/[re
`deriveUserRooms(url)` in `src/app/rooms.ts` read it. "Joined" in the UI means "in this list",
which is separate from NIP-43 membership on the relay: `src/routes/spaces/[relay]/+layout.svelte`
prompts `SpaceJoin` for any URL not in `userSpaceUrls`. A deployment with `VITE_PLATFORM_RELAYS`
uses `PLATFORM_RELAYS` in place of the list for sync and navigation.
uses `PLATFORM_RELAYS` in place of the list for sync and navigation: `activeSpaceUrls` in
`src/app/rooms.ts` is that choice, read by background sync, notifications and editor suggestions.
### NIP-11 relay info
@ -40,7 +41,7 @@ domain `Relay`. These fields drive protocol decisions:
| Read | Decides |
|---|---|
| `hasNip(29)` | whether the space has rooms. Without it everything lives in the space chat: `makeSpaceEntryPath` (`src/app/routes.ts`), `shareEvent` (`src/app/share.ts`), room search, notification grouping, `SpaceMenuRooms` |
| `hasNip(29)` | whether the space has rooms, read through `isGroupSpace(url)` / `deriveIsGroupSpace(url)` in `src/app/relays.ts`. Without it everything lives in the space chat (`makeSpaceChatPath`): `shareEvent` (`src/app/share.ts`), room search, notification grouping, `SpaceMenuNavItems`, `SpaceMenuRooms` |
| `hasNip(70)` | whether space content is marked protected (below) |
| `self` | the relay's own pubkey, the trust anchor for relay-signed state |
| `pubkey` | the space's owner, who writes space-wide content that has no other author (`deriveUserIsSpaceOwner`) |
@ -104,9 +105,9 @@ user holds a method for — reports under `banevent`, join requests under `allow
- `deriveUserRoomMembershipStatus`: an admin is always `Granted`.
- `addRoomMembers`: allows each non-member at the relay (NIP-86 `allowPubkey`) before publishing
9000, because a room member the relay won't serve can't read the room.
- `deriveUserRooms`, `deriveOtherRooms`, `deriveOtherVoiceRooms`: rooms from the user's 10009
list and the rest of the space, limited to rooms the relay still advertises. `meta.hasLivekit()`
marks a voice room.
- `deriveUserRooms`, `deriveOtherRooms(url, type)`: rooms from the user's 10009 list and the rest
of the space by `RoomType`, limited to rooms the relay still advertises. `meta.hasLivekit()`
marks a voice room (`getRoomType`).
### Room flows

View file

@ -99,7 +99,9 @@ mobile. Pages outside a space use `Page`, `PageBar` and `PageContent` themselves
`makeCalendarPath`, `makeGoalPath`, `makePollPath`, `makeLibraryPath`, `makeArticleCreatePath`).
- `makeContentPath(url, kind, idOrAddress)` maps a kind to its page. `makeEventPath` builds on it
for any event, covering DMs, room messages (`?at=`) and comments (through the parent's `K`,
`A` and `E` tags), and falls back to `entityLink` from `src/app/env.ts`, an external link.
`A` and `E` tags), and returns `undefined` for an event with no page in the app. A caller that
wants an external link then calls `entityLink` from `src/app/env.ts` itself, as
`makeEventPermalink` does.
- `goToEvent(event)` scrolls to the event if it is already rendered with a `data-event` attribute
and navigates otherwise; `makeEventPermalink` is the shareable form.
- `goToSpace(url)` goes to `makeSpaceEntryPath(url)`: the last page visited in that space, which

View file

@ -1,5 +1,4 @@
<script lang="ts">
import {Capacitor} from "@capacitor/core"
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
import ServerPath from "@assets/icons/server-path.svg?dataurl"
import GalleryMinimalistic from "@assets/icons/gallery-minimalistic.svg?dataurl"
@ -14,6 +13,8 @@
import Profile from "@app/components/Profile.svelte"
import LogOut from "@app/components/LogOut.svelte"
import {user} from "@app/core"
import {ENABLE_ZAPS} from "@app/env"
import {HOSTING_ENABLED} from "@app/hosting"
import {pushModal} from "@app/modal"
import {makeProfilePath} from "@app/routes"
@ -39,7 +40,7 @@
<Icon icon={Bell} size={5} />
Alerts
</Link>
{#if Capacitor.getPlatform() !== "ios"}
{#if ENABLE_ZAPS}
<Link
href="/settings/wallet"
class="aspect-square button button-neutral h-[unset] flex flex-col gap-2 text-center">
@ -47,12 +48,14 @@
Wallet
</Link>
{/if}
<Link
href="/settings/hosting"
class="aspect-square button button-neutral h-[unset] flex flex-col gap-2 text-center">
<Icon icon={ServerPath} size={5} />
Hosting
</Link>
{#if HOSTING_ENABLED}
<Link
href="/settings/hosting"
class="aspect-square button button-neutral h-[unset] flex flex-col gap-2 text-center">
<Icon icon={ServerPath} size={5} />
Hosting
</Link>
{/if}
<Link
href="/settings/content"
class="aspect-square button button-neutral h-[unset] flex flex-col gap-2 text-center">

View file

@ -3,6 +3,7 @@
import {remove} from "@welshman/lib"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import {preventDefault} from "@lib/html"
import Button from "@lib/components/Button.svelte"
@ -21,7 +22,7 @@
import {chatSearch} from "@app/chats"
import {user} from "@app/core"
import {deriveUserRoomSearch} from "@app/rooms"
import {makeChatPath, makeRoomPath} from "@app/routes"
import {makeChatPath} from "@app/routes"
import {shareTo, type Share} from "@app/share"
type Props = {
@ -65,8 +66,7 @@
{#if roomResults.length > 0}
<p class="text-xs uppercase tracking-wide opacity-60">Rooms</p>
<div class="flex flex-col gap-2">
{#each roomResults as { url, h } (makeRoomPath(url, h))}
{@const path = makeRoomPath(url, h)}
{#each roomResults as { url, h, path } (path)}
<Button
type="button"
class={cx(
@ -74,7 +74,14 @@
selection === path ? "button-primary" : "button-neutral",
)}
onclick={() => toggle(path)}>
<RoomNameWithImage {url} {h} />
{#if h}
<RoomNameWithImage {url} {h} />
{:else}
<div class="inline-flex min-w-0 items-center gap-2">
<Icon icon={Hashtag} size={5} />
<span class="truncate min-w-0">chat</span>
</div>
{/if}
<RelayName {url} class="min-w-0 truncate text-xs opacity-60" />
</Button>
{/each}

View file

@ -17,10 +17,11 @@
import Icon from "@lib/components/Icon.svelte"
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
import SpaceSearch from "@app/components/SpaceSearch.svelte"
import {events, relays} from "@app/core"
import {events} from "@app/core"
import {ENABLE_ZAPS} from "@app/env"
import {CONTENT_KINDS} from "@app/content"
import {makeSpacePath} from "@app/routes"
import {deriveIsGroupSpace} from "@app/relays"
import {makeSpaceChatPath, makeSpacePath} from "@app/routes"
import {allNotifications, notifications} from "@app/notifications"
import {pushModal} from "@app/modal"
@ -30,8 +31,8 @@
const {url}: Props = $props()
const relay = $relays.one(url)
const chatPath = makeSpacePath(url, "chat")
const isGroupSpace = deriveIsGroupSpace(url)
const chatPath = makeSpaceChatPath(url)
const goalsPath = makeSpacePath(url, "goals")
const threadsPath = makeSpacePath(url, "threads")
const classifiedsPath = makeSpacePath(url, "classifieds")
@ -53,8 +54,6 @@
const showSection = (kind: number, path: string) =>
$spaceKinds.has(kind) || $allNotifications.has(path) || $page.url.pathname.startsWith(path)
const hasNip29 = $derived($relay?.hasNip(29) ?? false)
const openSearch = () => pushModal(SpaceSearch, {url})
$effect(() => setJson(spaceKindsKey, [...$spaceKinds]))
@ -63,7 +62,7 @@
<SecondaryNavItem href={makeSpacePath(url, "about")}>
<Icon icon={Home} /> Space Details
</SecondaryNavItem>
{#if !hasNip29}
{#if !$isGroupSpace}
<SecondaryNavItem href={chatPath} notification={$notifications.has(chatPath)}>
<Icon icon={ChatRound} /> Chat
</SecondaryNavItem>
@ -106,7 +105,7 @@
<Icon icon={Revote} /> Polls
</SecondaryNavItem>
{/if}
{#if hasNip29}
{#if $isGroupSpace}
<SecondaryNavItem onclick={openSearch}>
<Icon icon={Magnifier} /> Search
</SecondaryNavItem>

View file

@ -8,9 +8,9 @@
import SecondaryNavHeader from "@lib/components/SecondaryNavHeader.svelte"
import SpaceMenuRoomItem from "@app/components/SpaceMenuRoomItem.svelte"
import RoomCreate from "@app/components/RoomCreate.svelte"
import {relays} from "@app/core"
import {deriveUserCanCreateRoom} from "@app/management"
import {deriveUserRooms, deriveOtherRooms, deriveOtherVoiceRooms, displayRoom} from "@app/rooms"
import {deriveIsGroupSpace} from "@app/relays"
import {RoomType, deriveUserRooms, deriveOtherRooms, displayRoom} from "@app/rooms"
import {pushModal} from "@app/modal"
type Props = {
@ -20,10 +20,10 @@
const {url, mobile = false}: Props = $props()
const relay = $relays.one(url)
const isGroupSpace = deriveIsGroupSpace(url)
const userRooms = deriveUserRooms(url)
const otherRooms = deriveOtherRooms(url)
const otherVoiceRooms = deriveOtherVoiceRooms(url)
const otherRooms = deriveOtherRooms(url, RoomType.Text)
const otherVoiceRooms = deriveOtherRooms(url, RoomType.Voice)
const canCreateRoom = deriveUserCanCreateRoom(url)
const roomSearch = derived(otherRooms, $otherRooms =>
@ -95,7 +95,7 @@
{/if}
{/snippet}
{#if ($relay?.hasNip(29) ?? false) && ($userRooms.length > 0 || $otherRooms.length > 0 || $otherVoiceRooms.length > 0 || $canCreateRoom)}
{#if $isGroupSpace && ($userRooms.length > 0 || $otherRooms.length > 0 || $otherVoiceRooms.length > 0 || $canCreateRoom)}
{#if mobile}
<div class="card space-menu__card flex flex-col gap-1">
{@render content()}

View file

@ -32,8 +32,7 @@ import {RoomReferenceExtension} from "@app/editor/RoomReferenceExtension"
import RoomSuggestion from "@app/editor/RoomSuggestion.svelte"
import {NativeClipboardPasteExtension} from "@app/editor/clipboard"
import {compressFileForUpload, makeImetaTag, uploadFile} from "@app/uploads"
import {userSpaceUrls} from "@app/rooms"
import {PLATFORM_RELAYS} from "@app/env"
import {activeSpaceUrls} from "@app/rooms"
import {pushToast} from "@app/toast"
const isEmpty = (editor: Editor) => editor.getText({blockSeparator: "\n"}).trim() === ""
@ -79,13 +78,9 @@ export const makeEditor = async ({
)
const roomReferenceSearch = derived(
[throttled(800, userSpaceUrls), throttled(800, rooms.get().byUrl.$)],
([$userSpaceUrls, $roomsByUrl]) => {
// Platform relays, where configured, are the only spaces suggestions come from.
const spaceUrls =
PLATFORM_RELAYS.length > 0
? PLATFORM_RELAYS
: uniq(url ? [url, ...$userSpaceUrls] : $userSpaceUrls)
[throttled(800, activeSpaceUrls), throttled(800, rooms.get().byUrl.$)],
([$activeSpaceUrls, $roomsByUrl]) => {
const spaceUrls = uniq(url ? [url, ...$activeSpaceUrls] : $activeSpaceUrls)
const options = spaceUrls.flatMap(spaceUrl =>
($roomsByUrl.get(spaceUrl) ?? []).map((room: Room) => ({

View file

@ -14,7 +14,7 @@ export const PUSH_SERVER = env("VITE_PUSH_SERVER")
export const PUSH_BRIDGE = normalizeRelayUrl(env("VITE_PUSH_BRIDGE"))
export const ENABLE_ZAPS = Capacitor.getPlatform() != "ios"
export const ENABLE_ZAPS = Capacitor.getPlatform() !== "ios"
export const SIGNER_RELAYS = fromCsv(env("VITE_SIGNER_RELAYS")).map(normalizeRelayUrl)

View file

@ -15,13 +15,15 @@ import {
} from "@welshman/util"
import {Comment} from "@welshman/domain"
import {synced, throttled, withGetter} from "@welshman/store"
import {Events, Relays, RoomLists} from "@welshman/app"
import {Events, Relays} from "@welshman/app"
import {app, fromApp, reader} from "@app/core"
import {makeRoomPath, makeSpaceChatPath, makeChatPath, makeContentPath} from "@app/routes"
import {CONTENT_KINDS, makeCommentFilter} from "@app/content"
import {getIsMuted, notificationSettings, userSettingsValues} from "@app/settings"
import {Chats} from "@app/chats"
import {dufflepud, DUFFLEPUD_URL, PLATFORM_RELAYS} from "@app/env"
import {dufflepud, DUFFLEPUD_URL} from "@app/env"
import {isGroupSpace} from "@app/relays"
import {activeSpaceUrls} from "@app/rooms"
import {kv} from "@app/storage"
// Checked state
@ -231,10 +233,10 @@ export const latestActivityByPath = derived(
1000,
derived(
[
app,
activeSpaceUrls,
fromApp($app => $app.use(Chats).index.$),
// Only here to recompute once a relay's NIP-11 says whether it's a group space
fromApp($app => $app.use(Relays).index.$),
fromApp($app => $app.use(RoomLists).index.$),
fromApp(
$app =>
$app
@ -247,7 +249,7 @@ export const latestActivityByPath = derived(
identity,
),
),
([$app, $chatsById, $relays, $roomLists, eventsByIdByUrl, $settings]) => {
([$activeSpaceUrls, $chatsById, , eventsByIdByUrl, $settings]) => {
const activity = new Map<string, Activity>()
for (const {pubkeys, messages} of $chatsById.values()) {
@ -256,13 +258,10 @@ export const latestActivityByPath = derived(
activity.set(path, {path, pubkeys, event: messages[0]})
}
const roomList = $app.user?.pubkey ? $roomLists.get($app.user.pubkey) : undefined
const urls = PLATFORM_RELAYS.length > 0 ? PLATFORM_RELAYS : (roomList?.urls() ?? [])
for (const url of urls) {
for (const url of $activeSpaceUrls) {
const events = sortEventsDesc((eventsByIdByUrl.get(url) || new Map()).values())
if ($relays.get(url)?.hasNip(29)) {
if (isGroupSpace(url)) {
for (const [h, [event]] of groupBy(e => tagValue(tagSpec("h"), e.tags), events)) {
// A muted room is left out entirely, so it can't light up its own badge or the space's
if (h && !getIsMuted($settings, url, h)) {

View file

@ -137,8 +137,7 @@ export const onPushNotificationAction = async (action: {
const target = event?.kind === WRAP ? await loadNotificationRumor(event) : event
const path = target && makeEventPath(target, [relay])
// Kinds we have no route for get a coracle.social permalink, which navigate refuses
if (path && !path.includes("://")) {
if (path) {
navigate(path)
} else if (event?.kind === WRAP) {
goToChat()

View file

@ -3,7 +3,7 @@ import {call, on, simpleCache} from "@welshman/lib"
import {normalizeRelayUrl} from "@welshman/util"
import {AuthStateEvent, AuthStatus, SocketEvent, SocketStatus} from "@welshman/net"
import {throttled} from "@welshman/store"
import {app} from "@app/core"
import {app, relays} from "@app/core"
import {relaysMostlyRestricted} from "@app/policies"
import {colorFor} from "@app/theme"
@ -95,6 +95,12 @@ export const deriveSocketStatus = (url: string) =>
}),
)
// A space that isn't a NIP-29 relay has no rooms, only one space-wide chat.
export const isGroupSpace = (url: string) => Boolean(relays.get().get(url)?.hasNip(29))
export const deriveIsGroupSpace = (url: string) =>
derived(relays.get().one(url), $relay => Boolean($relay?.hasNip(29)))
const livekitUrl = (url: string) =>
url.replace(/^ws/, "http").replace(/\/$/, "") + "/.well-known/nip29/livekit"

View file

@ -30,8 +30,10 @@ import {
user,
writer,
} from "@app/core"
import {PLATFORM_RELAYS} from "@app/env"
import {allowPubkeys, deriveSpacePermissions, deriveUserIsSpaceStaff} from "@app/management"
import {makeRoomPath} from "@app/routes"
import {isGroupSpace} from "@app/relays"
import {makeRoomPath, makeSpaceChatPath} from "@app/routes"
import {loadSendDelay} from "@app/settings"
export const PROTECTED = ["-"]
@ -180,6 +182,11 @@ export const userRoomList = deriveUserItem(RoomLists)
export const userSpaceUrls = derived(userRoomList, $userRoomList => $userRoomList?.urls() ?? [])
// Platform relays, where configured, stand in for the spaces the user has joined.
export const activeSpaceUrls = derived(userSpaceUrls, $userSpaceUrls =>
PLATFORM_RELAYS.length > 0 ? PLATFORM_RELAYS : $userSpaceUrls,
)
// Spaces get reordered from lists showing only some of them, so urls go back in the slots they replace.
export const reorderSpaceUrls = (urls: string[]) => {
let index = 0
@ -200,19 +207,10 @@ export const deriveUserRooms = (url: string) =>
},
)
export const deriveOtherRooms = (url: string) =>
export const deriveOtherRooms = (url: string, type: RoomType) =>
derived([deriveUserRooms(url), rooms.get().forUrl(url).$], ([$userRooms, $spaceRooms]) => {
const result = $spaceRooms
.filter(room => !room.meta?.hasLivekit() && !$userRooms.includes(room.h))
.map(room => room.h)
return sortBy(roomComparator(url), uniq(result))
})
export const deriveOtherVoiceRooms = (url: string) =>
derived([deriveUserRooms(url), rooms.get().forUrl(url).$], ([$userRooms, $spaceRooms]) => {
const result = $spaceRooms
.filter(room => room.meta?.hasLivekit() && !$userRooms.includes(room.h))
.filter(room => getRoomType(room) === type && !$userRooms.includes(room.h))
.map(room => room.h)
return sortBy(roomComparator(url), uniq(result))
@ -310,23 +308,26 @@ export const deriveUserRoomSearch = () =>
derived(
[userSpaceUrls, userRoomList, rooms.get().byUrl.$],
([$userSpaceUrls, $userRoomList, $roomsByUrl]) => {
const options = $userSpaceUrls.flatMap(url => {
const favorites = new Set($userRoomList?.roomsForUrl(url) ?? [])
// Spaces that aren't NIP-29 relays keep all their messages in the space-wide chat
const roomIds = relays.get().get(url)?.hasNip(29)
? ($roomsByUrl.get(url) ?? []).map(room => room.h)
: ["chat"]
type Option = {url: string; h?: string; path: string; name: string; isFavorite: boolean}
return roomIds.map(h => ({
url,
h,
name: displayRoom(url, h),
isFavorite: favorites.has(h),
}))
const options = $userSpaceUrls.flatMap((url): Option[] => {
if (isGroupSpace(url)) {
const favorites = new Set($userRoomList?.roomsForUrl(url) ?? [])
return ($roomsByUrl.get(url) ?? []).map(({h}) => ({
url,
h,
path: makeRoomPath(url, h),
name: displayRoom(url, h),
isFavorite: favorites.has(h),
}))
} else {
return [{url, path: makeSpaceChatPath(url), name: "chat", isFavorite: false}]
}
})
return createSearch(options, {
getValue: option => makeRoomPath(option.url, option.h),
getValue: option => option.path,
fuseOptions: {keys: ["name", "url"]},
sortFn: ({item}) => (item.isFavorite ? 0 : 1),
})

View file

@ -2,6 +2,7 @@ import {get, writable} from "svelte/store"
import * as nip19 from "nostr-tools/nip19"
import {page} from "$app/stores"
import {identity, tryCatch} from "@welshman/lib"
import type {Maybe} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {
CLASSIFIED,
@ -203,7 +204,7 @@ export const makeContentPath = (url: string, kind: number, idOrAddress?: string)
}
}
export const makeEventPath = (event: TrustedEvent, urls: string[]) => {
export const makeEventPath = (event: TrustedEvent, urls: string[]): Maybe<string> => {
if (DM_KINDS.includes(event.kind)) {
return makeChatPath([event.pubkey, ...tagValues(hexTags("p"), event.tags)])
}
@ -245,21 +246,18 @@ export const makeEventPath = (event: TrustedEvent, urls: string[]) => {
return `/notes/${nip19.neventEncode({id, author, kind, relays: urls.slice(0, 3)})}`
}
return entityLink(nip19.neventEncode({id: event.id, relays: urls}))
}
export const makeEventPermalink = (event: TrustedEvent, url?: string) => {
const urls = url ? [url] : Array.from(app.get().tracker.getRelays(event.id))
const path = makeEventPath(event, urls)
if (path.includes("://")) {
return path
}
const pointer = nip19.neventEncode({id: event.id, relays: urls})
return `${PLATFORM_URL}${path}${path.includes("?") ? "&" : "?"}event=${pointer}`
if (path) {
return `${PLATFORM_URL}${path}${path.includes("?") ? "&" : "?"}event=${pointer}`
} else {
return entityLink(pointer)
}
}
export const getPermalinkTarget = (url: URL) => {
@ -299,11 +297,13 @@ export const goToEvent = (event: TrustedEvent, options: Record<string, any> = {}
const path = makeEventPath(event, urls)
if (path.includes("://")) {
window.open(path)
} else if (!scrollToEvent(event.id)) {
const replaceState = path.replace(/\?.*$/, "") === get(page).url.pathname
if (path) {
if (!scrollToEvent(event.id)) {
const replaceState = path.replace(/\?.*$/, "") === get(page).url.pathname
navigate(path, {replaceState, ...options})
navigate(path, {replaceState, ...options})
}
} else {
window.open(entityLink(nip19.neventEncode({id: event.id, relays: urls})))
}
}

View file

@ -4,8 +4,9 @@ import type {PluginListenerHandle} from "@capacitor/core"
import {noop} from "@welshman/lib"
import type {Maybe} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {app, relays} from "@app/core"
import {app} from "@app/core"
import {navigate, pushModal} from "@app/modal"
import {isGroupSpace} from "@app/relays"
import {makeSpaceChatPath} from "@app/routes"
import {pushToast} from "@app/toast"
import ShareDialog from "@app/components/Share.svelte"
@ -30,7 +31,7 @@ export const shareTo = async (
}
export const shareEvent = (url: string, noun: string, event: TrustedEvent) => {
if (relays.get().get(url)?.hasNip(29)) {
if (isGroupSpace(url)) {
pushModal(ShareEvent, {url, noun, event})
} else {
shareTo(makeSpaceChatPath(url), {type: "event", value: event})

View file

@ -30,16 +30,9 @@ import {
unionFilters,
} from "@welshman/util"
import type {Filter} from "@welshman/util"
import type {FollowListReader, RoomListReader} from "@welshman/domain"
import type {FollowListReader} from "@welshman/domain"
import {merged, synced, throttled, withGetter} from "@welshman/store"
import {
FollowLists,
MessagingRelayLists,
RelayLists,
RoomLists,
SearchRelayLists,
Sync,
} from "@welshman/app"
import {FollowLists, MessagingRelayLists, RelayLists, SearchRelayLists, Sync} from "@welshman/app"
import {
app,
blockedRelayLists,
@ -59,9 +52,10 @@ import {
} from "@app/core"
import {LIVEKIT_PARTICIPANTS} from "@app/call"
import {CONTENT_KINDS, REACTION_KINDS, makeCommentFilter} from "@app/content"
import {INDEXER_RELAYS, PLATFORM_RELAYS} from "@app/env"
import {INDEXER_RELAYS} from "@app/env"
import {FEATURED_CONTENT_D} from "@app/featured"
import {decodeRelay} from "@app/relays"
import {activeSpaceUrls, userSpaceUrls} from "@app/rooms"
import {Settings} from "@app/settings"
import {kv} from "@app/storage"
import {hasBlossomSupport} from "@app/uploads"
@ -95,8 +89,6 @@ const pullAndListen = ({url, signal, filters}: SyncOpts) => {
})
}
const userRoomList = deriveUserItem(RoomLists)
const userRelayList = deriveUserItem(RelayLists)
const userFollowList = deriveUserItem(FollowLists)
@ -105,9 +97,6 @@ const userMessagingRelayList = deriveUserItem(MessagingRelayLists)
const userSearchRelayList = deriveUserItem(SearchRelayLists)
const getSpaceUrls = ($roomList: Maybe<RoomListReader>) =>
PLATFORM_RELAYS.length > 0 ? PLATFORM_RELAYS : ($roomList?.urls() ?? [])
// Relays
const syncRelays = () => {
@ -124,8 +113,8 @@ const syncRelays = () => {
}
})
const unsubscribeSpaceUrls = userRoomList.subscribe($roomList => {
for (const url of $roomList?.urls() ?? []) {
const unsubscribeSpaceUrls = userSpaceUrls.subscribe($userSpaceUrls => {
for (const url of $userSpaceUrls) {
relays.get().load(url)
}
})
@ -169,21 +158,19 @@ const syncUserData = () => {
const unsubscribersByUrl = new Map<string, Unsubscriber>()
const $pubkey = app.get().user?.pubkey
const syncRoomList = ($roomList: Maybe<RoomListReader>) => {
if ($roomList) {
const urls = new Set(getSpaceUrls($roomList))
const syncSpaceMembership = ($activeSpaceUrls: string[]) => {
const urls = new Set($activeSpaceUrls)
for (const url of urls) {
if (!unsubscribersByUrl.has(url)) {
unsubscribersByUrl.set(url, syncUserSpaceMembership(url))
}
for (const url of urls) {
if (!unsubscribersByUrl.has(url)) {
unsubscribersByUrl.set(url, syncUserSpaceMembership(url))
}
}
for (const [url, unsubscribe] of unsubscribersByUrl.entries()) {
if (!urls.has(url)) {
unsubscribersByUrl.delete(url)
unsubscribe()
}
for (const [url, unsubscribe] of unsubscribersByUrl.entries()) {
if (!urls.has(url)) {
unsubscribersByUrl.delete(url)
unsubscribe()
}
}
}
@ -218,13 +205,13 @@ const syncUserData = () => {
relayLists.get().load($pubkey)
}
const unsubscribeRoomList = userRoomList.subscribe(syncRoomList)
const unsubscribeSpaceMembership = activeSpaceUrls.subscribe(syncSpaceMembership)
const unsubscribeRelayList = userRelayList.subscribe(syncUserLists)
const unsubscribeFollowList = userFollowList.subscribe(syncFollowNetwork)
return () => {
unsubscribersByUrl.forEach(call)
unsubscribeRoomList()
unsubscribeSpaceMembership()
unsubscribeRelayList()
unsubscribeFollowList()
}
@ -304,8 +291,8 @@ const syncSpaces = () => {
let detailUrl: Maybe<string>
let unsubscribeDetail: Unsubscriber = noop
const unsubscribe = merged([userRoomList, page]).subscribe(([$roomList, $page]) => {
const urls = new Set(getSpaceUrls($roomList))
const unsubscribe = merged([activeSpaceUrls, page]).subscribe(([$activeSpaceUrls, $page]) => {
const urls = new Set($activeSpaceUrls)
const currentUrl = $page.params.relay ? decodeRelay($page.params.relay) : undefined
if (currentUrl) {

View file

@ -1,6 +1,5 @@
<script lang="ts">
import type {Snippet} from "svelte"
import {Capacitor} from "@capacitor/core"
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
import Wallet from "@assets/icons/wallet.svg?dataurl"
import ServerPath from "@assets/icons/server-path.svg?dataurl"
@ -15,6 +14,7 @@
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte"
import LogOut from "@app/components/LogOut.svelte"
import {ENABLE_ZAPS} from "@app/env"
import {HOSTING_ENABLED} from "@app/hosting"
import {pushModal} from "@app/modal"
@ -38,7 +38,7 @@
<SecondaryNavItem href="/settings/alerts">
<Icon icon={Bell} /> Alerts
</SecondaryNavItem>
{#if Capacitor.getPlatform() !== "ios"}
{#if ENABLE_ZAPS}
<SecondaryNavItem href="/settings/wallet">
<Icon icon={Wallet} /> Wallet
</SecondaryNavItem>

View file

@ -1,5 +1,4 @@
<script lang="ts">
import {Capacitor} from "@capacitor/core"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
@ -10,7 +9,7 @@
import HeadphonesRound from "@assets/icons/headphones-round.svg?dataurl"
import Zap from "@app/components/Zap.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte"
import {PLATFORM_NAME, PLATFORM_ABOUT} from "@app/env"
import {ENABLE_ZAPS, PLATFORM_NAME, PLATFORM_ABOUT} from "@app/env"
import {pushModal} from "@app/modal"
import {makeSpacePath} from "@app/routes"
@ -27,7 +26,7 @@
<p class="text-center text-2xl">Thanks for using</p>
<h1 class="mb-4 text-center text-5xl font-bold uppercase">{PLATFORM_NAME}</h1>
<div class="grid grid-cols-1 gap-8 lg:grid-cols-2">
{#if Capacitor.getPlatform() !== "ios"}
{#if ENABLE_ZAPS}
<Card class="flex flex-col gap-2 text-center">
<h3 class="text-2xl sm:h-12">Donate</h3>
<p class="sm:h-16">Funds will be used to support development.</p>