flotilla/src/app/state.ts

772 lines
19 KiB
TypeScript
Raw Normal View History

2024-10-08 18:39:16 +00:00
import twColors from "tailwindcss/colors"
2025-04-15 18:03:27 +00:00
import {get, derived, 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 {
remove,
sortBy,
sort,
uniq,
nth,
pushToMapKey,
nthEq,
2024-10-17 20:56:53 +00:00
shuffle,
2024-10-17 22:46:26 +00:00
parseJson,
2024-12-10 01:04:39 +00:00
fromPairs,
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,
2024-10-08 18:39:16 +00:00
} from "@welshman/lib"
2025-04-09 22:32:18 +00:00
import {load} from "@welshman/net"
2024-08-16 17:50:38 +00:00
import {
getIdFilters,
2024-10-01 23:27:25 +00:00
WRAP,
2024-11-19 17:25:02 +00:00
CLIENT_AUTH,
AUTH_JOIN,
2024-08-30 23:25:41 +00:00
REACTION,
ZAP_RESPONSE,
2024-10-01 23:27:25 +00:00
DIRECT_MESSAGE,
2024-12-10 01:04:39 +00:00
GROUP_META,
2024-12-10 21:07:17 +00:00
MESSAGE,
GROUPS,
THREAD,
COMMENT,
2025-04-15 18:03:27 +00:00
PROFILE,
2024-12-05 23:32:27 +00:00
getGroupTags,
getRelayTagValues,
2024-10-01 23:27:25 +00:00
getPubkeyTagValues,
2024-10-04 00:04:00 +00:00
isHashedEvent,
displayProfile,
2024-10-08 22:30:18 +00:00
readList,
getListTags,
2025-04-15 18:03:27 +00:00
readProfile,
2024-10-08 22:30:18 +00:00
asDecryptedEvent,
2024-10-21 17:30:12 +00:00
normalizeRelayUrl,
2025-04-15 18:03:27 +00:00
displayPubkey,
2024-08-16 17:50:38 +00:00
} from "@welshman/util"
2025-04-15 18:03:27 +00:00
import {LOCAL_RELAY_URL} from "@welshman/relay"
import type {TrustedEvent, Profile, SignedEvent, PublishedList, List, Filter} from "@welshman/util"
2025-02-25 21:36:32 +00:00
import {Nip59, decrypt} from "@welshman/signer"
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-08-30 23:25:41 +00:00
collection,
profilesByPubkey,
2024-10-04 00:04:00 +00:00
tracker,
2024-12-09 22:03:59 +00:00
makeTrackerStore,
2024-12-16 18:30:42 +00:00
makeRepositoryStore,
2024-10-04 00:04:00 +00:00
relay,
getSession,
getSigner,
createSearch,
2024-10-17 20:56:53 +00:00
userFollows,
2024-10-17 22:46:26 +00:00
ensurePlaintext,
thunks,
walkThunks,
2025-02-25 21:36:32 +00:00
signer,
2025-04-09 22:32:18 +00:00
Router,
2025-04-11 15:41:50 +00:00
makeOutboxLoader,
2025-04-09 22:32:18 +00:00
routerContext,
appContext,
2025-04-15 18:03:27 +00:00
deriveProfile,
makeCachedLoader,
2024-08-30 23:25:41 +00:00
} from "@welshman/app"
2024-12-16 19:24:10 +00:00
import type {Thunk, Relay} from "@welshman/app"
import {deriveEvents, deriveEventsMapped, withGetter, synced} from "@welshman/store"
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-05 21:37:15 +00:00
export const GENERAL = "_"
2024-09-23 17:28:33 +00:00
2024-12-09 19:59:42 +00:00
export const PROTECTED = ["-"]
2025-02-25 21:36:32 +00:00
export const ALERT = 32830
export const ALERT_STATUS = 32831
export const NOTIFIER_PUBKEY = "27b7c2ed89ef78322114225ea3ebf5f72c7767c2528d4d0c1854d039c00085df"
// export const NOTIFIER_RELAY = 'wss://notifier.flotilla.social/'
export const NOTIFIER_RELAY = "ws://localhost:4738/"
2025-04-03 22:35:56 +00:00
export const INDEXER_RELAYS = fromCsv(import.meta.env.VITE_INDEXER_RELAYS)
2024-08-13 19:41:25 +00:00
2025-04-03 22:35:56 +00:00
export const SIGNER_RELAYS = fromCsv(import.meta.env.VITE_SIGNER_RELAYS)
export const PLATFORM_URL = window.location.origin
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
export const PLATFORM_LOGO = PLATFORM_URL + "/pwa-192x192.png"
2024-10-22 18:04:40 +00:00
export const PLATFORM_NAME = import.meta.env.VITE_PLATFORM_NAME
export const PLATFORM_RELAY = import.meta.env.VITE_PLATFORM_RELAY
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
2024-11-27 00:39:49 +00:00
export const BURROW_URL = import.meta.env.VITE_BURROW_URL
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-09-24 21:12:38 +00:00
export const IMGPROXY_URL = "https://imgproxy.coracle.social"
2024-08-30 23:25:41 +00:00
export const REACTION_KINDS = [REACTION, ZAP_RESPONSE]
2024-08-13 19:41:25 +00:00
2024-11-19 17:25:02 +00:00
export const NIP46_PERMS =
2024-11-27 11:46:31 +00:00
"nip04_encrypt,nip04_decrypt,nip44_encrypt,nip44_decrypt," +
2024-12-10 21:07:17 +00:00
[CLIENT_AUTH, AUTH_JOIN, MESSAGE, THREAD, COMMENT, GROUPS, WRAP, REACTION]
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
export const imgproxy = (url: string, {w = 640, h = 1024} = {}) => {
if (!url || url.match("gif$")) {
return url
}
url = url.split("?")[0]
try {
return url ? `${IMGPROXY_URL}/x/s:${w}:${h}/${btoa(url)}` : url
} catch (e) {
return url
}
}
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}))
2024-12-05 21:37:15 +00:00
export const tagRoom = (room: string, url: string) => [ROOM, room]
2024-10-01 18:47:21 +00:00
2024-10-17 20:56:53 +00:00
export const getDefaultPubkeys = () => {
2024-10-22 18:04:40 +00:00
const appPubkeys = DEFAULT_PUBKEYS.split(",")
2024-10-17 20:56:53 +00:00
const userPubkeys = shuffle(getPubkeyTagValues(getListTags(get(userFollows))))
return userPubkeys.length > 5 ? userPubkeys : [...userPubkeys, ...appPubkeys]
}
const failedUnwraps = new Set()
2024-10-04 00:04:00 +00:00
export const ensureUnwrapped = async (event: TrustedEvent) => {
if (event.kind !== WRAP) {
return event
}
let rumor = repository.eventsByWrap.get(event.id)
if (rumor || failedUnwraps.has(event.id)) {
2024-10-04 00:04:00 +00:00
return rumor
}
for (const recipient of getPubkeyTagValues(event.tags)) {
const session = getSession(recipient)
const signer = getSigner(session)
if (signer) {
try {
rumor = await Nip59.fromSigner(signer).unwrap(event as SignedEvent)
break
} catch (e) {
// pass
}
}
}
if (rumor && isHashedEvent(rumor)) {
2024-10-08 20:20:55 +00:00
// Copy urls over to the rumor
2024-10-04 00:04:00 +00:00
tracker.copy(event.id, rumor.id)
2024-10-08 20:20:55 +00:00
// Send the rumor via our relay so listeners get updated
2024-10-04 00:04:00 +00:00
relay.send("EVENT", rumor)
} else {
failedUnwraps.add(event.id)
2024-10-04 00:04:00 +00:00
}
return rumor
}
2024-12-09 22:03:59 +00:00
export const trackerStore = makeTrackerStore()
2024-12-16 18:30:42 +00:00
export const repositoryStore = makeRepositoryStore()
2024-08-16 17:50:38 +00:00
export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
let attempted = false
const filters = getIdFilters([idOrAddress])
const relays = [...hints, ...INDEXER_RELAYS]
return derived(
deriveEvents(repository, {filters, includeDeleted: true}),
2024-08-19 16:06:39 +00:00
(events: TrustedEvent[]) => {
2024-08-16 17:50:38 +00:00
if (!attempted && events.length === 0) {
load({relays, filters})
attempted = true
}
return events[0]
},
)
}
export const getUrlsForEvent = derived([trackerStore, thunks], ([$tracker, $thunks]) => {
const getThunksByEventId = memoize(() => {
const thunksByEventId = new Map<string, Thunk[]>()
for (const thunk of walkThunks(Object.values($thunks))) {
pushToMapKey(thunksByEventId, thunk.event.id, thunk)
}
return thunksByEventId
})
return (id: string) => {
const urls = Array.from($tracker.getRelays(id))
for (const thunk of getThunksByEventId().get(id) || []) {
2025-04-14 23:45:52 +00:00
for (const url of thunk.options.relays) {
urls.push(url)
}
}
return uniq(urls)
}
})
2024-11-14 18:53:13 +00:00
2025-02-06 00:26:22 +00:00
export const getEventsForUrl = (url: string, filters: Filter[]) => {
2024-12-12 00:59:16 +00:00
const $getUrlsForEvent = get(getUrlsForEvent)
const $events = repository.query(filters)
return sortBy(
e => -e.created_at,
$events.filter(e => $getUrlsForEvent(e.id).includes(url)),
)
}
2024-10-24 22:48:45 +00:00
export const deriveEventsForUrl = (url: string, filters: Filter[]) =>
derived([deriveEvents(repository, {filters}), getUrlsForEvent], ([$events, $getUrlsForEvent]) =>
sortBy(
e => -e.created_at,
$events.filter(e => $getUrlsForEvent(e.id).includes(url)),
2024-10-11 00:15:20 +00:00
),
)
2024-12-05 21:37:15 +00:00
// Context
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
2024-10-17 22:46:26 +00:00
// Settings
export const canDecrypt = synced("canDecrypt", false)
2024-10-17 22:48:35 +00:00
export const SETTINGS = 38489
2024-10-17 22:46:26 +00:00
export type Settings = {
2024-10-17 23:03:50 +00:00
event: TrustedEvent
2024-10-17 22:46:26 +00:00
values: {
2024-10-24 18:02:16 +00:00
show_media: boolean
2024-10-17 22:46:26 +00:00
hide_sensitive: boolean
report_usage: boolean
report_errors: boolean
2024-10-31 19:24:40 +00:00
send_delay: number
2024-11-16 16:04:09 +00:00
upload_type: "nip96" | "blossom"
nip96_urls: string[]
blossom_urls: string[]
2024-10-17 23:03:50 +00:00
}
2024-10-17 22:46:26 +00:00
}
export const defaultSettings = {
2024-10-24 18:02:16 +00:00
show_media: true,
2024-10-17 22:46:26 +00:00
hide_sensitive: true,
report_usage: true,
report_errors: false,
2024-10-31 17:37:20 +00:00
send_delay: 3000,
2024-11-16 16:04:09 +00:00
upload_type: "nip96",
nip96_urls: ["https://nostr.build"],
blossom_urls: ["https://cdn.satellite.earth"],
2024-10-17 22:46:26 +00:00
}
export const settings = deriveEventsMapped<Settings>(repository, {
2024-10-17 22:48:35 +00:00
filters: [{kinds: [SETTINGS]}],
2024-10-17 22:46:26 +00:00
itemToEvent: item => item.event,
2024-10-17 23:03:50 +00:00
eventToItem: async (event: TrustedEvent) => ({
event,
values: {...defaultSettings, ...parseJson(await ensurePlaintext(event))},
}),
2024-10-17 22:46:26 +00:00
})
export const {
indexStore: settingsByPubkey,
deriveItem: deriveSettings,
loadItem: loadSettings,
} = collection({
name: "settings",
store: settings,
getKey: settings => settings.event.pubkey,
2025-04-11 16:27:19 +00:00
load: makeOutboxLoader(SETTINGS),
2024-10-17 22:46:26 +00:00
})
2025-02-25 21:36:32 +00:00
// Alerts
export type Alert = {
event: TrustedEvent
tags: string[][]
}
export const alerts = deriveEventsMapped<Alert>(repository, {
filters: [{kinds: [ALERT]}],
itemToEvent: item => item.event,
eventToItem: async event => {
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content))
return {event, tags}
},
})
// Alert Statuses
export type AlertStatus = {
event: TrustedEvent
tags: string[][]
}
export const alertStatuses = deriveEventsMapped<AlertStatus>(repository, {
filters: [{kinds: [ALERT_STATUS]}],
itemToEvent: item => item.event,
eventToItem: async event => {
const tags = parseJson(await decrypt(signer.get(), NOTIFIER_PUBKEY, event.content))
return {event, tags}
},
})
2025-04-15 18:03:27 +00:00
// Aliases
export type Alias = {
url: string
pubkey: string
profile: Profile
}
export const encodeAliasKey = (pubkey: string, url: string) => `${pubkey}:${url}`
export const decodeAliasKey = (key: string) => {
const [pubkey, url] = key.split(/:(.*)/s)
return {pubkey, url}
}
export const aliasesByKey = withGetter(writable(new Map<string, Alias>()))
export const loadAliasByKey = makeCachedLoader({
name: "aliases",
indexStore: aliasesByKey,
load: (key: string) => {
const {pubkey, url} = decodeAliasKey(key)
return load({
relays: [url],
filters: [{kinds: [PROFILE], authors: [pubkey]}],
onEvent: (event: TrustedEvent) => {
const profile = readProfile(event)
aliasesByKey.update($aliasesByKey => {
$aliasesByKey.set(key, {url, pubkey, profile})
return $aliasesByKey
})
},
})
},
})
export const deriveAlias = (pubkey: string, url?: string) => {
const membershipUrls = getMembershipUrls(userMembership.get())
// Attempt to load all relevant aliases
for (const $url of [url, ...membershipUrls]) {
if ($url) {
const key = encodeAliasKey(pubkey, $url)
loadAliasByKey(key)
}
}
return derived([aliasesByKey, deriveProfile(pubkey)], ([$aliasesByKey, $profile]) => {
// Try to find an alias for the url we were asked about
if (url) {
const alias = $aliasesByKey.get(encodeAliasKey(pubkey, url))
if (alias) {
return alias
}
}
// Fall back to global profiles
if ($profile) {
return {
pubkey,
url: LOCAL_RELAY_URL,
profile: $profile,
}
}
// Fall back to other aliases we know about
for (const $url of membershipUrls) {
const alias = $aliasesByKey.get(encodeAliasKey(pubkey, $url))
if (alias) {
return alias
}
}
})
}
export const deriveAliasDisplay = (pubkey: string, url?: string) =>
derived(deriveAlias(pubkey, url), $alias =>
displayProfile($alias?.profile, displayPubkey(pubkey)),
)
// Membership
2024-08-16 19:42:00 +00:00
export const hasMembershipUrl = (list: List | undefined, url: string) =>
getListTags(list).some(t => {
if (t[0] === "r") return t[1] === url
2024-12-05 21:37:15 +00:00
if (t[0] === "group") return t[2] === url
return false
})
2024-12-05 23:32:27 +00:00
export const getMembershipUrls = (list?: List) => {
const tags = getListTags(list)
2025-02-14 20:14:00 +00:00
return sort(
uniq([...getRelayTagValues(tags), ...getGroupTags(tags).map(nth(2))]).map(url =>
normalizeRelayUrl(url),
),
)
2024-12-05 23:32:27 +00:00
}
2024-08-06 22:46:37 +00:00
export const getMembershipRooms = (list?: List) =>
getGroupTags(getListTags(list)).map(([_, room, url, name = ""]) => ({url, room, name}))
2024-10-08 22:30:18 +00:00
export const getMembershipRoomsByUrl = (url: string, list?: List) =>
2025-02-25 21:36:32 +00:00
sort(getGroupTags(getListTags(list)).filter(nthEq(2, url)).map(nth(1)))
2024-08-13 19:41:25 +00:00
2024-10-08 22:30:18 +00:00
export const memberships = deriveEventsMapped<PublishedList>(repository, {
2024-12-10 21:07:17 +00:00
filters: [{kinds: [GROUPS]}],
2024-08-13 19:41:25 +00:00
itemToEvent: item => item.event,
2024-10-09 23:15:09 +00:00
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
2024-08-13 19:41:25 +00:00
})
2024-08-06 22:46:37 +00:00
2024-08-13 19:41:25 +00:00
export const {
indexStore: membershipByPubkey,
deriveItem: deriveMembership,
loadItem: loadMembership,
2024-08-30 23:25:41 +00:00
} = collection({
name: "memberships",
store: memberships,
2024-10-08 22:30:18 +00:00
getKey: list => list.event.pubkey,
2025-04-11 16:27:19 +00:00
load: makeOutboxLoader(GROUPS),
2024-08-15 19:08:07 +00:00
})
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 const chatMessages = deriveEvents(repository, {filters: [{kinds: [DIRECT_MESSAGE]}]})
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
}
export const makeChatId = (pubkeys: string[]) => sort(uniq(pubkeys.concat(pubkey.get()!))).join(",")
2024-10-01 23:27:25 +00:00
2024-10-03 21:43:17 +00:00
export const splitChatId = (id: string) => id.split(",")
2024-10-01 23:27:25 +00:00
2024-10-08 18:39:16 +00:00
export const chats = derived(
[pubkey, chatMessages, profilesByPubkey],
([$pubkey, $messages, $profilesByPubkey]) => {
const messagesByChatId = new Map<string, TrustedEvent[]>()
2024-10-01 23:27:25 +00:00
2024-10-08 18:39:16 +00:00
for (const message of $messages) {
2024-10-08 20:20:55 +00:00
const chatId = makeChatId(getPubkeyTagValues(message.tags).concat(message.pubkey))
2024-10-01 23:27:25 +00:00
2024-10-08 18:39:16 +00:00
pushToMapKey(messagesByChatId, chatId, message)
}
2024-10-01 23:27:25 +00:00
2025-01-17 23:01:05 +00:00
const displayPubkey = (pubkey: string) => {
const profile = $profilesByPubkey.get(pubkey)
return profile ? displayProfile(profile) : ""
}
2024-10-08 18:39:16 +00:00
return sortBy(
c => -c.last_activity,
Array.from(messagesByChatId.entries()).map(([id, events]): Chat => {
2025-01-17 23:01:05 +00:00
const pubkeys = remove($pubkey!, splitChatId(id))
2024-10-08 18:39:16 +00:00
const messages = sortBy(e => -e.created_at, events)
const last_activity = messages[0].created_at
2025-01-17 23:01:05 +00:00
const search_text =
pubkeys.length === 0
? displayPubkey($pubkey!) + " note to self"
: pubkeys.map(displayPubkey).join(" ")
2024-10-08 18:39:16 +00:00
return {id, pubkeys, messages, last_activity, search_text}
}),
)
},
)
2024-10-01 23:27:25 +00:00
export const {
2024-10-03 21:43:17 +00:00
indexStore: chatsById,
deriveItem: deriveChat,
loadItem: loadChat,
2024-10-01 23:27:25 +00:00
} = collection({
2024-10-03 21:43:17 +00:00
name: "chats",
store: chats,
getKey: chat => chat.id,
2025-04-15 18:03:27 +00:00
load: always(Promise.resolve()),
2024-10-01 23:27:25 +00:00
})
2024-10-04 00:04:00 +00:00
export const chatSearch = derived(chats, $chats =>
createSearch($chats, {
getValue: (chat: Chat) => chat.id,
fuseOptions: {keys: ["search_text"]},
}),
)
2024-12-05 21:37:15 +00:00
// Messages
2024-12-10 23:33:36 +00:00
export const messages = derived(
2025-01-24 17:36:24 +00:00
deriveEvents(repository, {filters: [{kinds: [MESSAGE]}]}),
$events => $events,
)
2024-12-05 21:37:15 +00:00
2024-12-12 22:49:30 +00:00
// Nip29
2024-12-10 01:04:39 +00:00
export const groupMeta = deriveEvents(repository, {filters: [{kinds: [GROUP_META]}]})
2024-12-12 22:49:30 +00:00
export const hasNip29 = (relay?: Relay) =>
relay?.profile?.supported_nips?.map(String)?.includes("29")
2024-12-05 21:37:15 +00:00
// Channels
2024-12-10 01:04:39 +00:00
export type ChannelMeta = {
access: "public" | "private"
membership: "open" | "closed"
picture?: string
about?: string
}
2024-12-05 21:37:15 +00:00
export type Channel = {
url: string
room: string
name: string
2024-12-10 01:04:39 +00:00
meta?: ChannelMeta
2024-12-05 21:37:15 +00:00
}
export const makeChannelId = (url: string, room: string) => `${url}|${room}`
export const splitChannelId = (id: string) => id.split("|")
2024-12-10 01:04:39 +00:00
export const channelsById = withGetter(
derived(
[groupMeta, memberships, messages, getUrlsForEvent],
([$groupMeta, $memberships, $messages, $getUrlsForEvent]) => {
const channelsById = new Map<string, Channel>()
2024-12-05 21:37:15 +00:00
// Add meta using group meta events
for (const event of $groupMeta) {
const meta = fromPairs(event.tags)
const room = meta.d
2024-12-05 21:37:15 +00:00
2024-12-10 01:04:39 +00:00
if (room) {
for (const url of $getUrlsForEvent(event.id)) {
const id = makeChannelId(url, room)
channelsById.set(id, {
url,
room,
name: meta.name || room,
meta: {
access: meta.private ? "private" : "public",
membership: meta.closed ? "closed" : "open",
picture: meta.picture,
about: meta.about,
},
})
2024-12-10 01:04:39 +00:00
}
2024-12-05 21:37:15 +00:00
}
}
// Add known rooms based on membership events
for (const membership of $memberships) {
for (const {url, room, name} of getMembershipRooms(membership)) {
const id = makeChannelId(url, room)
2024-12-05 21:37:15 +00:00
if (!channelsById.has(id)) {
channelsById.set(id, {url, room, name})
2024-12-10 01:04:39 +00:00
}
2024-12-05 21:37:15 +00:00
}
}
// Add rooms based on known messages
for (const event of $messages) {
const [_, room] = event.tags.find(nthEq(0, ROOM)) || []
2024-12-10 01:04:39 +00:00
if (room) {
for (const url of $getUrlsForEvent(event.id)) {
2024-12-10 01:04:39 +00:00
const id = makeChannelId(url, room)
if (!channelsById.has(id)) {
channelsById.set(id, {url, room, name: room})
2024-12-10 01:04:39 +00:00
}
}
}
}
2024-12-05 21:37:15 +00:00
2024-12-10 01:04:39 +00:00
return channelsById
},
),
2024-12-05 21:37:15 +00:00
)
export const deriveChannel = (url: string, room: string) =>
derived(channelsById, $channelsById => $channelsById.get(makeChannelId(url, room)))
2024-12-10 01:04:39 +00:00
export const channelsByUrl = derived(channelsById, $channelsById => {
const $channelsByUrl = new Map<string, Channel[]>()
2024-09-11 19:19:45 +00:00
2024-12-10 01:04:39 +00:00
for (const channel of $channelsById.values()) {
pushToMapKey($channelsByUrl, channel.url, channel)
2024-09-11 19:19:45 +00:00
}
2024-12-10 01:04:39 +00:00
return $channelsByUrl
2024-09-11 19:19:45 +00:00
})
2024-12-10 01:04:39 +00:00
export const displayChannel = (url: string, room: string) => {
2024-12-05 21:37:15 +00:00
if (room === GENERAL) {
return "general"
}
2024-12-10 01:04:39 +00:00
return channelsById.get().get(makeChannelId(url, room))?.name || room
2024-12-05 21:37:15 +00:00
}
2024-12-10 21:02:41 +00:00
export const roomComparator = (url: string) => (room: string) =>
displayChannel(url, room).toLowerCase()
2024-12-10 21:38:34 +00:00
export const channelIsLocked = (channel?: Channel) =>
channel?.meta?.access === "private" && channel?.meta?.membership === "closed"
// User stuff
2024-10-17 22:46:26 +00:00
export const userSettings = withGetter(
derived([pubkey, settingsByPubkey], ([$pubkey, $settingsByPubkey]) => {
if (!$pubkey) return undefined
loadSettings($pubkey)
return $settingsByPubkey.get($pubkey)
}),
)
2024-11-16 16:04:09 +00:00
export const userSettingValues = withGetter(
derived(userSettings, $s => $s?.values || defaultSettings),
)
2025-01-02 23:08:16 +00:00
export const getSetting = <T>(key: keyof Settings["values"]) => userSettingValues.get()[key] as T
2024-10-24 18:02:16 +00:00
export const userMembership = withGetter(
derived([pubkey, membershipByPubkey], ([$pubkey, $membershipByPubkey]) => {
2024-10-08 22:30:18 +00:00
if (!$pubkey) return undefined
loadMembership($pubkey)
return $membershipByPubkey.get($pubkey)
}),
2024-08-16 17:50:38 +00:00
)
2024-12-11 00:38:22 +00:00
export const userRoomsByUrl = withGetter(
derived(userMembership, $userMembership => {
2024-12-16 18:30:42 +00:00
const tags = getListTags($userMembership)
2024-12-11 00:38:22 +00:00
const $userRoomsByUrl = new Map<string, Set<string>>()
2024-12-16 18:30:42 +00:00
for (const [_, room, url] of getGroupTags(tags)) {
2024-12-11 00:38:22 +00:00
addToMapKey($userRoomsByUrl, url, room)
}
2024-12-16 18:30:42 +00:00
for (const url of getRelayTagValues(tags)) {
addToMapKey($userRoomsByUrl, url, GENERAL)
}
2024-12-11 00:38:22 +00:00
return $userRoomsByUrl
}),
)
2024-12-10 21:02:41 +00:00
export const deriveUserRooms = (url: string) =>
2024-12-11 00:38:22 +00:00
derived(userRoomsByUrl, $userRoomsByUrl =>
2024-12-17 01:07:52 +00:00
sortBy(roomComparator(url), uniq(Array.from($userRoomsByUrl.get(url) || [GENERAL]))),
2024-12-11 00:38:22 +00:00
)
2024-12-10 21:02:41 +00:00
export const deriveOtherRooms = (url: string) =>
derived([deriveUserRooms(url), channelsByUrl], ([$userRooms, $channelsByUrl]) =>
sortBy(
roomComparator(url),
($channelsByUrl.get(url) || []).filter(c => !$userRooms.includes(c.room)).map(c => c.room),
),
)
// 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
}