Spruce up join button on spaces page

This commit is contained in:
Jon Staab
2024-08-15 12:08:07 -07:00
parent 6c3bcac61f
commit ae0450d408
10 changed files with 195 additions and 79 deletions
+2 -34
View File
@@ -37,37 +37,5 @@ export const updateList = async (kind: number, modifyTags: ModifyTags) => {
await publish({event, relays}) await publish({event, relays})
} }
export const joinGroup = async (id: string) => { export const updateGroupMemberships = (newTags: string[][]) =>
const [url, nom] = splitGroupId(id) updateList(GROUPS, (tags: string[][]) => uniqBy(t => t.join(''), [...tags, ...newTags]))
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!"
})
}
+38 -3
View File
@@ -11,17 +11,52 @@
import {pushModal} from '@app/modal' import {pushModal} from '@app/modal'
import {pushToast} from '@app/toast' import {pushToast} from '@app/toast'
import {GROUP_DELIMITER, splitGroupId, loadRelay, loadGroup} from '@app/state' import {GROUP_DELIMITER, splitGroupId, loadRelay, loadGroup} from '@app/state'
import {joinGroup} from '@app/commands' import {updateGroupMemberships} from '@app/commands'
const back = () => history.back() 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 () => { const join = async () => {
loading = true loading = true
try { try {
await joinGroup(id) await joinQualifiedGroup(id)
} finally { } finally {
loading = false loading = false
} }
+2
View File
@@ -1,6 +1,8 @@
import type {Page} from '@sveltejs/kit' import type {Page} from '@sveltejs/kit'
import {userGroupsByNom} from '@app/state' import {userGroupsByNom} from '@app/state'
export const makeSpacePath = (nom: string) => `/spaces/${nom}`
export const getPrimaryNavItem = ($page: Page) => { export const getPrimaryNavItem = ($page: Page) => {
if ($page.route?.id?.match('^/(spaces|themes)$')) return 'discover' if ($page.route?.id?.match('^/(spaces|themes)$')) return 'discover'
if ($page.route?.id?.startsWith('/spaces')) return 'space' if ($page.route?.id?.startsWith('/spaces')) return 'space'
+84 -29
View File
@@ -1,6 +1,6 @@
import type {Readable} from "svelte/store" import type {Readable} from "svelte/store"
import type {FuseResult} from 'fuse.js' 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 type {Maybe} from "@welshman/lib"
import {uniq, uniqBy, groupBy, pushToMapKey, nthEq, batcher, postJson, stripProtocol, assoc, indexBy, now} 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" 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, getIndex: getRelaysByUrl,
deriveItem: deriveRelay, deriveItem: deriveRelay,
loadItem: loadRelay, loadItem: loadRelay,
// getItem: getRelay,
} = createCollection({ } = createCollection({
name: 'relays', name: 'relays',
store: relays, store: relays,
@@ -168,7 +167,6 @@ export const {
getIndex: getHandlesByNip05, getIndex: getHandlesByNip05,
deriveItem: deriveHandle, deriveItem: deriveHandle,
loadItem: loadHandle, loadItem: loadHandle,
// getItem: getHandle,
} = createCollection({ } = createCollection({
name: 'handles', name: 'handles',
store: handles, store: handles,
@@ -191,8 +189,7 @@ export const {
// Profiles // Profiles
export const profiles = deriveEventsMapped<PublishedProfile>({ export const profiles = deriveEventsMapped<PublishedProfile>(repository, {
repository,
filters: [{kinds: [PROFILE]}], filters: [{kinds: [PROFILE]}],
eventToItem: readProfile, eventToItem: readProfile,
itemToEvent: item => item.event, itemToEvent: item => item.event,
@@ -203,7 +200,6 @@ export const {
getIndex: getProfilesByPubkey, getIndex: getProfilesByPubkey,
deriveItem: deriveProfile, deriveItem: deriveProfile,
loadItem: loadProfile, loadItem: loadProfile,
// getItem: getProfile,
} = createCollection({ } = createCollection({
name: 'profiles', name: 'profiles',
store: profiles, store: profiles,
@@ -224,14 +220,13 @@ export const getReadRelayUrls = (event?: CustomEvent): string[] =>
export const getWriteRelayUrls = (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])) 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 { export const {
indexStore: relaySelectionsByPubkey, indexStore: relaySelectionsByPubkey,
getIndex: getRelaySelectionsByPubkey, getIndex: getRelaySelectionsByPubkey,
deriveItem: deriveRelaySelections, deriveItem: deriveRelaySelections,
loadItem: loadRelaySelections, loadItem: loadRelaySelections,
// getItem: getRelaySelections,
} = createCollection({ } = createCollection({
name: 'relaySelections', name: 'relaySelections',
store: relaySelections, store: relaySelections,
@@ -246,8 +241,7 @@ export const {
// Follows // Follows
export const follows = deriveEventsMapped<PublishedList>({ export const follows = deriveEventsMapped<PublishedList>(repository, {
repository,
filters: [{kinds: [FOLLOWS]}], filters: [{kinds: [FOLLOWS]}],
itemToEvent: item => item.event, itemToEvent: item => item.event,
eventToItem: async (event: CustomEvent) => eventToItem: async (event: CustomEvent) =>
@@ -263,7 +257,6 @@ export const {
getIndex: getFollowsByPubkey, getIndex: getFollowsByPubkey,
deriveItem: deriveFollows, deriveItem: deriveFollows,
loadItem: loadFollows, loadItem: loadFollows,
// getItem: getFollows,
} = createCollection({ } = createCollection({
name: 'follows', name: 'follows',
store: follows, store: follows,
@@ -278,8 +271,7 @@ export const {
// Mutes // Mutes
export const mutes = deriveEventsMapped<PublishedList>({ export const mutes = deriveEventsMapped<PublishedList>(repository, {
repository,
filters: [{kinds: [MUTES]}], filters: [{kinds: [MUTES]}],
itemToEvent: item => item.event, itemToEvent: item => item.event,
eventToItem: async (event: CustomEvent) => eventToItem: async (event: CustomEvent) =>
@@ -295,7 +287,6 @@ export const {
getIndex: getMutesByPubkey, getIndex: getMutesByPubkey,
deriveItem: deriveMutes, deriveItem: deriveMutes,
loadItem: loadMutes, loadItem: loadMutes,
// getItem: getMutes,
} = createCollection({ } = createCollection({
name: 'mutes', name: 'mutes',
store: mutes, store: mutes,
@@ -350,8 +341,7 @@ export const readGroup = (event: CustomEvent) => {
return {nom, name, about, picture, event} return {nom, name, about, picture, event}
} }
export const groups = deriveEventsMapped<PublishedGroup>({ export const groups = deriveEventsMapped<PublishedGroup>(repository, {
repository,
filters: [{kinds: [GROUP_META]}], filters: [{kinds: [GROUP_META]}],
eventToItem: readGroup, eventToItem: readGroup,
itemToEvent: item => item.event, itemToEvent: item => item.event,
@@ -362,7 +352,6 @@ export const {
getIndex: getGroupsByNom, getIndex: getGroupsByNom,
deriveItem: deriveGroup, deriveItem: deriveGroup,
loadItem: loadGroup, loadItem: loadGroup,
// getItem: getGroup,
} = createCollection({ } = createCollection({
name: 'groups', name: 'groups',
store: 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 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 // Group membership
export type GroupMembership = { export type GroupMembership = {
@@ -439,8 +440,7 @@ export const readGroupMembership = (event: CustomEvent) => {
return {event, ids, noms, urls} return {event, ids, noms, urls}
} }
export const groupMemberships = deriveEventsMapped<PublishedGroupMembership>({ export const groupMemberships = deriveEventsMapped<PublishedGroupMembership>(repository, {
repository,
filters: [{kinds: [GROUPS]}], filters: [{kinds: [GROUPS]}],
eventToItem: readGroupMembership, eventToItem: readGroupMembership,
itemToEvent: item => item.event, itemToEvent: item => item.event,
@@ -451,17 +451,72 @@ export const {
getIndex: getGroupMembersipByPubkey, getIndex: getGroupMembersipByPubkey,
deriveItem: deriveGroupMembership, deriveItem: deriveGroupMembership,
loadItem: loadGroupMembership, loadItem: loadGroupMembership,
// getItem: getGroupMembership,
} = createCollection({ } = createCollection({
name: 'groupMemberships', name: 'groupMemberships',
store: groupMemberships, store: groupMemberships,
getKey: groupMembership => groupMembership.event.pubkey, getKey: groupMembership => groupMembership.event.pubkey,
load: (pubkey: string, relays = [], request: Partial<SubscribeRequest> = {}) => load: (pubkey: string, relays = [], request: Partial<SubscribeRequest> = {}) =>
load({ load({
...request, ...request,
relays: [...relays, ...INDEXER_RELAYS], relays: [...relays, ...INDEXER_RELAYS],
filters: [{kinds: [GROUPS], authors: [pubkey]}], filters: [{kinds: [GROUPS], authors: [pubkey]}],
}) })
})
// Group Messages
export type GroupMessage = {
nom: string
event: CustomEvent
}
export const readGroupMessage = (event: CustomEvent): Maybe<GroupMessage> => {
const nom = event.tags.find(nthEq(0, 'h'))?.[1]
if (!nom) {
return undefined
}
return {nom, event}
}
export const groupMessages = deriveEventsMapped<GroupMessage>(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<SubscribeRequest> = {}) => {
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 // User stuff
+4
View File
@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="#1C274C" stroke-width="1.5"/>
<path d="M8.5 12.5L10.5 14.5L15.5 9.5" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 296 B

+2
View File
@@ -12,6 +12,7 @@
import AddCircle from "@assets/icons/Add Circle.svg?dataurl" import AddCircle from "@assets/icons/Add Circle.svg?dataurl"
import AltArrowRight from "@assets/icons/Alt Arrow Right.svg?dataurl" import AltArrowRight from "@assets/icons/Alt Arrow Right.svg?dataurl"
import AltArrowLeft from "@assets/icons/Alt Arrow Left.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 ClipboardText from "@assets/icons/Clipboard Text.svg?dataurl"
import CloseCircle from "@assets/icons/Close Circle.svg?dataurl" import CloseCircle from "@assets/icons/Close Circle.svg?dataurl"
import Copy from "@assets/icons/Copy.svg?dataurl" import Copy from "@assets/icons/Copy.svg?dataurl"
@@ -47,6 +48,7 @@
"add-circle": AddCircle, "add-circle": AddCircle,
"alt-arrow-right": AltArrowRight, "alt-arrow-right": AltArrowRight,
"alt-arrow-left": AltArrowLeft, "alt-arrow-left": AltArrowLeft,
"check-circle": CheckCircle,
"clipboard-text": ClipboardText, "clipboard-text": ClipboardText,
"close-circle": CloseCircle, "close-circle": CloseCircle,
copy: Copy, copy: Copy,
+5 -2
View File
@@ -1,9 +1,12 @@
<script lang="ts"> <script lang="ts">
import {goto} from '$app/navigation'
import CardButton from "@lib/components/CardButton.svelte" import CardButton from "@lib/components/CardButton.svelte"
import SpaceCreate from '@app/components/SpaceCreate.svelte' import SpaceCreate from '@app/components/SpaceCreate.svelte'
import {pushModal} from '@app/modal' import {pushModal} from '@app/modal'
export const createSpace = () => pushModal(SpaceCreate) const createSpace = () => pushModal(SpaceCreate)
const browseSpaces = () => goto("/spaces")
</script> </script>
<div class="hero min-h-screen bg-base-200"> <div class="hero min-h-screen bg-base-200">
@@ -15,7 +18,7 @@
<CardButton icon="add-circle" title="Create a space" class="h-24" on:click={createSpace}> <CardButton icon="add-circle" title="Create a space" class="h-24" on:click={createSpace}>
Invite all your friends, do life together. Invite all your friends, do life together.
</CardButton> </CardButton>
<CardButton icon="compass" title="Discover spaces" class="h-24"> <CardButton icon="compass" title="Discover spaces" class="h-24" on:click={browseSpaces}>
Find a community based on your hobbies or interests. Find a community based on your hobbies or interests.
</CardButton> </CardButton>
<CardButton icon="plain" title="Leave feedback" class="h-24"> <CardButton icon="plain" title="Leave feedback" class="h-24">
+44 -8
View File
@@ -1,11 +1,29 @@
<script lang="ts"> <script lang="ts">
import {onMount} from 'svelte' import {onMount} from 'svelte'
import Masonry from 'svelte-bricks' import Masonry from 'svelte-bricks'
import {GROUP_META} from '@welshman/util' import {append, remove} from '@welshman/lib'
import {GROUP_META, displayRelayUrl} from '@welshman/util'
import Icon from '@lib/components/Icon.svelte' import Icon from '@lib/components/Icon.svelte'
import {load, relays, groups, searchGroups} from '@app/state' import Button from '@lib/components/Button.svelte'
import Spinner from '@lib/components/Spinner.svelte'
import {makeSpacePath} from '@app/routes'
import {load, relays, groups, searchGroups, relayUrlsByNom, userMembership} from '@app/state'
import {updateGroupMemberships} from '@app/commands'
const getRelayUrls = (nom: string): string[] => $relayUrlsByNom.get(nom) || []
const join = async (nom: string) => {
loading = append(nom, loading)
try {
await updateGroupMemberships(getRelayUrls(nom).map(url => ["group", nom, url]))
} finally {
loading = remove(nom, loading)
}
}
let term = "" let term = ""
let loading: string[] = []
onMount(() => { onMount(() => {
load({ load({
@@ -22,22 +40,40 @@
<Icon icon="magnifer" /> <Icon icon="magnifer" />
<input bind:value={term} class="grow" type="text" placeholder="Search for spaces..." /> <input bind:value={term} class="grow" type="text" placeholder="Search for spaces..." />
</label> </label>
<Masonry animate={false} items={$searchGroups.searchOptions(term)} minColWidth={250} maxColWidth={800} gap={16} idKey="nom" let:item> <Masonry animate={false} items={$searchGroups.searchOptions(term)} minColWidth={250} maxColWidth={800} gap={16} idKey="nom" let:item={group}>
<div class="card bg-base-100 shadow-xl"> <div class="card bg-base-100 shadow-xl">
<div class="avatar center mt-8"> <div class="avatar center mt-8">
<div class="w-20 rounded-full bg-base-300 border-2 border-solid border-base-300 !flex center"> <div class="w-20 rounded-full bg-base-300 border-2 border-solid border-base-300 !flex center">
{#if item?.picture} {#if group?.picture}
<img alt="" src={item.picture} /> <img alt="" src={group.picture} />
{:else} {:else}
<Icon icon="ghost" size={7} /> <Icon icon="ghost" size={7} />
{/if} {/if}
</div> </div>
</div> </div>
<div class="card-body"> <div class="card-body">
<h2 class="card-title justify-center">{item.name}</h2> <a href={makeSpacePath(group.nom)}>
<p class="text-sm py-4">{item.about}</p> <h2 class="card-title justify-center">{group.name}</h2>
</a>
<div class="text-sm text-center">
{#each getRelayUrls(group.nom) as url}
<div class="badge badge-neutral">{displayRelayUrl(url)}</div>
{/each}
</div>
<p class="text-sm py-4">{group.about}</p>
<div class="card-actions"> <div class="card-actions">
<button class="btn btn-primary w-full">Join Space</button> <Button
class="btn btn-primary w-full"
disabled={loading.includes(group.nom) || $userMembership?.noms.has(group.nom)}
on:click={() => join(group.nom)}>
{#if $userMembership?.noms.has(group.nom)}
<Icon icon="check-circle" />
Joined
{:else}
<Spinner loading={loading.includes(group.nom)} />
Join Space
{/if}
</Button>
</div> </div>
</div> </div>
</div> </div>
+13 -2
View File
@@ -1,5 +1,16 @@
<script lang="ts"> <script lang="ts">
export let nom import {page} from '$app/stores'
import {deriveGroup, deriveGroupConversation} from '@app/state'
const group = deriveGroup($page.params.nom)
const conversation = deriveGroupConversation($page.params.nom)
</script> </script>
{nom} <div class="h-screen flex flex-col">
<div class="min-h-32 bg-base-100">
</div>
<div class="flex-grow overflow-auto">
</div>
<div class="min-h-32 bg-base-100">
</div>
</div>
+1 -1
View File
@@ -12,7 +12,7 @@
<div class="content column gap-4"> <div class="content column gap-4">
<h1 class="superheading mt-20">Discover Themes</h1> <h1 class="superheading mt-20">Discover Themes</h1>
<p class="text-center">Make Flotilla look just how you like it</p> <p class="text-center">Make your community feel like home</p>
<label class="input input-bordered w-full flex items-center gap-2"> <label class="input input-bordered w-full flex items-center gap-2">
<Icon icon="magnifer" /> <Icon icon="magnifer" />
<input bind:value={term} class="grow" type="text" placeholder="Search for themes..." /> <input bind:value={term} class="grow" type="text" placeholder="Search for themes..." />