Add remove group, format

This commit is contained in:
Jon Staab
2024-08-16 10:50:38 -07:00
parent 437cfa7bc4
commit bd8fcd3264
51 changed files with 800 additions and 435 deletions
+10 -9
View File
@@ -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
View File
@@ -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])))
+37 -22
View File
@@ -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>
+9 -10
View File
@@ -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>
+9 -11
View File
@@ -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>
+4 -4
View File
@@ -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)
+19 -22
View File
@@ -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>
+26 -13
View File
@@ -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]}
+48 -39
View File
@@ -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
View File
@@ -1 +0,0 @@
+8 -8
View File
@@ -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>
+11 -15
View File
@@ -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
+53
View File
@@ -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>
+20 -22
View File
@@ -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
+4 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,3 +1,3 @@
import {synced} from '@lib/util'
import {synced} from "@lib/util"
export const theme = synced<string>("theme", "dark")
+1 -1
View File
@@ -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
View File
@@ -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"