forked from coracle/flotilla
Convert to simple relay-based groups from nip29
This commit is contained in:
+12
-35
@@ -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<string, string>) => [
|
||||
// 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)))
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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"
|
||||
</script>
|
||||
|
||||
@@ -17,22 +16,8 @@
|
||||
This means that anyone can host their own data, making the web more decentralized and resilient.
|
||||
</p>
|
||||
<p>
|
||||
Only some relays support spaces. You can find a list of suggested relays below, or you can <Link
|
||||
external
|
||||
href="https://coracle.tools">host your own</Link
|
||||
>. 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.
|
||||
</p>
|
||||
{#each DEFAULT_RELAYS as url}
|
||||
<div class="card2 card2-alt card2-sm flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon="remote-controller-minimalistic" />
|
||||
{displayRelayUrl(url)}
|
||||
</div>
|
||||
<Button on:click={() => clip(url)}>
|
||||
<Icon icon="copy" />
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
<Button class="btn btn-primary" on:click={() => history.back()}>Got it</Button>
|
||||
</div>
|
||||
@@ -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",
|
||||
|
||||
@@ -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} />
|
||||
</PrimaryNavItem>
|
||||
{#each $userGroupsByNom.entries() as [nom, qualifiedGroups] (nom)}
|
||||
{@const qualifiedGroup = qualifiedGroups[0]}
|
||||
<PrimaryNavItem title={displayGroup(qualifiedGroup?.group)} href="/spaces/{nom}">
|
||||
<Avatar
|
||||
icon="ghost"
|
||||
class="!h-10 !w-10 border border-solid border-base-300"
|
||||
alt={displayGroup(qualifiedGroup?.group)}
|
||||
src={qualifiedGroup?.group.picture}
|
||||
size={7} />
|
||||
{#each $userMembership?.topicsByUrl.keys() || [] as url (url)}
|
||||
<PrimaryNavItem title={displayRelayUrl(url)} href={makeSpacePath(url)}>
|
||||
<SpaceAvatar {url} />
|
||||
</PrimaryNavItem>
|
||||
{/each}
|
||||
<PrimaryNavItem title="Add Space" on:click={addSpace}>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {append, remove} from "@welshman/lib"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import {deriveRelay} from "@welshman/app"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {addRoomMembership} from "@app/commands"
|
||||
import {makeSpacePath} from '@app/routes'
|
||||
|
||||
export let url
|
||||
|
||||
const relay = deriveRelay(url)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const tryCreate = async () => {
|
||||
await addRoomMembership(url, topic)
|
||||
|
||||
goto(makeSpacePath(url, topic))
|
||||
}
|
||||
|
||||
const create = async () => {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
await tryCreate()
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
let topic = ""
|
||||
let loading = false
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" on:submit|preventDefault={create}>
|
||||
<h1 class="heading">
|
||||
Create a Room
|
||||
</h1>
|
||||
<p class="text-center">
|
||||
On <span class="text-primary">{displayRelayUrl(url)}</span>
|
||||
</p>
|
||||
<Field>
|
||||
<p slot="label">Room Name</p>
|
||||
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
||||
<Icon icon="hashtag" />
|
||||
<input bind:value={topic} class="grow" type="text" />
|
||||
</label>
|
||||
</Field>
|
||||
<div class="flex flex-row items-center justify-between gap-4">
|
||||
<Button class="btn btn-link" on:click={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={!topic || loading}>
|
||||
<Spinner {loading}>Create Room</Spinner>
|
||||
<Icon icon="alt-arrow-right" />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import {deriveRelay} from "@welshman/app"
|
||||
|
||||
export let url
|
||||
|
||||
const relay = deriveRelay(url)
|
||||
</script>
|
||||
|
||||
<Avatar
|
||||
icon="ghost"
|
||||
class="!h-10 !w-10 border border-solid border-base-300"
|
||||
alt={displayRelayUrl(url)}
|
||||
src={$relay?.profile?.icon}
|
||||
size={7} />
|
||||
@@ -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 @@
|
||||
<input bind:value={relay} class="grow" type="text" />
|
||||
</label>
|
||||
<p slot="info">
|
||||
This should be a NIP-29 compatible nostr relay where you'd like to host your space.
|
||||
<Button class="link" on:click={() => pushModal(InfoNip29)}>What is a relay?</Button>
|
||||
This can be any nostr relay where you'd like to host your space.
|
||||
<Button class="link" on:click={() => pushModal(InfoRelay)}>What is a relay?</Button>
|
||||
</p>
|
||||
</Field>
|
||||
<div class="flex flex-row items-center justify-between gap-4">
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import {deriveGroup} from "@app/state"
|
||||
import {removeGroupMemberships} from "@app/commands"
|
||||
import {removeSpaceMembership} from "@app/commands"
|
||||
|
||||
export let nom
|
||||
|
||||
const group = deriveGroup(nom)
|
||||
export let url
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
@@ -16,7 +14,7 @@
|
||||
loading = true
|
||||
|
||||
try {
|
||||
await removeGroupMemberships([nom])
|
||||
await removeSpaceMembership(url)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
@@ -29,7 +27,7 @@
|
||||
|
||||
<form class="column gap-4" on:submit|preventDefault={exit}>
|
||||
<h1 class="heading">
|
||||
You are leaving <span class="text-primary">{$group?.name || "[no name]"}</span>
|
||||
You are leaving <span class="text-primary">{displayRelayUrl(url)}</span>
|
||||
</h1>
|
||||
<p class="text-center">Are you sure you want to leave?</p>
|
||||
<div class="flex flex-row items-center justify-between gap-4">
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {tryCatch} from "@welshman/lib"
|
||||
import {isRelayUrl, normalizeRelayUrl} from "@welshman/util"
|
||||
import {loadRelay} from "@welshman/app"
|
||||
import CardButton from "@lib/components/CardButton.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
@@ -7,43 +9,28 @@
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {splitGroupId, loadGroup} from "@app/state"
|
||||
import {addGroupMemberships} from "@app/commands"
|
||||
import {addSpaceMembership} from "@app/commands"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const browse = () => goto("/discover")
|
||||
|
||||
const joinQualifiedGroup = async (id: string) => {
|
||||
const [url, nom] = splitGroupId(id)
|
||||
const joinRelay = async (url: string) => {
|
||||
url = normalizeRelayUrl(url)
|
||||
|
||||
const relay = await loadRelay(url)
|
||||
|
||||
if (!relay) {
|
||||
if (!relay?.profile) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Sorry, we weren't able to find that relay.",
|
||||
})
|
||||
}
|
||||
|
||||
if (!relay.profile?.supported_nips?.includes(29)) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Sorry, it looks like that relay doesn't support nostr spaces.",
|
||||
})
|
||||
}
|
||||
await addSpaceMembership(url)
|
||||
|
||||
const group = await loadGroup(nom, [url])
|
||||
|
||||
if (!group) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Sorry, we weren't able to find that space.",
|
||||
})
|
||||
}
|
||||
|
||||
await addGroupMemberships([["group", nom, url]])
|
||||
|
||||
goto(`/spaces/${nom}`)
|
||||
goto(makeSpacePath(url))
|
||||
pushToast({
|
||||
message: "Welcome to the space!",
|
||||
})
|
||||
@@ -53,16 +40,16 @@
|
||||
loading = true
|
||||
|
||||
try {
|
||||
await joinQualifiedGroup(id)
|
||||
await joinRelay(url)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
let id = ""
|
||||
let url = ""
|
||||
let loading = false
|
||||
|
||||
$: linkIsValid = Boolean(id.match(/.+\..+'.+/))
|
||||
$: linkIsValid = Boolean(tryCatch(() => isRelayUrl(normalizeRelayUrl(url))))
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" on:submit|preventDefault={join}>
|
||||
@@ -74,7 +61,7 @@
|
||||
<p slot="label">Invite Link*</p>
|
||||
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
||||
<Icon icon="link-round" />
|
||||
<input bind:value={id} class="grow" type="text" />
|
||||
<input bind:value={url} class="grow" type="text" />
|
||||
</label>
|
||||
</Field>
|
||||
<CardButton icon="compass" title="Don't have an invite?" on:click={browse}>
|
||||
|
||||
@@ -1,37 +1,22 @@
|
||||
<script lang="ts">
|
||||
import {append, remove} from "@welshman/lib"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import {deriveRelay} from "@welshman/app"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import InfoNip29 from "@app/components/InfoNip29.svelte"
|
||||
import {pushModal, clearModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {deriveGroup, displayGroup, relayUrlsByNom} from "@app/state"
|
||||
import {sendJoinRequest, addGroupMemberships} from "@app/commands"
|
||||
import {addSpaceMembership} from "@app/commands"
|
||||
|
||||
export let nom
|
||||
export let url
|
||||
|
||||
const group = deriveGroup(nom)
|
||||
const relay = deriveRelay(url)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const onUrlChange = (e: any) => {
|
||||
urls = urls.includes(e.target.value)
|
||||
? remove(e.target.value, urls)
|
||||
: append(e.target.value, urls)
|
||||
}
|
||||
|
||||
const tryJoin = async () => {
|
||||
for (const url of urls) {
|
||||
const [ok, message] = await sendJoinRequest(nom, url)
|
||||
|
||||
if (!ok) {
|
||||
return pushToast({theme: "error", message})
|
||||
}
|
||||
}
|
||||
|
||||
await addGroupMemberships(urls.map(url => ["group", nom, url]))
|
||||
await addSpaceMembership(url)
|
||||
|
||||
clearModal()
|
||||
}
|
||||
@@ -47,40 +32,19 @@
|
||||
}
|
||||
|
||||
let loading = false
|
||||
let urls: string[] = $relayUrlsByNom.get(nom) || []
|
||||
|
||||
$: hasUrls = urls.length > 0
|
||||
$: urlOptions = $relayUrlsByNom.get(nom)?.toSorted() || []
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" on:submit|preventDefault={join}>
|
||||
<h1 class="heading">
|
||||
Joining <span class="text-primary">{displayGroup($group)}</span>
|
||||
Joining <span class="text-primary">{displayRelayUrl(url)}</span>
|
||||
</h1>
|
||||
<p class="text-center">
|
||||
Please select which relays you'd like to use for this group.
|
||||
<Button class="link" on:click={() => pushModal(InfoNip29)}>What is a relay?</Button>
|
||||
</p>
|
||||
{#each urlOptions as url}
|
||||
<div class="alert !flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<Icon icon="remote-controller-minimalistic" />
|
||||
{displayRelayUrl(url)}
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
value={url}
|
||||
class="toggle toggle-primary"
|
||||
checked={urls.includes(url)}
|
||||
on:change={onUrlChange} />
|
||||
</div>
|
||||
{/each}
|
||||
<p class="text-center">Are you sure you'd like to join this space?</p>
|
||||
<div class="flex flex-row items-center justify-between gap-4">
|
||||
<Button class="btn btn-link" on:click={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={!hasUrls || loading}>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||
<Spinner {loading}>Join Space</Spinner>
|
||||
<Icon icon="alt-arrow-right" />
|
||||
</Button>
|
||||
|
||||
@@ -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),
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
+18
-5
@@ -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
|
||||
}
|
||||
|
||||
+118
-236
@@ -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<string, string[]>()))
|
||||
|
||||
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<string, string[]>()))
|
||||
|
||||
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<string, string[]>
|
||||
event?: TrustedEvent
|
||||
}
|
||||
|
||||
export type PublishedGroup = Omit<Group, "event"> & {
|
||||
export type PublishedMembership = Omit<Membership, "event"> & {
|
||||
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<string, string[]>()
|
||||
|
||||
return {nom, name, about, picture, event}
|
||||
}
|
||||
|
||||
export const groups = deriveEventsMapped<PublishedGroup>(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<SubscribeRequest> = {}) => {
|
||||
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<PublishedGroup>) => {
|
||||
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<string>
|
||||
noms: Set<string>
|
||||
urls: Set<string>
|
||||
event?: TrustedEvent
|
||||
}
|
||||
|
||||
export type PublishedGroupMembership = Omit<GroupMembership, "event"> & {
|
||||
event: TrustedEvent
|
||||
}
|
||||
|
||||
export const readGroupMembership = (event: TrustedEvent) => {
|
||||
const ids = new Set<string>()
|
||||
const noms = new Set<string>()
|
||||
const urls = new Set<string>()
|
||||
|
||||
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<PublishedGroupMembership>(repository, {
|
||||
filters: [{kinds: [GROUPS]}],
|
||||
eventToItem: readGroupMembership,
|
||||
export const memberships = deriveEventsMapped<PublishedMembership>(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<SubscribeRequest> = {}) => {
|
||||
const relays = getWriteRelayUrls(await loadRelaySelections(pubkey, hints))
|
||||
|
||||
return load({
|
||||
name: "memberships",
|
||||
store: memberships,
|
||||
getKey: membership => membership.event.pubkey,
|
||||
load: (pubkey: string, request: Partial<SubscribeRequestWithHandlers> = {}) =>
|
||||
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<GroupMessage> => {
|
||||
const nom = event.tags.find(nthEq(0, "h"))?.[1]
|
||||
export const readMessage = (event: TrustedEvent): Maybe<Message[]> => {
|
||||
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<GroupMessage>(repository, {
|
||||
export const messages = deriveEventsMapped<Message>(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<SubscribeRequest> = {}) => {
|
||||
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<SubscribeRequestWithHandlers> = {}) => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
+24
-19
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import Masonry from "svelte-bricks"
|
||||
import {GROUP_META, displayRelayUrl} from "@welshman/util"
|
||||
import {load, relays} from "@welshman/app"
|
||||
import {displayRelayUrl} from "@welshman/util"
|
||||
import {load, relaySearch} from "@welshman/app"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
import {
|
||||
displayGroup,
|
||||
searchGroups,
|
||||
relayUrlsByNom,
|
||||
userMembership,
|
||||
DEFAULT_RELAYS,
|
||||
} from "@app/state"
|
||||
import {userMembership, discoverRelays} from "@app/state"
|
||||
|
||||
let term = ""
|
||||
|
||||
$: groups = $searchGroups.searchOptions(term).filter(g => $relayUrlsByNom.get(g.nom)?.length > 0)
|
||||
$: relays = $relaySearch.searchOptions(term)
|
||||
|
||||
onMount(() => {
|
||||
load({
|
||||
relays: [...DEFAULT_RELAYS, ...$relays.map(r => r.url)],
|
||||
filters: [{kinds: [GROUP_META]}],
|
||||
})
|
||||
const sub = discoverRelays()
|
||||
|
||||
return () => sub.close()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -34,26 +27,26 @@
|
||||
</label>
|
||||
<Masonry
|
||||
animate={false}
|
||||
items={groups}
|
||||
items={relays}
|
||||
minColWidth={250}
|
||||
maxColWidth={800}
|
||||
gap={16}
|
||||
idKey="nom"
|
||||
let:item={group}>
|
||||
idKey="url"
|
||||
let:item={relay}>
|
||||
<a
|
||||
href={makeSpacePath(group.nom)}
|
||||
href={makeSpacePath(relay.url)}
|
||||
class="card bg-base-100 shadow-xl transition-all hover:shadow-2xl hover:brightness-[1.1]">
|
||||
<div class="center avatar mt-8">
|
||||
<div
|
||||
class="center relative !flex w-20 rounded-full border-2 border-solid border-base-300 bg-base-300">
|
||||
{#if group.picture}
|
||||
<img alt="" src={group.picture} />
|
||||
{#if relay.profile?.icon}
|
||||
<img alt="" src={relay.profile.icon} />
|
||||
{:else}
|
||||
<Icon icon="ghost" size={7} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if $userMembership?.noms.has(group.nom)}
|
||||
{#if $userMembership?.topicsByUrl.has(relay.url)}
|
||||
<div class="center absolute flex w-full">
|
||||
<div
|
||||
class="tooltip relative left-8 top-[38px] h-5 w-5 rounded-full bg-primary"
|
||||
@@ -63,14 +56,9 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div class="card-body">
|
||||
<h2 class="card-title justify-center">{displayGroup(group)}</h2>
|
||||
<div class="text-center text-sm">
|
||||
{#each $relayUrlsByNom.get(group.nom) || [] as url}
|
||||
<div class="badge badge-neutral">{displayRelayUrl(url)}</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if group.about}
|
||||
<p class="py-4 text-sm">{group.about}</p>
|
||||
<h2 class="card-title justify-center">{displayRelayUrl(relay.url)}</h2>
|
||||
{#if relay.profile?.description}
|
||||
<p class="py-4 text-sm text-center">{relay.profile.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
+27
-13
@@ -1,5 +1,8 @@
|
||||
<script lang="ts">
|
||||
import {nip19} from 'nostr-tools'
|
||||
import {page} from "$app/stores"
|
||||
import {sort} from '@welshman/lib'
|
||||
import {displayRelayUrl} from '@welshman/util'
|
||||
import {fly} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Page from "@lib/components/Page.svelte"
|
||||
@@ -11,8 +14,10 @@
|
||||
import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte"
|
||||
import SpaceExit from "@app/components/SpaceExit.svelte"
|
||||
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
||||
import {deriveGroup, userMembership, displayGroup} from "@app/state"
|
||||
import RoomCreate from "@app/components/RoomCreate.svelte"
|
||||
import {userMembership, decodeNEvent} from "@app/state"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
|
||||
const openMenu = () => {
|
||||
showMenu = true
|
||||
@@ -22,22 +27,24 @@
|
||||
showMenu = !showMenu
|
||||
}
|
||||
|
||||
const leaveSpace = () => pushModal(SpaceExit, {nom})
|
||||
const leaveSpace = () => pushModal(SpaceExit, {url})
|
||||
|
||||
const joinSpace = () => pushModal(SpaceJoin, {nom})
|
||||
const joinSpace = () => pushModal(SpaceJoin, {url})
|
||||
|
||||
const addRoom = () => pushModal(RoomCreate, {url})
|
||||
|
||||
let showMenu = false
|
||||
|
||||
$: nom = $page.params.nom
|
||||
$: group = deriveGroup(nom)
|
||||
$: url = decodeNEvent($page.params.nrelay)
|
||||
$: rooms = sort($userMembership?.topicsByUrl?.get(url) || [])
|
||||
</script>
|
||||
|
||||
{#key nom}
|
||||
{#key url}
|
||||
<SecondaryNav>
|
||||
<SecondaryNavSection>
|
||||
<div>
|
||||
<SecondaryNavItem class="w-full !justify-between" on:click={openMenu}>
|
||||
<strong>{displayGroup($group)}</strong>
|
||||
<strong>{displayRelayUrl(url)}</strong>
|
||||
<Icon icon="alt-arrow-down" />
|
||||
</SecondaryNavItem>
|
||||
{#if showMenu}
|
||||
@@ -45,7 +52,7 @@
|
||||
<ul
|
||||
transition:fly|local
|
||||
class="menu absolute z-popover mt-2 w-full rounded-box bg-base-100 p-2 shadow-xl">
|
||||
{#if $userMembership?.noms.has(nom)}
|
||||
{#if $userMembership?.topicsByUrl.has(url)}
|
||||
<li class="text-error">
|
||||
<Button on:click={leaveSpace}>
|
||||
<Icon icon="exit" />
|
||||
@@ -66,22 +73,22 @@
|
||||
</div>
|
||||
<div class="my-3 h-px bg-base-200" />
|
||||
<div in:fly|local>
|
||||
<SecondaryNavItem href="/spaces/{nom}">
|
||||
<SecondaryNavItem href={makeSpacePath(url)}>
|
||||
<Icon icon="chat-round" /> Chat
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
<div in:fly|local={{delay: 50}}>
|
||||
<SecondaryNavItem href="/spaces/{nom}/threads">
|
||||
<SecondaryNavItem href={makeSpacePath(url, "threads")}>
|
||||
<Icon icon="notes-minimalistic" /> Threads
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
<div in:fly|local={{delay: 100}}>
|
||||
<SecondaryNavItem href="/spaces/{nom}/events">
|
||||
<SecondaryNavItem href={makeSpacePath(url, "events")}>
|
||||
<Icon icon="calendar-minimalistic" /> Calendar
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
<div in:fly|local={{delay: 150}}>
|
||||
<SecondaryNavItem href="/spaces/{nom}/listings">
|
||||
<SecondaryNavItem href={makeSpacePath(url, "listings")}>
|
||||
<Icon icon="shop-minimalistic" /> Market
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
@@ -89,11 +96,18 @@
|
||||
<div class="h-2" />
|
||||
<SecondaryNavHeader>
|
||||
Rooms
|
||||
<Button on:click={() => alert("Uh, I don't know how to do rooms on NIP 29")}>
|
||||
<Button on:click={addRoom}>
|
||||
<Icon icon="add-circle" />
|
||||
</Button>
|
||||
</SecondaryNavHeader>
|
||||
</div>
|
||||
{#each rooms as topic, i (topic)}
|
||||
<div transition:fly|local={{delay: 250 + i * 50}}>
|
||||
<SecondaryNavItem href={makeSpacePath(url, topic)}>
|
||||
<Icon icon="hashtag" /> {topic}
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
{/each}
|
||||
</SecondaryNavSection>
|
||||
</SecondaryNav>
|
||||
<Page>
|
||||
+9
-9
@@ -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 @@
|
||||
</Spinner>
|
||||
</p>
|
||||
</div>
|
||||
<GroupCompose {nom} />
|
||||
<GroupCompose {url} {topic} />
|
||||
</div>
|
||||
Reference in New Issue
Block a user