diff --git a/src/app/commands.ts b/src/app/commands.ts index c3f00d5..7af8665 100644 --- a/src/app/commands.ts +++ b/src/app/commands.ts @@ -37,37 +37,5 @@ export const updateList = async (kind: number, modifyTags: ModifyTags) => { await publish({event, relays}) } -export const joinGroup = async (id: string) => { - const [url, nom] = splitGroupId(id) - const relay = await loadRelay(url) - - if (!relay) { - return pushToast({ - theme: "error", - message: "Sorry, we weren't able to find that relay." - }) - } - - if (!relay.supported_nips?.includes(29)) { - return pushToast({ - theme: "error", - message: "Sorry, it looks like that relay doesn't support nostr spaces." - }) - } - - const group = await loadGroup(nom, [url]) - - if (!group) { - return pushToast({ - theme: "error", - message: "Sorry, we weren't able to find that space." - }) - } - - await updateList(GROUPS, (tags: string[][]) => uniqBy(t => t.join(''), append(["group", nom, url], tags))) - - goto(`/spaces/${nom}`) - pushToast({ - message: "Welcome to the space!" - }) -} +export const updateGroupMemberships = (newTags: string[][]) => + updateList(GROUPS, (tags: string[][]) => uniqBy(t => t.join(''), [...tags, ...newTags])) diff --git a/src/app/components/SpaceJoin.svelte b/src/app/components/SpaceJoin.svelte index ab296f9..e73bb0b 100644 --- a/src/app/components/SpaceJoin.svelte +++ b/src/app/components/SpaceJoin.svelte @@ -11,17 +11,52 @@ import {pushModal} from '@app/modal' import {pushToast} from '@app/toast' import {GROUP_DELIMITER, splitGroupId, loadRelay, loadGroup} from '@app/state' - import {joinGroup} from '@app/commands' + import {updateGroupMemberships} from '@app/commands' const back = () => history.back() - const browse = () => goto("/browse", {state: {}}) + const browse = () => goto("/spaces") + + const joinQualifiedGroup = async (id: string) => { + const [url, nom] = splitGroupId(id) + const relay = await loadRelay(url) + + if (!relay) { + return pushToast({ + theme: "error", + message: "Sorry, we weren't able to find that relay." + }) + } + + if (!relay.supported_nips?.includes(29)) { + return pushToast({ + theme: "error", + message: "Sorry, it looks like that relay doesn't support nostr spaces." + }) + } + + const group = await loadGroup(nom, [url]) + + if (!group) { + return pushToast({ + theme: "error", + message: "Sorry, we weren't able to find that space." + }) + } + + await updateGroupMemberships([["group", nom, url]]) + + goto(`/spaces/${nom}`) + pushToast({ + message: "Welcome to the space!" + }) + } const join = async () => { loading = true try { - await joinGroup(id) + await joinQualifiedGroup(id) } finally { loading = false } diff --git a/src/app/routes.ts b/src/app/routes.ts index 5819e3e..adb9d88 100644 --- a/src/app/routes.ts +++ b/src/app/routes.ts @@ -1,6 +1,8 @@ import type {Page} from '@sveltejs/kit' import {userGroupsByNom} from '@app/state' +export const makeSpacePath = (nom: string) => `/spaces/${nom}` + export const getPrimaryNavItem = ($page: Page) => { if ($page.route?.id?.match('^/(spaces|themes)$')) return 'discover' if ($page.route?.id?.startsWith('/spaces')) return 'space' diff --git a/src/app/state.ts b/src/app/state.ts index ff9a3b1..d3f0946 100644 --- a/src/app/state.ts +++ b/src/app/state.ts @@ -1,6 +1,6 @@ import type {Readable} from "svelte/store" import type {FuseResult} from 'fuse.js' -import {writable, readable, derived} from "svelte/store" +import {get, writable, readable, derived} from "svelte/store" import type {Maybe} from "@welshman/lib" import {uniq, uniqBy, groupBy, pushToMapKey, nthEq, batcher, postJson, stripProtocol, assoc, indexBy, now} from "@welshman/lib" import {getIdentifier, getRelayTags, getRelayTagValues, normalizeRelayUrl, getPubkeyTagValues, GROUP_META, PROFILE, RELAYS, FOLLOWS, MUTES, GROUPS, getGroupTags, readProfile, readList, asDecryptedEvent, editList, makeList, createList} from "@welshman/util" @@ -138,7 +138,6 @@ export const { getIndex: getRelaysByUrl, deriveItem: deriveRelay, loadItem: loadRelay, - // getItem: getRelay, } = createCollection({ name: 'relays', store: relays, @@ -168,7 +167,6 @@ export const { getIndex: getHandlesByNip05, deriveItem: deriveHandle, loadItem: loadHandle, - // getItem: getHandle, } = createCollection({ name: 'handles', store: handles, @@ -191,8 +189,7 @@ export const { // Profiles -export const profiles = deriveEventsMapped({ - repository, +export const profiles = deriveEventsMapped(repository, { filters: [{kinds: [PROFILE]}], eventToItem: readProfile, itemToEvent: item => item.event, @@ -203,7 +200,6 @@ export const { getIndex: getProfilesByPubkey, deriveItem: deriveProfile, loadItem: loadProfile, - // getItem: getProfile, } = createCollection({ name: 'profiles', store: profiles, @@ -224,14 +220,13 @@ export const getReadRelayUrls = (event?: CustomEvent): string[] => export const getWriteRelayUrls = (event?: CustomEvent): string[] => getRelayTags(event?.tags || []).filter((t: string[]) => !t[2] || t[2] === 'write').map((t: string[]) => normalizeRelayUrl(t[1])) -export const relaySelections = deriveEvents({repository, filters: [{kinds: [RELAYS]}]}) +export const relaySelections = deriveEvents(repository, {filters: [{kinds: [RELAYS]}]}) export const { indexStore: relaySelectionsByPubkey, getIndex: getRelaySelectionsByPubkey, deriveItem: deriveRelaySelections, loadItem: loadRelaySelections, - // getItem: getRelaySelections, } = createCollection({ name: 'relaySelections', store: relaySelections, @@ -246,8 +241,7 @@ export const { // Follows -export const follows = deriveEventsMapped({ - repository, +export const follows = deriveEventsMapped(repository, { filters: [{kinds: [FOLLOWS]}], itemToEvent: item => item.event, eventToItem: async (event: CustomEvent) => @@ -263,7 +257,6 @@ export const { getIndex: getFollowsByPubkey, deriveItem: deriveFollows, loadItem: loadFollows, - // getItem: getFollows, } = createCollection({ name: 'follows', store: follows, @@ -278,8 +271,7 @@ export const { // Mutes -export const mutes = deriveEventsMapped({ - repository, +export const mutes = deriveEventsMapped(repository, { filters: [{kinds: [MUTES]}], itemToEvent: item => item.event, eventToItem: async (event: CustomEvent) => @@ -295,7 +287,6 @@ export const { getIndex: getMutesByPubkey, deriveItem: deriveMutes, loadItem: loadMutes, - // getItem: getMutes, } = createCollection({ name: 'mutes', store: mutes, @@ -350,8 +341,7 @@ export const readGroup = (event: CustomEvent) => { return {nom, name, about, picture, event} } -export const groups = deriveEventsMapped({ - repository, +export const groups = deriveEventsMapped(repository, { filters: [{kinds: [GROUP_META]}], eventToItem: readGroup, itemToEvent: item => item.event, @@ -362,7 +352,6 @@ export const { getIndex: getGroupsByNom, deriveItem: deriveGroup, loadItem: loadGroup, - // getItem: getGroup, } = createCollection({ name: 'groups', store: groups, @@ -412,6 +401,18 @@ export const qualifiedGroups = derived([relaysByPubkey, groups], ([$relaysByPubk 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 = 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 = { @@ -439,8 +440,7 @@ export const readGroupMembership = (event: CustomEvent) => { return {event, ids, noms, urls} } -export const groupMemberships = deriveEventsMapped({ - repository, +export const groupMemberships = deriveEventsMapped(repository, { filters: [{kinds: [GROUPS]}], eventToItem: readGroupMembership, itemToEvent: item => item.event, @@ -451,17 +451,72 @@ export const { getIndex: getGroupMembersipByPubkey, deriveItem: deriveGroupMembership, loadItem: loadGroupMembership, - // getItem: getGroupMembership, } = createCollection({ - name: 'groupMemberships', - store: groupMemberships, - getKey: groupMembership => groupMembership.event.pubkey, - load: (pubkey: string, relays = [], request: Partial = {}) => - load({ - ...request, - relays: [...relays, ...INDEXER_RELAYS], - filters: [{kinds: [GROUPS], authors: [pubkey]}], - }) + name: 'groupMemberships', + store: groupMemberships, + getKey: groupMembership => groupMembership.event.pubkey, + load: (pubkey: string, relays = [], request: Partial = {}) => + load({ + ...request, + relays: [...relays, ...INDEXER_RELAYS], + filters: [{kinds: [GROUPS], authors: [pubkey]}], + }) +}) + +// Group Messages + +export type GroupMessage = { + nom: string + event: CustomEvent +} + +export const readGroupMessage = (event: CustomEvent): Maybe => { + const nom = event.tags.find(nthEq(0, 'h'))?.[1] + + if (!nom) { + return undefined + } + + return {nom, event} +} + +export const groupMessages = deriveEventsMapped(repository, { + filters: [{}], + eventToItem: readGroupMessage, + itemToEvent: item => item.event, +}) + +// Group Conversations + +export type GroupConversation = { + nom: string + messages: GroupMessage[] +} + +export const groupConversations = derived(groupMessages, $groupMessages => { + const groupMessagesByNom = groupBy($groupMessage => $groupMessage.nom, $groupMessages) + + return Array.from(groupMessagesByNom.entries()).map(([nom, messages]) => ({nom, messages})) +}) + +export const { + indexStore: groupConversationByNom, + getIndex: getGroupMembersipByNom, + deriveItem: deriveGroupConversation, + loadItem: loadGroupConversation, +} = createCollection({ + name: 'groupConversations', + store: groupConversations, + getKey: groupConversation => groupConversation.nom, + load: (nom: string, hints = [], request: Partial = {}) => { + const relays = [...hints, ...get(relayUrlsByNom).get(nom) || []] + + if (relays.length === 0) { + console.warn(`Attempted to load conversation for ${nom} with no qualified groups`) + } + + return load({...request, relays, filters: [{'#h': [nom]}]}) + }, }) // User stuff diff --git a/src/assets/icons/Check Circle.svg b/src/assets/icons/Check Circle.svg new file mode 100644 index 0000000..67f0695 --- /dev/null +++ b/src/assets/icons/Check Circle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/lib/components/Icon.svelte b/src/lib/components/Icon.svelte index 276830f..0ec4d59 100644 --- a/src/lib/components/Icon.svelte +++ b/src/lib/components/Icon.svelte @@ -12,6 +12,7 @@ import AddCircle from "@assets/icons/Add Circle.svg?dataurl" import AltArrowRight from "@assets/icons/Alt Arrow Right.svg?dataurl" import AltArrowLeft from "@assets/icons/Alt Arrow Left.svg?dataurl" + import CheckCircle from "@assets/icons/Check Circle.svg?dataurl" import ClipboardText from "@assets/icons/Clipboard Text.svg?dataurl" import CloseCircle from "@assets/icons/Close Circle.svg?dataurl" import Copy from "@assets/icons/Copy.svg?dataurl" @@ -47,6 +48,7 @@ "add-circle": AddCircle, "alt-arrow-right": AltArrowRight, "alt-arrow-left": AltArrowLeft, + "check-circle": CheckCircle, "clipboard-text": ClipboardText, "close-circle": CloseCircle, copy: Copy, diff --git a/src/routes/home/+page.svelte b/src/routes/home/+page.svelte index a172d47..857d2bc 100644 --- a/src/routes/home/+page.svelte +++ b/src/routes/home/+page.svelte @@ -1,9 +1,12 @@
@@ -15,7 +18,7 @@ Invite all your friends, do life together. - + Find a community based on your hobbies or interests. diff --git a/src/routes/spaces/+page.svelte b/src/routes/spaces/+page.svelte index da48779..044b259 100644 --- a/src/routes/spaces/+page.svelte +++ b/src/routes/spaces/+page.svelte @@ -1,11 +1,29 @@ -{nom} +
+
+
+
+
+
+
+
diff --git a/src/routes/themes/+page.svelte b/src/routes/themes/+page.svelte index 15780b5..cd8f969 100644 --- a/src/routes/themes/+page.svelte +++ b/src/routes/themes/+page.svelte @@ -12,7 +12,7 @@

Discover Themes

-

Make Flotilla look just how you like it

+

Make your community feel like home