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.
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 {
@apply flex justify-center items-center;
@apply flex items-center justify-center;
}
.content {
@apply max-w-3xl w-full p-12 m-auto;
@apply m-auto w-full max-w-3xl p-12;
}
.heading {
@apply text-2xl text-center;
@apply text-center text-2xl;
}
.subheading {
@apply text-xl text-center;
@apply text-center text-xl;
}
.superheading {
@apply text-4xl text-center;
@apply text-center text-4xl;
}
.link {
@apply text-primary underline cursor-pointer;
@apply cursor-pointer text-primary underline;
}
.input input::placeholder {

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
},
})

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])))

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">
<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($parentEvent.pubkey))}<p>
<p class="whitespace-nowrap overflow-hidden text-ellipsis">{$parentEvent.content}</p>
</div>
<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>

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>

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>

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)

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>

View File

@@ -7,23 +7,31 @@
<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
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]}

View File

@@ -1,13 +1,17 @@
<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 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
@@ -18,11 +22,15 @@
<Icon icon="pallete-2" /> Themes
</SecondaryNavItem>
</div>
{:else if getPrimaryNavItem($page) === 'space'}
<!-- pass -->
{:else if getPrimaryNavItem($page) === 'settings'}
</SecondaryNavSection>
{:else if getPrimaryNavItem($page) === "space"}
{#key $page.params.nom}
<SecondaryNavForSpace nom={$page.params.nom} />
{/key}
{:else if getPrimaryNavItem($page) === "settings"}
<!-- pass -->
{:else}
<SecondaryNavSection>
<div in:fly>
<SecondaryNavItem href="/home">
<Icon icon="home-smile" /> Home
@@ -38,13 +46,14 @@
<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">
<div in:fly={{delay: 150}}>
<SecondaryNavHeader>
Conversations
<div class="cursor-pointer">
<Icon icon="add-circle" />
</div>
</SecondaryNavHeader>
</div>
</SecondaryNavSection>
{/if}
</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">
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>

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

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

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>

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")
}

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

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,14 +439,12 @@ export const {
load({
...request,
relays,
filters: [{kinds: [GROUP_META], '#d': [nom]}],
filters: [{kinds: [GROUP_META], "#d": [nom]}],
}),
])
]),
})
export const searchGroups = derived(
groups,
$groups =>
export const searchGroups = derived(groups, $groups =>
createSearch($groups, {
getValue: (group: PublishedGroup) => group.nom,
sortFn: (result: FuseResult<PublishedGroup>) => {
@@ -380,7 +455,7 @@ export const searchGroups = derived(
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,15 +611,19 @@ export const userProfile = derived([pk, profilesByPubkey], ([$pk, $profilesByPub
return $profilesByPubkey.get($pk)
})
export const userMembership = derived([pk, groupMembershipByPubkey], ([$pk, $groupMembershipByPubkey]) => {
export const userMembership = derived(
[pk, groupMembershipByPubkey],
([$pk, $groupMembershipByPubkey]) => {
if (!$pk) return null
loadGroupMembership($pk)
return $groupMembershipByPubkey.get($pk)
})
},
)
export const userGroupsByNom = withGetter(derived([userMembership, qualifiedGroupsById], ([$userMembership, $qualifiedGroupsById]) => {
export const userGroupsByNom = withGetter(
derived([userMembership, qualifiedGroupsById], ([$userMembership, $qualifiedGroupsById]) => {
const $userGroupsByNom = new Map()
for (const id of $userMembership?.ids || []) {
@@ -558,4 +641,5 @@ export const userGroupsByNom = withGetter(derived([userMembership, qualifiedGrou
}
return $userGroupsByNom
}))
}),
)

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)),
)
}

View File

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

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

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"

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">
import cx from 'classnames'
import cx from "classnames"
import Icon from "@lib/components/Icon.svelte"
export let src
@@ -7,11 +7,11 @@
</script>
<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;`}>
{#if src}
<img alt="" {src} />
{:else}
<Icon icon="user-rounded" size={Math.round(size * .7)} />
<Icon icon="user-rounded" size={Math.round(size * 0.7)} />
{/if}
</div>

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import {randomId} from '@welshman/lib'
import Icon from '@lib/components/Icon.svelte'
import {randomId} from "@welshman/lib"
import Icon from "@lib/components/Icon.svelte"
export let file: File | null = null
export let url: string | null = null
@@ -42,13 +42,18 @@
if (file) {
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)
} else {
url = initialUrl
}
}
</script>
<form>
@@ -57,14 +62,14 @@
for={id}
aria-label="Drag and drop files here."
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}
on:dragenter|preventDefault|stopPropagation={onDragEnter}
on:dragover|preventDefault|stopPropagation={onDragOver}
on:dragleave|preventDefault|stopPropagation={onDragLeave}
on:drop|preventDefault|stopPropagation={onDrop}>
<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-primary={!file}>
{#if file}
@@ -73,10 +78,10 @@
tabindex="-1"
on:mousedown|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>
{:else}
<Icon icon="add-circle" class="!bg-base-300 scale-150" />
<Icon icon="add-circle" class="scale-150 !bg-base-300" />
{/if}
</div>
{#if !file}

View File

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

View File

@@ -3,18 +3,18 @@
const modalHeight = tweened(0, {
duration: 700,
easing: quintOut
easing: quintOut,
})
emitter.on('close', () => modalHeight.set(0))
emitter.on("close", () => modalHeight.set(0))
</script>
<script lang="ts">
import {onMount} from 'svelte'
import {slide} from 'svelte/transition'
import {quintOut} from 'svelte/easing'
import {tweened} from 'svelte/motion'
import {last} from '@welshman/lib'
import {onMount} from "svelte"
import {slide} from "svelte/transition"
import {quintOut} from "svelte/easing"
import {tweened} from "svelte/motion"
import {last} from "@welshman/lib"
export let component
export let props = {}
@@ -29,7 +29,11 @@
})
</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}>
<svelte:component this={component} {...props} />
</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,23 +1,6 @@
<script lang="ts">
import {page} from "$app/stores"
export let href
$: active = $page.route.id?.startsWith(href)
</script>
<a
{href}
class="group justify-start border-none transition-all hover:bg-base-100 hover:text-base-content"
class:text-base-content={active}
class:bg-base-100={active}>
<div class="flex items-center gap-3">
<slot />
</div>
</a>
<style>
a {
a,
button {
padding: 12px 16px;
display: flex;
border-radius: var(--rounded-btn, 0.5rem);
@@ -29,8 +12,46 @@ a {
}
a:active:hover,
a:active:focus {
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">
import cx from "classnames"
import {page} from "$app/stores"
export let href: string = ""
$: active = $page.route.id?.startsWith(href)
</script>
{#if href}
<a
{...$$props}
{href}
on:click
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 />
</a>
{:else}
<button
{...$$props}
on:click
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 />
</button>
{/if}

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">
import {slide, fade} from 'svelte/transition'
import {slide, fade} from "svelte/transition"
export let loading
</script>
<span class="flex items-center">
{#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>
{/if}

View File

@@ -1,9 +1,9 @@
import Fuse from "fuse.js"
import type {IFuseOptions, FuseResult} from 'fuse.js'
import {throttle} from 'throttle-debounce'
import {writable} from 'svelte/store'
import {sortBy} from '@welshman/lib'
import {browser} from '$app/environment'
import type {IFuseOptions, FuseResult} from "fuse.js"
import {throttle} from "throttle-debounce"
import {writable} from "svelte/store"
import {sortBy} from "@welshman/lib"
import {browser} from "$app/environment"
export const parseJson = (json: string) => {
if (!json) return null
@@ -15,8 +15,7 @@ export const parseJson = (json: string) => {
}
}
export const getJson = (k: string) =>
browser ? parseJson(localStorage.getItem(k) || "") : null
export const getJson = (k: string) => (browser ? parseJson(localStorage.getItem(k) || "") : null)
export const setJson = (k: string, v: any) => {
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 dateToSeconds = (date: Date) => Math.round(date.valueOf() / 1000)

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import "@src/app.css"
import {onMount} from 'svelte'
import {onMount} from "svelte"
import {page} from "$app/stores"
import {createEventStore} from '@welshman/store'
import {createEventStore} from "@welshman/store"
import {fly} from "@lib/transition"
import ModalBox from "@lib/components/ModalBox.svelte"
import Toast from "@app/components/Toast.svelte"
@@ -38,20 +38,20 @@
onMount(() => {
ready = initStorage({
events: {
keyPath: 'id',
keyPath: "id",
store: createEventStore(repository),
},
relays: {
keyPath: 'url',
keyPath: "url",
store: relays,
},
handles: {
keyPath: 'nip05',
keyPath: "nip05",
store: handles,
},
})
dialog.addEventListener('close', () => {
dialog.addEventListener("close", () => {
if (modal) {
clearModal()
}
@@ -66,11 +66,11 @@
<div class="flex h-screen">
<PrimaryNav />
<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 />
</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}
{#key prev}
<ModalBox {...prev} />

View File

@@ -1,8 +1,8 @@
<script lang="ts">
import {goto} from '$app/navigation'
import {goto} from "$app/navigation"
import CardButton from "@lib/components/CardButton.svelte"
import SpaceCreate from '@app/components/SpaceCreate.svelte'
import {pushModal} from '@app/modal'
import SpaceCreate from "@app/components/SpaceCreate.svelte"
import {pushModal} from "@app/modal"
const createSpace = () => pushModal(SpaceCreate)
@@ -14,7 +14,7 @@
<div class="column content gap-4">
<h1 class="text-center text-5xl">Welcome to</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}>
Invite all your friends, do life together.
</CardButton>

View File

@@ -1,12 +1,12 @@
<script lang="ts">
import {onMount} from 'svelte'
import Masonry from 'svelte-bricks'
import {append, remove} from '@welshman/lib'
import {GROUP_META, displayRelayUrl} from '@welshman/util'
import Icon from '@lib/components/Icon.svelte'
import {makeSpacePath} from '@app/routes'
import {load, relays, groups, searchGroups, relayUrlsByNom, userMembership} from '@app/state'
import {updateGroupMemberships} from '@app/commands'
import {onMount} from "svelte"
import Masonry from "svelte-bricks"
import {append, remove} from "@welshman/lib"
import {GROUP_META, displayRelayUrl} from "@welshman/util"
import Icon from "@lib/components/Icon.svelte"
import {makeSpacePath} from "@app/routes"
import {load, relays, groups, searchGroups, relayUrlsByNom, userMembership} from "@app/state"
import {addGroupMemberships} from "@app/commands"
const getRelayUrls = (nom: string): string[] => $relayUrlsByNom.get(nom) || []
@@ -23,14 +23,24 @@
<div class="content column gap-4">
<h1 class="superheading mt-20">Discover Spaces</h1>
<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" />
<input bind:value={term} class="grow" type="text" placeholder="Search for spaces..." />
</label>
<Masonry animate={false} items={$searchGroups.searchOptions(term)} minColWidth={250} maxColWidth={800} gap={16} idKey="nom" let:item={group}>
<a href={makeSpacePath(group.nom)} class="card bg-base-100 shadow-xl hover:shadow-2xl hover:brightness-[1.1] transition-all">
<div class="avatar center mt-8">
<div class="w-20 rounded-full bg-base-300 border-2 border-solid border-base-300 !flex center relative">
<Masonry
animate={false}
items={$searchGroups.searchOptions(term)}
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}
<img alt="" src={group.picture} />
{:else}
@@ -39,20 +49,22 @@
</div>
</div>
{#if $userMembership?.noms.has(group.nom)}
<div class="absolute flex center 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="center absolute flex w-full">
<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" />
</div>
</div>
{/if}
<div class="card-body">
<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}
<div class="badge badge-neutral">{displayRelayUrl(url)}</div>
{/each}
</div>
<p class="text-sm py-4">{group.about}</p>
<p class="py-4 text-sm">{group.about}</p>
</div>
</a>
</Masonry>

View File

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

View File

@@ -1,9 +1,9 @@
<script lang="ts">
import themes from "daisyui/src/theming/themes"
import {identity} from '@welshman/lib'
import {createSearch} from '@lib/util'
import {identity} from "@welshman/lib"
import {createSearch} from "@lib/util"
import Icon from "@lib/components/Icon.svelte"
import {theme} from '@app/theme'
import {theme} from "@app/theme"
let term = ""
@@ -13,17 +13,18 @@
<div class="content column gap-4">
<h1 class="superheading mt-20">Discover Themes</h1>
<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" />
<input bind:value={term} class="grow" type="text" placeholder="Search for themes..." />
</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}
<div class="card bg-base-100 shadow-xl" data-theme={name}>
<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">
<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>

View File

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