forked from coracle/flotilla
Add remove group, format
This commit is contained in:
+10
-9
@@ -1,15 +1,15 @@
|
||||
import {derived, writable} from "svelte/store"
|
||||
import {memoize, assoc} from '@welshman/lib'
|
||||
import type {CustomEvent} from '@welshman/util'
|
||||
import {memoize, assoc} from "@welshman/lib"
|
||||
import type {CustomEvent} from "@welshman/util"
|
||||
import {Repository, createEvent, Relay} from "@welshman/util"
|
||||
import {withGetter} from "@welshman/store"
|
||||
import {NetworkContext, Tracker} from "@welshman/net"
|
||||
import type {ISigner} from "@welshman/signer"
|
||||
import {Nip46Broker, Nip46Signer, Nip07Signer, Nip01Signer} from '@welshman/signer'
|
||||
import {synced} from '@lib/util'
|
||||
import {Nip46Broker, Nip46Signer, Nip07Signer, Nip01Signer} from "@welshman/signer"
|
||||
import {synced} from "@lib/util"
|
||||
import type {Session} from "@app/types"
|
||||
|
||||
export const INDEXER_RELAYS = ["wss://purplepag.es", "wss://relay.damus.io", 'wss://nos.lol']
|
||||
export const INDEXER_RELAYS = ["wss://purplepag.es", "wss://relay.damus.io", "wss://nos.lol"]
|
||||
|
||||
export const DUFFLEPUD_URL = "https://dufflepud.onrender.com"
|
||||
|
||||
@@ -19,11 +19,13 @@ export const relay = new Relay(repository)
|
||||
|
||||
export const tracker = new Tracker()
|
||||
|
||||
export const pk = withGetter(synced<string | null>('pk', null))
|
||||
export const pk = withGetter(synced<string | null>("pk", null))
|
||||
|
||||
export const sessions = withGetter(synced<Record<string, Session>>('sessions', {}))
|
||||
export const sessions = withGetter(synced<Record<string, Session>>("sessions", {}))
|
||||
|
||||
export const session = withGetter(derived([pk, sessions], ([$pk, $sessions]) => $pk ? $sessions[$pk] : null))
|
||||
export const session = withGetter(
|
||||
derived([pk, sessions], ([$pk, $sessions]) => ($pk ? $sessions[$pk] : null)),
|
||||
)
|
||||
|
||||
export const getSession = (pubkey: string) => sessions.get()[pubkey]
|
||||
|
||||
@@ -73,4 +75,3 @@ Object.assign(NetworkContext, {
|
||||
return event
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
+18
-7
@@ -1,9 +1,17 @@
|
||||
import {goto} from '$app/navigation'
|
||||
import {append, uniqBy, now} from '@welshman/lib'
|
||||
import {goto} from "$app/navigation"
|
||||
import {append, uniqBy, now} from "@welshman/lib"
|
||||
import {GROUPS, asDecryptedEvent, readList, editList, makeList, createList} from "@welshman/util"
|
||||
import {pushToast} from '@app/toast'
|
||||
import {pk, signer, repository, INDEXER_RELAYS} from '@app/base'
|
||||
import {splitGroupId, loadRelay, loadGroup, getWriteRelayUrls, loadRelaySelections, publish, ensurePlaintext} from '@app/state'
|
||||
import {pushToast} from "@app/toast"
|
||||
import {pk, signer, repository, INDEXER_RELAYS} from "@app/base"
|
||||
import {
|
||||
splitGroupId,
|
||||
loadRelay,
|
||||
loadGroup,
|
||||
getWriteRelayUrls,
|
||||
loadRelaySelections,
|
||||
publish,
|
||||
ensurePlaintext,
|
||||
} from "@app/state"
|
||||
|
||||
export type ModifyTags = (tags: string[][]) => string[][]
|
||||
|
||||
@@ -37,5 +45,8 @@ export const updateList = async (kind: number, modifyTags: ModifyTags) => {
|
||||
await publish({event, relays})
|
||||
}
|
||||
|
||||
export const updateGroupMemberships = (newTags: string[][]) =>
|
||||
updateList(GROUPS, (tags: string[][]) => uniqBy(t => t.join(''), [...tags, ...newTags]))
|
||||
export const addGroupMemberships = (newTags: string[][]) =>
|
||||
updateList(GROUPS, (tags: string[][]) => uniqBy(t => t.join(""), [...tags, ...newTags]))
|
||||
|
||||
export const removeGroupMemberships = (noms: string[]) =>
|
||||
updateList(GROUPS, (tags: string[][]) => tags.filter(t => !noms.includes(t[1])))
|
||||
|
||||
@@ -1,35 +1,37 @@
|
||||
<script lang="ts">
|
||||
import {readable} from 'svelte/store'
|
||||
import type {CustomEvent} from '@welshman/util'
|
||||
import {GROUP_REPLY, getAncestorTags, displayProfile, displayPubkey} from '@welshman/util'
|
||||
import {deriveEvent} from '@welshman/store'
|
||||
import {fly} from '@lib/transition'
|
||||
import Icon from '@lib/components/Icon.svelte'
|
||||
import Avatar from '@lib/components/Avatar.svelte'
|
||||
import {repository} from '@app/base'
|
||||
import {deriveProfile} from '@app/state'
|
||||
import {readable} from "svelte/store"
|
||||
import type {CustomEvent} from "@welshman/util"
|
||||
import {GROUP_REPLY, getAncestorTags, displayProfile, displayPubkey} from "@welshman/util"
|
||||
import {fly} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Avatar from "@lib/components/Avatar.svelte"
|
||||
import {repository} from "@app/base"
|
||||
import {deriveProfile, deriveEvent} from "@app/state"
|
||||
|
||||
export let event: CustomEvent
|
||||
export let showPubkey: boolean
|
||||
|
||||
const profile = deriveProfile(event.pubkey)
|
||||
const {replies} = getAncestorTags(event.tags)
|
||||
const parentEvent = replies.length > 0
|
||||
? deriveEvent(repository, replies[0][1])
|
||||
: readable(null)
|
||||
const parentEvent =
|
||||
replies.length > 0 ? deriveEvent(replies[0][1], [replies[0][2]]) : readable(null)
|
||||
|
||||
$: parentProfile = deriveProfile($parentEvent?.pubkey)
|
||||
$: parentPubkey = $parentEvent?.pubkey || replies[0]?.[4]
|
||||
$: parentProfile = deriveProfile(parentPubkey)
|
||||
</script>
|
||||
|
||||
<div in:fly>
|
||||
<div in:fly class="group relative flex flex-col gap-1 p-2 transition-colors hover:bg-base-300">
|
||||
{#if event.kind === GROUP_REPLY}
|
||||
<div class="pl-12">
|
||||
<div class="text-xs flex gap-1">
|
||||
<Icon icon="arrow-right" />
|
||||
<Avatar src={$parentProfile?.picture} size={4}/>
|
||||
<p class="text-primary">{displayProfile($parentProfile, displayPubkey($parentEvent.pubkey))}<p>
|
||||
<p class="whitespace-nowrap overflow-hidden text-ellipsis">{$parentEvent.content}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 pl-12 text-xs">
|
||||
<Icon icon="arrow-right" />
|
||||
<Avatar src={$parentProfile?.picture} size={4} />
|
||||
<p class="text-primary">{displayProfile($parentProfile, displayPubkey(parentPubkey))}</p>
|
||||
<p></p>
|
||||
<p
|
||||
class="flex cursor-pointer items-center gap-1 overflow-hidden text-ellipsis whitespace-nowrap opacity-75 hover:underline">
|
||||
<Icon icon="square-share-line" size={3} />
|
||||
{$parentEvent?.content || "View note"}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex gap-2">
|
||||
@@ -40,9 +42,22 @@
|
||||
{/if}
|
||||
<div class="-mt-1">
|
||||
{#if showPubkey}
|
||||
<strong class="text-sm text-primary">{displayProfile($profile, displayPubkey(event.pubkey))}</strong>
|
||||
<strong class="text-sm text-primary"
|
||||
>{displayProfile($profile, displayPubkey(event.pubkey))}</strong>
|
||||
{/if}
|
||||
<p class="text-sm">{event.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="join absolute -top-2 right-0 border border-solid border-neutral text-xs opacity-0 transition-all group-hover:opacity-100">
|
||||
<button class="btn join-item btn-xs">
|
||||
<Icon icon="reply" size={4} />
|
||||
</button>
|
||||
<button class="btn join-item btn-xs">
|
||||
<Icon icon="smile-circle" size={4} />
|
||||
</button>
|
||||
<button class="btn join-item btn-xs">
|
||||
<Icon icon="menu-dots" size={4} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Link from '@lib/components/Link.svelte'
|
||||
import Icon from '@lib/components/Icon.svelte'
|
||||
import {clip} from '@app/toast'
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import {clip} from "@app/toast"
|
||||
</script>
|
||||
|
||||
<div class="column gap-4">
|
||||
@@ -15,18 +15,17 @@
|
||||
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
|
||||
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.
|
||||
</p>
|
||||
<div class="card2 flex-row justify-between">
|
||||
groups.fiatjaf.com
|
||||
<Button on:click={() => clip('groups.fiatjaf.com')}>
|
||||
<Button on:click={() => clip("groups.fiatjaf.com")}>
|
||||
<Icon icon="copy" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button class="btn btn-primary" on:click={() => history.back()}>
|
||||
Got it
|
||||
</Button>
|
||||
<Button class="btn btn-primary" on:click={() => history.back()}>Got it</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import Link from '@lib/components/Link.svelte'
|
||||
import Icon from '@lib/components/Icon.svelte'
|
||||
import Link from "@lib/components/Link.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import {clip} from '@app/toast'
|
||||
import {clip} from "@app/toast"
|
||||
</script>
|
||||
|
||||
<div class="column gap-4">
|
||||
@@ -11,15 +11,13 @@
|
||||
</div>
|
||||
<p>
|
||||
<Link external href="https://nostr.com/">Nostr</Link> is way to build social apps that talk to eachother.
|
||||
Users own their social identity instead of renting it from a tech company, and can bring it with them from
|
||||
app to app.
|
||||
Users own their social identity instead of renting it from a tech company, and can bring it with
|
||||
them from app to app.
|
||||
</p>
|
||||
<p>
|
||||
This can be a little confusing when you're just getting started, but as long as you're using Flotilla, it
|
||||
will work just like a normal app. When you're ready to start exploring nostr, visit your settings page to
|
||||
learn more.
|
||||
This can be a little confusing when you're just getting started, but as long as you're using
|
||||
Flotilla, it will work just like a normal app. When you're ready to start exploring nostr, visit
|
||||
your settings page to learn more.
|
||||
</p>
|
||||
<Button class="btn btn-primary" on:click={() => history.back()}>
|
||||
Got it
|
||||
</Button>
|
||||
<Button class="btn btn-primary" on:click={() => history.back()}>Got it</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import CardButton from '@lib/components/CardButton.svelte'
|
||||
import LogIn from '@app/components/LogIn.svelte'
|
||||
import SignUp from '@app/components/SignUp.svelte'
|
||||
import {pushModal} from '@app/modal'
|
||||
import CardButton from "@lib/components/CardButton.svelte"
|
||||
import LogIn from "@app/components/LogIn.svelte"
|
||||
import SignUp from "@app/components/SignUp.svelte"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
const logIn = () => pushModal(LogIn)
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<script lang="ts">
|
||||
import {nip19} from 'nostr-tools'
|
||||
import {makeSecret, Nip46Broker} from '@welshman/signer'
|
||||
import Icon from '@lib/components/Icon.svelte'
|
||||
import Field from '@lib/components/Field.svelte'
|
||||
import Button from '@lib/components/Button.svelte'
|
||||
import Spinner from '@lib/components/Spinner.svelte'
|
||||
import CardButton from '@lib/components/CardButton.svelte'
|
||||
import InfoNostr from '@app/components/LogIn.svelte'
|
||||
import {pushModal, clearModal} from '@app/modal'
|
||||
import {pushToast} from '@app/toast'
|
||||
import {addSession} from '@app/base'
|
||||
import {loadHandle} from '@app/state'
|
||||
import {nip19} from "nostr-tools"
|
||||
import {makeSecret, Nip46Broker} from "@welshman/signer"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import CardButton from "@lib/components/CardButton.svelte"
|
||||
import InfoNostr from "@app/components/LogIn.svelte"
|
||||
import {pushModal, clearModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {addSession} from "@app/base"
|
||||
import {loadHandle} from "@app/state"
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
if (!handle?.pubkey) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Sorry, it looks like you don't have an account yet. Try signing up instead."
|
||||
message: "Sorry, it looks like you don't have an account yet. Try signing up instead.",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
} else {
|
||||
pushToast({
|
||||
theme: "error",
|
||||
message: "Something went wrong! Please try again."
|
||||
message: "Something went wrong! Please try again.",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -66,14 +66,13 @@
|
||||
<h1 class="heading">Log in with Nostr</h1>
|
||||
<p class="text-center">
|
||||
Flotilla is built using the
|
||||
<Button class="link" on:click={() => pushModal(InfoNostr)}>
|
||||
nostr protocol
|
||||
</Button>, which allows you to own your social identity.
|
||||
<Button class="link" on:click={() => pushModal(InfoNostr)}>nostr protocol</Button>, which
|
||||
allows you to own your social identity.
|
||||
</p>
|
||||
</div>
|
||||
<Field>
|
||||
<div class="flex gap-2 items-center" slot="input">
|
||||
<label class="input input-bordered w-full flex items-center gap-2">
|
||||
<div class="flex items-center gap-2" slot="input">
|
||||
<label class="input input-bordered flex w-full items-center gap-2">
|
||||
<Icon icon="user-rounded" />
|
||||
<input bind:value={username} class="grow" type="text" placeholder="username" />
|
||||
</label>
|
||||
@@ -87,9 +86,7 @@
|
||||
</Button>
|
||||
<div class="text-sm">
|
||||
Need an account?
|
||||
<Button class="link" on:click={back}>
|
||||
Register
|
||||
</Button>
|
||||
<Button class="link" on:click={back}>Register</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -7,24 +7,32 @@
|
||||
|
||||
<script lang="ts">
|
||||
import {page} from "$app/stores"
|
||||
import {goto} from '$app/navigation'
|
||||
import {derived} from 'svelte/store'
|
||||
import {tweened} from 'svelte/motion'
|
||||
import {quintOut} from 'svelte/easing'
|
||||
import {identity, nth} from '@welshman/lib'
|
||||
import {goto} from "$app/navigation"
|
||||
import {derived} from "svelte/store"
|
||||
import {tweened} from "svelte/motion"
|
||||
import {quintOut} from "svelte/easing"
|
||||
import {identity, nth} from "@welshman/lib"
|
||||
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 SpaceAdd from "@app/components/SpaceAdd.svelte"
|
||||
import {session} from "@app/base"
|
||||
import {userProfile, userGroupsByNom, makeGroupId, loadGroup, deriveProfile, qualifiedGroupsById, splitGroupId} from "@app/state"
|
||||
import {
|
||||
userProfile,
|
||||
userGroupsByNom,
|
||||
makeGroupId,
|
||||
loadGroup,
|
||||
deriveProfile,
|
||||
qualifiedGroupsById,
|
||||
splitGroupId,
|
||||
} from "@app/state"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {getPrimaryNavItemIndex} from "@app/routes"
|
||||
|
||||
const activeOffset = tweened(-44, {
|
||||
duration: 300,
|
||||
easing: quintOut
|
||||
})
|
||||
duration: 300,
|
||||
easing: quintOut,
|
||||
})
|
||||
|
||||
const addSpace = () => pushModal(SpaceAdd)
|
||||
|
||||
@@ -42,7 +50,7 @@
|
||||
$: {
|
||||
if (element) {
|
||||
const index = getPrimaryNavItemIndex($page)
|
||||
const navItems: any = Array.from(element.querySelectorAll('.z-nav-item') || [])
|
||||
const navItems: any = Array.from(element.querySelectorAll(".z-nav-item") || [])
|
||||
|
||||
activeOffset.set(navItems[index].offsetTop - 44)
|
||||
}
|
||||
@@ -50,11 +58,16 @@
|
||||
</script>
|
||||
|
||||
<div class="relative w-14 bg-base-100" bind:this={element}>
|
||||
<div class="absolute z-nav-active ml-2 h-[144px] w-12 bg-base-300" style={`top: ${$activeOffset}px`} />
|
||||
<div
|
||||
class="absolute z-nav-active ml-2 h-[144px] w-12 bg-base-300"
|
||||
style={`top: ${$activeOffset}px`} />
|
||||
<div class="flex h-full flex-col justify-between">
|
||||
<div>
|
||||
<PrimaryNavItem on:click={gotoHome}>
|
||||
<Avatar src={$userProfile?.picture} class="border border-solid border-base-300 !w-10 !h-10" size={7} />
|
||||
<Avatar
|
||||
src={$userProfile?.picture}
|
||||
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]}
|
||||
|
||||
@@ -1,50 +1,59 @@
|
||||
<script lang="ts">
|
||||
import {page} from "$app/stores"
|
||||
import {fly} from '@lib/transition'
|
||||
import {fly} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
||||
import {getPrimaryNavItem} from '@app/routes'
|
||||
import SecondaryNavHeader from "@lib/components/SecondaryNavHeader.svelte"
|
||||
import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte"
|
||||
import SecondaryNavForSpace from "@app/components/SecondaryNavForSpace.svelte"
|
||||
import {getPrimaryNavItem} from "@app/routes"
|
||||
</script>
|
||||
|
||||
<div class="flex w-60 flex-col gap-1 bg-base-300 px-2 py-4">
|
||||
{#if getPrimaryNavItem($page) === 'discover'}
|
||||
<div in:fly>
|
||||
<SecondaryNavItem href="/spaces">
|
||||
<Icon icon="widget" /> Spaces
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
<div in:fly={{delay: 50}}>
|
||||
<SecondaryNavItem href="/themes">
|
||||
<Icon icon="pallete-2" /> Themes
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
{:else if getPrimaryNavItem($page) === 'space'}
|
||||
<!-- pass -->
|
||||
{:else if getPrimaryNavItem($page) === 'settings'}
|
||||
<div class="flex w-60 flex-col gap-1 bg-base-300">
|
||||
{#if getPrimaryNavItem($page) === "discover"}
|
||||
<SecondaryNavSection>
|
||||
<div in:fly>
|
||||
<SecondaryNavItem href="/spaces">
|
||||
<Icon icon="widget" /> Spaces
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
<div in:fly={{delay: 50}}>
|
||||
<SecondaryNavItem href="/themes">
|
||||
<Icon icon="pallete-2" /> Themes
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
</SecondaryNavSection>
|
||||
{:else if getPrimaryNavItem($page) === "space"}
|
||||
{#key $page.params.nom}
|
||||
<SecondaryNavForSpace nom={$page.params.nom} />
|
||||
{/key}
|
||||
{:else if getPrimaryNavItem($page) === "settings"}
|
||||
<!-- pass -->
|
||||
{:else}
|
||||
<div in:fly>
|
||||
<SecondaryNavItem href="/home">
|
||||
<Icon icon="home-smile" /> Home
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
<div in:fly={{delay: 50}}>
|
||||
<SecondaryNavItem href="/people">
|
||||
<Icon icon="user-heart" /> People
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
<div in:fly={{delay: 100}}>
|
||||
<SecondaryNavItem href="/notes">
|
||||
<Icon icon="clipboard-text" /> Saved Notes
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
<div
|
||||
in:fly={{delay: 150}}
|
||||
class="flex items-center justify-between px-4 py-2 text-sm font-bold uppercase">
|
||||
Conversations
|
||||
<div class="cursor-pointer">
|
||||
<Icon icon="add-circle" />
|
||||
<SecondaryNavSection>
|
||||
<div in:fly>
|
||||
<SecondaryNavItem href="/home">
|
||||
<Icon icon="home-smile" /> Home
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
</div>
|
||||
<div in:fly={{delay: 50}}>
|
||||
<SecondaryNavItem href="/people">
|
||||
<Icon icon="user-heart" /> People
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
<div in:fly={{delay: 100}}>
|
||||
<SecondaryNavItem href="/notes">
|
||||
<Icon icon="clipboard-text" /> Saved Notes
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
<div in:fly={{delay: 150}}>
|
||||
<SecondaryNavHeader>
|
||||
Conversations
|
||||
<div class="cursor-pointer">
|
||||
<Icon icon="add-circle" />
|
||||
</div>
|
||||
</SecondaryNavHeader>
|
||||
</div>
|
||||
</SecondaryNavSection>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {fly} from "@lib/transition"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Popover from "@lib/components/Popover.svelte"
|
||||
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
||||
import SecondaryNavHeader from "@lib/components/SecondaryNavHeader.svelte"
|
||||
import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte"
|
||||
import SpaceExit from "@app/components/SpaceExit.svelte"
|
||||
import {deriveGroup} from "@app/state"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {removeGroupMemberships} from "@app/commands"
|
||||
|
||||
export let nom
|
||||
|
||||
const group = deriveGroup(nom)
|
||||
|
||||
const openMenu = () => {
|
||||
showMenu = true
|
||||
}
|
||||
|
||||
const toggleMenu = () => {
|
||||
showMenu = !showMenu
|
||||
}
|
||||
|
||||
const leaveSpace = () => pushModal(SpaceExit, {nom})
|
||||
|
||||
let showMenu = false
|
||||
</script>
|
||||
|
||||
<SecondaryNavSection>
|
||||
<div>
|
||||
<SecondaryNavItem class="w-full !justify-between" on:click={openMenu}>
|
||||
<strong>{$group?.name || "[no name]"}</strong>
|
||||
<Icon icon="alt-arrow-down" />
|
||||
</SecondaryNavItem>
|
||||
{#if showMenu}
|
||||
<Popover onClose={toggleMenu}>
|
||||
<ul
|
||||
transition:fly|local
|
||||
class="menu absolute z-popover mt-2 w-full rounded-box bg-base-100 p-2 shadow-xl">
|
||||
<li class="text-error">
|
||||
<Button on:click={leaveSpace}>
|
||||
<Icon icon="exit" />
|
||||
Leave Space
|
||||
</Button>
|
||||
</li>
|
||||
</ul>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="h-2" />
|
||||
<SecondaryNavHeader>
|
||||
Rooms
|
||||
<div class="cursor-pointer">
|
||||
<Icon icon="add-circle" />
|
||||
</div>
|
||||
</SecondaryNavHeader>
|
||||
<div in:fly>
|
||||
<SecondaryNavItem href="/spaces">
|
||||
<Icon icon="hashtag" /> Spaces
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
<div in:fly={{delay: 50}}>
|
||||
<SecondaryNavItem href="/themes">
|
||||
<Icon icon="hashtag" /> Themes
|
||||
</SecondaryNavItem>
|
||||
</div>
|
||||
</SecondaryNavSection>
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import CardButton from '@lib/components/CardButton.svelte'
|
||||
import SpaceCreate from '@app/components/SpaceCreate.svelte'
|
||||
import SpaceJoin from '@app/components/SpaceJoin.svelte'
|
||||
import {pushModal} from '@app/modal'
|
||||
import CardButton from "@lib/components/CardButton.svelte"
|
||||
import SpaceCreate from "@app/components/SpaceCreate.svelte"
|
||||
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
const startCreate = () => pushModal(SpaceCreate)
|
||||
|
||||
@@ -13,15 +13,15 @@
|
||||
<div class="column gap-4">
|
||||
<div class="py-2">
|
||||
<h1 class="heading">Add a Space</h1>
|
||||
<p class="text-center">Spaces are places where communities come together to work, play, and hang out.</p>
|
||||
<p class="text-center">
|
||||
Spaces are places where communities come together to work, play, and hang out.
|
||||
</p>
|
||||
</div>
|
||||
<CardButton icon="add-circle" title="Get started" on:click={startCreate}>
|
||||
Just a few questions and you'll be on your way.
|
||||
</CardButton>
|
||||
<div class="card2 column gap-4">
|
||||
<h2 class="subheading">Have an invite?</h2>
|
||||
<Button class="btn btn-primary" on:click={startJoin}>
|
||||
Join a Space
|
||||
</Button>
|
||||
<Button class="btn btn-primary" on:click={startJoin}>Join a Space</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import InputProfilePicture from '@lib/components/InputProfilePicture.svelte'
|
||||
import InputProfilePicture from "@lib/components/InputProfilePicture.svelte"
|
||||
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 SpaceCreateFinish from '@app/components/SpaceCreateFinish.svelte'
|
||||
import {pushModal} from '@app/modal'
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import InfoNip29 from "@app/components/InfoNip29.svelte"
|
||||
import SpaceCreateFinish from "@app/components/SpaceCreateFinish.svelte"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
@@ -19,34 +19,30 @@
|
||||
<form class="column gap-4" on:submit|preventDefault={next}>
|
||||
<div class="py-2">
|
||||
<h1 class="heading">Customize your Space</h1>
|
||||
<p class="text-center">
|
||||
Give people a few details to go on. You can always change this later.
|
||||
</p>
|
||||
<p class="text-center">Give people a few details to go on. You can always change this later.</p>
|
||||
</div>
|
||||
<div class="flex justify-center py-2">
|
||||
<InputProfilePicture bind:file />
|
||||
</div>
|
||||
<Field>
|
||||
<p slot="label">Space Name</p>
|
||||
<label class="input input-bordered w-full flex items-center gap-2" slot="input">
|
||||
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
||||
<Icon icon="fire-minimalistic" />
|
||||
<input bind:value={name} class="grow" type="text" />
|
||||
</label>
|
||||
</Field>
|
||||
<Field>
|
||||
<p slot="label">Relay</p>
|
||||
<label class="input input-bordered w-full flex items-center gap-2" slot="input">
|
||||
<label class="input input-bordered flex w-full items-center gap-2" slot="input">
|
||||
<Icon icon="remote-controller-minimalistic" />
|
||||
<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)}>
|
||||
More information
|
||||
</Button>
|
||||
<Button class="link" on:click={() => pushModal(InfoNip29)}>More information</Button>
|
||||
</p>
|
||||
</Field>
|
||||
<div class="flex flex-row justify-between items-center gap-4">
|
||||
<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
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import {append, uniqBy} from "@welshman/lib"
|
||||
import {GROUPS} from "@welshman/util"
|
||||
import CardButton from "@lib/components/CardButton.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import SpaceCreateFinish from "@app/components/SpaceCreateFinish.svelte"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {GROUP_DELIMITER, splitGroupId, loadRelay, loadGroup, deriveGroup} from "@app/state"
|
||||
import {removeGroupMemberships} from "@app/commands"
|
||||
|
||||
export let nom
|
||||
|
||||
const group = deriveGroup(nom)
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const exit = async () => {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
await removeGroupMemberships([nom])
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
|
||||
goto("/home")
|
||||
}
|
||||
|
||||
let loading = false
|
||||
</script>
|
||||
|
||||
<form class="column gap-4" on:submit|preventDefault={exit}>
|
||||
<h1 class="heading">
|
||||
You are leaving <span class="text-primary">{$group?.name || "[no name]"}</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">
|
||||
<Button class="btn btn-link" on:click={back}>
|
||||
<Icon icon="alt-arrow-left" />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||
<Spinner {loading}>Confirm</Spinner>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import {goto} from '$app/navigation'
|
||||
import {append, uniqBy} from '@welshman/lib'
|
||||
import {GROUPS} from '@welshman/util'
|
||||
import CardButton from '@lib/components/CardButton.svelte'
|
||||
import Spinner from '@lib/components/Spinner.svelte'
|
||||
import {goto} from "$app/navigation"
|
||||
import {append, uniqBy} from "@welshman/lib"
|
||||
import {GROUPS} from "@welshman/util"
|
||||
import CardButton from "@lib/components/CardButton.svelte"
|
||||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Field from '@lib/components/Field.svelte'
|
||||
import Icon from '@lib/components/Icon.svelte'
|
||||
import SpaceCreateFinish from '@app/components/SpaceCreateFinish.svelte'
|
||||
import {pushModal} from '@app/modal'
|
||||
import {pushToast} from '@app/toast'
|
||||
import {GROUP_DELIMITER, splitGroupId, loadRelay, loadGroup} from '@app/state'
|
||||
import {updateGroupMemberships} from '@app/commands'
|
||||
import Field from "@lib/components/Field.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import SpaceCreateFinish from "@app/components/SpaceCreateFinish.svelte"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {GROUP_DELIMITER, splitGroupId, loadRelay, loadGroup} from "@app/state"
|
||||
import {addGroupMemberships} from "@app/commands"
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
@@ -24,14 +24,14 @@
|
||||
if (!relay) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Sorry, we weren't able to find that relay."
|
||||
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."
|
||||
message: "Sorry, it looks like that relay doesn't support nostr spaces.",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -40,15 +40,15 @@
|
||||
if (!group) {
|
||||
return pushToast({
|
||||
theme: "error",
|
||||
message: "Sorry, we weren't able to find that space."
|
||||
message: "Sorry, we weren't able to find that space.",
|
||||
})
|
||||
}
|
||||
|
||||
await updateGroupMemberships([["group", nom, url]])
|
||||
await addGroupMemberships([["group", nom, url]])
|
||||
|
||||
goto(`/spaces/${nom}`)
|
||||
pushToast({
|
||||
message: "Welcome to the space!"
|
||||
message: "Welcome to the space!",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -71,13 +71,11 @@
|
||||
<form class="column gap-4" on:submit|preventDefault={join}>
|
||||
<div class="py-2">
|
||||
<h1 class="heading">Join a Space</h1>
|
||||
<p class="text-center">
|
||||
Enter an invite link below to join an existing space.
|
||||
</p>
|
||||
<p class="text-center">Enter an invite link below to join an existing space.</p>
|
||||
</div>
|
||||
<Field>
|
||||
<p slot="label">Invite Link*</p>
|
||||
<label class="input input-bordered w-full flex items-center gap-2" slot="input">
|
||||
<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" />
|
||||
</label>
|
||||
@@ -85,7 +83,7 @@
|
||||
<CardButton icon="compass" title="Don't have an invite?" on:click={browse}>
|
||||
Browse other spaces on the discover page.
|
||||
</CardButton>
|
||||
<div class="flex flex-row justify-between items-center gap-4">
|
||||
<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
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
{#if $toast}
|
||||
{#key $toast.id}
|
||||
<div transition:fly class="toast z-toast">
|
||||
<div role="alert" class="alert flex justify-center" class:alert-error={$toast.theme === "error"}>
|
||||
<div
|
||||
role="alert"
|
||||
class="alert flex justify-center"
|
||||
class:alert-error={$toast.theme === "error"}>
|
||||
{$toast.message}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -17,6 +17,6 @@ export const pushModal = (component: ComponentType, props: Record<string, any> =
|
||||
}
|
||||
|
||||
export const clearModal = () => {
|
||||
goto('#')
|
||||
emitter.emit('close')
|
||||
goto("#")
|
||||
emitter.emit("close")
|
||||
}
|
||||
|
||||
+9
-9
@@ -1,22 +1,22 @@
|
||||
import type {Page} from '@sveltejs/kit'
|
||||
import {userGroupsByNom} from '@app/state'
|
||||
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'
|
||||
if ($page.route?.id?.startsWith('/settings')) return 'settings'
|
||||
return 'home'
|
||||
if ($page.route?.id?.match("^/(spaces|themes)$")) return "discover"
|
||||
if ($page.route?.id?.startsWith("/spaces")) return "space"
|
||||
if ($page.route?.id?.startsWith("/settings")) return "settings"
|
||||
return "home"
|
||||
}
|
||||
|
||||
export const getPrimaryNavItemIndex = ($page: Page) => {
|
||||
switch (getPrimaryNavItem($page)) {
|
||||
case 'discover':
|
||||
case "discover":
|
||||
return userGroupsByNom.get().size + 2
|
||||
case 'space':
|
||||
case "space":
|
||||
return Array.from(userGroupsByNom.get().keys()).findIndex(nom => nom === $page.params.nom) + 1
|
||||
case 'settings':
|
||||
case "settings":
|
||||
return userGroupsByNom.get().size + 3
|
||||
default:
|
||||
return 0
|
||||
|
||||
+164
-80
@@ -1,17 +1,67 @@
|
||||
import type {Readable} from "svelte/store"
|
||||
import type {FuseResult} from 'fuse.js'
|
||||
import type {FuseResult} from "fuse.js"
|
||||
import {get, writable, readable, derived} from "svelte/store"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import {max, uniq, between, 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, GROUP_JOIN, GROUP_ADD_USER} from "@welshman/util"
|
||||
import type {Filter, SignedEvent, CustomEvent, PublishedProfile, PublishedList} from '@welshman/util'
|
||||
import type {SubscribeRequest, PublishRequest} from '@welshman/net'
|
||||
import {publish as basePublish, subscribe} from '@welshman/net'
|
||||
import {decrypt} from '@welshman/signer'
|
||||
import {
|
||||
max,
|
||||
uniq,
|
||||
between,
|
||||
uniqBy,
|
||||
groupBy,
|
||||
pushToMapKey,
|
||||
nthEq,
|
||||
batcher,
|
||||
postJson,
|
||||
stripProtocol,
|
||||
assoc,
|
||||
indexBy,
|
||||
now,
|
||||
} from "@welshman/lib"
|
||||
import {
|
||||
getIdFilters,
|
||||
getIdentifier,
|
||||
getRelayTags,
|
||||
getRelayTagValues,
|
||||
normalizeRelayUrl,
|
||||
getPubkeyTagValues,
|
||||
GROUP_META,
|
||||
PROFILE,
|
||||
RELAYS,
|
||||
FOLLOWS,
|
||||
MUTES,
|
||||
GROUPS,
|
||||
getGroupTags,
|
||||
readProfile,
|
||||
readList,
|
||||
asDecryptedEvent,
|
||||
editList,
|
||||
makeList,
|
||||
createList,
|
||||
GROUP_JOIN,
|
||||
GROUP_ADD_USER,
|
||||
} from "@welshman/util"
|
||||
import type {
|
||||
Filter,
|
||||
SignedEvent,
|
||||
CustomEvent,
|
||||
PublishedProfile,
|
||||
PublishedList,
|
||||
} from "@welshman/util"
|
||||
import type {SubscribeRequest, PublishRequest} from "@welshman/net"
|
||||
import {publish as basePublish, subscribe} from "@welshman/net"
|
||||
import {decrypt} from "@welshman/signer"
|
||||
import {deriveEvents, deriveEventsMapped, getter, withGetter} from "@welshman/store"
|
||||
import {parseJson, createSearch} from '@lib/util'
|
||||
import type {Session, Handle, Relay} from '@app/types'
|
||||
import {INDEXER_RELAYS, DUFFLEPUD_URL, repository, pk, getSession, getSigner, signer} from "@app/base"
|
||||
import {parseJson, createSearch} from "@lib/util"
|
||||
import type {Session, Handle, Relay} from "@app/types"
|
||||
import {
|
||||
INDEXER_RELAYS,
|
||||
DUFFLEPUD_URL,
|
||||
repository,
|
||||
pk,
|
||||
getSession,
|
||||
getSigner,
|
||||
signer,
|
||||
} from "@app/base"
|
||||
|
||||
// Utils
|
||||
|
||||
@@ -21,15 +71,15 @@ export const createCollection = <T>({
|
||||
getKey,
|
||||
load,
|
||||
}: {
|
||||
name: string,
|
||||
store: Readable<T[]>,
|
||||
getKey: (item: T) => string,
|
||||
name: string
|
||||
store: Readable<T[]>
|
||||
getKey: (item: T) => string
|
||||
load: (key: string, ...args: any) => Promise<any>
|
||||
}) => {
|
||||
const indexStore = derived(store, $items => indexBy(getKey, $items))
|
||||
const getIndex = getter(indexStore)
|
||||
const getItem = (key: string) => getIndex().get(key)
|
||||
const pending = new Map<string, Promise<Maybe<T>>>
|
||||
const pending = new Map<string, Promise<Maybe<T>>>()
|
||||
|
||||
const loadItem = async (key: string, ...args: any[]) => {
|
||||
if (getFreshness(name, key) > now() - 3600) {
|
||||
@@ -68,6 +118,25 @@ export const createCollection = <T>({
|
||||
return {indexStore, getIndex, deriveItem, loadItem, getItem}
|
||||
}
|
||||
|
||||
export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
|
||||
let attempted = false
|
||||
|
||||
const filters = getIdFilters([idOrAddress])
|
||||
const relays = [...hints, ...INDEXER_RELAYS]
|
||||
|
||||
return derived(
|
||||
deriveEvents(repository, {filters, includeDeleted: true}),
|
||||
(events: CustomEvent[]) => {
|
||||
if (!attempted && events.length === 0) {
|
||||
load({relays, filters})
|
||||
attempted = true
|
||||
}
|
||||
|
||||
return events[0]
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export const publish = (request: PublishRequest) => {
|
||||
repository.publish(request.event)
|
||||
|
||||
@@ -79,12 +148,12 @@ export const load = (request: SubscribeRequest) =>
|
||||
const sub = subscribe({closeOnEose: true, timeout: 3000, delay: 50, ...request})
|
||||
const events: CustomEvent[] = []
|
||||
|
||||
sub.emitter.on('event', (url: string, e: SignedEvent) => {
|
||||
sub.emitter.on("event", (url: string, e: SignedEvent) => {
|
||||
repository.publish(e)
|
||||
events.push(e)
|
||||
})
|
||||
|
||||
sub.emitter.on('complete', () => resolve(events))
|
||||
sub.emitter.on("complete", () => resolve(events))
|
||||
})
|
||||
|
||||
// Freshness
|
||||
@@ -93,9 +162,11 @@ export const freshness = withGetter(writable<Record<string, number>>({}))
|
||||
|
||||
export const getFreshnessKey = (ns: string, key: string) => `${ns}:${key}`
|
||||
|
||||
export const getFreshness = (ns: string, key: string) => freshness.get()[getFreshnessKey(ns, key)] || 0
|
||||
export const getFreshness = (ns: string, key: string) =>
|
||||
freshness.get()[getFreshnessKey(ns, key)] || 0
|
||||
|
||||
export const setFreshness = (ns: string, key: string, ts: number) => freshness.update(assoc(getFreshnessKey(ns, key), ts))
|
||||
export const setFreshness = (ns: string, key: string, ts: number) =>
|
||||
freshness.update(assoc(getFreshnessKey(ns, key), ts))
|
||||
|
||||
export const setFreshnessBulk = (ns: string, updates: Record<string, number>) =>
|
||||
freshness.update($freshness => {
|
||||
@@ -131,7 +202,9 @@ export const ensurePlaintext = async (e: CustomEvent) => {
|
||||
|
||||
export const relays = writable<Relay[]>([])
|
||||
|
||||
export const relaysByPubkey = derived(relays, $relays => groupBy(($relay: Relay) => $relay.pubkey, $relays))
|
||||
export const relaysByPubkey = derived(relays, $relays =>
|
||||
groupBy(($relay: Relay) => $relay.pubkey, $relays),
|
||||
)
|
||||
|
||||
export const {
|
||||
indexStore: relaysByUrl,
|
||||
@@ -139,7 +212,7 @@ export const {
|
||||
deriveItem: deriveRelay,
|
||||
loadItem: loadRelay,
|
||||
} = createCollection({
|
||||
name: 'relays',
|
||||
name: "relays",
|
||||
store: relays,
|
||||
getKey: (relay: Relay) => relay.url,
|
||||
load: batcher(800, async (urls: string[]) => {
|
||||
@@ -168,7 +241,7 @@ export const {
|
||||
deriveItem: deriveHandle,
|
||||
loadItem: loadHandle,
|
||||
} = createCollection({
|
||||
name: 'handles',
|
||||
name: "handles",
|
||||
store: handles,
|
||||
getKey: (handle: Handle) => handle.nip05,
|
||||
load: batcher(800, async (nip05s: string[]) => {
|
||||
@@ -201,7 +274,7 @@ export const {
|
||||
deriveItem: deriveProfile,
|
||||
loadItem: loadProfile,
|
||||
} = createCollection({
|
||||
name: 'profiles',
|
||||
name: "profiles",
|
||||
store: profiles,
|
||||
getKey: profile => profile.event.pubkey,
|
||||
load: (pubkey: string, relays = [], request: Partial<SubscribeRequest> = {}) =>
|
||||
@@ -215,10 +288,14 @@ export const {
|
||||
// Relay selections
|
||||
|
||||
export const getReadRelayUrls = (event?: CustomEvent): string[] =>
|
||||
getRelayTags(event?.tags || []).filter((t: string[]) => !t[2] || t[2] === 'read').map((t: string[]) => normalizeRelayUrl(t[1]))
|
||||
getRelayTags(event?.tags || [])
|
||||
.filter((t: string[]) => !t[2] || t[2] === "read")
|
||||
.map((t: string[]) => normalizeRelayUrl(t[1]))
|
||||
|
||||
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]}]})
|
||||
|
||||
@@ -228,7 +305,7 @@ export const {
|
||||
deriveItem: deriveRelaySelections,
|
||||
loadItem: loadRelaySelections,
|
||||
} = createCollection({
|
||||
name: 'relaySelections',
|
||||
name: "relaySelections",
|
||||
store: relaySelections,
|
||||
getKey: relaySelections => relaySelections.pubkey,
|
||||
load: (pubkey: string, relays = [], request: Partial<SubscribeRequest> = {}) =>
|
||||
@@ -236,7 +313,7 @@ export const {
|
||||
...request,
|
||||
relays: [...relays, ...INDEXER_RELAYS],
|
||||
filters: [{kinds: [RELAYS], authors: [pubkey]}],
|
||||
})
|
||||
}),
|
||||
})
|
||||
|
||||
// Follows
|
||||
@@ -258,7 +335,7 @@ export const {
|
||||
deriveItem: deriveFollows,
|
||||
loadItem: loadFollows,
|
||||
} = createCollection({
|
||||
name: 'follows',
|
||||
name: "follows",
|
||||
store: follows,
|
||||
getKey: follows => follows.event.pubkey,
|
||||
load: (pubkey: string, relays = [], request: Partial<SubscribeRequest> = {}) =>
|
||||
@@ -266,7 +343,7 @@ export const {
|
||||
...request,
|
||||
relays: [...relays, ...INDEXER_RELAYS],
|
||||
filters: [{kinds: [FOLLOWS], authors: [pubkey]}],
|
||||
})
|
||||
}),
|
||||
})
|
||||
|
||||
// Mutes
|
||||
@@ -288,7 +365,7 @@ export const {
|
||||
deriveItem: deriveMutes,
|
||||
loadItem: loadMutes,
|
||||
} = createCollection({
|
||||
name: 'mutes',
|
||||
name: "mutes",
|
||||
store: mutes,
|
||||
getKey: mute => mute.event.pubkey,
|
||||
load: (pubkey: string, relays = [], request: Partial<SubscribeRequest> = {}) =>
|
||||
@@ -296,7 +373,7 @@ export const {
|
||||
...request,
|
||||
relays: [...relays, ...INDEXER_RELAYS],
|
||||
filters: [{kinds: [MUTES], authors: [pubkey]}],
|
||||
})
|
||||
}),
|
||||
})
|
||||
|
||||
// Groups
|
||||
@@ -304,7 +381,7 @@ export const {
|
||||
export const GROUP_DELIMITER = `'`
|
||||
|
||||
export const makeGroupId = (url: string, nom: string) =>
|
||||
[stripProtocol(url).replace(/\/$/, ''), nom].join(GROUP_DELIMITER)
|
||||
[stripProtocol(url).replace(/\/$/, ""), nom].join(GROUP_DELIMITER)
|
||||
|
||||
export const splitGroupId = (groupId: string) => {
|
||||
const [url, nom] = groupId.split(GROUP_DELIMITER)
|
||||
@@ -321,10 +398,10 @@ export const getGroupName = (e?: CustomEvent) => e?.tags.find(nthEq(0, "name"))?
|
||||
export const getGroupPicture = (e?: CustomEvent) => e?.tags.find(nthEq(0, "picture"))?.[1]
|
||||
|
||||
export type Group = {
|
||||
nom: string,
|
||||
name?: string,
|
||||
about?: string,
|
||||
picture?: string,
|
||||
nom: string
|
||||
name?: string
|
||||
about?: string
|
||||
picture?: string
|
||||
event?: CustomEvent
|
||||
}
|
||||
|
||||
@@ -353,7 +430,7 @@ export const {
|
||||
deriveItem: deriveGroup,
|
||||
loadItem: loadGroup,
|
||||
} = createCollection({
|
||||
name: 'groups',
|
||||
name: "groups",
|
||||
store: groups,
|
||||
getKey: (group: PublishedGroup) => group.nom,
|
||||
load: (nom: string, relays: string[] = [], request: Partial<SubscribeRequest> = {}) =>
|
||||
@@ -362,25 +439,23 @@ export const {
|
||||
load({
|
||||
...request,
|
||||
relays,
|
||||
filters: [{kinds: [GROUP_META], '#d': [nom]}],
|
||||
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
|
||||
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}],
|
||||
},
|
||||
})
|
||||
return result.score! * scale
|
||||
},
|
||||
fuseOptions: {
|
||||
keys: ["name", {name: "about", weight: 0.3}],
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
// Qualified groups
|
||||
@@ -396,12 +471,16 @@ export const qualifiedGroups = derived([relaysByPubkey, groups], ([$relaysByPubk
|
||||
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 qualifiedGroupsById = derived(qualifiedGroups, $qualifiedGroups =>
|
||||
indexBy($qg => $qg.id, $qualifiedGroups),
|
||||
)
|
||||
|
||||
export const qualifiedGroupsByNom = derived(qualifiedGroups, $qualifiedGroups => groupBy($qg => $qg.group.nom, $qualifiedGroups))
|
||||
export const qualifiedGroupsByNom = derived(qualifiedGroups, $qualifiedGroups =>
|
||||
groupBy($qg => $qg.group.nom, $qualifiedGroups),
|
||||
)
|
||||
|
||||
export const relayUrlsByNom = derived(qualifiedGroups, $qualifiedGroups => {
|
||||
const $relayUrlsByNom = new Map()
|
||||
@@ -452,7 +531,7 @@ export const {
|
||||
deriveItem: deriveGroupMembership,
|
||||
loadItem: loadGroupMembership,
|
||||
} = createCollection({
|
||||
name: 'groupMemberships',
|
||||
name: "groupMemberships",
|
||||
store: groupMemberships,
|
||||
getKey: groupMembership => groupMembership.event.pubkey,
|
||||
load: (pubkey: string, relays = [], request: Partial<SubscribeRequest> = {}) =>
|
||||
@@ -460,7 +539,7 @@ export const {
|
||||
...request,
|
||||
relays: [...relays, ...INDEXER_RELAYS],
|
||||
filters: [{kinds: [GROUPS], authors: [pubkey]}],
|
||||
})
|
||||
}),
|
||||
})
|
||||
|
||||
// Group Messages
|
||||
@@ -471,7 +550,7 @@ export type GroupMessage = {
|
||||
}
|
||||
|
||||
export const readGroupMessage = (event: CustomEvent): Maybe<GroupMessage> => {
|
||||
const nom = event.tags.find(nthEq(0, 'h'))?.[1]
|
||||
const nom = event.tags.find(nthEq(0, "h"))?.[1]
|
||||
|
||||
if (!nom || between(GROUP_ADD_USER - 1, GROUP_JOIN + 1, event.kind)) {
|
||||
return undefined
|
||||
@@ -505,20 +584,20 @@ export const {
|
||||
deriveItem: deriveGroupConversation,
|
||||
loadItem: loadGroupConversation,
|
||||
} = createCollection({
|
||||
name: 'groupConversations',
|
||||
name: "groupConversations",
|
||||
store: groupConversations,
|
||||
getKey: groupConversation => groupConversation.nom,
|
||||
load: (nom: string, hints = [], request: Partial<SubscribeRequest> = {}) => {
|
||||
const relays = [...hints, ...get(relayUrlsByNom).get(nom) || []]
|
||||
const relays = [...hints, ...(get(relayUrlsByNom).get(nom) || [])]
|
||||
const conversation = get(groupConversations).find(c => c.nom === nom)
|
||||
const timestamps = conversation?.messages.map(m => m.event.created_at) || []
|
||||
const since = Math.min(0, max(timestamps) - 3600)
|
||||
const since = Math.max(0, max(timestamps) - 3600)
|
||||
|
||||
if (relays.length === 0) {
|
||||
console.warn(`Attempted to load conversation for ${nom} with no qualified groups`)
|
||||
}
|
||||
|
||||
return load({...request, relays, filters: [{'#h': [nom], since}]})
|
||||
return load({...request, relays, filters: [{"#h": [nom], since}]})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -532,30 +611,35 @@ export const userProfile = derived([pk, profilesByPubkey], ([$pk, $profilesByPub
|
||||
return $profilesByPubkey.get($pk)
|
||||
})
|
||||
|
||||
export const userMembership = derived([pk, groupMembershipByPubkey], ([$pk, $groupMembershipByPubkey]) => {
|
||||
if (!$pk) return null
|
||||
export const userMembership = derived(
|
||||
[pk, groupMembershipByPubkey],
|
||||
([$pk, $groupMembershipByPubkey]) => {
|
||||
if (!$pk) return null
|
||||
|
||||
loadGroupMembership($pk)
|
||||
loadGroupMembership($pk)
|
||||
|
||||
return $groupMembershipByPubkey.get($pk)
|
||||
})
|
||||
return $groupMembershipByPubkey.get($pk)
|
||||
},
|
||||
)
|
||||
|
||||
export const userGroupsByNom = withGetter(derived([userMembership, qualifiedGroupsById], ([$userMembership, $qualifiedGroupsById]) => {
|
||||
const $userGroupsByNom = new Map()
|
||||
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) || []
|
||||
for (const id of $userMembership?.ids || []) {
|
||||
const [url, nom] = splitGroupId(id)
|
||||
const group = $qualifiedGroupsById.get(id)
|
||||
const groups = $userGroupsByNom.get(nom) || []
|
||||
|
||||
loadGroup(nom, [url])
|
||||
loadGroup(nom, [url])
|
||||
|
||||
if (group) {
|
||||
groups.push(group)
|
||||
if (group) {
|
||||
groups.push(group)
|
||||
}
|
||||
|
||||
$userGroupsByNom.set(nom, groups)
|
||||
}
|
||||
|
||||
$userGroupsByNom.set(nom, groups)
|
||||
}
|
||||
|
||||
return $userGroupsByNom
|
||||
}))
|
||||
return $userGroupsByNom
|
||||
}),
|
||||
)
|
||||
|
||||
+12
-10
@@ -1,12 +1,12 @@
|
||||
import {openDB, deleteDB} from "idb"
|
||||
import type {IDBPDatabase} from "idb"
|
||||
import {throttle} from 'throttle-debounce'
|
||||
import {writable} from 'svelte/store'
|
||||
import type {Unsubscriber, Writable} from 'svelte/store'
|
||||
import {isNil, randomInt} from '@welshman/lib'
|
||||
import {withGetter} from '@welshman/store'
|
||||
import {getJson, setJson} from '@lib/util'
|
||||
import {pk, sessions, repository} from '@app/base'
|
||||
import {throttle} from "throttle-debounce"
|
||||
import {writable} from "svelte/store"
|
||||
import type {Unsubscriber, Writable} from "svelte/store"
|
||||
import {isNil, randomInt} from "@welshman/lib"
|
||||
import {withGetter} from "@welshman/store"
|
||||
import {getJson, setJson} from "@lib/util"
|
||||
import {pk, sessions, repository} from "@app/base"
|
||||
|
||||
export type Item = Record<string, any>
|
||||
|
||||
@@ -74,7 +74,10 @@ export const initIndexedDbAdapter = async (name: string, adapter: IndexedDbAdapt
|
||||
}
|
||||
|
||||
if (removedRecords.length > 0) {
|
||||
await bulkDelete(name, removedRecords.map(item => item[adapter.keyPath]))
|
||||
await bulkDelete(
|
||||
name,
|
||||
removedRecords.map(item => item[adapter.keyPath]),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -106,8 +109,7 @@ export const initStorage = async (adapters: Record<string, IndexedDbAdapter>) =>
|
||||
})
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(adapters)
|
||||
.map(([name, config]) => initIndexedDbAdapter(name, config))
|
||||
Object.entries(adapters).map(([name, config]) => initIndexedDbAdapter(name, config)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
import {synced} from '@lib/util'
|
||||
import {synced} from "@lib/util"
|
||||
|
||||
export const theme = synced<string>("theme", "dark")
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import {writable} from "svelte/store"
|
||||
import {randomId} from "@welshman/lib"
|
||||
import {copyToClipboard} from '@lib/html'
|
||||
import {copyToClipboard} from "@lib/html"
|
||||
|
||||
export type ToastParams = {
|
||||
message: string
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import {verifiedSymbol} from 'nostr-tools'
|
||||
import {verifiedSymbol} from "nostr-tools"
|
||||
import type {Nip46Handler} from "@welshman/signer"
|
||||
import type {SignedEvent, TrustedEvent, RelayProfile} from "@welshman/util"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user