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

View File

@@ -2,5 +2,4 @@
A discord-like nostr client. WIP. A discord-like nostr client. WIP.
Figure out state management. Add fetched_at to all events. `fetch` batches and loads, `get` gets the value, `derive` returns a store. For optimization, create getters for everything that uses `get` a lot. Figure out state management. Add fetched_at to all events. `fetch` batches and loads, `get` gets the value, `derive` returns a store. For optimization, create getters for everything that uses `get` a lot.

View File

@@ -53,27 +53,27 @@
} }
.center { .center {
@apply flex justify-center items-center; @apply flex items-center justify-center;
} }
.content { .content {
@apply max-w-3xl w-full p-12 m-auto; @apply m-auto w-full max-w-3xl p-12;
} }
.heading { .heading {
@apply text-2xl text-center; @apply text-center text-2xl;
} }
.subheading { .subheading {
@apply text-xl text-center; @apply text-center text-xl;
} }
.superheading { .superheading {
@apply text-4xl text-center; @apply text-center text-4xl;
} }
.link { .link {
@apply text-primary underline cursor-pointer; @apply cursor-pointer text-primary underline;
} }
.input input::placeholder { .input input::placeholder {

View File

@@ -1,15 +1,15 @@
import {derived, writable} from "svelte/store" import {derived, writable} from "svelte/store"
import {memoize, assoc} from '@welshman/lib' import {memoize, assoc} from "@welshman/lib"
import type {CustomEvent} from '@welshman/util' import type {CustomEvent} from "@welshman/util"
import {Repository, createEvent, Relay} from "@welshman/util" import {Repository, createEvent, Relay} from "@welshman/util"
import {withGetter} from "@welshman/store" import {withGetter} from "@welshman/store"
import {NetworkContext, Tracker} from "@welshman/net" import {NetworkContext, Tracker} from "@welshman/net"
import type {ISigner} from "@welshman/signer" import type {ISigner} from "@welshman/signer"
import {Nip46Broker, Nip46Signer, Nip07Signer, Nip01Signer} from '@welshman/signer' import {Nip46Broker, Nip46Signer, Nip07Signer, Nip01Signer} from "@welshman/signer"
import {synced} from '@lib/util' import {synced} from "@lib/util"
import type {Session} from "@app/types" 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" 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 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] export const getSession = (pubkey: string) => sessions.get()[pubkey]
@@ -73,4 +75,3 @@ Object.assign(NetworkContext, {
return event return event
}, },
}) })

View File

@@ -1,9 +1,17 @@
import {goto} from '$app/navigation' import {goto} from "$app/navigation"
import {append, uniqBy, now} from '@welshman/lib' import {append, uniqBy, now} from "@welshman/lib"
import {GROUPS, asDecryptedEvent, readList, editList, makeList, createList} from "@welshman/util" import {GROUPS, asDecryptedEvent, readList, editList, makeList, createList} from "@welshman/util"
import {pushToast} from '@app/toast' import {pushToast} from "@app/toast"
import {pk, signer, repository, INDEXER_RELAYS} from '@app/base' import {pk, signer, repository, INDEXER_RELAYS} from "@app/base"
import {splitGroupId, loadRelay, loadGroup, getWriteRelayUrls, loadRelaySelections, publish, ensurePlaintext} from '@app/state' import {
splitGroupId,
loadRelay,
loadGroup,
getWriteRelayUrls,
loadRelaySelections,
publish,
ensurePlaintext,
} from "@app/state"
export type ModifyTags = (tags: string[][]) => string[][] export type ModifyTags = (tags: string[][]) => string[][]
@@ -37,5 +45,8 @@ export const updateList = async (kind: number, modifyTags: ModifyTags) => {
await publish({event, relays}) await publish({event, relays})
} }
export const updateGroupMemberships = (newTags: string[][]) => export const addGroupMemberships = (newTags: string[][]) =>
updateList(GROUPS, (tags: string[][]) => uniqBy(t => t.join(''), [...tags, ...newTags])) 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])))

View File

@@ -1,35 +1,37 @@
<script lang="ts"> <script lang="ts">
import {readable} from 'svelte/store' import {readable} from "svelte/store"
import type {CustomEvent} from '@welshman/util' import type {CustomEvent} from "@welshman/util"
import {GROUP_REPLY, getAncestorTags, displayProfile, displayPubkey} from '@welshman/util' import {GROUP_REPLY, getAncestorTags, displayProfile, displayPubkey} from "@welshman/util"
import {deriveEvent} from '@welshman/store' import {fly} from "@lib/transition"
import {fly} from '@lib/transition' import Icon from "@lib/components/Icon.svelte"
import Icon from '@lib/components/Icon.svelte' import Avatar from "@lib/components/Avatar.svelte"
import Avatar from '@lib/components/Avatar.svelte' import {repository} from "@app/base"
import {repository} from '@app/base' import {deriveProfile, deriveEvent} from "@app/state"
import {deriveProfile} from '@app/state'
export let event: CustomEvent export let event: CustomEvent
export let showPubkey: boolean export let showPubkey: boolean
const profile = deriveProfile(event.pubkey) const profile = deriveProfile(event.pubkey)
const {replies} = getAncestorTags(event.tags) const {replies} = getAncestorTags(event.tags)
const parentEvent = replies.length > 0 const parentEvent =
? deriveEvent(repository, replies[0][1]) replies.length > 0 ? deriveEvent(replies[0][1], [replies[0][2]]) : readable(null)
: readable(null)
$: parentProfile = deriveProfile($parentEvent?.pubkey) $: parentPubkey = $parentEvent?.pubkey || replies[0]?.[4]
$: parentProfile = deriveProfile(parentPubkey)
</script> </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} {#if event.kind === GROUP_REPLY}
<div class="pl-12"> <div class="flex items-center gap-1 pl-12 text-xs">
<div class="text-xs flex gap-1"> <Icon icon="arrow-right" />
<Icon icon="arrow-right" /> <Avatar src={$parentProfile?.picture} size={4} />
<Avatar src={$parentProfile?.picture} size={4}/> <p class="text-primary">{displayProfile($parentProfile, displayPubkey(parentPubkey))}</p>
<p class="text-primary">{displayProfile($parentProfile, displayPubkey($parentEvent.pubkey))}<p> <p></p>
<p class="whitespace-nowrap overflow-hidden text-ellipsis">{$parentEvent.content}</p> <p
</div> 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> </div>
{/if} {/if}
<div class="flex gap-2"> <div class="flex gap-2">
@@ -40,9 +42,22 @@
{/if} {/if}
<div class="-mt-1"> <div class="-mt-1">
{#if showPubkey} {#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} {/if}
<p class="text-sm">{event.content}</p> <p class="text-sm">{event.content}</p>
</div> </div>
</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> </div>

View File

@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Link from '@lib/components/Link.svelte' import Link from "@lib/components/Link.svelte"
import Icon from '@lib/components/Icon.svelte' import Icon from "@lib/components/Icon.svelte"
import {clip} from '@app/toast' import {clip} from "@app/toast"
</script> </script>
<div class="column gap-4"> <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. This means that anyone can host their own data, making the web more decentralized and resilient.
</p> </p>
<p> <p>
Only some relays support spaces. You can find a list of suggested relays below, Only some relays support spaces. You can find a list of suggested relays below, or you can <Link
or you can <Link external href="https://coracle.tools">host your own</Link>. external
If you do decide to join someone else's, make sure to follow their directions for registering 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. as a user.
</p> </p>
<div class="card2 flex-row justify-between"> <div class="card2 flex-row justify-between">
groups.fiatjaf.com groups.fiatjaf.com
<Button on:click={() => clip('groups.fiatjaf.com')}> <Button on:click={() => clip("groups.fiatjaf.com")}>
<Icon icon="copy" /> <Icon icon="copy" />
</Button> </Button>
</div> </div>
<Button class="btn btn-primary" on:click={() => history.back()}> <Button class="btn btn-primary" on:click={() => history.back()}>Got it</Button>
Got it
</Button>
</div> </div>

View File

@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import Link from '@lib/components/Link.svelte' import Link from "@lib/components/Link.svelte"
import Icon from '@lib/components/Icon.svelte' import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import {clip} from '@app/toast' import {clip} from "@app/toast"
</script> </script>
<div class="column gap-4"> <div class="column gap-4">
@@ -11,15 +11,13 @@
</div> </div>
<p> <p>
<Link external href="https://nostr.com/">Nostr</Link> is way to build social apps that talk to eachother. <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 Users own their social identity instead of renting it from a tech company, and can bring it with
app to app. them from app to app.
</p> </p>
<p> <p>
This can be a little confusing when you're just getting started, but as long as you're using Flotilla, it This can be a little confusing when you're just getting started, but as long as you're using
will work just like a normal app. When you're ready to start exploring nostr, visit your settings page to Flotilla, it will work just like a normal app. When you're ready to start exploring nostr, visit
learn more. your settings page to learn more.
</p> </p>
<Button class="btn btn-primary" on:click={() => history.back()}> <Button class="btn btn-primary" on:click={() => history.back()}>Got it</Button>
Got it
</Button>
</div> </div>

View File

@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import CardButton from '@lib/components/CardButton.svelte' import CardButton from "@lib/components/CardButton.svelte"
import LogIn from '@app/components/LogIn.svelte' import LogIn from "@app/components/LogIn.svelte"
import SignUp from '@app/components/SignUp.svelte' import SignUp from "@app/components/SignUp.svelte"
import {pushModal} from '@app/modal' import {pushModal} from "@app/modal"
const logIn = () => pushModal(LogIn) const logIn = () => pushModal(LogIn)

View File

@@ -1,16 +1,16 @@
<script lang="ts"> <script lang="ts">
import {nip19} from 'nostr-tools' import {nip19} from "nostr-tools"
import {makeSecret, Nip46Broker} from '@welshman/signer' import {makeSecret, Nip46Broker} from "@welshman/signer"
import Icon from '@lib/components/Icon.svelte' import Icon from "@lib/components/Icon.svelte"
import Field from '@lib/components/Field.svelte' import Field from "@lib/components/Field.svelte"
import Button from '@lib/components/Button.svelte' import Button from "@lib/components/Button.svelte"
import Spinner from '@lib/components/Spinner.svelte' import Spinner from "@lib/components/Spinner.svelte"
import CardButton from '@lib/components/CardButton.svelte' import CardButton from "@lib/components/CardButton.svelte"
import InfoNostr from '@app/components/LogIn.svelte' import InfoNostr from "@app/components/LogIn.svelte"
import {pushModal, clearModal} from '@app/modal' import {pushModal, clearModal} from "@app/modal"
import {pushToast} from '@app/toast' import {pushToast} from "@app/toast"
import {addSession} from '@app/base' import {addSession} from "@app/base"
import {loadHandle} from '@app/state' import {loadHandle} from "@app/state"
const back = () => history.back() const back = () => history.back()
@@ -22,7 +22,7 @@
if (!handle?.pubkey) { if (!handle?.pubkey) {
return pushToast({ return pushToast({
theme: "error", 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 { } else {
pushToast({ pushToast({
theme: "error", 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> <h1 class="heading">Log in with Nostr</h1>
<p class="text-center"> <p class="text-center">
Flotilla is built using the Flotilla is built using the
<Button class="link" on:click={() => pushModal(InfoNostr)}> <Button class="link" on:click={() => pushModal(InfoNostr)}>nostr protocol</Button>, which
nostr protocol allows you to own your social identity.
</Button>, which allows you to own your social identity.
</p> </p>
</div> </div>
<Field> <Field>
<div class="flex gap-2 items-center" slot="input"> <div class="flex items-center gap-2" slot="input">
<label class="input input-bordered w-full flex items-center gap-2"> <label class="input input-bordered flex w-full items-center gap-2">
<Icon icon="user-rounded" /> <Icon icon="user-rounded" />
<input bind:value={username} class="grow" type="text" placeholder="username" /> <input bind:value={username} class="grow" type="text" placeholder="username" />
</label> </label>
@@ -87,9 +86,7 @@
</Button> </Button>
<div class="text-sm"> <div class="text-sm">
Need an account? Need an account?
<Button class="link" on:click={back}> <Button class="link" on:click={back}>Register</Button>
Register
</Button>
</div> </div>
</div> </div>
</form> </form>

View File

@@ -7,24 +7,32 @@
<script lang="ts"> <script lang="ts">
import {page} from "$app/stores" import {page} from "$app/stores"
import {goto} from '$app/navigation' import {goto} from "$app/navigation"
import {derived} from 'svelte/store' import {derived} from "svelte/store"
import {tweened} from 'svelte/motion' import {tweened} from "svelte/motion"
import {quintOut} from 'svelte/easing' import {quintOut} from "svelte/easing"
import {identity, nth} from '@welshman/lib' import {identity, nth} from "@welshman/lib"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Avatar from "@lib/components/Avatar.svelte" import Avatar from "@lib/components/Avatar.svelte"
import PrimaryNavItem from "@lib/components/PrimaryNavItem.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 {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 {pushModal} from "@app/modal"
import {getPrimaryNavItemIndex} from "@app/routes" import {getPrimaryNavItemIndex} from "@app/routes"
const activeOffset = tweened(-44, { const activeOffset = tweened(-44, {
duration: 300, duration: 300,
easing: quintOut easing: quintOut,
}) })
const addSpace = () => pushModal(SpaceAdd) const addSpace = () => pushModal(SpaceAdd)
@@ -42,7 +50,7 @@
$: { $: {
if (element) { if (element) {
const index = getPrimaryNavItemIndex($page) 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) activeOffset.set(navItems[index].offsetTop - 44)
} }
@@ -50,11 +58,16 @@
</script> </script>
<div class="relative w-14 bg-base-100" bind:this={element}> <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 class="flex h-full flex-col justify-between">
<div> <div>
<PrimaryNavItem on:click={gotoHome}> <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> </PrimaryNavItem>
{#each $userGroupsByNom.entries() as [nom, qualifiedGroups] (nom)} {#each $userGroupsByNom.entries() as [nom, qualifiedGroups] (nom)}
{@const qualifiedGroup = qualifiedGroups[0]} {@const qualifiedGroup = qualifiedGroups[0]}

View File

@@ -1,50 +1,59 @@
<script lang="ts"> <script lang="ts">
import {page} from "$app/stores" import {page} from "$app/stores"
import {fly} from '@lib/transition' import {fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import SecondaryNavItem from "@lib/components/SecondaryNavItem.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> </script>
<div class="flex w-60 flex-col gap-1 bg-base-300 px-2 py-4"> <div class="flex w-60 flex-col gap-1 bg-base-300">
{#if getPrimaryNavItem($page) === 'discover'} {#if getPrimaryNavItem($page) === "discover"}
<div in:fly> <SecondaryNavSection>
<SecondaryNavItem href="/spaces"> <div in:fly>
<Icon icon="widget" /> Spaces <SecondaryNavItem href="/spaces">
</SecondaryNavItem> <Icon icon="widget" /> Spaces
</div> </SecondaryNavItem>
<div in:fly={{delay: 50}}> </div>
<SecondaryNavItem href="/themes"> <div in:fly={{delay: 50}}>
<Icon icon="pallete-2" /> Themes <SecondaryNavItem href="/themes">
</SecondaryNavItem> <Icon icon="pallete-2" /> Themes
</div> </SecondaryNavItem>
{:else if getPrimaryNavItem($page) === 'space'} </div>
<!-- pass --> </SecondaryNavSection>
{:else if getPrimaryNavItem($page) === 'settings'} {:else if getPrimaryNavItem($page) === "space"}
{#key $page.params.nom}
<SecondaryNavForSpace nom={$page.params.nom} />
{/key}
{:else if getPrimaryNavItem($page) === "settings"}
<!-- pass --> <!-- pass -->
{:else} {:else}
<div in:fly> <SecondaryNavSection>
<SecondaryNavItem href="/home"> <div in:fly>
<Icon icon="home-smile" /> Home <SecondaryNavItem href="/home">
</SecondaryNavItem> <Icon icon="home-smile" /> Home
</div> </SecondaryNavItem>
<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" />
</div> </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} {/if}
</div> </div>

View File

@@ -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>

View File

@@ -1 +0,0 @@

View File

@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import CardButton from '@lib/components/CardButton.svelte' import CardButton from "@lib/components/CardButton.svelte"
import SpaceCreate from '@app/components/SpaceCreate.svelte' import SpaceCreate from "@app/components/SpaceCreate.svelte"
import SpaceJoin from '@app/components/SpaceJoin.svelte' import SpaceJoin from "@app/components/SpaceJoin.svelte"
import {pushModal} from '@app/modal' import {pushModal} from "@app/modal"
const startCreate = () => pushModal(SpaceCreate) const startCreate = () => pushModal(SpaceCreate)
@@ -13,15 +13,15 @@
<div class="column gap-4"> <div class="column gap-4">
<div class="py-2"> <div class="py-2">
<h1 class="heading">Add a Space</h1> <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> </div>
<CardButton icon="add-circle" title="Get started" on:click={startCreate}> <CardButton icon="add-circle" title="Get started" on:click={startCreate}>
Just a few questions and you'll be on your way. Just a few questions and you'll be on your way.
</CardButton> </CardButton>
<div class="card2 column gap-4"> <div class="card2 column gap-4">
<h2 class="subheading">Have an invite?</h2> <h2 class="subheading">Have an invite?</h2>
<Button class="btn btn-primary" on:click={startJoin}> <Button class="btn btn-primary" on:click={startJoin}>Join a Space</Button>
Join a Space
</Button>
</div> </div>
</div> </div>

View File

@@ -1,11 +1,11 @@
<script lang="ts"> <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 Button from "@lib/components/Button.svelte"
import Field from '@lib/components/Field.svelte' import Field from "@lib/components/Field.svelte"
import Icon from '@lib/components/Icon.svelte' import Icon from "@lib/components/Icon.svelte"
import InfoNip29 from '@app/components/InfoNip29.svelte' import InfoNip29 from "@app/components/InfoNip29.svelte"
import SpaceCreateFinish from '@app/components/SpaceCreateFinish.svelte' import SpaceCreateFinish from "@app/components/SpaceCreateFinish.svelte"
import {pushModal} from '@app/modal' import {pushModal} from "@app/modal"
const back = () => history.back() const back = () => history.back()
@@ -19,34 +19,30 @@
<form class="column gap-4" on:submit|preventDefault={next}> <form class="column gap-4" on:submit|preventDefault={next}>
<div class="py-2"> <div class="py-2">
<h1 class="heading">Customize your Space</h1> <h1 class="heading">Customize your Space</h1>
<p class="text-center"> <p class="text-center">Give people a few details to go on. You can always change this later.</p>
Give people a few details to go on. You can always change this later.
</p>
</div> </div>
<div class="flex justify-center py-2"> <div class="flex justify-center py-2">
<InputProfilePicture bind:file /> <InputProfilePicture bind:file />
</div> </div>
<Field> <Field>
<p slot="label">Space Name</p> <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" /> <Icon icon="fire-minimalistic" />
<input bind:value={name} class="grow" type="text" /> <input bind:value={name} class="grow" type="text" />
</label> </label>
</Field> </Field>
<Field> <Field>
<p slot="label">Relay</p> <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" /> <Icon icon="remote-controller-minimalistic" />
<input bind:value={relay} class="grow" type="text" /> <input bind:value={relay} class="grow" type="text" />
</label> </label>
<p slot="info"> <p slot="info">
This should be a NIP-29 compatible nostr relay where you'd like to host your space. This should be a NIP-29 compatible nostr relay where you'd like to host your space.
<Button class="link" on:click={() => pushModal(InfoNip29)}> <Button class="link" on:click={() => pushModal(InfoNip29)}>More information</Button>
More information
</Button>
</p> </p>
</Field> </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}> <Button class="btn btn-link" on:click={back}>
<Icon icon="alt-arrow-left" /> <Icon icon="alt-arrow-left" />
Go back Go back

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>

View File

@@ -1,17 +1,17 @@
<script lang="ts"> <script lang="ts">
import {goto} from '$app/navigation' import {goto} from "$app/navigation"
import {append, uniqBy} from '@welshman/lib' import {append, uniqBy} from "@welshman/lib"
import {GROUPS} from '@welshman/util' import {GROUPS} from "@welshman/util"
import CardButton from '@lib/components/CardButton.svelte' import CardButton from "@lib/components/CardButton.svelte"
import Spinner from '@lib/components/Spinner.svelte' import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Field from '@lib/components/Field.svelte' import Field from "@lib/components/Field.svelte"
import Icon from '@lib/components/Icon.svelte' import Icon from "@lib/components/Icon.svelte"
import SpaceCreateFinish from '@app/components/SpaceCreateFinish.svelte' import SpaceCreateFinish from "@app/components/SpaceCreateFinish.svelte"
import {pushModal} from '@app/modal' import {pushModal} from "@app/modal"
import {pushToast} from '@app/toast' import {pushToast} from "@app/toast"
import {GROUP_DELIMITER, splitGroupId, loadRelay, loadGroup} from '@app/state' import {GROUP_DELIMITER, splitGroupId, loadRelay, loadGroup} from "@app/state"
import {updateGroupMemberships} from '@app/commands' import {addGroupMemberships} from "@app/commands"
const back = () => history.back() const back = () => history.back()
@@ -24,14 +24,14 @@
if (!relay) { if (!relay) {
return pushToast({ return pushToast({
theme: "error", 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)) { if (!relay.supported_nips?.includes(29)) {
return pushToast({ return pushToast({
theme: "error", 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) { if (!group) {
return pushToast({ return pushToast({
theme: "error", 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}`) goto(`/spaces/${nom}`)
pushToast({ pushToast({
message: "Welcome to the space!" message: "Welcome to the space!",
}) })
} }
@@ -71,13 +71,11 @@
<form class="column gap-4" on:submit|preventDefault={join}> <form class="column gap-4" on:submit|preventDefault={join}>
<div class="py-2"> <div class="py-2">
<h1 class="heading">Join a Space</h1> <h1 class="heading">Join a Space</h1>
<p class="text-center"> <p class="text-center">Enter an invite link below to join an existing space.</p>
Enter an invite link below to join an existing space.
</p>
</div> </div>
<Field> <Field>
<p slot="label">Invite Link*</p> <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" /> <Icon icon="link-round" />
<input bind:value={id} class="grow" type="text" /> <input bind:value={id} class="grow" type="text" />
</label> </label>
@@ -85,7 +83,7 @@
<CardButton icon="compass" title="Don't have an invite?" on:click={browse}> <CardButton icon="compass" title="Don't have an invite?" on:click={browse}>
Browse other spaces on the discover page. Browse other spaces on the discover page.
</CardButton> </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}> <Button class="btn btn-link" on:click={back}>
<Icon icon="alt-arrow-left" /> <Icon icon="alt-arrow-left" />
Go back Go back

View File

@@ -6,7 +6,10 @@
{#if $toast} {#if $toast}
{#key $toast.id} {#key $toast.id}
<div transition:fly class="toast z-toast"> <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} {$toast.message}
</div> </div>
</div> </div>

View File

@@ -17,6 +17,6 @@ export const pushModal = (component: ComponentType, props: Record<string, any> =
} }
export const clearModal = () => { export const clearModal = () => {
goto('#') goto("#")
emitter.emit('close') emitter.emit("close")
} }

View File

@@ -1,22 +1,22 @@
import type {Page} from '@sveltejs/kit' import type {Page} from "@sveltejs/kit"
import {userGroupsByNom} from '@app/state' import {userGroupsByNom} from "@app/state"
export const makeSpacePath = (nom: string) => `/spaces/${nom}` export const makeSpacePath = (nom: string) => `/spaces/${nom}`
export const getPrimaryNavItem = ($page: Page) => { export const getPrimaryNavItem = ($page: Page) => {
if ($page.route?.id?.match('^/(spaces|themes)$')) return 'discover' if ($page.route?.id?.match("^/(spaces|themes)$")) return "discover"
if ($page.route?.id?.startsWith('/spaces')) return 'space' if ($page.route?.id?.startsWith("/spaces")) return "space"
if ($page.route?.id?.startsWith('/settings')) return 'settings' if ($page.route?.id?.startsWith("/settings")) return "settings"
return 'home' return "home"
} }
export const getPrimaryNavItemIndex = ($page: Page) => { export const getPrimaryNavItemIndex = ($page: Page) => {
switch (getPrimaryNavItem($page)) { switch (getPrimaryNavItem($page)) {
case 'discover': case "discover":
return userGroupsByNom.get().size + 2 return userGroupsByNom.get().size + 2
case 'space': case "space":
return Array.from(userGroupsByNom.get().keys()).findIndex(nom => nom === $page.params.nom) + 1 return Array.from(userGroupsByNom.get().keys()).findIndex(nom => nom === $page.params.nom) + 1
case 'settings': case "settings":
return userGroupsByNom.get().size + 3 return userGroupsByNom.get().size + 3
default: default:
return 0 return 0

View File

@@ -1,17 +1,67 @@
import type {Readable} from "svelte/store" import type {Readable} from "svelte/store"
import type {FuseResult} from 'fuse.js' import type {FuseResult} from "fuse.js"
import {get, writable, readable, derived} from "svelte/store" import {get, writable, readable, derived} from "svelte/store"
import type {Maybe} from "@welshman/lib" import type {Maybe} from "@welshman/lib"
import {max, uniq, between, uniqBy, groupBy, pushToMapKey, nthEq, batcher, postJson, stripProtocol, assoc, indexBy, now} from "@welshman/lib" import {
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" max,
import type {Filter, SignedEvent, CustomEvent, PublishedProfile, PublishedList} from '@welshman/util' uniq,
import type {SubscribeRequest, PublishRequest} from '@welshman/net' between,
import {publish as basePublish, subscribe} from '@welshman/net' uniqBy,
import {decrypt} from '@welshman/signer' 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 {deriveEvents, deriveEventsMapped, getter, withGetter} from "@welshman/store"
import {parseJson, createSearch} from '@lib/util' import {parseJson, createSearch} from "@lib/util"
import type {Session, Handle, Relay} from '@app/types' import type {Session, Handle, Relay} from "@app/types"
import {INDEXER_RELAYS, DUFFLEPUD_URL, repository, pk, getSession, getSigner, signer} from "@app/base" import {
INDEXER_RELAYS,
DUFFLEPUD_URL,
repository,
pk,
getSession,
getSigner,
signer,
} from "@app/base"
// Utils // Utils
@@ -21,15 +71,15 @@ export const createCollection = <T>({
getKey, getKey,
load, load,
}: { }: {
name: string, name: string
store: Readable<T[]>, store: Readable<T[]>
getKey: (item: T) => string, getKey: (item: T) => string
load: (key: string, ...args: any) => Promise<any> load: (key: string, ...args: any) => Promise<any>
}) => { }) => {
const indexStore = derived(store, $items => indexBy(getKey, $items)) const indexStore = derived(store, $items => indexBy(getKey, $items))
const getIndex = getter(indexStore) const getIndex = getter(indexStore)
const getItem = (key: string) => getIndex().get(key) 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[]) => { const loadItem = async (key: string, ...args: any[]) => {
if (getFreshness(name, key) > now() - 3600) { if (getFreshness(name, key) > now() - 3600) {
@@ -68,6 +118,25 @@ export const createCollection = <T>({
return {indexStore, getIndex, deriveItem, loadItem, getItem} 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) => { export const publish = (request: PublishRequest) => {
repository.publish(request.event) repository.publish(request.event)
@@ -79,12 +148,12 @@ export const load = (request: SubscribeRequest) =>
const sub = subscribe({closeOnEose: true, timeout: 3000, delay: 50, ...request}) const sub = subscribe({closeOnEose: true, timeout: 3000, delay: 50, ...request})
const events: CustomEvent[] = [] const events: CustomEvent[] = []
sub.emitter.on('event', (url: string, e: SignedEvent) => { sub.emitter.on("event", (url: string, e: SignedEvent) => {
repository.publish(e) repository.publish(e)
events.push(e) events.push(e)
}) })
sub.emitter.on('complete', () => resolve(events)) sub.emitter.on("complete", () => resolve(events))
}) })
// Freshness // Freshness
@@ -93,9 +162,11 @@ export const freshness = withGetter(writable<Record<string, number>>({}))
export const getFreshnessKey = (ns: string, key: string) => `${ns}:${key}` 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>) => export const setFreshnessBulk = (ns: string, updates: Record<string, number>) =>
freshness.update($freshness => { freshness.update($freshness => {
@@ -131,7 +202,9 @@ export const ensurePlaintext = async (e: CustomEvent) => {
export const relays = writable<Relay[]>([]) 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 { export const {
indexStore: relaysByUrl, indexStore: relaysByUrl,
@@ -139,7 +212,7 @@ export const {
deriveItem: deriveRelay, deriveItem: deriveRelay,
loadItem: loadRelay, loadItem: loadRelay,
} = createCollection({ } = createCollection({
name: 'relays', name: "relays",
store: relays, store: relays,
getKey: (relay: Relay) => relay.url, getKey: (relay: Relay) => relay.url,
load: batcher(800, async (urls: string[]) => { load: batcher(800, async (urls: string[]) => {
@@ -168,7 +241,7 @@ export const {
deriveItem: deriveHandle, deriveItem: deriveHandle,
loadItem: loadHandle, loadItem: loadHandle,
} = createCollection({ } = createCollection({
name: 'handles', name: "handles",
store: handles, store: handles,
getKey: (handle: Handle) => handle.nip05, getKey: (handle: Handle) => handle.nip05,
load: batcher(800, async (nip05s: string[]) => { load: batcher(800, async (nip05s: string[]) => {
@@ -201,7 +274,7 @@ export const {
deriveItem: deriveProfile, deriveItem: deriveProfile,
loadItem: loadProfile, loadItem: loadProfile,
} = createCollection({ } = createCollection({
name: 'profiles', name: "profiles",
store: profiles, store: profiles,
getKey: profile => profile.event.pubkey, getKey: profile => profile.event.pubkey,
load: (pubkey: string, relays = [], request: Partial<SubscribeRequest> = {}) => load: (pubkey: string, relays = [], request: Partial<SubscribeRequest> = {}) =>
@@ -215,10 +288,14 @@ export const {
// Relay selections // Relay selections
export const getReadRelayUrls = (event?: CustomEvent): string[] => 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[] => export const getWriteRelayUrls = (event?: CustomEvent): string[] =>
getRelayTags(event?.tags || []).filter((t: string[]) => !t[2] || t[2] === 'write').map((t: string[]) => normalizeRelayUrl(t[1])) getRelayTags(event?.tags || [])
.filter((t: string[]) => !t[2] || t[2] === "write")
.map((t: string[]) => normalizeRelayUrl(t[1]))
export const relaySelections = deriveEvents(repository, {filters: [{kinds: [RELAYS]}]}) export const relaySelections = deriveEvents(repository, {filters: [{kinds: [RELAYS]}]})
@@ -228,7 +305,7 @@ export const {
deriveItem: deriveRelaySelections, deriveItem: deriveRelaySelections,
loadItem: loadRelaySelections, loadItem: loadRelaySelections,
} = createCollection({ } = createCollection({
name: 'relaySelections', name: "relaySelections",
store: relaySelections, store: relaySelections,
getKey: relaySelections => relaySelections.pubkey, getKey: relaySelections => relaySelections.pubkey,
load: (pubkey: string, relays = [], request: Partial<SubscribeRequest> = {}) => load: (pubkey: string, relays = [], request: Partial<SubscribeRequest> = {}) =>
@@ -236,7 +313,7 @@ export const {
...request, ...request,
relays: [...relays, ...INDEXER_RELAYS], relays: [...relays, ...INDEXER_RELAYS],
filters: [{kinds: [RELAYS], authors: [pubkey]}], filters: [{kinds: [RELAYS], authors: [pubkey]}],
}) }),
}) })
// Follows // Follows
@@ -258,7 +335,7 @@ export const {
deriveItem: deriveFollows, deriveItem: deriveFollows,
loadItem: loadFollows, loadItem: loadFollows,
} = createCollection({ } = createCollection({
name: 'follows', name: "follows",
store: follows, store: follows,
getKey: follows => follows.event.pubkey, getKey: follows => follows.event.pubkey,
load: (pubkey: string, relays = [], request: Partial<SubscribeRequest> = {}) => load: (pubkey: string, relays = [], request: Partial<SubscribeRequest> = {}) =>
@@ -266,7 +343,7 @@ export const {
...request, ...request,
relays: [...relays, ...INDEXER_RELAYS], relays: [...relays, ...INDEXER_RELAYS],
filters: [{kinds: [FOLLOWS], authors: [pubkey]}], filters: [{kinds: [FOLLOWS], authors: [pubkey]}],
}) }),
}) })
// Mutes // Mutes
@@ -288,7 +365,7 @@ export const {
deriveItem: deriveMutes, deriveItem: deriveMutes,
loadItem: loadMutes, loadItem: loadMutes,
} = createCollection({ } = createCollection({
name: 'mutes', name: "mutes",
store: mutes, store: mutes,
getKey: mute => mute.event.pubkey, getKey: mute => mute.event.pubkey,
load: (pubkey: string, relays = [], request: Partial<SubscribeRequest> = {}) => load: (pubkey: string, relays = [], request: Partial<SubscribeRequest> = {}) =>
@@ -296,7 +373,7 @@ export const {
...request, ...request,
relays: [...relays, ...INDEXER_RELAYS], relays: [...relays, ...INDEXER_RELAYS],
filters: [{kinds: [MUTES], authors: [pubkey]}], filters: [{kinds: [MUTES], authors: [pubkey]}],
}) }),
}) })
// Groups // Groups
@@ -304,7 +381,7 @@ export const {
export const GROUP_DELIMITER = `'` export const GROUP_DELIMITER = `'`
export const makeGroupId = (url: string, nom: string) => 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) => { export const splitGroupId = (groupId: string) => {
const [url, nom] = groupId.split(GROUP_DELIMITER) 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 const getGroupPicture = (e?: CustomEvent) => e?.tags.find(nthEq(0, "picture"))?.[1]
export type Group = { export type Group = {
nom: string, nom: string
name?: string, name?: string
about?: string, about?: string
picture?: string, picture?: string
event?: CustomEvent event?: CustomEvent
} }
@@ -353,7 +430,7 @@ export const {
deriveItem: deriveGroup, deriveItem: deriveGroup,
loadItem: loadGroup, loadItem: loadGroup,
} = createCollection({ } = createCollection({
name: 'groups', name: "groups",
store: groups, store: groups,
getKey: (group: PublishedGroup) => group.nom, getKey: (group: PublishedGroup) => group.nom,
load: (nom: string, relays: string[] = [], request: Partial<SubscribeRequest> = {}) => load: (nom: string, relays: string[] = [], request: Partial<SubscribeRequest> = {}) =>
@@ -362,25 +439,23 @@ export const {
load({ load({
...request, ...request,
relays, relays,
filters: [{kinds: [GROUP_META], '#d': [nom]}], filters: [{kinds: [GROUP_META], "#d": [nom]}],
}), }),
]) ]),
}) })
export const searchGroups = derived( export const searchGroups = derived(groups, $groups =>
groups, createSearch($groups, {
$groups => getValue: (group: PublishedGroup) => group.nom,
createSearch($groups, { sortFn: (result: FuseResult<PublishedGroup>) => {
getValue: (group: PublishedGroup) => group.nom, const scale = result.item.picture ? 0.5 : 1
sortFn: (result: FuseResult<PublishedGroup>) => {
const scale = result.item.picture ? 0.5 : 1
return result.score! * scale return result.score! * scale
}, },
fuseOptions: { fuseOptions: {
keys: ["name", {name: "about", weight: 0.3}], keys: ["name", {name: "about", weight: 0.3}],
}, },
}) }),
) )
// Qualified groups // Qualified groups
@@ -396,12 +471,16 @@ export const qualifiedGroups = derived([relaysByPubkey, groups], ([$relaysByPubk
const relays = $relaysByPubkey.get(group.event.pubkey) || [] const relays = $relaysByPubkey.get(group.event.pubkey) || []
return relays.map(relay => ({id: makeGroupId(relay.url, group.nom), relay, group})) 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 => { export const relayUrlsByNom = derived(qualifiedGroups, $qualifiedGroups => {
const $relayUrlsByNom = new Map() const $relayUrlsByNom = new Map()
@@ -452,7 +531,7 @@ export const {
deriveItem: deriveGroupMembership, deriveItem: deriveGroupMembership,
loadItem: loadGroupMembership, loadItem: loadGroupMembership,
} = createCollection({ } = createCollection({
name: 'groupMemberships', name: "groupMemberships",
store: groupMemberships, store: groupMemberships,
getKey: groupMembership => groupMembership.event.pubkey, getKey: groupMembership => groupMembership.event.pubkey,
load: (pubkey: string, relays = [], request: Partial<SubscribeRequest> = {}) => load: (pubkey: string, relays = [], request: Partial<SubscribeRequest> = {}) =>
@@ -460,7 +539,7 @@ export const {
...request, ...request,
relays: [...relays, ...INDEXER_RELAYS], relays: [...relays, ...INDEXER_RELAYS],
filters: [{kinds: [GROUPS], authors: [pubkey]}], filters: [{kinds: [GROUPS], authors: [pubkey]}],
}) }),
}) })
// Group Messages // Group Messages
@@ -471,7 +550,7 @@ export type GroupMessage = {
} }
export const readGroupMessage = (event: CustomEvent): Maybe<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)) { if (!nom || between(GROUP_ADD_USER - 1, GROUP_JOIN + 1, event.kind)) {
return undefined return undefined
@@ -505,20 +584,20 @@ export const {
deriveItem: deriveGroupConversation, deriveItem: deriveGroupConversation,
loadItem: loadGroupConversation, loadItem: loadGroupConversation,
} = createCollection({ } = createCollection({
name: 'groupConversations', name: "groupConversations",
store: groupConversations, store: groupConversations,
getKey: groupConversation => groupConversation.nom, getKey: groupConversation => groupConversation.nom,
load: (nom: string, hints = [], request: Partial<SubscribeRequest> = {}) => { 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 conversation = get(groupConversations).find(c => c.nom === nom)
const timestamps = conversation?.messages.map(m => m.event.created_at) || [] 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) { if (relays.length === 0) {
console.warn(`Attempted to load conversation for ${nom} with no qualified groups`) 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) return $profilesByPubkey.get($pk)
}) })
export const userMembership = derived([pk, groupMembershipByPubkey], ([$pk, $groupMembershipByPubkey]) => { export const userMembership = derived(
if (!$pk) return null [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]) => { export const userGroupsByNom = withGetter(
const $userGroupsByNom = new Map() derived([userMembership, qualifiedGroupsById], ([$userMembership, $qualifiedGroupsById]) => {
const $userGroupsByNom = new Map()
for (const id of $userMembership?.ids || []) { for (const id of $userMembership?.ids || []) {
const [url, nom] = splitGroupId(id) const [url, nom] = splitGroupId(id)
const group = $qualifiedGroupsById.get(id) const group = $qualifiedGroupsById.get(id)
const groups = $userGroupsByNom.get(nom) || [] const groups = $userGroupsByNom.get(nom) || []
loadGroup(nom, [url]) loadGroup(nom, [url])
if (group) { if (group) {
groups.push(group) groups.push(group)
}
$userGroupsByNom.set(nom, groups)
} }
$userGroupsByNom.set(nom, groups) return $userGroupsByNom
} }),
)
return $userGroupsByNom
}))

View File

@@ -1,12 +1,12 @@
import {openDB, deleteDB} from "idb" import {openDB, deleteDB} from "idb"
import type {IDBPDatabase} from "idb" import type {IDBPDatabase} from "idb"
import {throttle} from 'throttle-debounce' import {throttle} from "throttle-debounce"
import {writable} from 'svelte/store' import {writable} from "svelte/store"
import type {Unsubscriber, Writable} from 'svelte/store' import type {Unsubscriber, Writable} from "svelte/store"
import {isNil, randomInt} from '@welshman/lib' import {isNil, randomInt} from "@welshman/lib"
import {withGetter} from '@welshman/store' import {withGetter} from "@welshman/store"
import {getJson, setJson} from '@lib/util' import {getJson, setJson} from "@lib/util"
import {pk, sessions, repository} from '@app/base' import {pk, sessions, repository} from "@app/base"
export type Item = Record<string, any> export type Item = Record<string, any>
@@ -74,7 +74,10 @@ export const initIndexedDbAdapter = async (name: string, adapter: IndexedDbAdapt
} }
if (removedRecords.length > 0) { 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( await Promise.all(
Object.entries(adapters) Object.entries(adapters).map(([name, config]) => initIndexedDbAdapter(name, config)),
.map(([name, config]) => initIndexedDbAdapter(name, config))
) )
} }

View File

@@ -1,3 +1,3 @@
import {synced} from '@lib/util' import {synced} from "@lib/util"
export const theme = synced<string>("theme", "dark") export const theme = synced<string>("theme", "dark")

View File

@@ -1,6 +1,6 @@
import {writable} from "svelte/store" import {writable} from "svelte/store"
import {randomId} from "@welshman/lib" import {randomId} from "@welshman/lib"
import {copyToClipboard} from '@lib/html' import {copyToClipboard} from "@lib/html"
export type ToastParams = { export type ToastParams = {
message: string message: string

View File

@@ -1,4 +1,4 @@
import {verifiedSymbol} from 'nostr-tools' import {verifiedSymbol} from "nostr-tools"
import type {Nip46Handler} from "@welshman/signer" import type {Nip46Handler} from "@welshman/signer"
import type {SignedEvent, TrustedEvent, RelayProfile} from "@welshman/util" import type {SignedEvent, TrustedEvent, RelayProfile} from "@welshman/util"

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 9L12 15L5 9" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 214 B

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 4.5H8C5.64298 4.5 4.46447 4.5 3.73223 5.23223C3 5.96447 3 7.14298 3 9.5V14.5C3 16.857 3 18.0355 3.73223 18.7678C4.46447 19.5 5.64298 19.5 8 19.5H9" stroke="#1C274C" stroke-width="1.5"/>
<path d="M9 6.4764C9 4.18259 9 3.03569 9.70725 2.4087C10.4145 1.78171 11.4955 1.97026 13.6576 2.34736L15.9864 2.75354C18.3809 3.17118 19.5781 3.37999 20.2891 4.25826C21 5.13652 21 6.40672 21 8.94711V15.0529C21 17.5933 21 18.8635 20.2891 19.7417C19.5781 20.62 18.3809 20.8288 15.9864 21.2465L13.6576 21.6526C11.4955 22.0297 10.4145 22.2183 9.70725 21.5913C9 20.9643 9 19.8174 9 17.5236V6.4764Z" stroke="#1C274C" stroke-width="1.5"/>
<path d="M12 11V13" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 814 B

View File

@@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 3L5 21" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19 3L14 21" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 9H4" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20 16H2" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 523 B

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="5" cy="12" r="2" stroke="#1C274C" stroke-width="1.5"/>
<circle cx="12" cy="12" r="2" stroke="#1C274C" stroke-width="1.5"/>
<circle cx="19" cy="12" r="2" stroke="#1C274C" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 306 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.5 12L9.5 7M4.5 12L9.5 17M4.5 12L14.5 12C16.1667 12 19.5 13 19.5 17" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 268 B

View File

@@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="#1C274C" stroke-width="1.5"/>
<path d="M9 16C9.85038 16.6303 10.8846 17 12 17C13.1154 17 14.1496 16.6303 15 16" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>
<path d="M16 10.5C16 11.3284 15.5523 12 15 12C14.4477 12 14 11.3284 14 10.5C14 9.67157 14.4477 9 15 9C15.5523 9 16 9.67157 16 10.5Z" fill="#1C274C"/>
<ellipse cx="9" cy="10.5" rx="1" ry="1.5" fill="#1C274C"/>
</svg>

After

Width:  |  Height:  |  Size: 524 B

View File

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 13.9979C21.9711 17.4119 21.7815 19.294 20.5404 20.5352C19.0755 22 16.7179 22 12.0026 22C7.28733 22 4.9297 22 3.46485 20.5352C2 19.0703 2 16.7127 2 11.9974C2 7.28212 2 4.92448 3.46485 3.45963C4.70599 2.21848 6.58807 2.02895 10.0021 2" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>
<path d="M22 7H14C12.1824 7 11.0867 7.89202 10.6804 8.30029C10.5546 8.42673 10.4917 8.48996 10.4908 8.49082C10.49 8.49168 10.4267 8.55459 10.3003 8.68042C9.89202 9.08671 9 10.1824 9 12V15M22 7L17 2M22 7L17 12" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 706 B

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import cx from 'classnames' import cx from "classnames"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
export let src export let src
@@ -7,11 +7,11 @@
</script> </script>
<div <div
class={cx($$props.class, "!flex items-center justify-center rounded-full overflow-hidden")} class={cx($$props.class, "!flex items-center justify-center overflow-hidden rounded-full")}
style={`width: ${size * 4}px; height: ${size * 4}px;`}> style={`width: ${size * 4}px; height: ${size * 4}px;`}>
{#if src} {#if src}
<img alt="" {src} /> <img alt="" {src} />
{:else} {:else}
<Icon icon="user-rounded" size={Math.round(size * .7)} /> <Icon icon="user-rounded" size={Math.round(size * 0.7)} />
{/if} {/if}
</div> </div>

View File

@@ -1,4 +1,3 @@
<button on:click type="button" {...$$props}> <button on:click type="button" {...$$props}>
<slot /> <slot />
</button> </button>

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import cx from 'classnames' import cx from "classnames"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -7,10 +7,10 @@
export let title export let title
</script> </script>
<Button on:click class={cx($$props.class, "btn btn-neutral btn-lg text-left h-24")}> <Button on:click class={cx($$props.class, "btn btn-neutral btn-lg h-24 text-left")}>
<div class="flex gap-6 py-4 flex-grow items-center"> <div class="flex flex-grow items-center gap-6 py-4">
<Icon class="bg-accent" size={7} {icon} /> <Icon class="bg-accent" size={7} {icon} />
<div class="flex flex-col gap-1 flex-grow"> <div class="flex flex-grow flex-col gap-1">
<p class="text-bold">{title}</p> <p class="text-bold">{title}</p>
<p class="text-xs"><slot /></p> <p class="text-xs"><slot /></p>
</div> </div>

View File

@@ -10,6 +10,7 @@
import {switcher} from "@welshman/lib" import {switcher} from "@welshman/lib"
import AddSquare from "@assets/icons/Add Square.svg?dataurl" import AddSquare from "@assets/icons/Add Square.svg?dataurl"
import AddCircle from "@assets/icons/Add Circle.svg?dataurl" import AddCircle from "@assets/icons/Add Circle.svg?dataurl"
import AltArrowDown from "@assets/icons/Alt Arrow Down.svg?dataurl"
import AltArrowRight from "@assets/icons/Alt Arrow Right.svg?dataurl" import AltArrowRight from "@assets/icons/Alt Arrow Right.svg?dataurl"
import AltArrowLeft from "@assets/icons/Alt Arrow Left.svg?dataurl" import AltArrowLeft from "@assets/icons/Alt Arrow Left.svg?dataurl"
import ArrowRight from "@assets/icons/Arrow Right.svg?dataurl" import ArrowRight from "@assets/icons/Arrow Right.svg?dataurl"
@@ -19,9 +20,11 @@
import Copy from "@assets/icons/Copy.svg?dataurl" import Copy from "@assets/icons/Copy.svg?dataurl"
import Compass from "@assets/icons/Compass.svg?dataurl" import Compass from "@assets/icons/Compass.svg?dataurl"
import CompassBig from "@assets/icons/Compass Big.svg?dataurl" import CompassBig from "@assets/icons/Compass Big.svg?dataurl"
import Exit from "@assets/icons/Exit.svg?dataurl"
import FireMinimalistic from "@assets/icons/Fire Minimalistic.svg?dataurl" import FireMinimalistic from "@assets/icons/Fire Minimalistic.svg?dataurl"
import GallerySend from "@assets/icons/Gallery Send.svg?dataurl" import GallerySend from "@assets/icons/Gallery Send.svg?dataurl"
import Ghost from "@assets/icons/Ghost.svg?dataurl" import Ghost from "@assets/icons/Ghost.svg?dataurl"
import Hashtag from "@assets/icons/Hashtag.svg?dataurl"
import HandPills from "@assets/icons/Hand Pills.svg?dataurl" import HandPills from "@assets/icons/Hand Pills.svg?dataurl"
import HomeSmile from "@assets/icons/Home Smile.svg?dataurl" import HomeSmile from "@assets/icons/Home Smile.svg?dataurl"
import InfoCircle from "@assets/icons/Info Circle.svg?dataurl" import InfoCircle from "@assets/icons/Info Circle.svg?dataurl"
@@ -29,10 +32,14 @@
import Login from "@assets/icons/Login.svg?dataurl" import Login from "@assets/icons/Login.svg?dataurl"
import Login2 from "@assets/icons/Login 2.svg?dataurl" import Login2 from "@assets/icons/Login 2.svg?dataurl"
import Magnifer from "@assets/icons/Magnifer.svg?dataurl" import Magnifer from "@assets/icons/Magnifer.svg?dataurl"
import MenuDots from "@assets/icons/Menu Dots.svg?dataurl"
import Pallete2 from "@assets/icons/Pallete 2.svg?dataurl" import Pallete2 from "@assets/icons/Pallete 2.svg?dataurl"
import Plain from "@assets/icons/Plain.svg?dataurl" import Plain from "@assets/icons/Plain.svg?dataurl"
import RemoteControllerMinimalistic from "@assets/icons/Remote Controller Minimalistic.svg?dataurl" import RemoteControllerMinimalistic from "@assets/icons/Remote Controller Minimalistic.svg?dataurl"
import Reply from "@assets/icons/Reply.svg?dataurl"
import Settings from "@assets/icons/Settings.svg?dataurl" import Settings from "@assets/icons/Settings.svg?dataurl"
import SmileCircle from "@assets/icons/Smile Circle.svg?dataurl"
import SquareShareLine from "@assets/icons/Square Share Line.svg?dataurl"
import UFO3 from "@assets/icons/UFO 3.svg?dataurl" import UFO3 from "@assets/icons/UFO 3.svg?dataurl"
import UserHeart from "@assets/icons/User Heart.svg?dataurl" import UserHeart from "@assets/icons/User Heart.svg?dataurl"
import UserRounded from "@assets/icons/User Rounded.svg?dataurl" import UserRounded from "@assets/icons/User Rounded.svg?dataurl"
@@ -47,6 +54,7 @@
const data = switcher(icon, { const data = switcher(icon, {
"add-square": AddSquare, "add-square": AddSquare,
"add-circle": AddCircle, "add-circle": AddCircle,
"alt-arrow-down": AltArrowDown,
"alt-arrow-right": AltArrowRight, "alt-arrow-right": AltArrowRight,
"alt-arrow-left": AltArrowLeft, "alt-arrow-left": AltArrowLeft,
"arrow-right": ArrowRight, "arrow-right": ArrowRight,
@@ -56,24 +64,30 @@
copy: Copy, copy: Copy,
compass: Compass, compass: Compass,
"compass-big": CompassBig, "compass-big": CompassBig,
exit: Exit,
"fire-minimalistic": FireMinimalistic, "fire-minimalistic": FireMinimalistic,
"gallery-send": GallerySend, "gallery-send": GallerySend,
"ghost": Ghost, ghost: Ghost,
hashtag: Hashtag,
"hand-pills": HandPills, "hand-pills": HandPills,
"home-smile": HomeSmile, "home-smile": HomeSmile,
"info-circle": InfoCircle, "info-circle": InfoCircle,
"link-round": LinkRound, "link-round": LinkRound,
"login": Login, login: Login,
"login-2": Login2, "login-2": Login2,
'magnifer': Magnifer, magnifer: Magnifer,
'pallete-2': Pallete2, "menu-dots": MenuDots,
"pallete-2": Pallete2,
plain: Plain, plain: Plain,
'remote-controller-minimalistic': RemoteControllerMinimalistic, reply: Reply,
"remote-controller-minimalistic": RemoteControllerMinimalistic,
"smile-circle": SmileCircle,
settings: Settings, settings: Settings,
"ufo-3": UFO3, "ufo-3": UFO3,
"square-share-line": SquareShareLine,
"user-heart": UserHeart, "user-heart": UserHeart,
"user-rounded": UserRounded, "user-rounded": UserRounded,
"widget": Widget, widget: Widget,
"wifi-router-round": WiFiRouterRound, "wifi-router-round": WiFiRouterRound,
}) })

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {randomId} from '@welshman/lib' import {randomId} from "@welshman/lib"
import Icon from '@lib/components/Icon.svelte' import Icon from "@lib/components/Icon.svelte"
export let file: File | null = null export let file: File | null = null
export let url: string | null = null export let url: string | null = null
@@ -42,13 +42,18 @@
if (file) { if (file) {
const reader = new FileReader() const reader = new FileReader()
reader.addEventListener("load", () => { url = reader.result as string }, false) reader.addEventListener(
"load",
() => {
url = reader.result as string
},
false,
)
reader.readAsDataURL(file) reader.readAsDataURL(file)
} else { } else {
url = initialUrl url = initialUrl
} }
} }
</script> </script>
<form> <form>
@@ -57,14 +62,14 @@
for={id} for={id}
aria-label="Drag and drop files here." aria-label="Drag and drop files here."
style="background-image: url({url});" style="background-image: url({url});"
class="relative flex justify-center items-center w-24 h-24 rounded-full border-2 border-dashed border-base-content transition-all bg-base-300 cursor-pointer bg-cover bg-center shrink-0" class="relative flex h-24 w-24 shrink-0 cursor-pointer items-center justify-center rounded-full border-2 border-dashed border-base-content bg-base-300 bg-cover bg-center transition-all"
class:border-primary={active} class:border-primary={active}
on:dragenter|preventDefault|stopPropagation={onDragEnter} on:dragenter|preventDefault|stopPropagation={onDragEnter}
on:dragover|preventDefault|stopPropagation={onDragOver} on:dragover|preventDefault|stopPropagation={onDragOver}
on:dragleave|preventDefault|stopPropagation={onDragLeave} on:dragleave|preventDefault|stopPropagation={onDragLeave}
on:drop|preventDefault|stopPropagation={onDrop}> on:drop|preventDefault|stopPropagation={onDrop}>
<div <div
class="bg-primary right-0 top-0 absolute rounded-full overflow-hidden" class="absolute right-0 top-0 overflow-hidden rounded-full bg-primary"
class:bg-error={file} class:bg-error={file}
class:bg-primary={!file}> class:bg-primary={!file}>
{#if file} {#if file}
@@ -73,10 +78,10 @@
tabindex="-1" tabindex="-1"
on:mousedown|stopPropagation={onClear} on:mousedown|stopPropagation={onClear}
on:touchstart|stopPropagation={onClear}> on:touchstart|stopPropagation={onClear}>
<Icon icon="close-circle" class="!bg-base-300 scale-150" /> <Icon icon="close-circle" class="scale-150 !bg-base-300" />
</span> </span>
{:else} {:else}
<Icon icon="add-circle" class="!bg-base-300 scale-150" /> <Icon icon="add-circle" class="scale-150 !bg-base-300" />
{/if} {/if}
</div> </div>
{#if !file} {#if !file}

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import cx from 'classnames' import cx from "classnames"
export let href export let href
export let external = false export let external = false
@@ -8,7 +8,7 @@
<a <a
{href} {href}
{...$$props} {...$$props}
class={cx($$props.class, "cursor-pointer underline text-primary")} class={cx($$props.class, "cursor-pointer text-primary underline")}
rel={external ? "noopener noreferer" : ""} rel={external ? "noopener noreferer" : ""}
target={external ? "_blank" : ""}> target={external ? "_blank" : ""}>
<slot /> <slot />

View File

@@ -2,22 +2,22 @@
import {emitter} from "@app/modal" import {emitter} from "@app/modal"
const modalHeight = tweened(0, { const modalHeight = tweened(0, {
duration: 700, duration: 700,
easing: quintOut easing: quintOut,
}) })
emitter.on('close', () => modalHeight.set(0)) emitter.on("close", () => modalHeight.set(0))
</script> </script>
<script lang="ts"> <script lang="ts">
import {onMount} from 'svelte' import {onMount} from "svelte"
import {slide} from 'svelte/transition' import {slide} from "svelte/transition"
import {quintOut} from 'svelte/easing' import {quintOut} from "svelte/easing"
import {tweened} from 'svelte/motion' import {tweened} from "svelte/motion"
import {last} from '@welshman/lib' import {last} from "@welshman/lib"
export let component export let component
export let props = {} export let props = {}
let box: HTMLElement let box: HTMLElement
let content: HTMLElement let content: HTMLElement
@@ -29,7 +29,11 @@
}) })
</script> </script>
<div class="modal-box" bind:this={box} style={`height: ${$modalHeight}px`} class:overflow-hidden={$modalHeight !== naturalHeight}> <div
class="modal-box"
bind:this={box}
style={`height: ${$modalHeight}px`}
class:overflow-hidden={$modalHeight !== naturalHeight}>
<div bind:this={content}> <div bind:this={content}>
<svelte:component this={component} {...props} /> <svelte:component this={component} {...props} />
</div> </div>

View File

@@ -0,0 +1,28 @@
<script lang="ts">
import {fly} from "@lib/transition"
export let onClose
export let hideOnClick = false
const onMouseUp = (e: any) => {
if (hideOnClick || !element.contains(e.target)) {
setTimeout(onClose)
}
}
const onKeyDown = (e: any) => {
if (e.key === "Escape") {
setTimeout(onClose)
}
}
let element: HTMLElement
</script>
<svelte:window on:mouseup={onMouseUp} on:keydown={onKeyDown} />
<div class="relative w-full" bind:this={element}>
<div transition:fly|local class="absolute z-popover w-full">
<slot />
</div>
</div>

View File

@@ -0,0 +1,3 @@
<div class="flex items-center justify-between px-4 py-2 text-sm font-bold uppercase">
<slot />
</div>

View File

@@ -1,36 +1,57 @@
<style>
a,
button {
padding: 12px 16px;
display: flex;
border-radius: var(--rounded-btn, 0.5rem);
cursor: pointer;
animation: nav-button-pop 200ms ease-out;
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
a:active:hover,
a:active:focus,
button:active:hover,
button:active:focus {
animation: button-pop 0s ease-out;
transform: scale(var(--btn-focus-scale, 0.97));
}
</style>
<script lang="ts"> <script lang="ts">
import cx from "classnames"
import {page} from "$app/stores" import {page} from "$app/stores"
export let href export let href: string = ""
$: active = $page.route.id?.startsWith(href) $: active = $page.route.id?.startsWith(href)
</script> </script>
<a {#if href}
{href} <a
class="group justify-start border-none transition-all hover:bg-base-100 hover:text-base-content" {...$$props}
class:text-base-content={active} {href}
class:bg-base-100={active}> on:click
<div class="flex items-center gap-3"> class={cx(
$$props.class,
"flex items-center gap-3 transition-all hover:bg-base-100 hover:text-base-content",
)}
class:text-base-content={active}
class:bg-base-100={active}>
<slot /> <slot />
</div> </a>
</a> {:else}
<button
<style> {...$$props}
a { on:click
padding: 12px 16px; class={cx(
display: flex; $$props.class,
border-radius: var(--rounded-btn, 0.5rem); "flex items-center gap-3 transition-all hover:bg-base-100 hover:text-base-content",
cursor: pointer; )}
animation: nav-button-pop 200ms ease-out; class:text-base-content={active}
transition-property: all; class:bg-base-100={active}>
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); <slot />
transition-duration: 150ms; </button>
} {/if}
a:active:hover,
a:active:focus {
animation: button-pop 0s ease-out;
transform: scale(var(--btn-focus-scale, 0.97));
}
</style>

View File

@@ -0,0 +1,3 @@
<div class="flex flex-col gap-1 px-2 py-4">
<slot />
</div>

View File

@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import {slide, fade} from 'svelte/transition' import {slide, fade} from "svelte/transition"
export let loading export let loading
</script> </script>
<span class="flex items-center"> <span class="flex items-center">
{#if loading} {#if loading}
<span class="pr-3" transition:slide|local={{axis: 'x'}}> <span class="pr-3" transition:slide|local={{axis: "x"}}>
<span class="loading loading-spinner" transition:fade|local={{duration: 100}} /> <span class="loading loading-spinner" transition:fade|local={{duration: 100}} />
</span> </span>
{/if} {/if}

View File

@@ -1,9 +1,9 @@
import Fuse from "fuse.js" import Fuse from "fuse.js"
import type {IFuseOptions, FuseResult} from 'fuse.js' import type {IFuseOptions, FuseResult} from "fuse.js"
import {throttle} from 'throttle-debounce' import {throttle} from "throttle-debounce"
import {writable} from 'svelte/store' import {writable} from "svelte/store"
import {sortBy} from '@welshman/lib' import {sortBy} from "@welshman/lib"
import {browser} from '$app/environment' import {browser} from "$app/environment"
export const parseJson = (json: string) => { export const parseJson = (json: string) => {
if (!json) return null if (!json) return null
@@ -15,8 +15,7 @@ export const parseJson = (json: string) => {
} }
} }
export const getJson = (k: string) => export const getJson = (k: string) => (browser ? parseJson(localStorage.getItem(k) || "") : null)
browser ? parseJson(localStorage.getItem(k) || "") : null
export const setJson = (k: string, v: any) => { export const setJson = (k: string, v: any) => {
if (browser) { if (browser) {
@@ -61,7 +60,6 @@ export const createSearch = <V, T>(data: T[], opts: SearchOptions<V, T>) => {
} }
} }
export const secondsToDate = (ts: number) => new Date(ts * 1000) export const secondsToDate = (ts: number) => new Date(ts * 1000)
export const dateToSeconds = (date: Date) => Math.round(date.valueOf() / 1000) export const dateToSeconds = (date: Date) => Math.round(date.valueOf() / 1000)

View File

@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import "@src/app.css" import "@src/app.css"
import {onMount} from 'svelte' import {onMount} from "svelte"
import {page} from "$app/stores" import {page} from "$app/stores"
import {createEventStore} from '@welshman/store' import {createEventStore} from "@welshman/store"
import {fly} from "@lib/transition" import {fly} from "@lib/transition"
import ModalBox from "@lib/components/ModalBox.svelte" import ModalBox from "@lib/components/ModalBox.svelte"
import Toast from "@app/components/Toast.svelte" import Toast from "@app/components/Toast.svelte"
@@ -38,20 +38,20 @@
onMount(() => { onMount(() => {
ready = initStorage({ ready = initStorage({
events: { events: {
keyPath: 'id', keyPath: "id",
store: createEventStore(repository), store: createEventStore(repository),
}, },
relays: { relays: {
keyPath: 'url', keyPath: "url",
store: relays, store: relays,
}, },
handles: { handles: {
keyPath: 'nip05', keyPath: "nip05",
store: handles, store: handles,
}, },
}) })
dialog.addEventListener('close', () => { dialog.addEventListener("close", () => {
if (modal) { if (modal) {
clearModal() clearModal()
} }
@@ -66,11 +66,11 @@
<div class="flex h-screen"> <div class="flex h-screen">
<PrimaryNav /> <PrimaryNav />
<SecondaryNav /> <SecondaryNav />
<div class="flex-grow bg-base-200 max-h-screen overflow-auto"> <div class="max-h-screen flex-grow overflow-auto bg-base-200">
<slot /> <slot />
</div> </div>
</div> </div>
<dialog bind:this={dialog} class="modal modal-bottom sm:modal-middle !z-modal"> <dialog bind:this={dialog} class="modal modal-bottom !z-modal sm:modal-middle">
{#if prev} {#if prev}
{#key prev} {#key prev}
<ModalBox {...prev} /> <ModalBox {...prev} />

View File

@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import {goto} from '$app/navigation' import {goto} from "$app/navigation"
import CardButton from "@lib/components/CardButton.svelte" import CardButton from "@lib/components/CardButton.svelte"
import SpaceCreate from '@app/components/SpaceCreate.svelte' import SpaceCreate from "@app/components/SpaceCreate.svelte"
import {pushModal} from '@app/modal' import {pushModal} from "@app/modal"
const createSpace = () => pushModal(SpaceCreate) const createSpace = () => pushModal(SpaceCreate)
@@ -14,18 +14,18 @@
<div class="column content gap-4"> <div class="column content gap-4">
<h1 class="text-center text-5xl">Welcome to</h1> <h1 class="text-center text-5xl">Welcome to</h1>
<h1 class="mb-4 text-center text-5xl font-bold uppercase">Flotilla</h1> <h1 class="mb-4 text-center text-5xl font-bold uppercase">Flotilla</h1>
<div class="grid lg:grid-cols-2 gap-3"> <div class="grid gap-3 lg:grid-cols-2">
<CardButton icon="add-circle" title="Create a space" class="h-24" on:click={createSpace}> <CardButton icon="add-circle" title="Create a space" class="h-24" on:click={createSpace}>
Invite all your friends, do life together. Invite all your friends, do life together.
</CardButton> </CardButton>
<CardButton icon="compass" title="Discover spaces" class="h-24" on:click={browseSpaces}> <CardButton icon="compass" title="Discover spaces" class="h-24" on:click={browseSpaces}>
Find a community based on your hobbies or interests. Find a community based on your hobbies or interests.
</CardButton> </CardButton>
<CardButton icon="plain" title="Leave feedback" class="h-24"> <CardButton icon="plain" title="Leave feedback" class="h-24">
Let us know how we can improve by giving us feedback. Let us know how we can improve by giving us feedback.
</CardButton> </CardButton>
<CardButton icon="hand-pills" title="Donate to Flotilla" class="h-24"> <CardButton icon="hand-pills" title="Donate to Flotilla" class="h-24">
Support the project by donating to the developer. Support the project by donating to the developer.
</CardButton> </CardButton>
</div> </div>
</div> </div>

View File

@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import {onMount} from 'svelte' import {onMount} from "svelte"
import Masonry from 'svelte-bricks' import Masonry from "svelte-bricks"
import {append, remove} from '@welshman/lib' import {append, remove} from "@welshman/lib"
import {GROUP_META, displayRelayUrl} from '@welshman/util' import {GROUP_META, displayRelayUrl} from "@welshman/util"
import Icon from '@lib/components/Icon.svelte' import Icon from "@lib/components/Icon.svelte"
import {makeSpacePath} from '@app/routes' import {makeSpacePath} from "@app/routes"
import {load, relays, groups, searchGroups, relayUrlsByNom, userMembership} from '@app/state' import {load, relays, groups, searchGroups, relayUrlsByNom, userMembership} from "@app/state"
import {updateGroupMemberships} from '@app/commands' import {addGroupMemberships} from "@app/commands"
const getRelayUrls = (nom: string): string[] => $relayUrlsByNom.get(nom) || [] const getRelayUrls = (nom: string): string[] => $relayUrlsByNom.get(nom) || []
@@ -23,14 +23,24 @@
<div class="content column gap-4"> <div class="content column gap-4">
<h1 class="superheading mt-20">Discover Spaces</h1> <h1 class="superheading mt-20">Discover Spaces</h1>
<p class="text-center">Find communities all across the nostr network</p> <p class="text-center">Find communities all across the nostr network</p>
<label class="input input-bordered w-full flex items-center gap-2"> <label class="input input-bordered flex w-full items-center gap-2">
<Icon icon="magnifer" /> <Icon icon="magnifer" />
<input bind:value={term} class="grow" type="text" placeholder="Search for spaces..." /> <input bind:value={term} class="grow" type="text" placeholder="Search for spaces..." />
</label> </label>
<Masonry animate={false} items={$searchGroups.searchOptions(term)} minColWidth={250} maxColWidth={800} gap={16} idKey="nom" let:item={group}> <Masonry
<a href={makeSpacePath(group.nom)} class="card bg-base-100 shadow-xl hover:shadow-2xl hover:brightness-[1.1] transition-all"> animate={false}
<div class="avatar center mt-8"> items={$searchGroups.searchOptions(term)}
<div class="w-20 rounded-full bg-base-300 border-2 border-solid border-base-300 !flex center relative"> minColWidth={250}
maxColWidth={800}
gap={16}
idKey="nom"
let:item={group}>
<a
href={makeSpacePath(group.nom)}
class="card bg-base-100 shadow-xl transition-all hover:shadow-2xl hover:brightness-[1.1]">
<div class="center avatar mt-8">
<div
class="center relative !flex w-20 rounded-full border-2 border-solid border-base-300 bg-base-300">
{#if group?.picture} {#if group?.picture}
<img alt="" src={group.picture} /> <img alt="" src={group.picture} />
{:else} {:else}
@@ -39,20 +49,22 @@
</div> </div>
</div> </div>
{#if $userMembership?.noms.has(group.nom)} {#if $userMembership?.noms.has(group.nom)}
<div class="absolute flex center w-full"> <div class="center absolute flex w-full">
<div class="bg-primary rounded-full relative top-[38px] left-8 tooltip" data-tip="You are already a member of this space."> <div
class="tooltip relative left-8 top-[38px] rounded-full bg-primary"
data-tip="You are already a member of this space.">
<Icon icon="check-circle" class="scale-110" /> <Icon icon="check-circle" class="scale-110" />
</div> </div>
</div> </div>
{/if} {/if}
<div class="card-body"> <div class="card-body">
<h2 class="card-title justify-center">{group.name}</h2> <h2 class="card-title justify-center">{group.name}</h2>
<div class="text-sm text-center"> <div class="text-center text-sm">
{#each getRelayUrls(group.nom) as url} {#each getRelayUrls(group.nom) as url}
<div class="badge badge-neutral">{displayRelayUrl(url)}</div> <div class="badge badge-neutral">{displayRelayUrl(url)}</div>
{/each} {/each}
</div> </div>
<p class="text-sm py-4">{group.about}</p> <p class="py-4 text-sm">{group.about}</p>
</div> </div>
</a> </a>
</Masonry> </Masonry>

View File

@@ -1,22 +1,22 @@
<script lang="ts"> <script lang="ts">
import {sleep, sortBy} from '@welshman/lib' import {sleep, sortBy} from "@welshman/lib"
import type {CustomEvent} from '@welshman/util' import type {CustomEvent} from "@welshman/util"
import {page} from '$app/stores' import {page} from "$app/stores"
import {fly} from '@lib/transition' import {fly} from "@lib/transition"
import {formatTimestampAsDate} from '@lib/util' import {formatTimestampAsDate} from "@lib/util"
import Spinner from '@lib/components/Spinner.svelte' import Spinner from "@lib/components/Spinner.svelte"
import Avatar from '@lib/components/Avatar.svelte' import Avatar from "@lib/components/Avatar.svelte"
import GroupNote from '@app/components/GroupNote.svelte' import GroupNote from "@app/components/GroupNote.svelte"
import {deriveGroup, deriveGroupConversation} from '@app/state' import {deriveGroup, deriveGroupConversation} from "@app/state"
const group = deriveGroup($page.params.nom) $: group = deriveGroup($page.params.nom)
const conversation = deriveGroupConversation($page.params.nom) $: conversation = deriveGroupConversation($page.params.nom)
const assertEvent = (e: any) => e as CustomEvent const assertEvent = (e: any) => e as CustomEvent
type Element = { type Element = {
id: string id: string
type: 'date' | 'note', type: "date" | "note"
value: string | CustomEvent value: string | CustomEvent
showPubkey: boolean showPubkey: boolean
} }
@@ -35,12 +35,12 @@
const date = formatTimestampAsDate(created_at) const date = formatTimestampAsDate(created_at)
if (date !== previousDate) { if (date !== previousDate) {
elements.push({type: 'date', value: date, id: date, showPubkey: false}) elements.push({type: "date", value: date, id: date, showPubkey: false})
} }
elements.push({ elements.push({
id, id,
type: 'note', type: "note",
value: event, value: event,
showPubkey: date !== previousDate || previousPubkey !== pubkey, showPubkey: date !== previousDate || previousPubkey !== pubkey,
}) })
@@ -57,13 +57,12 @@
}, 3000) }, 3000)
</script> </script>
<div class="h-screen flex flex-col"> <div class="flex h-screen flex-col">
<div class="min-h-24 bg-base-100 shadow-xl"> <div class="min-h-24 bg-base-100 shadow-xl"></div>
</div> <div class="flex flex-grow flex-col-reverse overflow-auto">
<div class="flex-grow overflow-auto flex flex-col-reverse gap-2 p-2"> {#each elements as { type, id, value, showPubkey } (id)}
{#each elements as {type, id, value, showPubkey} (id)}
{#if type === "date"} {#if type === "date"}
<div class="flex gap-2 items-center text-xs opacity-50"> <div class="flex items-center gap-2 py-2 text-xs opacity-50">
<div class="h-px flex-grow bg-base-content" /> <div class="h-px flex-grow bg-base-content" />
<p>{value}</p> <p>{value}</p>
<div class="h-px flex-grow bg-base-content" /> <div class="h-px flex-grow bg-base-content" />
@@ -72,7 +71,7 @@
<GroupNote event={assertEvent(value)} {showPubkey} /> <GroupNote event={assertEvent(value)} {showPubkey} />
{/if} {/if}
{/each} {/each}
<p class="flex justify-center items-center py-20 h-10"> <p class="flex h-10 items-center justify-center py-20">
<Spinner {loading}> <Spinner {loading}>
{#if loading} {#if loading}
Looking for messages... Looking for messages...
@@ -82,7 +81,5 @@
</Spinner> </Spinner>
</p> </p>
</div> </div>
<div class="min-h-32 bg-base-100 shadow-top-xl"> <div class="shadow-top-xl min-h-32 bg-base-100"></div>
</div>
</div> </div>

View File

@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import themes from "daisyui/src/theming/themes" import themes from "daisyui/src/theming/themes"
import {identity} from '@welshman/lib' import {identity} from "@welshman/lib"
import {createSearch} from '@lib/util' import {createSearch} from "@lib/util"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import {theme} from '@app/theme' import {theme} from "@app/theme"
let term = "" let term = ""
@@ -13,17 +13,18 @@
<div class="content column gap-4"> <div class="content column gap-4">
<h1 class="superheading mt-20">Discover Themes</h1> <h1 class="superheading mt-20">Discover Themes</h1>
<p class="text-center">Make your community feel like home</p> <p class="text-center">Make your community feel like home</p>
<label class="input input-bordered w-full flex items-center gap-2"> <label class="input input-bordered flex w-full items-center gap-2">
<Icon icon="magnifer" /> <Icon icon="magnifer" />
<input bind:value={term} class="grow" type="text" placeholder="Search for themes..." /> <input bind:value={term} class="grow" type="text" placeholder="Search for themes..." />
</label> </label>
<div class="grid grid-cols-2 md:grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4 md:grid-cols-2">
{#each searchThemes.searchValues(term) as name} {#each searchThemes.searchValues(term) as name}
<div class="card bg-base-100 shadow-xl" data-theme={name}> <div class="card bg-base-100 shadow-xl" data-theme={name}>
<div class="card-body"> <div class="card-body">
<h2 class="card-title justify-center capitalize card2">{name}</h2> <h2 class="card2 card-title justify-center capitalize">{name}</h2>
<div class="card-actions"> <div class="card-actions">
<button class="btn btn-primary w-full" on:click={() => theme.set(name)}>Use Theme</button> <button class="btn btn-primary w-full" on:click={() => theme.set(name)}
>Use Theme</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -9,8 +9,9 @@ export default {
none: 0, none: 0,
"nav-active": 1, "nav-active": 1,
"nav-item": 2, "nav-item": 2,
"modal": 3, popover: 3,
"toast": 4, modal: 4,
toast: 5,
}, },
}, },
plugins: [daisyui], plugins: [daisyui],