flotilla/src/app/core/state.ts

1276 lines
34 KiB
TypeScript
Raw Normal View History

2024-10-08 18:39:16 +00:00
import twColors from "tailwindcss/colors"
2025-12-18 02:43:13 +00:00
import {context as pomadeContext} from "@pomade/core"
2025-07-17 21:39:59 +00:00
import {Capacitor} from "@capacitor/core"
import {derived, readable, writable} from "svelte/store"
2025-04-09 22:32:18 +00:00
import * as nip19 from "nostr-tools/nip19"
2024-10-08 18:39:16 +00:00
import {
2025-06-05 03:19:29 +00:00
on,
2025-10-30 22:52:24 +00:00
gt,
max,
2025-11-20 23:54:06 +00:00
find,
spec,
2025-06-05 03:19:29 +00:00
call,
first,
uniqBy,
2024-10-08 18:39:16 +00:00
sortBy,
2025-11-20 23:54:06 +00:00
append,
2026-03-10 22:13:33 +00:00
reject,
2024-10-08 18:39:16 +00:00
sort,
uniq,
2025-11-20 23:54:06 +00:00
indexBy,
partition,
2024-10-17 20:56:53 +00:00
shuffle,
2024-10-17 22:46:26 +00:00
parseJson,
memoize,
2024-12-11 00:38:22 +00:00
addToMapKey,
2025-04-03 22:35:56 +00:00
identity,
2025-04-09 22:32:18 +00:00
always,
2026-01-28 01:15:22 +00:00
randomId,
tryCatch,
2025-11-05 00:39:34 +00:00
fromPairs,
2026-02-18 01:15:00 +00:00
groupBy,
2026-01-06 22:52:13 +00:00
remove,
2026-02-26 21:35:13 +00:00
simpleCache,
2024-10-08 18:39:16 +00:00
} from "@welshman/lib"
2025-11-20 23:54:06 +00:00
import type {Override} from "@welshman/lib"
import type {RepositoryUpdate} from "@welshman/net"
2025-10-21 00:05:22 +00:00
import {
Pool,
load,
SocketStatus,
AuthStateEvent,
AuthStatus,
SocketEvent,
netContext,
} from "@welshman/net"
2025-11-20 23:54:06 +00:00
import {
getter,
throttled,
2026-01-24 00:51:02 +00:00
withGetter,
2025-11-20 23:54:06 +00:00
deriveArray,
makeDeriveEvent,
makeLoadItem,
makeDeriveItem,
2026-02-18 01:15:00 +00:00
deriveItems,
2025-11-20 23:54:06 +00:00
deriveItemsByKey,
deriveDeduplicated,
deriveEventsById,
2025-11-20 23:54:06 +00:00
deriveEventsByIdByUrl,
deriveEventsByIdForUrl,
2025-12-04 23:56:05 +00:00
getEventsByIdForUrl,
2026-02-13 00:22:25 +00:00
deriveEventsAsc,
deriveEventsDesc,
2025-11-20 23:54:06 +00:00
} from "@welshman/store"
2024-08-16 17:50:38 +00:00
import {
2026-02-18 01:15:00 +00:00
FEED,
FEEDS,
APP_DATA,
2024-11-19 17:25:02 +00:00
CLIENT_AUTH,
COMMENT,
DELETE,
2025-06-03 23:46:30 +00:00
DIRECT_MESSAGE_FILE,
DIRECT_MESSAGE,
EVENT_TIME,
2024-12-10 21:07:17 +00:00
MESSAGE,
REACTION,
RELAY_ADD_MEMBER,
RELAY_JOIN,
RELAY_LEAVE,
RELAY_MEMBERS,
RELAY_REMOVE_MEMBER,
2025-10-06 18:23:19 +00:00
REPORT,
ROOM_ADD_MEMBER,
ROOM_CREATE_PERMISSION,
ROOM_JOIN,
ROOM_LEAVE,
ROOM_MEMBERS,
2025-10-24 20:36:59 +00:00
ROOM_ADMINS,
ROOM_META,
2025-10-24 20:36:59 +00:00
ROOM_DELETE,
ROOM_REMOVE_MEMBER,
ROOMS,
THREAD,
2026-02-03 19:00:13 +00:00
CLASSIFIED,
WRAP,
2025-11-20 23:54:06 +00:00
PROFILE,
2025-10-06 18:23:19 +00:00
ZAP_GOAL,
ZAP_REQUEST,
ZAP_RESPONSE,
2026-02-18 18:22:23 +00:00
REPOST,
GENERIC_REPOST,
asDecryptedEvent,
getGroupTags,
2024-10-08 22:30:18 +00:00
getListTags,
getPubkeyTagValues,
getRelayTagValues,
2025-05-28 23:46:41 +00:00
getTagValues,
isRelayUrl,
normalizeRelayUrl,
readList,
verifyEvent,
2025-10-30 22:52:24 +00:00
readRoomMeta,
2025-10-31 23:18:58 +00:00
makeRoomMeta,
2025-11-05 00:55:26 +00:00
ManagementMethod,
sortEventsDesc,
2026-02-18 01:15:00 +00:00
getAddress,
Address,
getIdFilters,
2026-02-18 18:22:23 +00:00
getEventTagValues,
getAddressTagValues,
getParentIds,
getParentAddrs,
2026-02-18 01:15:00 +00:00
} from "@welshman/util"
import type {
TrustedEvent,
RelayProfile,
PublishedList,
PublishedRoomMeta,
RoomMeta,
2026-02-18 01:15:00 +00:00
List,
Filter,
2025-10-30 22:52:24 +00:00
} from "@welshman/util"
import {routerContext, Router} from "@welshman/router"
2024-08-26 21:43:43 +00:00
import {
2024-08-30 23:25:41 +00:00
pubkey,
2024-08-26 21:43:43 +00:00
repository,
2024-10-04 00:04:00 +00:00
tracker,
createSearch,
2026-02-18 18:22:23 +00:00
userMuteList,
2025-11-20 23:54:06 +00:00
userFollowList,
2024-10-17 22:46:26 +00:00
ensurePlaintext,
2025-04-11 15:41:50 +00:00
makeOutboxLoader,
2025-04-09 22:32:18 +00:00
appContext,
deriveRelay,
makeUserData,
makeUserLoader,
2025-11-05 00:55:26 +00:00
manageRelay,
2025-11-20 23:54:06 +00:00
displayProfileByPubkey,
2026-02-18 18:22:23 +00:00
getProfile,
2024-08-30 23:25:41 +00:00
} from "@welshman/app"
import {checkRelayHasLivekit} from "$lib/livekit"
2026-02-18 01:15:00 +00:00
import {readFeed} from "@lib/feeds"
2024-08-13 19:41:25 +00:00
2025-04-03 22:35:56 +00:00
export const fromCsv = (s: string) => (s || "").split(",").filter(identity)
2024-12-05 21:37:15 +00:00
export const ROOM = "h"
2024-09-23 17:28:33 +00:00
2024-12-09 19:59:42 +00:00
export const PROTECTED = ["-"]
2026-02-13 23:37:17 +00:00
export const IMAGE_CONTENT_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"]
export const VIDEO_CONTENT_TYPES = ["video/quicktime", "video/webm", "video/mp4"]
2025-07-17 21:39:59 +00:00
export const ENABLE_ZAPS = Capacitor.getPlatform() != "ios"
export const PUSH_SERVER = import.meta.env.VITE_PUSH_SERVER
export const PUSH_BRIDGE = normalizeRelayUrl(import.meta.env.VITE_PUSH_BRIDGE)
2025-06-19 17:22:17 +00:00
export const VAPID_PUBLIC_KEY = import.meta.env.VITE_VAPID_PUBLIC_KEY
2026-01-08 20:08:13 +00:00
export const SIGNER_RELAYS = fromCsv(import.meta.env.VITE_SIGNER_RELAYS).map(normalizeRelayUrl)
2024-08-13 19:41:25 +00:00
2026-01-20 18:40:33 +00:00
export const BLOCKED_RELAYS = fromCsv(import.meta.env.VITE_BLOCKED_RELAYS).map(normalizeRelayUrl)
2026-01-08 20:08:13 +00:00
export const INDEXER_RELAYS = fromCsv(import.meta.env.VITE_INDEXER_RELAYS).map(normalizeRelayUrl)
export const DEFAULT_RELAYS = fromCsv(import.meta.env.VITE_DEFAULT_RELAYS).map(normalizeRelayUrl)
export const DEFAULT_MESSAGING_RELAYS = fromCsv(import.meta.env.VITE_DEFAULT_MESSAGING_RELAYS).map(
normalizeRelayUrl,
)
2026-01-08 20:08:13 +00:00
export const PLATFORM_RELAYS = fromCsv(import.meta.env.VITE_PLATFORM_RELAYS).map(normalizeRelayUrl)
2025-10-28 22:40:28 +00:00
export const PLATFORM_URL = import.meta.env.VITE_PLATFORM_URL
2025-01-16 22:54:43 +00:00
export const PLATFORM_TERMS = import.meta.env.VITE_PLATFORM_TERMS
export const PLATFORM_PRIVACY = import.meta.env.VITE_PLATFORM_PRIVACY
2025-09-09 17:38:24 +00:00
export const PLATFORM_LOGO = PLATFORM_URL + "/logo.png"
2024-10-22 18:04:40 +00:00
export const PLATFORM_NAME = import.meta.env.VITE_PLATFORM_NAME
2024-10-22 23:53:39 +00:00
export const PLATFORM_ACCENT = import.meta.env.VITE_PLATFORM_ACCENT
export const PLATFORM_DESCRIPTION = import.meta.env.VITE_PLATFORM_DESCRIPTION
2025-12-18 02:43:13 +00:00
export const POMADE_SIGNERS = fromCsv(import.meta.env.VITE_POMADE_SIGNERS)
2024-11-27 00:39:49 +00:00
2026-01-08 20:08:13 +00:00
export const DEFAULT_BLOSSOM_SERVERS = fromCsv(import.meta.env.VITE_DEFAULT_BLOSSOM_SERVERS)
2024-10-22 18:04:40 +00:00
export const DEFAULT_PUBKEYS = import.meta.env.VITE_DEFAULT_PUBKEYS
2024-08-30 23:25:41 +00:00
export const DUFFLEPUD_URL = "https://dufflepud.onrender.com"
2024-08-13 19:41:25 +00:00
2024-11-19 17:25:02 +00:00
export const NIP46_PERMS =
2025-04-28 22:46:48 +00:00
"nip44_encrypt,nip44_decrypt," +
2026-02-03 19:00:13 +00:00
[
CLIENT_AUTH,
RELAY_JOIN,
MESSAGE,
THREAD,
CLASSIFIED,
COMMENT,
ROOMS,
WRAP,
REACTION,
ZAP_REQUEST,
]
2024-11-19 17:25:02 +00:00
.map(k => `sign_event:${k}`)
.join(",")
2024-10-08 18:39:16 +00:00
export const colors = [
["amber", twColors.amber[600]],
["blue", twColors.blue[600]],
["cyan", twColors.cyan[600]],
["emerald", twColors.emerald[600]],
["fuchsia", twColors.fuchsia[600]],
["green", twColors.green[600]],
["indigo", twColors.indigo[600]],
["sky", twColors.sky[600]],
["lime", twColors.lime[600]],
["orange", twColors.orange[600]],
["pink", twColors.pink[600]],
["purple", twColors.purple[600]],
["red", twColors.red[600]],
["rose", twColors.rose[600]],
["sky", twColors.sky[600]],
["teal", twColors.teal[600]],
["violet", twColors.violet[600]],
["yellow", twColors.yellow[600]],
["zinc", twColors.zinc[600]],
]
export const dufflepud = (path: string) => DUFFLEPUD_URL + "/" + path
2024-09-24 21:12:38 +00:00
2024-09-24 23:30:32 +00:00
export const entityLink = (entity: string) => `https://coracle.social/${entity}`
2024-09-24 21:12:38 +00:00
2025-04-09 22:32:18 +00:00
export const pubkeyLink = (pubkey: string, relays = Router.get().FromPubkeys([pubkey]).getUrls()) =>
entityLink(nip19.nprofileEncode({pubkey, relays}))
2025-11-20 23:54:06 +00:00
export const bootstrapPubkeys = derived(userFollowList, $userFollowList => {
2024-10-22 18:04:40 +00:00
const appPubkeys = DEFAULT_PUBKEYS.split(",")
2025-11-20 23:54:06 +00:00
const userPubkeys = shuffle(getPubkeyTagValues(getListTags($userFollowList)))
2024-10-17 20:56:53 +00:00
return userPubkeys.length > 5 ? userPubkeys : [...userPubkeys, ...appPubkeys]
2025-09-06 00:20:12 +00:00
})
2024-10-17 20:56:53 +00:00
2025-11-20 23:54:06 +00:00
export const deriveEvent = makeDeriveEvent({
repository,
includeDeleted: true,
onDerive: (filters: Filter[], relays: string[]) => load({filters, relays}),
2025-11-03 22:52:12 +00:00
})
2024-11-14 18:53:13 +00:00
export const deriveEvents = (filters: Filter[] = [{}]) =>
deriveEventsDesc(deriveEventsById({repository, filters}))
export const getEventsForUrl = (url: string, filters: Filter[] = [{}]) =>
2025-12-04 23:56:05 +00:00
getEventsByIdForUrl({url, tracker, repository, filters}).values()
export const deriveEventsForUrl = (url: string, filters: Filter[] = [{}]) =>
2025-11-20 23:54:06 +00:00
deriveArray(deriveEventsByIdForUrl({url, tracker, repository, filters}))
2026-02-13 00:22:25 +00:00
export const deriveEventsForUrlAsc = (url: string, filters: Filter[] = [{}]) =>
deriveEventsAsc(deriveEventsByIdForUrl({url, tracker, repository, filters}))
export const deriveEventsForUrlDesc = (url: string, filters: Filter[] = [{}]) =>
deriveEventsDesc(deriveEventsByIdForUrl({url, tracker, repository, filters}))
export const deriveLatestEventForUrl = (url: string, filters: Filter[] = [{}]) =>
deriveDeduplicated(deriveEventsByIdForUrl({url, tracker, repository, filters}), $eventsById =>
first(sortEventsDesc($eventsById.values())),
)
export const deriveRelaySignedEvents = (url: string, filters: Filter[] = [{}]) =>
2025-10-24 20:36:59 +00:00
derived(
2025-11-20 23:54:06 +00:00
[deriveRelay(url), deriveEventsForUrl(url, filters)],
([relay, events]) => events,
// khatru doesn't support relay.self, uncomment when it's ready
// filter(spec({pubkey: relay.self}), events)
)
2024-12-05 21:37:15 +00:00
// Context
2026-02-20 22:21:38 +00:00
pomadeContext.setSignerUrls(POMADE_SIGNERS)
2025-12-18 02:43:13 +00:00
2026-01-08 20:08:13 +00:00
pomadeContext.setArgonWorker(import("@pomade/core/argon-worker.js?worker"))
2025-12-18 02:43:13 +00:00
2025-04-09 22:32:18 +00:00
appContext.dufflepudUrl = DUFFLEPUD_URL
routerContext.getIndexerRelays = always(INDEXER_RELAYS)
2024-12-05 21:37:15 +00:00
netContext.isEventValid = (event: TrustedEvent, url: string) =>
getSetting<string[]>("trusted_relays").includes(url) || verifyEvent(event)
2025-10-06 18:23:19 +00:00
// Filters
export const makeCommentFilter = (kinds: number[], extra: Filter = {}) => ({
kinds: [COMMENT],
"#K": kinds.map(String),
...extra,
})
2026-02-18 18:22:23 +00:00
export const REPOST_KINDS = [REPOST, GENERIC_REPOST]
2025-10-06 18:23:19 +00:00
export const REACTION_KINDS = [REPORT, DELETE, REACTION]
if (ENABLE_ZAPS) {
REACTION_KINDS.push(ZAP_RESPONSE)
}
2026-02-03 19:00:13 +00:00
export const CONTENT_KINDS = [ZAP_GOAL, EVENT_TIME, THREAD, CLASSIFIED]
2025-10-06 18:23:19 +00:00
2025-10-28 18:29:59 +00:00
export const MESSAGE_KINDS = [...CONTENT_KINDS, MESSAGE]
export const DM_KINDS = [DIRECT_MESSAGE, DIRECT_MESSAGE_FILE]
2024-10-17 22:46:26 +00:00
// Settings
export const SETTINGS = "flotilla/settings"
2026-01-16 21:49:35 +00:00
export enum RelayAuthMode {
Aggressive = "aggressive",
Conservative = "conservative",
}
2026-01-30 16:41:58 +00:00
export type SpaceNotificationSettings = {
url: string
notify: boolean
exceptions: string[]
}
export type SettingsValues = {
show_media: boolean
hide_sensitive: boolean
trusted_relays: string[]
report_usage: boolean
report_errors: boolean
2026-01-16 21:49:35 +00:00
relay_auth: RelayAuthMode
send_delay: number
font_size: number
2026-01-30 16:41:58 +00:00
alerts: SpaceNotificationSettings[]
}
2024-10-17 22:46:26 +00:00
export type Settings = {
2024-10-17 23:03:50 +00:00
event: TrustedEvent
values: SettingsValues
2024-10-17 22:46:26 +00:00
}
2026-01-16 21:49:35 +00:00
export const defaultSettings: SettingsValues = {
2024-10-24 18:02:16 +00:00
show_media: true,
2024-10-17 22:46:26 +00:00
hide_sensitive: true,
trusted_relays: [],
report_usage: true,
2025-05-05 17:11:02 +00:00
report_errors: true,
2026-01-16 21:49:35 +00:00
relay_auth: RelayAuthMode.Conservative,
2025-09-24 16:50:07 +00:00
send_delay: 0,
2025-12-05 17:51:15 +00:00
font_size: 1.1,
2026-01-30 16:41:58 +00:00
alerts: [],
2024-10-17 22:46:26 +00:00
}
2025-11-20 23:54:06 +00:00
export const settingsByPubkey = deriveItemsByKey({
repository,
getKey: settings => settings.event.pubkey,
filters: [{kinds: [APP_DATA], "#d": [SETTINGS]}],
2025-11-20 23:54:06 +00:00
eventToItem: async (event: TrustedEvent) => {
const values = {...defaultSettings, ...parseJson(await ensurePlaintext(event))}
2024-10-17 22:46:26 +00:00
2025-11-20 23:54:06 +00:00
return {event, values}
},
2024-10-17 22:46:26 +00:00
})
2025-11-20 23:54:06 +00:00
export const getSettingsByPubkey = getter(settingsByPubkey)
export const loadSettings = makeLoadItem(
makeOutboxLoader(APP_DATA, {"#d": [SETTINGS]}),
(pubkey: string) => getSettingsByPubkey().get(pubkey),
2025-11-20 23:54:06 +00:00
)
2025-11-25 19:55:32 +00:00
export const userSettings = makeUserData(settingsByPubkey, loadSettings)
export const loadUserSettings = makeUserLoader(loadSettings)
2025-11-03 22:52:12 +00:00
export const userSettingsValues = derived(userSettings, $s => $s?.values || defaultSettings)
export const getSettings = getter(userSettingsValues)
export const getSetting = <T>(key: keyof Settings["values"]) => getSettings()[key] as T
// Relays sending events with empty signatures that the user has to choose to trust
2025-11-03 22:52:12 +00:00
export const relaysPendingTrust = writable<string[]>([])
// Relays that mostly send restricted responses to requests and events
export const relaysMostlyRestricted = writable<Record<string, string>>({})
// Push notifications
2026-01-28 01:15:22 +00:00
export const device = withGetter(writable(randomId()))
2026-01-24 00:51:02 +00:00
export const notificationSettings = withGetter(
writable({
2026-01-24 00:51:02 +00:00
push: false,
sound: false,
badge: false,
spaces: true,
mentions: true,
messages: true,
2026-01-26 18:08:43 +00:00
}),
2026-01-24 00:51:02 +00:00
)
2026-01-28 01:15:22 +00:00
export type PushSubscription = {
key: string
callback: string
}
export type PushState = {
token?: string
useFallback?: boolean
2026-01-28 01:15:22 +00:00
subscription?: PushSubscription
}
export const pushState = withGetter(writable<PushState>({}))
2024-10-03 22:28:06 +00:00
// Chats
2024-10-01 23:27:25 +00:00
2024-10-03 21:43:17 +00:00
export type Chat = {
2024-10-01 23:27:25 +00:00
id: string
pubkeys: string[]
messages: TrustedEvent[]
2024-10-04 00:04:00 +00:00
last_activity: number
search_text: string
2024-10-01 23:27:25 +00:00
}
2026-01-06 22:52:13 +00:00
export const getChatPubkeys = (pubkeys: string[]) => sort(uniq(append(pubkey.get()!, pubkeys)))
2024-10-01 23:27:25 +00:00
2026-01-06 22:52:13 +00:00
export const getChatPubkeysFromEvent = (event: TrustedEvent) =>
getChatPubkeys(getPubkeyTagValues(event.tags).concat(event.pubkey))
export const makeChatId = (pubkeys: string[]) => {
const userPubkey = pubkey.get()!
const otherPubkeys = remove(userPubkey, uniq(pubkeys))
const visiblePubkeys = otherPubkeys.length === 0 ? [userPubkey] : otherPubkeys
return sort(visiblePubkeys).join(",")
}
export const splitChatId = (id: string) => getChatPubkeys(id.split(","))
2024-10-01 23:27:25 +00:00
2025-11-20 23:54:06 +00:00
export const chatsById = call(() => {
const chatsById = new Map<string, Chat>()
2026-03-10 22:13:33 +00:00
const chatsByPubkey = new Map<string, string[]>()
2024-10-01 23:27:25 +00:00
2025-11-20 23:54:06 +00:00
const addSearchText = (chat: Override<Chat, {search_text?: string}>) => {
chat.search_text =
chat.pubkeys.length === 1
? displayProfileByPubkey(chat.pubkeys[0]) + " note to self"
2026-01-06 22:52:13 +00:00
: remove(pubkey.get()!, chat.pubkeys).map(displayProfileByPubkey).join(" ")
2024-10-01 23:27:25 +00:00
2025-11-20 23:54:06 +00:00
return chat as Chat
}
2024-10-01 23:27:25 +00:00
2025-11-20 23:54:06 +00:00
return readable(chatsById, set => {
2026-03-10 22:13:33 +00:00
const indexChatByPubkeys = (chat: Chat) => {
for (const pubkey of chat.pubkeys) {
chatsByPubkey.set(pubkey, uniq(append(chat.id, chatsByPubkey.get(pubkey) || [])))
}
}
2025-11-25 23:05:45 +00:00
const addEvents = (events: TrustedEvent[]) => {
let dirty = false
for (const event of events) {
if (DM_KINDS.includes(event.kind)) {
2026-01-06 22:52:13 +00:00
const pubkeys = getChatPubkeysFromEvent(event)
2025-11-25 23:05:45 +00:00
const id = makeChatId(pubkeys)
const chat = chatsById.get(id)
2026-02-16 20:39:21 +00:00
const messages = sortBy(
e => -e.created_at,
uniqBy(e => e.id, append(event, chat?.messages || [])),
)
2025-11-25 23:05:45 +00:00
const last_activity = Math.max(chat?.last_activity || 0, event.created_at)
const updatedChat = addSearchText({id, pubkeys, messages, last_activity})
chatsById.set(id, updatedChat)
2026-03-10 22:13:33 +00:00
indexChatByPubkeys(updatedChat)
2025-01-17 23:01:05 +00:00
2025-11-25 23:05:45 +00:00
dirty = true
2025-11-20 23:54:06 +00:00
}
2025-01-17 23:01:05 +00:00
2025-11-25 23:05:45 +00:00
if (event.kind === PROFILE) {
2026-03-10 22:13:33 +00:00
for (const chatId of chatsByPubkey.get(event.pubkey) || []) {
const chat = chatsById.get(chatId)
if (chat) {
addSearchText(chat)
dirty = true
}
}
}
}
if (dirty) {
set(chatsById)
}
}
const removeEvents = (removed: Set<string>) => {
let dirty = false
for (const id of removed) {
const event = repository.getEvent(id)
if (event && DM_KINDS.includes(event.kind)) {
for (const chatId of chatsByPubkey.get(event.pubkey) || []) {
const chat = chatsById.get(chatId)
if (chat) {
chat.messages = reject(spec({id: event.id}), chat.messages)
dirty = true
}
2025-11-25 23:05:45 +00:00
}
2025-11-20 23:54:06 +00:00
}
2025-11-25 23:05:45 +00:00
}
if (dirty) {
set(chatsById)
}
}
2026-03-10 22:13:33 +00:00
addEvents(repository.query([{kinds: [...DM_KINDS, DELETE, PROFILE]}]))
2025-11-25 23:05:45 +00:00
const unsubscribers = [
2026-03-10 22:13:33 +00:00
on(repository, "update", ({added, removed}: RepositoryUpdate) => {
addEvents(added)
removeEvents(removed)
}),
2025-11-20 23:54:06 +00:00
]
2024-10-01 23:27:25 +00:00
2025-11-20 23:54:06 +00:00
return () => unsubscribers.forEach(call)
})
2024-10-01 23:27:25 +00:00
})
2025-11-25 23:05:45 +00:00
export const deriveChat = call(() => {
const _deriveChat = makeDeriveItem(chatsById)
return (pubkeys: string[]) => _deriveChat(makeChatId(pubkeys))
})
2025-11-20 23:54:06 +00:00
export const chatSearch = derived(throttled(800, chatsById), $chatsByPubkey => {
2026-01-26 18:08:43 +00:00
return createSearch(
sortBy(c => -c.last_activity, Array.from($chatsByPubkey.values())),
{
getValue: (chat: Chat) => chat.id,
fuseOptions: {keys: ["search_text"]},
},
)
2025-11-20 23:54:06 +00:00
})
2024-10-04 00:04:00 +00:00
2025-10-30 22:36:14 +00:00
// Rooms
2024-12-05 21:37:15 +00:00
export enum RoomType {
Text = "text",
Voice = "voice",
}
2025-10-30 22:52:24 +00:00
export type Room = PublishedRoomMeta & {
2025-05-27 22:30:39 +00:00
id: string
url: string
2024-12-10 01:04:39 +00:00
}
export const getRoomType = (room: RoomMeta): RoomType =>
room.livekit ? RoomType.Voice : RoomType.Text
2025-10-30 22:36:14 +00:00
export const makeRoomId = (url: string, h: string) => `${url}'${h}`
2024-12-05 21:37:15 +00:00
2025-10-30 22:36:14 +00:00
export const splitRoomId = (id: string) => id.split("'")
2024-12-05 21:37:15 +00:00
2025-10-24 16:38:57 +00:00
export const hasNip29 = (relay?: RelayProfile) =>
2026-03-11 22:48:36 +00:00
Boolean(relay?.supported_nips?.map?.(String)?.includes?.("29"))
2024-12-05 21:37:15 +00:00
2025-11-20 23:54:06 +00:00
export const roomMetaEventsByIdByUrl = deriveEventsByIdByUrl({
tracker,
repository,
filters: [{kinds: [ROOM_META, ROOM_DELETE]}],
2025-10-30 22:52:24 +00:00
})
2025-11-20 23:54:06 +00:00
export const roomsByUrl = derived(roomMetaEventsByIdByUrl, roomMetaEventsByIdByUrl => {
2025-12-05 01:06:50 +00:00
const metaByIdByUrl = new Map<string, Map<string, Room>>()
2025-10-30 22:52:24 +00:00
2025-11-20 23:54:06 +00:00
for (const [url, events] of roomMetaEventsByIdByUrl.entries()) {
const [metaEvents, deleteEvents] = partition(spec({kind: ROOM_META}), events.values())
2025-10-30 22:52:24 +00:00
const deletedByH = new Map<string, number>()
2025-10-24 20:36:59 +00:00
2025-11-20 23:54:06 +00:00
for (const event of deleteEvents) {
2025-10-30 22:52:24 +00:00
for (const h of getTagValues("h", event.tags)) {
deletedByH.set(h, max([deletedByH.get(h), event.created_at]))
}
}
2025-10-24 20:36:59 +00:00
2025-11-20 23:54:06 +00:00
for (const event of metaEvents) {
2025-12-18 02:43:13 +00:00
const meta = tryCatch(() => readRoomMeta(event))
2025-11-20 23:54:06 +00:00
2025-12-18 02:43:13 +00:00
if (!meta || gt(deletedByH.get(meta.h), meta.event.created_at)) {
2025-10-30 22:52:24 +00:00
continue
}
2025-12-05 01:06:50 +00:00
let metaById = metaByIdByUrl.get(url)
if (!metaById) {
metaById = new Map()
metaByIdByUrl.set(url, metaById)
}
const id = makeRoomId(url, meta.h)
metaById.set(id, {...meta, url, id})
2025-05-27 22:30:39 +00:00
}
2025-11-20 23:54:06 +00:00
}
2024-12-05 21:37:15 +00:00
2025-12-05 01:06:50 +00:00
const result = new Map<string, Room[]>()
for (const [url, metaById] of metaByIdByUrl.entries()) {
result.set(url, Array.from(metaById.values()))
}
2025-11-20 23:54:06 +00:00
return result
})
export const roomsById = derived(roomsByUrl, roomsByUrl =>
indexBy(room => room.id, Array.from(roomsByUrl.values()).flatMap(identity)),
2024-12-05 21:37:15 +00:00
)
2025-11-20 23:54:06 +00:00
export const getRoomsById = getter(roomsById)
export const getRoom = (id: string) => getRoomsById().get(id)
export const loadRoom = call(() => {
const _fetchRoom = async (id: string) => {
2025-10-30 22:36:14 +00:00
const [url, h] = splitRoomId(id)
2025-05-27 22:30:39 +00:00
await load({
relays: [url],
2025-10-30 22:33:34 +00:00
filters: [{kinds: [ROOM_META], "#d": [h]}],
2025-05-27 22:30:39 +00:00
})
2025-11-20 23:54:06 +00:00
}
const _loadRoom = makeLoadItem(_fetchRoom, getRoom)
return (url: string, h: string) => _loadRoom(makeRoomId(url, h))
2025-05-27 22:30:39 +00:00
})
2024-09-11 19:19:45 +00:00
2025-11-20 23:54:06 +00:00
export const deriveRoom = call(() => {
const _deriveRoom = makeDeriveItem(roomsById, loadRoom)
return (url: string, h: string) =>
2026-01-22 20:37:30 +00:00
derived(
_deriveRoom(makeRoomId(url, h)),
room => room || {url, id: makeRoomId(url, h), ...makeRoomMeta({h})},
)
2025-11-20 23:54:06 +00:00
})
2024-09-11 19:19:45 +00:00
2025-11-20 23:54:06 +00:00
export const displayRoom = (url: string, h: string) => getRoom(makeRoomId(url, h))?.name || h
2024-09-11 19:19:45 +00:00
2025-10-30 22:36:14 +00:00
export const roomComparator = (url: string) => (h: string) => displayRoom(url, h).toLowerCase()
2024-12-10 21:02:41 +00:00
export const deriveVoiceRooms = (url: string) =>
derived(roomsById, $roomsById => {
const set = new Set<string>()
for (const room of $roomsById.values()) {
if (room.url === url && room.livekit) {
set.add(room.h)
}
}
return set
})
export const deriveOtherVoiceRooms = (url: string) =>
derived([deriveVoiceRooms(url), deriveUserRooms(url)], ([$roomsWithLivekit, $userRooms]) => {
const rooms: string[] = []
for (const h of $roomsWithLivekit) {
if (!$userRooms.includes(h)) {
rooms.push(h)
}
}
return sortBy(roomComparator(url), uniq(rooms))
})
2025-11-20 23:12:16 +00:00
// User space/room lists
2025-11-20 23:54:06 +00:00
export const groupListsByPubkey = deriveItemsByKey({
repository,
filters: [{kinds: [ROOMS]}],
2025-11-20 23:54:06 +00:00
getKey: list => list.event.pubkey,
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
})
2025-11-20 23:54:06 +00:00
export const getGroupListsByPubkey = getter(groupListsByPubkey)
export const getGroupList = (pubkey: string) => getGroupListsByPubkey().get(pubkey)
export const loadGroupList = makeLoadItem(makeOutboxLoader(ROOMS), getGroupList)
export const deriveGroupList = makeDeriveItem(groupListsByPubkey, loadGroupList)
2025-12-01 18:26:43 +00:00
export const groupListPubkeysByUrl = derived(groupListsByPubkey, $groupListsByPubkey => {
const result = new Map<string, Set<string>>()
2024-10-17 22:46:26 +00:00
2025-11-20 23:54:06 +00:00
for (const list of $groupListsByPubkey.values()) {
const tags = getListTags(list)
2024-10-17 22:46:26 +00:00
for (const url of getRelayTagValues(tags)) {
addToMapKey(result, url, list.event.pubkey)
}
2024-10-17 22:46:26 +00:00
for (const tag of getGroupTags(tags)) {
const url = tag[2] || ""
2024-11-16 16:04:09 +00:00
if (isRelayUrl(url)) {
addToMapKey(result, url, list.event.pubkey)
}
}
}
2024-10-24 18:02:16 +00:00
return result
})
2025-12-01 18:26:43 +00:00
export const deriveGroupListPubkeys = (url: string) =>
derived(groupListPubkeysByUrl, $groupListPubkeysByUrl => new Set($groupListPubkeysByUrl.get(url)))
2025-11-20 23:54:06 +00:00
export const getSpaceUrlsFromGroupList = (groupList: List | undefined) => {
const tags = getListTags(groupList)
const urls = getRelayTagValues(tags)
for (const tag of getGroupTags(tags)) {
const url = tag[2] || ""
if (isRelayUrl(url)) {
urls.push(url)
}
}
2024-12-11 00:38:22 +00:00
return uniq(urls.map(normalizeRelayUrl))
}
2025-11-20 23:54:06 +00:00
export const getSpaceRoomsFromGroupList = (url: string, groupList: List | undefined) => {
const rooms: string[] = []
2025-11-20 23:54:06 +00:00
for (const [_, h, relay] of getGroupTags(getListTags(groupList))) {
if (url === relay) {
2025-10-30 22:33:34 +00:00
rooms.push(h)
}
}
2026-01-06 18:51:37 +00:00
return sortBy(roomComparator(url), uniq(rooms))
}
2025-11-25 19:55:32 +00:00
export const userGroupList = makeUserData(groupListsByPubkey, loadGroupList)
2025-11-20 23:12:16 +00:00
export const loadUserGroupList = makeUserLoader(loadGroupList)
2025-11-20 23:54:06 +00:00
export const userSpaceUrls = derived(userGroupList, getSpaceUrlsFromGroupList)
export const deriveUserRooms = (url: string) =>
2025-11-20 23:12:16 +00:00
derived([userGroupList, roomsById], ([$userGroupList, $roomsById]) => {
const rooms: string[] = []
2025-11-20 23:12:16 +00:00
for (const h of getSpaceRoomsFromGroupList(url, $userGroupList)) {
2025-10-30 22:36:14 +00:00
if ($roomsById.has(makeRoomId(url, h))) {
2025-10-30 22:33:34 +00:00
rooms.push(h)
}
2024-12-11 00:38:22 +00:00
}
return sortBy(roomComparator(url), rooms)
})
2024-12-11 00:38:22 +00:00
export const deriveOtherRooms = (url: string) =>
derived(
[deriveUserRooms(url), deriveVoiceRooms(url), roomsByUrl],
([$userRooms, voiceRooms, $roomsByUrl]) => {
const rooms: string[] = []
for (const {h} of $roomsByUrl.get(url) || []) {
if (!$userRooms.includes(h) && !voiceRooms.has(h)) {
rooms.push(h)
}
}
return sortBy(roomComparator(url), uniq(rooms))
},
)
// Space/room memberships
export const deriveSpaceMembers = (url: string) =>
derived(
2025-11-20 23:54:06 +00:00
deriveRelaySignedEvents(url, [{kinds: [RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER, RELAY_MEMBERS]}]),
$events => {
const membersEvent = $events.find(spec({kind: RELAY_MEMBERS}))
if (membersEvent) {
2025-11-13 23:25:18 +00:00
return uniq(getTagValues("member", membersEvent.tags))
}
const members = new Set<string>()
2025-10-24 20:36:59 +00:00
for (const event of sortBy(e => e.created_at, $events)) {
const pubkeys = getPubkeyTagValues(event.tags)
if (event.kind === RELAY_ADD_MEMBER) {
for (const pubkey of pubkeys) {
members.add(pubkey)
}
}
if (event.kind === RELAY_REMOVE_MEMBER) {
for (const pubkey of pubkeys) {
members.delete(pubkey)
}
}
}
return Array.from(members)
},
2024-12-11 00:38:22 +00:00
)
2024-12-10 21:02:41 +00:00
2025-11-13 21:25:34 +00:00
export type BannedPubkeyItem = {
pubkey: string
reason: string
}
export const spaceBannedPubkeyItems = new Map<string, BannedPubkeyItem[]>()
export const deriveSpaceBannedPubkeyItems = (url: string) => {
const store = writable(spaceBannedPubkeyItems.get(url) || [])
manageRelay(url, {method: ManagementMethod.ListBannedPubkeys, params: []}).then(res => {
spaceBannedPubkeyItems.set(url, res.result)
store.set(res.result)
})
return store
}
2025-11-20 23:54:06 +00:00
export const deriveRoomMembers = (url: string, h: string) => {
const filters: Filter[] = [
{kinds: [ROOM_MEMBERS], "#d": [h]},
{kinds: [ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]},
]
2025-11-20 23:54:06 +00:00
return derived(deriveEventsForUrl(url, filters), $events => {
const membersEvent = find(spec({kind: ROOM_MEMBERS}), $events)
2025-11-20 23:54:06 +00:00
if (membersEvent) {
return uniq(getPubkeyTagValues(membersEvent.tags))
}
2025-11-20 23:54:06 +00:00
const members = new Set<string>()
2025-11-20 23:54:06 +00:00
for (const event of sortBy(e => -e.created_at, $events)) {
const pubkeys = getPubkeyTagValues(event.tags)
if (event.kind === ROOM_ADD_MEMBER) {
for (const pubkey of pubkeys) {
members.add(pubkey)
}
2025-11-20 23:54:06 +00:00
}
2025-11-20 23:54:06 +00:00
if (event.kind === ROOM_REMOVE_MEMBER) {
for (const pubkey of pubkeys) {
members.delete(pubkey)
}
}
2025-11-20 23:54:06 +00:00
}
2025-11-20 23:54:06 +00:00
return Array.from(members)
})
}
2024-12-10 21:02:41 +00:00
2025-11-20 23:54:06 +00:00
export const deriveRoomAdmins = (url: string, h: string) => {
const filters: Filter[] = [{kinds: [ROOM_ADMINS], "#d": [h]}]
return derived(deriveEventsForUrl(url, filters), $events => {
2025-10-24 20:36:59 +00:00
const adminsEvent = first($events)
if (adminsEvent) {
return getPubkeyTagValues(adminsEvent.tags)
}
return []
})
2025-11-20 23:54:06 +00:00
}
2025-10-24 20:36:59 +00:00
// User membership status
2025-05-28 23:46:41 +00:00
export enum MembershipStatus {
Initial,
Pending,
Granted,
}
export const deriveUserIsSpaceAdmin = memoize((url?: string) => {
const store = writable(false)
if (url) {
manageRelay(url, {method: ManagementMethod.SupportedMethods, params: []}).then(res =>
store.set(Boolean(res.result?.length)),
)
}
return store
2025-11-13 21:25:34 +00:00
})
2025-11-20 23:54:06 +00:00
export const deriveUserSpaceMembershipStatus = (url: string) => {
const filters: Filter[] = [{kinds: [RELAY_JOIN, RELAY_LEAVE]}]
return derived(
2025-05-29 17:42:43 +00:00
[
pubkey,
deriveSpaceMembers(url),
2025-11-20 23:54:06 +00:00
deriveEventsForUrl(url, filters),
deriveUserIsSpaceAdmin(url),
2025-05-29 17:42:43 +00:00
],
([$pubkey, $members, $events, $isAdmin]) => {
const isMember = $members.includes($pubkey!) || $isAdmin
for (const event of $events) {
if (event.pubkey !== $pubkey) {
continue
}
if (event.kind === RELAY_JOIN) {
return isMember ? MembershipStatus.Granted : MembershipStatus.Pending
}
if (event.kind === RELAY_LEAVE) {
return MembershipStatus.Initial
}
}
return isMember ? MembershipStatus.Granted : MembershipStatus.Initial
},
)
2025-11-20 23:54:06 +00:00
}
export const deriveUserIsRoomAdmin = (url: string, h: string) =>
derived(
[pubkey, deriveRoomAdmins(url, h), deriveUserIsSpaceAdmin(url)],
([$pubkey, $admins, $isSpaceAdmin]) => $isSpaceAdmin || $admins.includes($pubkey!),
)
2025-11-20 23:54:06 +00:00
export const deriveUserRoomMembershipStatus = (url: string, h: string) => {
const filters: Filter[] = [{kinds: [ROOM_JOIN, ROOM_LEAVE], "#h": [h]}]
return derived(
[
pubkey,
2025-10-30 22:33:34 +00:00
deriveRoomMembers(url, h),
2025-11-20 23:54:06 +00:00
deriveEventsForUrl(url, filters),
deriveUserIsRoomAdmin(url, h),
],
([$pubkey, $members, $events, $isAdmin]) => {
const isMember = $members.includes($pubkey!) || $isAdmin
2025-05-28 23:46:41 +00:00
for (const event of $events) {
if (event.pubkey !== $pubkey) {
continue
2025-05-28 23:46:41 +00:00
}
if (event.kind === ROOM_JOIN) {
return isMember ? MembershipStatus.Granted : MembershipStatus.Pending
2025-05-28 23:46:41 +00:00
}
if (event.kind === ROOM_LEAVE) {
return MembershipStatus.Initial
2025-05-28 23:46:41 +00:00
}
}
return isMember ? MembershipStatus.Granted : MembershipStatus.Initial
2025-05-29 17:42:43 +00:00
},
2025-05-28 23:46:41 +00:00
)
2025-11-20 23:54:06 +00:00
}
2025-05-28 23:46:41 +00:00
2025-11-20 23:54:06 +00:00
export const deriveUserCanCreateRoom = (url: string) => {
const filters: Filter[] = [{kinds: [ROOM_CREATE_PERMISSION]}]
return derived(
[pubkey, deriveEventsForUrl(url, filters), deriveUserIsSpaceAdmin(url)],
([$pubkey, $events, $isAdmin]) => {
for (const event of $events) {
if (getPubkeyTagValues(event.tags).includes($pubkey!)) {
return true
}
}
return $isAdmin
},
)
2025-11-20 23:54:06 +00:00
}
2026-02-18 01:15:00 +00:00
// Feeds
export const feedsByAddress = deriveItemsByKey({
repository,
getKey: feed => getAddress(feed.event),
filters: [{kinds: [FEED]}],
eventToItem: readFeed,
})
export const getFeedsByAddress = getter(feedsByAddress)
export const feeds = deriveItems(feedsByAddress)
export const getFeeds = getter(feeds)
export const getFeed = (address: string) => getFeedsByAddress().get(address)
export const fetchFeed = (address: string) => {
const {pubkey} = Address.from(address)
return load({
relays: Router.get().FromPubkey(pubkey).getUrls(),
filters: getIdFilters([address]),
})
}
export const loadFeed = makeLoadItem(fetchFeed, getFeed)
export const deriveFeed = makeDeriveItem(feedsByAddress, loadFeed)
// Feeds by pubkey
export const feedsByPubkey = derived(feeds, $feeds => groupBy(f => f.event.pubkey, $feeds))
export const getFeedsByPubkey = getter(feedsByPubkey)
export const getFeedsForPubkey = (pubkey: string) => getFeedsByPubkey().get(pubkey)
export const loadFeedsForPubkey = makeLoadItem(makeOutboxLoader(FEED), getFeedsForPubkey)
export const userFeeds = makeUserData(feedsByPubkey, loadFeedsForPubkey)
export const loadUserFeeds = makeUserLoader(loadFeedsForPubkey)
// Feed favorites
export const feedFavoritesByPubkey = deriveItemsByKey<PublishedList>({
repository,
getKey: list => list.event.pubkey,
filters: [{kinds: [FEEDS]}],
eventToItem: async event =>
readList(
asDecryptedEvent(event, {
content: await ensurePlaintext(event),
}),
),
})
export const getFeedFavoritesByPubkey = getter(feedFavoritesByPubkey)
export const getFeedFavorites = (pubkey: string) => getFeedFavoritesByPubkey().get(pubkey)
export const loadFeedFavorites = makeLoadItem(makeOutboxLoader(FEEDS), getFeedFavorites)
export const userFeedFavorites = makeUserData(feedFavoritesByPubkey, loadFeedFavorites)
export const loadUserFeedFavorites = makeUserLoader(loadFeedFavorites)
2026-02-18 18:22:23 +00:00
// Mutes
export const isEventMuted = withGetter(
derived(userMuteList, $userMuteList => {
const pubkey = $userMuteList?.event.pubkey
const tags = getListTags($userMuteList)
const mutedEvents = new Set(getEventTagValues(tags))
const mutedPubkeys = new Set(getPubkeyTagValues(tags))
const mutedAddresses = new Set(getAddressTagValues(tags))
const mutedTopics = new Set(getTagValues("t", tags))
const mutedWords = getTagValues("word", tags)
const regex =
mutedWords.length > 0
? new RegExp(`\\b(${mutedWords.map(w => w.toLowerCase().trim()).join("|")})\\b`)
: null
return (e: TrustedEvent) => {
if (!pubkey) return false
if (pubkey === e.pubkey) return false
if (mutedPubkeys.has(e.pubkey)) return true
if (mutedEvents.has(e.id)) return true
if (mutedAddresses.has(getAddress(e))) return true
if (getParentIds(e).some(id => mutedEvents.has(id))) return true
if (getParentAddrs(e).some(address => mutedAddresses.has(address))) return true
if (getTagValues("t", e.tags).some(t => mutedTopics.has(t))) return true
if (regex) {
if (e.content?.toLowerCase().match(regex)) return true
if (displayProfileByPubkey(e.pubkey).toLowerCase().match(regex)) return true
if (tryCatch(() => getProfile(e.pubkey)?.nip05?.match(regex))) return true
}
return false
}
}),
)
// Other utils
2025-04-14 23:45:52 +00:00
export const encodeRelay = (url: string) =>
encodeURIComponent(
normalizeRelayUrl(url)
.replace(/^wss:\/\//, "")
.replace(/\/$/, ""),
)
2024-10-21 17:30:12 +00:00
export const decodeRelay = (url: string) => normalizeRelayUrl(decodeURIComponent(url))
export const displayReaction = (content: string) => {
2024-10-31 19:24:40 +00:00
if (!content || content === "+") return "❤️"
if (content === "-") return "👎"
return content
}
2025-06-05 03:19:29 +00:00
2025-11-20 23:54:06 +00:00
export const deriveSocket = (url: string) => {
const socket = Pool.get().get(url)
2025-06-05 03:19:29 +00:00
2025-11-20 23:54:06 +00:00
return readable(socket, set => {
2025-06-05 03:19:29 +00:00
const subs = [
on(socket, SocketEvent.Error, () => set(socket)),
on(socket, SocketEvent.Status, () => set(socket)),
on(socket.auth, AuthStateEvent.Status, () => set(socket)),
]
return () => subs.forEach(call)
})
2025-11-20 23:54:06 +00:00
}
2025-10-21 00:05:22 +00:00
export const deriveSocketStatus = (url: string) =>
throttled(
800,
derived([deriveSocket(url), relaysMostlyRestricted], ([$socket, $relaysMostlyRestricted]) => {
if ($socket.status === SocketStatus.Opening) {
return {theme: "warning", title: "Connecting"}
}
if ($socket.status === SocketStatus.Closing) {
return {theme: "gray-500", title: "Not Connected"}
}
if ($socket.status === SocketStatus.Closed) {
return {theme: "gray-500", title: "Not Connected"}
}
if ($socket.status === SocketStatus.Error) {
return {theme: "error", title: "Failed to Connect"}
}
if ($socket.auth.status === AuthStatus.Requested) {
return {theme: "warning", title: "Authenticating"}
}
if ($socket.auth.status === AuthStatus.PendingSignature) {
return {theme: "warning", title: "Authenticating"}
}
if ($socket.auth.status === AuthStatus.DeniedSignature) {
return {theme: "error", title: "Failed to Authenticate"}
}
if ($socket.auth.status === AuthStatus.PendingResponse) {
return {theme: "warning", title: "Authenticating"}
}
if ($socket.auth.status === AuthStatus.Forbidden) {
return {theme: "error", title: "Access Denied"}
}
if ($relaysMostlyRestricted[url]) {
return {theme: "error", title: "Access Denied"}
}
return {theme: "success", title: "Connected"}
}),
)
2026-02-26 21:35:13 +00:00
export const deriveSupportedMethods = simpleCache(([url]: [string]) => {
return readable<ManagementMethod[]>([], set => {
manageRelay(url, {
method: ManagementMethod.SupportedMethods,
params: [],
}).then(({result = []}) => set(result))
})
})
export const deriveHasLivekit = simpleCache(([url]: [string]) =>
readable<boolean | undefined>(undefined, set => {
checkRelayHasLivekit(url).then(has => set(has))
}),
)
export const deriveTimeout = (timeout: number) => {
const store = writable<boolean>(false)
setTimeout(() => store.set(true), timeout)
return derived(store, identity)
}
2025-11-04 23:36:20 +00:00
export const shouldIgnoreError = (error: string) => {
const isIgnored = error.startsWith("mute: ")
const isAborted = error.includes("Signing was aborted")
const isStrictNip29Relay = error.includes("missing group (`h`) tag")
return isIgnored || isAborted || isStrictNip29Relay
}
2026-01-09 00:01:36 +00:00
export const stripPrefix = (m: string) => m.replace(/^\w+: /, "")
2025-11-05 00:39:34 +00:00
export type InviteData = {url: string; claim: string}
export const parseInviteLink = (invite: string): InviteData | undefined => {
if (invite.length < 3 || !invite.includes(".")) {
return
}
2025-11-05 00:39:34 +00:00
return (
tryCatch(() => {
const {r: relay = "", c: claim = ""} = fromPairs(Array.from(new URL(invite).searchParams))
const url = normalizeRelayUrl(relay)
2025-11-05 00:39:34 +00:00
if (isRelayUrl(url)) {
return {url, claim}
}
}) ||
tryCatch(() => {
const url = normalizeRelayUrl(invite)
if (isRelayUrl(url)) {
return {url, claim: ""}
}
})
)
}
2026-01-30 16:41:58 +00:00
// Hierarchical notification helpers
2026-02-02 22:29:12 +00:00
export const getShouldNotify = ({alerts}: SettingsValues, url: string, h?: string) => {
2026-01-30 16:41:58 +00:00
const pref = alerts.find(spec({url}))
if (!pref) return true
if (!h) return pref.notify
if (pref.notify) return !pref.exceptions.includes(h)
if (!pref.notify) return pref.exceptions.includes(h)
}
2026-02-02 22:29:12 +00:00
export const shouldNotify = (url: string, h?: string) => getShouldNotify(getSettings(), url, h)
export const deriveShouldNotify = (url: string, h?: string) =>
derived(userSettingsValues, $settings => getShouldNotify($settings, url, h))
2026-03-11 22:48:36 +00:00
// Whatever who cares
export const hasNip50 = (relay?: RelayProfile) =>
Boolean(relay?.supported_nips?.map?.(String)?.includes?.("50"))