diff --git a/src/app/commands.ts b/src/app/commands.ts index 91ef1593..425476ce 100644 --- a/src/app/commands.ts +++ b/src/app/commands.ts @@ -1,12 +1,5 @@ -import {uniqBy, uniq, now, choice} from "@welshman/lib" -import { - GROUPS, - GROUP_JOIN, - getGroupTags, - getRelayTagValues, - createEvent, - displayProfile, -} from "@welshman/util" +import {uniqBy, equals, uniq, now, choice} from "@welshman/lib" +import {getRelayTagValues, createEvent, displayProfile} from "@welshman/util" import {PublishStatus} from "@welshman/net" import { pubkey, @@ -22,7 +15,7 @@ import { loadFollows, loadMutes, } from "@welshman/app" -import {loadGroup, loadGroupMembership, INDEXER_RELAYS} from "@app/state" +import {MEMBERSHIPS, INDEXER_RELAYS} from "@app/state" // Utils @@ -57,11 +50,7 @@ export const makeIMeta = (url: string, data: Record) => [ // Loaders export const loadUserData = (pubkey: string, hints: string[] = []) => - Promise.all([ - loadProfile(pubkey), - loadFollows(pubkey), - loadMutes(pubkey), - ]) + Promise.all([loadProfile(pubkey), loadFollows(pubkey), loadMutes(pubkey)]) // Updates @@ -80,26 +69,14 @@ export const updateList = async (kind: number, modifyTags: ModifyTags) => { publishThunk(makeThunk({event, relays})) } -export const addGroupMemberships = (newTags: string[][]) => - updateList(GROUPS, (tags: string[][]) => uniqBy(t => t.join(""), [...tags, ...newTags])) +export const addSpaceMembership = (url: string) => + updateList(MEMBERSHIPS, (tags: string[][]) => uniqBy(t => t.join(""), [...tags, ["r", url]])) -export const removeGroupMemberships = (noms: string[]) => - updateList(GROUPS, (tags: string[][]) => tags.filter(t => !noms.includes(t[1]))) +export const addRoomMembership = (url: string, topic: string) => + updateList(MEMBERSHIPS, (tags: string[][]) => uniqBy(t => t.join(""), [...tags, ["t", topic, url]])) -export const sendJoinRequest = async (nom: string, url: string): Promise<[boolean, string]> => { - const relays = [url] - const filters = [{kinds: [9000], "#h": [nom], "#p": [pubkey.get()!], since: now() - 30}] +export const removeSpaceMembership = (url: string) => + updateList(MEMBERSHIPS, (tags: string[][]) => tags.filter(t => !equals(["r", url], t) && t[2] !== url)) - const event = createEvent(GROUP_JOIN, {tags: [["h", nom]]}) - const statusData = await publishThunk(makeThunk({event, relays})) - const {status, message} = statusData[url] - - if (message.includes("already a member")) return [true, ""] - if (status !== PublishStatus.Success) return [false, message] - if (await load({filters, relays})) return [true, ""] - - return [ - false, - "Your request was not automatically approved, but may be approved manually later. Please try again later or contact the group admin.", - ] -} +export const removeRoomMembership = (url: string, topic: string) => + updateList(MEMBERSHIPS, (tags: string[][]) => tags.filter(t => !equals(["t", topic, url], t))) diff --git a/src/app/components/GroupCompose.svelte b/src/app/components/GroupCompose.svelte index 07513874..30f66b9d 100644 --- a/src/app/components/GroupCompose.svelte +++ b/src/app/components/GroupCompose.svelte @@ -3,17 +3,17 @@ import type {Readable} from "svelte/store" import {writable} from "svelte/store" import {createEditor, type Editor, EditorContent} from "svelte-tiptap" - import {NProfileExtension, TagExtension as TopicExtension, ImageExtension} from "nostr-editor" + import {NProfileExtension, ImageExtension} from "nostr-editor" import {createEvent, CHAT_MESSAGE} from "@welshman/util" import {publishThunk, makeThunk} from "@welshman/app" import {findNodes} from "@lib/tiptap" import Icon from "@lib/components/Icon.svelte" import Button from "@lib/components/Button.svelte" - import {userRelayUrlsByNom} from "@app/state" import {makeMention, makeIMeta} from "@app/commands" import {getChatEditorOptions, addFile} from "@app/editor" - export let nom + export let url + export let topic = "" const uploading = writable(false) @@ -21,22 +21,20 @@ const sendMessage = () => { const json = $editor.getJSON() - const relays = $userRelayUrlsByNom.get(nom) + const topicTag = topic ? ["t", topic] : [] + const mentionTags = findNodes(NProfileExtension.name, json).map(m => + makeMention(m.attrs!.pubkey, m.attrs!.relays), + ) + const imetaTags = findNodes(ImageExtension.name, json).map(({attrs: {src, sha256: x}}: any) => + makeIMeta(src, {x, ox: x}), + ) + const event = createEvent(CHAT_MESSAGE, { content: $editor.getText(), - tags: [ - ["h", nom], - ...findNodes(TopicExtension.name, json).map(t => ["t", t.attrs!.name.toLowerCase()]), - ...findNodes(NProfileExtension.name, json).map(m => - makeMention(m.attrs!.pubkey, m.attrs!.relays), - ), - ...findNodes(ImageExtension.name, json).map(({attrs: {src, sha256: x}}: any) => - makeIMeta(src, {x, ox: x}), - ), - ], + tags: [topicTag, ...mentionTags, ...imetaTags], }) - publishThunk(makeThunk({event, relays})) + publishThunk(makeThunk({event, relays: [url]})) $editor.chain().clearContent().run() } diff --git a/src/app/components/GroupNote.svelte b/src/app/components/GroupNote.svelte index a065162c..1a9638d8 100644 --- a/src/app/components/GroupNote.svelte +++ b/src/app/components/GroupNote.svelte @@ -27,7 +27,7 @@ import Icon from "@lib/components/Icon.svelte" import Button from "@lib/components/Button.svelte" import Avatar from "@lib/components/Avatar.svelte" - import {deriveEvent} from "@app/state" + import {deriveEvent, displayReaction} from "@app/state" import {getChatViewOptions} from "@app/editor" export let event: TrustedEvent @@ -66,12 +66,6 @@ const [colorName, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length] const ps = derived(publishStatusData, $m => Object.values($m[event.id] || {})) - const displayReaction = (content: string) => { - if (content === "+") return "❤️" - if (content === "-") return "👎" - return content - } - const findStatus = ($ps: PublishStatusData[], statuses: PublishStatus[]) => $ps.find(({status}) => statuses.includes(status)) diff --git a/src/app/components/InfoNip29.svelte b/src/app/components/InfoRelay.svelte similarity index 53% rename from src/app/components/InfoNip29.svelte rename to src/app/components/InfoRelay.svelte index f0cc28a6..6c06d4fb 100644 --- a/src/app/components/InfoNip29.svelte +++ b/src/app/components/InfoRelay.svelte @@ -3,7 +3,6 @@ import Button from "@lib/components/Button.svelte" import Link from "@lib/components/Link.svelte" import Icon from "@lib/components/Icon.svelte" - import {DEFAULT_RELAYS} from "@app/state" import {clip} from "@app/toast" @@ -17,22 +16,8 @@ This means that anyone can host their own data, making the web more decentralized and resilient.

- Only some relays support spaces. You can find a list of suggested relays below, or you can host your own. If you do decide to join someone else's, make sure to follow their directions for registering - as a user. + Different relays have different policies for access control and content retention. Be sure to + double check that you have access to the relays you try to use by visiting their website.

- {#each DEFAULT_RELAYS as url} -
-
- - {displayRelayUrl(url)} -
- -
- {/each} diff --git a/src/app/components/LogIn.svelte b/src/app/components/LogIn.svelte index dd730aae..9f7cce3c 100644 --- a/src/app/components/LogIn.svelte +++ b/src/app/components/LogIn.svelte @@ -60,7 +60,7 @@ const pubkey = await getNip07()?.getPublicKey() if (pubkey) { - await onSuccess({method: "extension", pubkey}) + await onSuccess({method: "nip07", pubkey}) } else { pushToast({ theme: "error", diff --git a/src/app/components/PrimaryNav.svelte b/src/app/components/PrimaryNav.svelte index 59d5cc74..ef007025 100644 --- a/src/app/components/PrimaryNav.svelte +++ b/src/app/components/PrimaryNav.svelte @@ -9,13 +9,15 @@ import {page} from "$app/stores" import {tweened} from "svelte/motion" import {quintOut} from "svelte/easing" + import {displayRelayUrl} from "@welshman/util" import Icon from "@lib/components/Icon.svelte" import Avatar from "@lib/components/Avatar.svelte" import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte" import SpaceAdd from "@app/components/SpaceAdd.svelte" - import {userProfile, displayGroup, userGroupsByNom} from "@app/state" + import SpaceAvatar from "@app/components/SpaceAvatar.svelte" + import {userProfile, userMembership} from "@app/state" import {pushModal} from "@app/modal" - import {getPrimaryNavItemIndex} from "@app/routes" + import {makeSpacePath, getPrimaryNavItemIndex} from "@app/routes" const activeOffset = tweened(-44, { duration: 300, @@ -49,15 +51,9 @@ class="!h-10 !w-10 border border-solid border-base-300" size={7} /> - {#each $userGroupsByNom.entries() as [nom, qualifiedGroups] (nom)} - {@const qualifiedGroup = qualifiedGroups[0]} - - + {#each $userMembership?.topicsByUrl.keys() || [] as url (url)} + + {/each} diff --git a/src/app/components/RoomCreate.svelte b/src/app/components/RoomCreate.svelte new file mode 100644 index 00000000..bbd83a0b --- /dev/null +++ b/src/app/components/RoomCreate.svelte @@ -0,0 +1,65 @@ + + +
+

+ Create a Room +

+

+ On {displayRelayUrl(url)} +

+ +

Room Name

+ +
+
+ + +
+
diff --git a/src/app/components/SpaceAvatar.svelte b/src/app/components/SpaceAvatar.svelte new file mode 100644 index 00000000..ea7222ee --- /dev/null +++ b/src/app/components/SpaceAvatar.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/app/components/SpaceCreate.svelte b/src/app/components/SpaceCreate.svelte index b661a3b4..905c20a6 100644 --- a/src/app/components/SpaceCreate.svelte +++ b/src/app/components/SpaceCreate.svelte @@ -3,7 +3,7 @@ import Button from "@lib/components/Button.svelte" import Field from "@lib/components/Field.svelte" import Icon from "@lib/components/Icon.svelte" - import InfoNip29 from "@app/components/InfoNip29.svelte" + import InfoRelay from "@app/components/InfoRelay.svelte" import SpaceCreateFinish from "@app/components/SpaceCreateFinish.svelte" import {pushModal} from "@app/modal" @@ -38,8 +38,8 @@

- This should be a NIP-29 compatible nostr relay where you'd like to host your space. - + This can be any nostr relay where you'd like to host your space. +

diff --git a/src/app/components/SpaceExit.svelte b/src/app/components/SpaceExit.svelte index 84658813..c6e810ea 100644 --- a/src/app/components/SpaceExit.svelte +++ b/src/app/components/SpaceExit.svelte @@ -1,14 +1,12 @@
@@ -74,7 +61,7 @@

Invite Link*

diff --git a/src/app/components/SpaceJoin.svelte b/src/app/components/SpaceJoin.svelte index 532c701a..9ef3dca2 100644 --- a/src/app/components/SpaceJoin.svelte +++ b/src/app/components/SpaceJoin.svelte @@ -1,37 +1,22 @@

- Joining {displayGroup($group)} + Joining {displayRelayUrl(url)}

-

- Please select which relays you'd like to use for this group. - -

- {#each urlOptions as url} -
-
- - {displayRelayUrl(url)} -
- -
- {/each} +

Are you sure you'd like to join this space?

- diff --git a/src/app/editor.ts b/src/app/editor.ts index 0486ddad..0028cfc6 100644 --- a/src/app/editor.ts +++ b/src/app/editor.ts @@ -19,7 +19,6 @@ import { ImageExtension, VideoExtension, FileUploadExtension, - TagExtension as TopicExtension, } from "nostr-editor" import type {StampedEvent} from "@welshman/util" import {signer, topicSearch, profileSearch} from "@welshman/app" @@ -111,35 +110,6 @@ export const getChatEditorOptions = ({uploading, sendMessage}: ChatComposeEditor VideoExtension.extend( asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeVideo)}), ).configure({defaultUploadUrl: "https://nostr.build", defaultUploadType: "nip96"}), - TopicExtension.extend({ - addNodeView: () => SvelteNodeViewRenderer(GroupComposeTopic), - renderHTML({mark, HTMLAttributes}) { - const attrs = { - ...mark.attrs, - ...HTMLAttributes, - target: "_blank", - rel: "noopener noreferer", - href: `https://coracle.social/topics/${mark.attrs.tag.toLowerCase()}`, - class: "underline", - } - - return ["a", attrs, 0] - }, - addProseMirrorPlugins() { - return [ - createSuggestions({ - char: "#", - name: "topic", - editor: this.editor, - search: topicSearch, - select: (name: string, props: any) => props.command({name}), - allowCreate: true, - suggestionComponent: GroupComposeTopicSuggestion, - suggestionsComponent: GroupComposeSuggestions, - }), - ] - }, - }), FileUploadExtension.configure({ immediateUpload: false, sign: (event: StampedEvent) => { @@ -180,8 +150,5 @@ export const getChatViewOptions = (content: string) => ({ NAddrExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeEvent)})), ImageExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeImage)})), VideoExtension.extend(asInline({addNodeView: () => SvelteNodeViewRenderer(GroupComposeVideo)})), - TopicExtension.extend({ - addNodeView: () => SvelteNodeViewRenderer(GroupComposeTopic), - }), ], }) diff --git a/src/app/routes.ts b/src/app/routes.ts index 4bf5829b..9c67a5e7 100644 --- a/src/app/routes.ts +++ b/src/app/routes.ts @@ -1,18 +1,31 @@ +import {nip19} from 'nostr-tools' import type {Page} from "@sveltejs/kit" -import {userGroupsByNom} from "@app/state" +import {userMembership, decodeNEvent} from "@app/state" -export const makeSpacePath = (nom: string) => `/spaces/${nom}` +export const makeSpacePath = (url: string, extra = "") => { + let path = `/spaces/${nip19.nrelayEncode(url)}` + + if (extra) { + path += '/' + extra + } + + return path +} export const getPrimaryNavItem = ($page: Page) => $page.route?.id?.split("/")[1] export const getPrimaryNavItemIndex = ($page: Page) => { + const urls = Array.from(userMembership.get()?.topicsByUrl.keys() || []) + switch (getPrimaryNavItem($page)) { case "discover": - return userGroupsByNom.get().size + 2 + return urls.length + 2 case "spaces": - return Array.from(userGroupsByNom.get().keys()).findIndex(nom => nom === $page.params.nom) + 1 + const routeUrl = decodeNEvent($page.params.nrelay) + + return urls.findIndex(url => url === routeUrl) + 1 case "settings": - return userGroupsByNom.get().size + 3 + return urls.length + 3 default: return 0 } diff --git a/src/app/state.ts b/src/app/state.ts index 3795bc2d..afd117cf 100644 --- a/src/app/state.ts +++ b/src/app/state.ts @@ -1,18 +1,29 @@ +import {nip19} from 'nostr-tools' import type {FuseResult} from "fuse.js" -import {get, derived} from "svelte/store" +import {get, derived, writable} from "svelte/store" import type {Maybe} from "@welshman/lib" -import {setContext, max, between, groupBy, pushToMapKey, nthEq, stripProtocol, indexBy} from "@welshman/lib" +import { + setContext, + max, + between, + groupBy, + pushToMapKey, + nthEq, + stripProtocol, + indexBy, + uniq, +} from "@welshman/lib" import { getIdFilters, getIdentifier, normalizeRelayUrl, - GROUP_META, - GROUPS, - getGroupTags, - GROUP_JOIN, - GROUP_ADD_USER, + RELAYS, + APP_DATA, REACTION, ZAP_RESPONSE, + getRelayTagValues, + getTopicTagValues, + isShareableRelayUrl, } from "@welshman/util" import type {TrustedEvent} from "@welshman/util" import { @@ -20,6 +31,7 @@ import { repository, createSearch, load, + subscribe, collection, loadRelay, relaysByPubkey, @@ -32,15 +44,14 @@ import { makeRouter, } from "@welshman/app" import type {Relay} from "@welshman/app" -import type {SubscribeRequest} from "@welshman/net" +import type {SubscribeRequestWithHandlers} from "@welshman/net" import {deriveEvents, deriveEventsMapped, withGetter} from "@welshman/store" -export const DEFAULT_RELAYS = [ - "wss://groups.fiatjaf.com/", - "wss://relay29.galaxoidlabs.com/", - "wss://devrelay.highlighter.com/", - "wss://relay.groups.nip29.com/", -] +export const MESSAGE = 209 + +export const REPLY = 210 + +export const MEMBERSHIPS = 30209 export const INDEXER_RELAYS = ["wss://purplepag.es/", "wss://relay.damus.io/", "wss://nos.lol/"] @@ -58,7 +69,6 @@ setContext({ }), }) - export const deriveEvent = (idOrAddress: string, hints: string[] = []) => { let attempted = false @@ -78,237 +88,122 @@ export const deriveEvent = (idOrAddress: string, hints: string[] = []) => { ) } -// Groups +// Persist relay/event mappings -export const GROUP_DELIMITER = `'` +export const relaysByMessage = withGetter(writable(new Map())) -export const makeGroupId = (url: string, nom: string) => - [stripProtocol(url).replace(/\/$/, ""), nom].join(GROUP_DELIMITER) +// Topics -export const splitGroupId = (groupId: string) => { - const [url, nom] = groupId.split(GROUP_DELIMITER) +export const topicsByUrl = withGetter(writable(new Map())) - return [normalizeRelayUrl(url), nom] -} +// Membership -export const getGroupUrl = (groupId: string) => splitGroupId(groupId)[0] - -export const getGroupNom = (groupId: string) => splitGroupId(groupId)[1] - -export const getGroupName = (e?: TrustedEvent) => e?.tags.find(nthEq(0, "name"))?.[1] - -export const getGroupPicture = (e?: TrustedEvent) => e?.tags.find(nthEq(0, "picture"))?.[1] - -export const displayGroup = (group?: Group) => group?.name || group?.nom || "[no name]" - -export type Group = { - nom: string - name?: string - about?: string - picture?: string +export type Membership = { + topicsByUrl: Map event?: TrustedEvent } -export type PublishedGroup = Omit & { +export type PublishedMembership = Omit & { event: TrustedEvent } -export const readGroup = (event: TrustedEvent) => { - const nom = getIdentifier(event)! - const name = event?.tags.find(nthEq(0, "name"))?.[1] - const about = event?.tags.find(nthEq(0, "about"))?.[1] - const picture = event?.tags.find(nthEq(0, "picture"))?.[1] +export const readMembership = (event: TrustedEvent): PublishedMembership => { + const topicsByUrl = new Map() - return {nom, name, about, picture, event} -} - -export const groups = deriveEventsMapped(repository, { - filters: [{kinds: [GROUP_META]}], - eventToItem: readGroup, - itemToEvent: item => item.event, -}) - -export const { - indexStore: groupsByNom, - deriveItem: deriveGroup, - loadItem: loadGroup, -} = collection({ - name: "groups", - store: groups, - getKey: (group: PublishedGroup) => group.nom, - load: async (nom: string, hints: string[] = [], request: Partial = {}) => { - if (hints.length === 0) { - hints = relayUrlsByNom.get().get(nom) || [] - } - - await Promise.all([ - ...hints.map(loadRelay), - load({ - ...request, - relays: hints, - filters: [{kinds: [GROUP_META], "#d": [nom]}], - }), - ]) - }, -}) - -export const searchGroups = derived(groups, $groups => - createSearch($groups, { - getValue: (group: PublishedGroup) => group.nom, - sortFn: (result: FuseResult) => { - const scale = result.item.picture ? 0.5 : 1 - - return result.score! * scale - }, - fuseOptions: { - keys: ["name", {name: "about", weight: 0.3}], - }, - }), -) - -// Qualified groups - -export type QualifiedGroup = { - id: string - relay: Relay - group: PublishedGroup -} - -export const qualifiedGroups = derived([relaysByPubkey, groups], ([$relaysByPubkey, $groups]) => - $groups.flatMap((group: PublishedGroup) => { - const relays = $relaysByPubkey.get(group.event.pubkey) || [] - - return relays.map(relay => ({id: makeGroupId(relay.url, group.nom), relay, group})) - }), -) - -export const qualifiedGroupsById = derived(qualifiedGroups, $qualifiedGroups => - indexBy($qg => $qg.id, $qualifiedGroups), -) - -export const qualifiedGroupsByNom = derived(qualifiedGroups, $qualifiedGroups => - groupBy($qg => $qg.group.nom, $qualifiedGroups), -) - -export const relayUrlsByNom = withGetter( - derived(qualifiedGroups, $qualifiedGroups => { - const $relayUrlsByNom = new Map() - - for (const {relay, group} of $qualifiedGroups) { - pushToMapKey($relayUrlsByNom, group.nom, relay.url) - } - - return $relayUrlsByNom - }), -) - -// Group membership - -export type GroupMembership = { - ids: Set - noms: Set - urls: Set - event?: TrustedEvent -} - -export type PublishedGroupMembership = Omit & { - event: TrustedEvent -} - -export const readGroupMembership = (event: TrustedEvent) => { - const ids = new Set() - const noms = new Set() - const urls = new Set() - - for (const [_, nom, url] of getGroupTags(event.tags)) { - ids.add(makeGroupId(url, nom)) - noms.add(nom) - urls.add(url) + for (const tag of event.tags.filter(nthEq(0, "r"))) { + topicsByUrl.set(tag[1], []) } - return {event, ids, noms, urls} + for (const tag of event.tags.filter(nthEq(0, "t"))) { + pushToMapKey(topicsByUrl, tag[2], tag[1]) + } + + return {event, topicsByUrl} } -export const groupMemberships = deriveEventsMapped(repository, { - filters: [{kinds: [GROUPS]}], - eventToItem: readGroupMembership, +export const memberships = deriveEventsMapped(repository, { + filters: [{kinds: [MEMBERSHIPS]}], + eventToItem: readMembership, itemToEvent: item => item.event, }) export const { - indexStore: groupMembershipByPubkey, - deriveItem: deriveGroupMembership, - loadItem: loadGroupMembership, + indexStore: membershipByPubkey, + deriveItem: deriveMembership, + loadItem: loadMembership, } = collection({ - name: "groupMemberships", - store: groupMemberships, - getKey: groupMembership => groupMembership.event.pubkey, - load: async (pubkey: string, hints = [], request: Partial = {}) => { - const relays = getWriteRelayUrls(await loadRelaySelections(pubkey, hints)) - - return load({ + name: "memberships", + store: memberships, + getKey: membership => membership.event.pubkey, + load: (pubkey: string, request: Partial = {}) => + load({ ...request, - relays: [...hints, ...relays, ...INDEXER_RELAYS], - filters: [{kinds: [GROUPS], authors: [pubkey]}], - }) - }, + filters: [{kinds: [MEMBERSHIPS], authors: [pubkey]}], + }), }) -// Group Messages +// Messages -export type GroupMessage = { - nom: string +export type Message = { + url: string + chat: string + topic: string event: TrustedEvent } -export const readGroupMessage = (event: TrustedEvent): Maybe => { - const nom = event.tags.find(nthEq(0, "h"))?.[1] +export const readMessage = (event: TrustedEvent): Maybe => { + const topics = getTopicTagValues(event.tags) - if ( - !nom || - between(GROUP_ADD_USER - 1, GROUP_JOIN + 1, event.kind) || - REACTION_KINDS.includes(event.kind) - ) { - return undefined - } + if (topics.length !== 1) return undefined - return {nom, event} + const topic = topics[0] + const urls = relaysByMessage.get().get(event.id) || [] + + return urls.map(url => ({url, topic, chat: getChatId(url, topic), event})) } -export const groupMessages = deriveEventsMapped(repository, { +export const messages = deriveEventsMapped(repository, { filters: [{}], - eventToItem: readGroupMessage, + eventToItem: readMessage, itemToEvent: item => item.event, }) -// Group Chats +// Chats -export type GroupChat = { - nom: string - messages: GroupMessage[] +export type Chat = { + id: string + url: string + topic: string + messages: Message[] } -export const groupChats = derived(groupMessages, $groupMessages => { - const groupMessagesByNom = groupBy($groupMessage => $groupMessage.nom, $groupMessages) +export const getChatId = (url: string, topic: string) => `${url}'${topic}` - return Array.from(groupMessagesByNom.entries()).map(([nom, messages]) => ({nom, messages})) -}) +export const splitChatId = (id: string) => id.split("'") + +export const chats = derived(messages, $messages => + Array.from(groupBy($message => $message.chat, $messages).values()).map(messages => { + const {chat, url, topic} = messages[0] + + return {id: chat, url, topic, messages} + }), +) export const { - indexStore: groupChatByNom, - deriveItem: deriveGroupChat, - loadItem: loadGroupChat, + indexStore: chatsById, + deriveItem: deriveChat, + loadItem: loadChat, } = collection({ - name: "groupChats", - store: groupChats, - getKey: groupChat => groupChat.nom, - load: (nom: string, hints = [], request: Partial = {}) => { - const relays = [...hints, ...(get(relayUrlsByNom).get(nom) || [])] - const chat = get(groupChats).find(c => c.nom === nom) + name: "chats", + store: chats, + getKey: chat => chat.id, + load: (id: string, request: Partial = {}) => { + const [url, topic] = splitChatId(id) + const chat = get(chatsById).get(id) const timestamps = chat?.messages.map(m => m.event.created_at) || [] const since = Math.max(0, max(timestamps) - 3600) - return load({...request, relays, filters: [{"#h": [nom], since}]}) + return load({...request, relays: [url], filters: [{"#t": [topic], since}]}) }, }) @@ -322,47 +217,34 @@ export const userProfile = derived([pubkey, profilesByPubkey], ([$pubkey, $profi return $profilesByPubkey.get($pubkey) }) -export const userMembership = derived( - [pubkey, groupMembershipByPubkey], - ([$pubkey, $groupMembershipByPubkey]) => { +export const userMembership = withGetter( + derived([pubkey, membershipByPubkey], ([$pubkey, $membershipByPubkey]) => { if (!$pubkey) return null - loadGroupMembership($pubkey) + loadMembership($pubkey) - return $groupMembershipByPubkey.get($pubkey) - }, -) - -export const userGroupsByNom = withGetter( - derived([userMembership, qualifiedGroupsById], ([$userMembership, $qualifiedGroupsById]) => { - const $userGroupsByNom = new Map() - - for (const id of $userMembership?.ids || []) { - const [url, nom] = splitGroupId(id) - const group = $qualifiedGroupsById.get(id) - const groups = $userGroupsByNom.get(nom) || [] - - loadGroup(nom, [url]) - - if (group) { - groups.push(group) - } - - $userGroupsByNom.set(nom, groups) - } - - return $userGroupsByNom + return $membershipByPubkey.get($pubkey) }), ) -export const userRelayUrlsByNom = derived(userGroupsByNom, $userGroupsByNom => { - const $userRelayUrlsByNom = new Map() +// Other utils - for (const [nom, groups] of $userGroupsByNom.entries()) { - for (const group of groups) { - pushToMapKey($userRelayUrlsByNom, nom, group.relay.url) - } - } +export const decodeNEvent = (nevent: string) => nip19.decode(nevent).data as string - return $userRelayUrlsByNom -}) +export const displayReaction = (content: string) => { + if (content === "+") return "❤️" + if (content === "-") return "👎" + return content +} + +export const discoverRelays = () => + subscribe({ + filters: [{kinds: [RELAYS]}], + onEvent: (event: TrustedEvent) => { + for (const url of getRelayTagValues(event.tags)) { + if (isShareableRelayUrl(url)) { + loadRelay(url) + } + } + }, + }) diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index f55b35b9..ddb8e0db 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -10,6 +10,7 @@ relays, handles, loadRelay, + db, initStorage, repository, session, @@ -27,7 +28,7 @@ import PrimaryNav from "@app/components/PrimaryNav.svelte" import {modals, clearModal} from "@app/modal" import {theme} from "@app/theme" - import {DEFAULT_RELAYS} from "@app/state" + import {INDEXER_RELAYS, topicsByUrl, relaysByMessage} from "@app/state" import {loadUserData} from "@app/commands" import * as state from "@app/state" @@ -60,23 +61,27 @@ onMount(() => { Object.assign(window, {get, state}) - ready = initStorage('flotilla', 3, { - events: { - keyPath: "id", - store: createEventStore(repository), - }, - relays: { - keyPath: "url", - store: relays, - }, - handles: { - keyPath: "nip05", - store: handles, - }, - publishStatus: storageAdapters.fromObjectStore(publishStatusData), - freshness: storageAdapters.fromObjectStore(freshness), - plaintext: storageAdapters.fromObjectStore(plaintext), - }).then(() => sleep(300)) // Wait an extra few ms because of repository throttle + ready = db + ? Promise.resolve() + : initStorage("flotilla", 2, { + events: { + keyPath: "id", + store: createEventStore(repository), + }, + relays: { + keyPath: "url", + store: relays, + }, + handles: { + keyPath: "nip05", + store: handles, + }, + topicsByUrl: storageAdapters.fromMapStore(topicsByUrl), + relaysByMessage: storageAdapters.fromMapStore(relaysByMessage), + publishStatus: storageAdapters.fromObjectStore(publishStatusData), + freshness: storageAdapters.fromObjectStore(freshness), + plaintext: storageAdapters.fromObjectStore(plaintext), + }).then(() => sleep(300)) // Wait an extra few ms because of repository throttle dialog.addEventListener("close", () => { if (modal) { @@ -85,7 +90,7 @@ }) ready.then(() => { - for (const url of DEFAULT_RELAYS) { + for (const url of INDEXER_RELAYS) { loadRelay(url) } diff --git a/src/routes/discover/+page.svelte b/src/routes/discover/+page.svelte index 33e4db85..6e2b3baa 100644 --- a/src/routes/discover/+page.svelte +++ b/src/routes/discover/+page.svelte @@ -1,27 +1,20 @@ @@ -34,26 +27,26 @@ + idKey="url" + let:item={relay}>
- {#if group.picture} - + {#if relay.profile?.icon} + {:else} {/if}
- {#if $userMembership?.noms.has(group.nom)} + {#if $userMembership?.topicsByUrl.has(relay.url)}
{/if}
-

{displayGroup(group)}

-
- {#each $relayUrlsByNom.get(group.nom) || [] as url} -
{displayRelayUrl(url)}
- {/each} -
- {#if group.about} -

{group.about}

+

{displayRelayUrl(relay.url)}

+ {#if relay.profile?.description} +

{relay.profile.description}

{/if}
diff --git a/src/routes/settings/relays/+page.svelte b/src/routes/settings/relays/+page.svelte index b0b19347..7840f1b6 100644 --- a/src/routes/settings/relays/+page.svelte +++ b/src/routes/settings/relays/+page.svelte @@ -6,9 +6,9 @@ import {subscribe, loadRelay, relaySearch} from "@welshman/app" import Button from "@lib/components/Button.svelte" import Icon from "@lib/components/Icon.svelte" - import {DEFAULT_RELAYS, INDEXER_RELAYS} from "@app/state" + import {INDEXER_RELAYS, discoverRelays} from "@app/state" - const relays = readable(DEFAULT_RELAYS) + const relays = readable(INDEXER_RELAYS) const removeRelay = (url: string) => null @@ -17,18 +17,7 @@ let term = "" onMount(() => { - const sub = subscribe({ - filters: [{kinds: [30166], "#N": ["29"]}], - relays: [...INDEXER_RELAYS, ...DEFAULT_RELAYS], - }) - - sub.emitter.on("event", (url: string, event: SignedEvent) => { - const d = event.tags.find(t => t[0] === "d")?.[1] || "" - - if (isShareableRelayUrl(d)) { - loadRelay(d) - } - }) + const sub = discoverRelays() return () => sub.close() }) diff --git a/src/routes/spaces/[nom]/+layout.svelte b/src/routes/spaces/[nrelay]/+layout.svelte similarity index 68% rename from src/routes/spaces/[nom]/+layout.svelte rename to src/routes/spaces/[nrelay]/+layout.svelte index 23414608..f9eb1130 100644 --- a/src/routes/spaces/[nom]/+layout.svelte +++ b/src/routes/spaces/[nrelay]/+layout.svelte @@ -1,5 +1,8 @@ -{#key nom} +{#key url}
- {displayGroup($group)} + {displayRelayUrl(url)} {#if showMenu} @@ -45,7 +52,7 @@
- + Chat
- + Threads
- + Calendar
- + Market
@@ -89,11 +96,18 @@
Rooms -
+ {#each rooms as topic, i (topic)} +
+ + {topic} + +
+ {/each} diff --git a/src/routes/spaces/[nom]/[[room]]/+page.svelte b/src/routes/spaces/[nrelay]/[[topic]]/+page.svelte similarity index 85% rename from src/routes/spaces/[nom]/[[room]]/+page.svelte rename to src/routes/spaces/[nrelay]/[[topic]]/+page.svelte index 2a5eec4a..54d62d46 100644 --- a/src/routes/spaces/[nom]/[[room]]/+page.svelte +++ b/src/routes/spaces/[nrelay]/[[topic]]/+page.svelte @@ -11,16 +11,16 @@ import {onMount} from "svelte" import {page} from "$app/stores" import {sortBy, now} from "@welshman/lib" - import type {TrustedEvent} from "@welshman/util" + import type {TrustedEvent, Filter} from "@welshman/util" import {subscribe, formatTimestampAsDate} from "@welshman/app" import Icon from "@lib/components/Icon.svelte" import Spinner from "@lib/components/Spinner.svelte" import GroupNote from "@app/components/GroupNote.svelte" import GroupCompose from "@app/components/GroupCompose.svelte" - import {deriveGroupChat, userRelayUrlsByNom} from "@app/state" + import {deriveChat, userMembership, MESSAGE, REPLY} from "@app/state" - const {nom} = $page.params - const chat = deriveGroupChat(nom) + const {url, topic} = $page.params + const chat = deriveChat(url) const assertEvent = (e: any) => e as TrustedEvent @@ -60,10 +60,10 @@ }, 3000) onMount(() => { - const sub = subscribe({ - filters: [{"#h": [nom], since: now() - 30}], - relays: $userRelayUrlsByNom.get(nom) || [], - }) + const since = now() - 30 + const kinds = [MESSAGE, REPLY] + const filter = topic ? {kinds, since, "#t": [topic]} : {kinds, since} as Filter + const sub = subscribe({filters: [filter], relays: [url]}) return () => sub.close() }) @@ -100,5 +100,5 @@

- +