Rough out chat

This commit is contained in:
Jon Staab
2024-10-08 11:39:16 -07:00
parent 7ffd02b736
commit 8698dcc359
59 changed files with 833 additions and 437 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ A discord-like nostr client based on the idea of "relays as groups". WIP.
- [ ] Profile settings - [ ] Profile settings
- [ ] Relay settings - [ ] Relay settings
------ ---
- [ ] Add person drawer with info and recent notes, where you can follow/mute them. Maybe same stuff as person search - [ ] Add person drawer with info and recent notes, where you can follow/mute them. Maybe same stuff as person search
- [ ] If the user isn't following anyone, show warning/fallback on people/notes pages - [ ] If the user isn't following anyone, show warning/fallback on people/notes pages
+5 -5
View File
@@ -48,13 +48,13 @@
"@tiptap/extension-text": "^2.6.6", "@tiptap/extension-text": "^2.6.6",
"@tiptap/suggestion": "^2.6.4", "@tiptap/suggestion": "^2.6.4",
"@types/throttle-debounce": "^5.0.2", "@types/throttle-debounce": "^5.0.2",
"@welshman/app": "^0.0.7",
"@welshman/content": "^0.0.11", "@welshman/content": "^0.0.11",
"@welshman/lib": "^0.0.19", "@welshman/lib": "^0.0.19",
"@welshman/net": "^0.0.23", "@welshman/util": "^0.0.36",
"@welshman/signer": "^0.0.6", "@welshman/store": "^0.0.9",
"@welshman/store": "^0.0.8", "@welshman/net": "^0.0.24",
"@welshman/util": "^0.0.34", "@welshman/signer": "^0.0.7",
"@welshman/app": "^0.0.11",
"daisyui": "^4.12.10", "daisyui": "^4.12.10",
"date-picker-svelte": "^2.13.0", "date-picker-svelte": "^2.13.0",
"emoji-picker-element": "^1.22.8", "emoji-picker-element": "^1.22.8",
+6 -3
View File
@@ -51,11 +51,13 @@
--secondary: oklch(var(--s)); --secondary: oklch(var(--s));
} }
.bg-alt, .bg-alt .bg-alt .bg-alt { .bg-alt,
.bg-alt .bg-alt .bg-alt {
@apply bg-base-100; @apply bg-base-100;
} }
.bg-alt .bg-alt, .bg-alt .bg-alt .bg-alt .bg-alt { .bg-alt .bg-alt,
.bg-alt .bg-alt .bg-alt .bg-alt {
@apply bg-base-300; @apply bg-base-300;
} }
@@ -139,7 +141,8 @@
@apply link-content; @apply link-content;
} }
.link-content, [tag] { .link-content,
[tag] {
@apply max-w-full overflow-hidden text-ellipsis whitespace-nowrap rounded bg-neutral px-1 text-neutral-content no-underline; @apply max-w-full overflow-hidden text-ellipsis whitespace-nowrap rounded bg-neutral px-1 text-neutral-content no-underline;
} }
+68 -24
View File
@@ -1,9 +1,24 @@
import {uniqBy, sleep, chunk, equals, nthNe, choice, append} from "@welshman/lib" import {get} from "svelte/store"
import {DELETE, PROFILE, INBOX_RELAYS, RELAYS, MUTES, FOLLOWS, REACTION, isSignedEvent, getPubkeyTagValues, createEvent, displayProfile, normalizeRelayUrl} from "@welshman/util" import {ctx, uniqBy, uniq, sleep, chunk, equals, choice, append} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import {
DELETE,
PROFILE,
INBOX_RELAYS,
RELAYS,
MUTES,
FOLLOWS,
REACTION,
isSignedEvent,
createEvent,
displayProfile,
normalizeRelayUrl,
} from "@welshman/util"
import type {TrustedEvent, EventTemplate} from "@welshman/util"
import type {SubscribeRequestWithHandlers} from "@welshman/net" import type {SubscribeRequestWithHandlers} from "@welshman/net"
import {Nip59, stamp} from "@welshman/signer"
import { import {
pubkey, pubkey,
signer,
repository, repository,
makeThunk, makeThunk,
publishThunk, publishThunk,
@@ -19,7 +34,7 @@ import {
tagPubkey, tagPubkey,
tagReactionTo, tagReactionTo,
getRelayUrls, getRelayUrls,
getInboxRelaySelections, userInboxRelaySelections,
} from "@welshman/app" } from "@welshman/app"
import {tagRoom, MEMBERSHIPS, INDEXER_RELAYS} from "@app/state" import {tagRoom, MEMBERSHIPS, INDEXER_RELAYS} from "@app/state"
@@ -157,7 +172,7 @@ export const setRelayPolicy = (url: string, read: boolean, write: boolean) =>
}) })
export const setInboxRelayPolicy = (url: string, enabled: boolean) => { export const setInboxRelayPolicy = (url: string, enabled: boolean) => {
const urls = getRelayUrls(getInboxRelaySelections(pubkey.get()!)) const urls = getRelayUrls(get(userInboxRelaySelections))
// Only update inbox policies if they already exist or we're adding them // Only update inbox policies if they already exist or we're adding them
if (enabled || urls.includes(url)) { if (enabled || urls.includes(url)) {
@@ -179,7 +194,7 @@ export const joinRelay = async (url: string, claim?: string) => {
makeThunk({ makeThunk({
event: createEvent(28934, {tags: [["claim", claim]]}), event: createEvent(28934, {tags: [["claim", claim]]}),
relays: [url], relays: [url],
}) }),
) )
} }
@@ -189,27 +204,56 @@ export const joinRelay = async (url: string, claim?: string) => {
// Actions // Actions
export const publishReaction = ({relays, event, content, tags = []}: { export const sendWrapped = async ({
relays: string[] template,
event: TrustedEvent, pubkeys,
content: string, }: {
tags?: string[][] template: EventTemplate
pubkeys: string[]
}) => { }) => {
const reaction = createEvent(REACTION, { const nip59 = Nip59.fromSigner(signer.get()!)
await Promise.all(
uniq(pubkeys).map(async recipient => {
const rumor = await nip59.wrap(recipient, stamp(template))
const thunk = makeThunk({
event: rumor.wrap,
relays: ctx.app.router.PublishMessage(recipient).getUrls(),
})
return publishThunk(thunk)
}),
)
}
export const makeReaction = ({
event,
content,
tags = [],
}: {
event: TrustedEvent
content: string
tags?: string[][]
}) =>
createEvent(REACTION, {
content, content,
tags: [ tags: [...tags, ...tagReactionTo(event)],
...tags,
...tagReactionTo(event),
],
}) })
publishThunk(makeThunk({event: reaction, relays})) export const publishReaction = ({
} relays,
event,
content,
tags = [],
}: {
relays: string[]
event: TrustedEvent
content: string
tags?: string[][]
}) => publishThunk(makeThunk({event: makeReaction({event, content, tags}), relays}))
export const publishDelete = ({relays, event}: {relays: string[], event: TrustedEvent}) => { export const makeDelete = ({event}: {event: TrustedEvent}) =>
const deleteEvent = createEvent(DELETE, { createEvent(DELETE, {tags: [["k", String(event.kind)], ...tagEvent(event)]})
tags: [["k", String(event.kind)], ...tagEvent(event)],
})
publishThunk(makeThunk({event: deleteEvent, relays})) export const publishDelete = ({relays, event}: {relays: string[]; event: TrustedEvent}) =>
} publishThunk(makeThunk({event: makeDelete({event}), relays}))
+3 -4
View File
@@ -3,12 +3,9 @@
import type {Readable} from "svelte/store" import type {Readable} from "svelte/store"
import {writable} from "svelte/store" import {writable} from "svelte/store"
import {createEditor, type Editor, EditorContent} from "svelte-tiptap" import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
import {createEvent} from "@welshman/util"
import {publishThunk, makeThunk} from "@welshman/app"
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 {getEditorOptions, getEditorTags, addFile} from "@lib/editor" import {getEditorOptions, getEditorTags, addFile} from "@lib/editor"
import {MESSAGE} from "@app/state"
import {getPubkeyHints} from "@app/commands" import {getPubkeyHints} from "@app/commands"
export let onSubmit export let onSubmit
@@ -27,7 +24,9 @@
} }
onMount(() => { onMount(() => {
editor = createEditor(getEditorOptions({submit, loading, getPubkeyHints, submitOnEnter: true, autofocus: true})) editor = createEditor(
getEditorOptions({submit, loading, getPubkeyHints, submitOnEnter: true, autofocus: true}),
)
}) })
</script> </script>
+9 -40
View File
@@ -1,9 +1,4 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte"
import type {SvelteComponent} from "svelte"
import type {NativeEmoji} from 'emoji-picker-element/shared'
import twColors from "tailwindcss/colors"
import type {Readable} from "svelte/store"
import {readable, derived} from "svelte/store" import {readable, derived} from "svelte/store"
import {hash, ellipsize, uniqBy, groupBy, now} from "@welshman/lib" import {hash, ellipsize, uniqBy, groupBy, now} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
@@ -14,24 +9,19 @@
deriveProfile, deriveProfile,
deriveProfileDisplay, deriveProfileDisplay,
formatTimestampAsTime, formatTimestampAsTime,
tagReactionTo,
tagEvent,
makeThunk,
publishThunk,
pubkey, pubkey,
} from "@welshman/app" } from "@welshman/app"
import type {PublishStatusData} from "@welshman/app" import type {PublishStatusData} from "@welshman/app"
import {REACTION, DELETE, ZAP_RESPONSE, createEvent, displayRelayUrl, getAncestorTags} from "@welshman/util" import {REACTION, ZAP_RESPONSE, displayRelayUrl} from "@welshman/util"
import {repository} from "@welshman/app" import {repository} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Avatar from "@lib/components/Avatar.svelte" import Avatar from "@lib/components/Avatar.svelte"
import Content from "@app/components/Content.svelte" import Content from "@app/components/Content.svelte"
import ChannelThread from '@app/components/ChannelThread.svelte' import ChannelThread from "@app/components/ChannelThread.svelte"
import ChannelMessageEmojiButton from "@app/components/ChannelMessageEmojiButton.svelte" import ChannelMessageEmojiButton from "@app/components/ChannelMessageEmojiButton.svelte"
import {tagRoom, REPLY, deriveEvent, displayReaction} from "@app/state" import {colors, tagRoom, deriveEvent, displayReaction} from "@app/state"
import {publishDelete, publishReaction} from "@app/commands" import {publishDelete, publishReaction} from "@app/commands"
import {pushModal} from '@app/modal' import {pushModal} from "@app/modal"
export let url export let url
export let room export let room
@@ -39,28 +29,6 @@
export let showPubkey = false export let showPubkey = false
export let hideParent = false export let hideParent = false
const colors = [
["amber", twColors.amber[600]],
["blue", twColors.blue[600]],
["cyan", twColors.cyan[600]],
["emerald", twColors.emerald[600]],
["fuchsia", twColors.fuchsia[600]],
["green", twColors.green[600]],
["indigo", twColors.indigo[600]],
["sky", twColors.sky[600]],
["lime", twColors.lime[600]],
["orange", twColors.orange[600]],
["pink", twColors.pink[600]],
["purple", twColors.purple[600]],
["red", twColors.red[600]],
["rose", twColors.rose[600]],
["sky", twColors.sky[600]],
["teal", twColors.teal[600]],
["violet", twColors.violet[600]],
["yellow", twColors.yellow[600]],
["zinc", twColors.zinc[600]],
]
const profile = deriveProfile(event.pubkey) const profile = deriveProfile(event.pubkey)
const profileDisplay = deriveProfileDisplay(event.pubkey) const profileDisplay = deriveProfileDisplay(event.pubkey)
const reactions = deriveEvents(repository, {filters: [{kinds: [REACTION], "#e": [event.id]}]}) const reactions = deriveEvents(repository, {filters: [{kinds: [REACTION], "#e": [event.id]}]})
@@ -96,8 +64,6 @@
} }
} }
let drawer: SvelteComponent
$: rootPubkey = $rootEvent?.pubkey || rootTag?.[4] $: rootPubkey = $rootEvent?.pubkey || rootTag?.[4]
$: rootProfile = deriveProfile(rootPubkey || "") $: rootProfile = deriveProfile(rootPubkey || "")
$: rootProfileDisplay = deriveProfileDisplay(rootPubkey || "") $: rootProfileDisplay = deriveProfileDisplay(rootPubkey || "")
@@ -107,7 +73,10 @@
!isPending && !isPublished && findStatus($ps, [PublishStatus.Failure, PublishStatus.Timeout]) !isPending && !isPublished && findStatus($ps, [PublishStatus.Failure, PublishStatus.Timeout])
</script> </script>
<button type="button" on:click={openThread} class="group relative flex flex-col gap-1 p-2 transition-colors hover:bg-base-300 text-left w-full"> <button
type="button"
on:click={openThread}
class="group relative flex w-full flex-col gap-1 p-2 text-left transition-colors hover:bg-base-300">
{#if $rootEvent && !hideParent} {#if $rootEvent && !hideParent}
<div class="flex items-center gap-1 pl-12 text-xs"> <div class="flex items-center gap-1 pl-12 text-xs">
<Icon icon="square-share-line" size={3} /> <Icon icon="square-share-line" size={3} />
@@ -154,7 +123,7 @@
</div> </div>
{#if $reactions.length > 0 || $zaps.length > 0} {#if $reactions.length > 0 || $zaps.length > 0}
<div class="ml-12 text-xs"> <div class="ml-12 text-xs">
{#each groupBy(e => e.content, uniqBy(e => e.pubkey + e.content, $reactions)).entries() as [content, events]} {#each groupBy( e => e.content, uniqBy(e => e.pubkey + e.content, $reactions), ).entries() as [content, events]}
{@const isOwn = events.some(e => e.pubkey === $pubkey)} {@const isOwn = events.some(e => e.pubkey === $pubkey)}
{@const onClick = () => onReactionClick(content, events)} {@const onClick = () => onReactionClick(content, events)}
<button <button
@@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
import tippy, {type Instance} from "tippy.js" import {type Instance} from "tippy.js"
import type {NativeEmoji} from 'emoji-picker-element/shared' import type {NativeEmoji} from "emoji-picker-element/shared"
import {between} from '@welshman/lib' import {between} from "@welshman/lib"
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 Tippy from "@lib/components/Tippy.svelte" import Tippy from "@lib/components/Tippy.svelte"
import EmojiPicker from "@lib/components/EmojiPicker.svelte" import EmojiPicker from "@lib/components/EmojiPicker.svelte"
import {tagRoom} from '@app/state' import {tagRoom} from "@app/state"
import {publishReaction} from '@app/commands' import {publishReaction} from "@app/commands"
export let url, room, event export let url, room, event
@@ -48,6 +48,5 @@
params={{ params={{
trigger: "manual", trigger: "manual",
interactive: true, interactive: true,
}} }} />
/>
</div> </div>
+14 -14
View File
@@ -1,17 +1,17 @@
<script lang="ts"> <script lang="ts">
import {sortBy, append, range} from '@welshman/lib' import {sortBy, append} from "@welshman/lib"
import {createEvent} from '@welshman/util' import {createEvent} from "@welshman/util"
import type {EventContent, TrustedEvent} from '@welshman/util' import type {EventContent, TrustedEvent} from "@welshman/util"
import {repository, makeThunk, publishThunk} from '@welshman/app' import {repository, makeThunk, publishThunk} from "@welshman/app"
import {deriveEvents} from '@welshman/store' import {deriveEvents} from "@welshman/store"
import ChannelMessage from '@app/components/ChannelMessage.svelte' import ChannelMessage from "@app/components/ChannelMessage.svelte"
import ChannelCompose from '@app/components/ChannelCompose.svelte' import ChannelCompose from "@app/components/ChannelCompose.svelte"
import {tagRoom, REPLY} from '@app/state' import {tagRoom, REPLY} from "@app/state"
export let url, room, event: TrustedEvent export let url, room, event: TrustedEvent
const replies = deriveEvents(repository, { const replies = deriveEvents(repository, {
filters: [{kinds: [REPLY], '#E': [event.id]}], filters: [{kinds: [REPLY], "#E": [event.id]}],
}) })
const onSubmit = ({content, tags}: EventContent) => { const onSubmit = ({content, tags}: EventContent) => {
@@ -30,11 +30,11 @@
} }
if (seenRoots.size === 0) { if (seenRoots.size === 0) {
tags.push(['K', String(event.kind)]) tags.push(["K", String(event.kind)])
tags.push(['E', event.id]) tags.push(["E", event.id])
} else { } else {
tags.push(['k', String(event.kind)]) tags.push(["k", String(event.kind)])
tags.push(['e', event.id]) tags.push(["e", event.id])
} }
const reply = createEvent(REPLY, {content, tags: append(tagRoom(room, url), tags)}) const reply = createEvent(REPLY, {content, tags: append(tagRoom(room, url), tags)})
@@ -43,7 +43,7 @@
} }
</script> </script>
<div class="fixed flex flex-col max-h-screen w-full gap-2"> <div class="fixed flex max-h-screen w-full flex-col gap-2">
<div class="overflow-auto pt-3"> <div class="overflow-auto pt-3">
<ChannelMessage {url} {room} {event} showPubkey /> <ChannelMessage {url} {room} {event} showPubkey />
{#each sortBy(e => e.created_at, $replies) as reply (reply.id)} {#each sortBy(e => e.created_at, $replies) as reply (reply.id)}
+114
View File
@@ -0,0 +1,114 @@
<script lang="ts">
import {derived} from "svelte/store"
import {hash, uniqBy, groupBy, now} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {deriveEvents} from "@welshman/store"
import {PublishStatus} from "@welshman/net"
import {
publishStatusData,
deriveProfile,
deriveProfileDisplay,
formatTimestampAsTime,
pubkey,
} from "@welshman/app"
import type {PublishStatusData} from "@welshman/app"
import {REACTION, ZAP_RESPONSE, displayRelayUrl} from "@welshman/util"
import {repository} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
import Avatar from "@lib/components/Avatar.svelte"
import Content from "@app/components/Content.svelte"
import ChatMessageEmojiButton from "@app/components/ChatMessageEmojiButton.svelte"
import {colors, displayReaction} from "@app/state"
import {makeDelete, makeReaction, sendWrapped} from "@app/commands"
export let event: TrustedEvent
export let pubkeys: string[]
export let showPubkey = false
const profile = deriveProfile(event.pubkey)
const profileDisplay = deriveProfileDisplay(event.pubkey)
const reactions = deriveEvents(repository, {filters: [{kinds: [REACTION], "#e": [event.id]}]})
const zaps = deriveEvents(repository, {filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}]})
const [colorName, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
const ps = derived(publishStatusData, $m => Object.values($m[event.id] || {}))
const findStatus = ($ps: PublishStatusData[], statuses: PublishStatus[]) =>
$ps.find(({status}) => statuses.includes(status))
const onReactionClick = async (content: string, events: TrustedEvent[]) => {
const reaction = events.find(e => e.pubkey === $pubkey)
const template = reaction ? makeDelete({event}) : makeReaction({event, content})
await sendWrapped({template, pubkeys})
}
$: isPublished = findStatus($ps, [PublishStatus.Success])
$: isPending = findStatus($ps, [PublishStatus.Pending]) && event.created_at > now() - 30
$: failure =
!isPending && !isPublished && findStatus($ps, [PublishStatus.Failure, PublishStatus.Timeout])
</script>
<button
type="button"
class="group relative flex w-full flex-col gap-1 p-2 text-left transition-colors hover:bg-base-300">
<div class="flex gap-2">
{#if showPubkey}
<Avatar src={$profile?.picture} class="border border-solid border-base-content" size={10} />
{:else}
<div class="w-10 min-w-10 max-w-10" />
{/if}
<div class="-mt-1 flex-grow pr-1">
{#if showPubkey}
<div class="flex items-center gap-2">
<strong class="text-sm" style="color: {colorValue}" data-color={colorName}
>{$profileDisplay}</strong>
<span class="text-xs opacity-50">{formatTimestampAsTime(event.created_at)}</span>
</div>
{/if}
<div class="text-sm">
<Content showEntire {event} />
{#if isPending}
<span class="flex-inline ml-1 gap-1">
<span class="loading loading-spinner mx-1 h-3 w-3 translate-y-px" />
<span class="opacity-50">Sending...</span>
</span>
{:else if failure}
<span
class="flex-inline tooltip ml-1 cursor-pointer gap-1"
data-tip="{failure.message} ({displayRelayUrl(failure.url)})">
<Icon icon="danger" class="translate-y-px" size={3} />
<span class="opacity-50">Failed to send!</span>
</span>
{/if}
</div>
</div>
</div>
{#if $reactions.length > 0 || $zaps.length > 0}
<div class="ml-12 text-xs">
{#each groupBy( e => e.content, uniqBy(e => e.pubkey + e.content, $reactions), ).entries() as [content, events]}
{@const isOwn = events.some(e => e.pubkey === $pubkey)}
{@const onClick = () => onReactionClick(content, events)}
<button
type="button"
class="flex-inline btn btn-neutral btn-xs mr-2 gap-1 rounded-full"
class:border={isOwn}
class:border-solid={isOwn}
class:border-primary={isOwn}
on:click|stopPropagation={onClick}>
<span>{displayReaction(content)}</span>
{#if events.length > 1}
<span>{events.length}</span>
{/if}
</button>
{/each}
</div>
{/if}
<button
class="join absolute -top-2 right-0 border border-solid border-neutral text-xs opacity-0 transition-all group-hover:opacity-100"
on:click|stopPropagation>
<ChatMessageEmojiButton {event} {pubkeys} />
<button class="btn join-item btn-xs">
<Icon icon="menu-dots" size={4} />
</button>
</button>
</button>
@@ -0,0 +1,62 @@
<script lang="ts">
import {type Instance} from "tippy.js"
import type {NativeEmoji} from "emoji-picker-element/shared"
import {ctx, uniq, between} from "@welshman/lib"
import {Nip59} from "@welshman/signer"
import type {TrustedEvent} from "@welshman/util"
import {makeThunk, signer, publishThunk} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Tippy from "@lib/components/Tippy.svelte"
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
import {makeReaction} from "@app/commands"
export let event: TrustedEvent
export let pubkeys: string[]
const open = () => popover.show()
const onClick = async (emoji: NativeEmoji) => {
const template = makeReaction({event, content: emoji.unicode})
const nip59 = Nip59.fromSigner($signer!)
for (const recipient of uniq(pubkeys)) {
const rumor = await nip59.wrap(recipient, template)
publishThunk(
makeThunk({
event: rumor.wrap,
relays: ctx.app.router.PublishMessage(recipient).getUrls(),
}),
)
}
popover.hide()
}
const onMouseMove = ({clientX, clientY}: any) => {
const {x, y, width, height} = popover.popper.getBoundingClientRect()
if (!between([x, x + width], clientX) || !between([y, y + height + 30], clientY)) {
popover.hide()
}
}
let popover: Instance
</script>
<svelte:document on:mousemove={onMouseMove} />
<div class="flex">
<Button class="btn join-item btn-xs" on:click={open}>
<Icon icon="smile-circle" size={4} />
</Button>
<Tippy
bind:popover
component={EmojiPicker}
props={{onClick}}
params={{
trigger: "manual",
interactive: true,
}} />
</div>
+1 -5
View File
@@ -1,8 +1,6 @@
<script lang="ts"> <script lang="ts">
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import {displayRelayUrl} from "@welshman/util"
import Field from "@lib/components/Field.svelte" import Field from "@lib/components/Field.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte" import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
@@ -19,9 +17,7 @@
<form class="column gap-4" on:submit|preventDefault={onSubmit}> <form class="column gap-4" on:submit|preventDefault={onSubmit}>
<h1 class="heading">Start a Chat</h1> <h1 class="heading">Start a Chat</h1>
<p class="text-center"> <p class="text-center">Create an encrypted chat room for private conversations.</p>
Create an encrypted chat room for private conversations.
</p>
<Field> <Field>
<p slot="label">Members</p> <p slot="label">Members</p>
<div slot="input"> <div slot="input">
+11 -13
View File
@@ -16,16 +16,16 @@
isAddress, isAddress,
isNewline, isNewline,
} from "@welshman/content" } from "@welshman/content"
import Link from '@lib/components/Link.svelte' import Link from "@lib/components/Link.svelte"
import ContentToken from '@app/components/ContentToken.svelte' import ContentToken from "@app/components/ContentToken.svelte"
import ContentCode from '@app/components/ContentCode.svelte' import ContentCode from "@app/components/ContentCode.svelte"
import ContentLinkInline from '@app/components/ContentLinkInline.svelte' import ContentLinkInline from "@app/components/ContentLinkInline.svelte"
import ContentLinkBlock from '@app/components/ContentLinkBlock.svelte' import ContentLinkBlock from "@app/components/ContentLinkBlock.svelte"
import ContentNewline from '@app/components/ContentNewline.svelte' import ContentNewline from "@app/components/ContentNewline.svelte"
import ContentQuote from '@app/components/ContentQuote.svelte' import ContentQuote from "@app/components/ContentQuote.svelte"
import ContentTopic from '@app/components/ContentTopic.svelte' import ContentTopic from "@app/components/ContentTopic.svelte"
import ContentMention from '@app/components/ContentMention.svelte' import ContentMention from "@app/components/ContentMention.svelte"
import {entityLink} from '@app/state' import {entityLink} from "@app/state"
export let event export let event
export let minLength = 500 export let minLength = 500
@@ -50,8 +50,6 @@
return false return false
} }
const isStartAndEnd = (i: number) => Boolean(isBoundary(i - 1) && isBoundary(i + 1))
const isStartOrEnd = (i: number) => Boolean(isBoundary(i - 1) || isBoundary(i + 1)) const isStartOrEnd = (i: number) => Boolean(isBoundary(i - 1) || isBoundary(i + 1))
const isBlock = (i: number) => { const isBlock = (i: number) => {
@@ -115,7 +113,7 @@
{/each} {/each}
</div> </div>
{#if ellipsize} {#if ellipsize}
<div class="z-feature relative flex justify-center bg-gradient-to-t from-base-100 pt-12 -m-6"> <div class="relative z-feature -m-6 flex justify-center bg-gradient-to-t from-base-100 pt-12">
<button type="button" class="btn" on:click|stopPropagation|preventDefault={expand}> <button type="button" class="btn" on:click|stopPropagation|preventDefault={expand}>
See more See more
</button> </button>
+3 -6
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {ellipsize, displayUrl, postJson} from "@welshman/lib" import {ellipsize, postJson} from "@welshman/lib"
import {dufflepud, imgproxy} from "@app/state" import {dufflepud, imgproxy} from "@app/state"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
@@ -22,16 +22,13 @@
external external
href={url} href={url}
style="background-color: rgba(15, 15, 14, 0.5)" style="background-color: rgba(15, 15, 14, 0.5)"
class="relative flex w-full flex-grow flex-col overflow-hidden rounded-xl my-2"> class="relative my-2 flex w-full flex-grow flex-col overflow-hidden rounded-xl">
{#if url.match(/\.(mov|webm|mp4)$/)} {#if url.match(/\.(mov|webm|mp4)$/)}
<video controls src={url} class="max-h-96 object-contain object-center"> <video controls src={url} class="max-h-96 object-contain object-center">
<track kind="captions" /> <track kind="captions" />
</video> </video>
{:else if url.match(/\.(jpe?g|png|gif|webp)$/)} {:else if url.match(/\.(jpe?g|png|gif|webp)$/)}
<img <img alt="Link preview" src={imgproxy(url)} class="max-h-96 object-cover object-center" />
alt="Link preview"
src={imgproxy(url)}
class="object-cover object-center max-h-96" />
{:else} {:else}
{#await loadPreview()} {#await loadPreview()}
<div class="center my-12 w-full"> <div class="center my-12 w-full">
+2 -2
View File
@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import {nip19} from 'nostr-tools' import {nip19} from "nostr-tools"
import {displayProfile} from "@welshman/util" import {displayProfile} from "@welshman/util"
import {deriveProfile} from "@welshman/app" import {deriveProfile} from "@welshman/app"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import {entityLink} from '@app/state' import {entityLink} from "@app/state"
export let value export let value
+4 -7
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {getAddress, Address} from "@welshman/util" import {Address} from "@welshman/util"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import NoteCard from "@app/components/NoteCard.svelte" import NoteCard from "@app/components/NoteCard.svelte"
import {deriveEvent} from "@app/state" import {deriveEvent} from "@app/state"
@@ -12,18 +12,15 @@
const event = deriveEvent(idOrAddress, relays) const event = deriveEvent(idOrAddress, relays)
let element: Element let element: Element
$: address = $event ? getAddress($event) : ""
$: isGroup = address.match(/^(34550|35834):/)
</script> </script>
<button class="block text-left my-2 max-w-full" bind:this={element} on:click|stopPropagation> <button class="my-2 block max-w-full text-left" bind:this={element} on:click|stopPropagation>
{#if $event} {#if $event}
<NoteCard event={$event} class="p-4 rounded-box bg-alt"> <NoteCard event={$event} class="bg-alt rounded-box p-4">
<slot name="note-content" event={$event} {depth} /> <slot name="note-content" event={$event} {depth} />
</NoteCard> </NoteCard>
{:else} {:else}
<div class="p-4 rounded-box"> <div class="rounded-box p-4">
<Spinner loading>Loading event...</Spinner> <Spinner loading>Loading event...</Spinner>
</div> </div>
{/if} {/if}
-3
View File
@@ -1,7 +1,4 @@
<script lang="ts"> <script lang="ts">
import cx from "classnames"
import type {NodeViewProps} from "@tiptap/core"
import {NodeViewWrapper} from "svelte-tiptap"
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"
+9 -4
View File
@@ -8,13 +8,18 @@
<h1 class="heading">What is a nostr address?</h1> <h1 class="heading">What is a nostr address?</h1>
</div> </div>
<p> <p>
Flotilla hosts spaces on the <Link external href="https://nostr.com/" class="underline">Nostr protocol</Link>. Flotilla hosts spaces on the <Link external href="https://nostr.com/" class="underline"
Nostr uses "nostr addresses" to make it easier for people to find you, without having to >Nostr protocol</Link
>. Nostr uses "nostr addresses" to make it easier for people to find you, without having to
memorize your public key (your user id). memorize your public key (your user id).
</p> </p>
<p> <p>
There are several providers of nostr addresses, including several clients. You can find a There are several providers of nostr addresses, including several clients. You can find a list
list and more information on <Link external href="https://nostr.how/en/guides/get-verified" class="underline">nostr.how</Link>. and more information on <Link
external
href="https://nostr.how/en/guides/get-verified"
class="underline">nostr.how</Link
>.
</p> </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> </div>
+5 -3
View File
@@ -8,9 +8,11 @@
<h1 class="heading">What is a relay?</h1> <h1 class="heading">What is a relay?</h1>
</div> </div>
<p> <p>
Flotilla hosts spaces on the <Link external href="https://nostr.com/" class="underline">Nostr protocol</Link>. Flotilla hosts spaces on the <Link external href="https://nostr.com/" class="underline"
Nostr uses "relays" to host data, which are special-purpose servers that speak nostr's language. >Nostr protocol</Link
This means that anyone can host their own data, making the web more decentralized and resilient. >. Nostr uses "relays" to host data, which are special-purpose servers that speak nostr's
language. This means that anyone can host their own data, making the web more decentralized and
resilient.
</p> </p>
<p> <p>
Different relays have different policies for access control and content retention. Be sure to Different relays have different policies for access control and content retention. Be sure to
+3 -5
View File
@@ -1,12 +1,10 @@
<script lang="ts"> <script lang="ts">
import {nip19} from 'nostr-tools' import {nip19} from "nostr-tools"
import {ctx} from "@welshman/lib" import {ctx} from "@welshman/lib"
import {displayPubkey} from "@welshman/util" import {formatTimestamp} from "@welshman/app"
import {deriveProfile, deriveProfileDisplay, formatTimestamp} from "@welshman/app"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import Avatar from "@lib/components/Avatar.svelte"
import Profile from "@app/components/Profile.svelte" import Profile from "@app/components/Profile.svelte"
import {entityLink} from '@app/state' import {entityLink} from "@app/state"
export let event export let event
+21 -12
View File
@@ -1,16 +1,23 @@
<script lang="ts"> <script lang="ts">
import {onMount} from 'svelte' import {onMount} from "svelte"
import {nip19} from 'nostr-tools' import {nip19} from "nostr-tools"
import {ago, append, first, sortBy, max, WEEK, ctx} from '@welshman/lib' import {ago, append, first, sortBy, WEEK, ctx} from "@welshman/lib"
import {NOTE, getAncestorTags, getListValues} from '@welshman/util' import {NOTE, getAncestorTags, getListTags, getPubkeyTagValues} from "@welshman/util"
import type {Filter} from '@welshman/util' import type {Filter} from "@welshman/util"
import {deriveEvents} from '@welshman/store' import {deriveEvents} from "@welshman/store"
import {repository, load, loadRelaySelections, userFollows, formatTimestamp, formatTimestampRelative} from '@welshman/app' import {
import Link from '@lib/components/Link.svelte' repository,
load,
loadRelaySelections,
userFollows,
formatTimestamp,
formatTimestampRelative,
} from "@welshman/app"
import Link from "@lib/components/Link.svelte"
import Profile from "@app/components/Profile.svelte" import Profile from "@app/components/Profile.svelte"
import ProfileInfo from "@app/components/ProfileInfo.svelte" import ProfileInfo from "@app/components/ProfileInfo.svelte"
import Content from "@app/components/Content.svelte" import Content from "@app/components/Content.svelte"
import {entityLink} from '@app/state' import {entityLink} from "@app/state"
export let pubkey export let pubkey
@@ -38,16 +45,18 @@
{@const event = first(sortBy(e => -e.created_at, roots))} {@const event = first(sortBy(e => -e.created_at, roots))}
{@const relays = ctx.app.router.Event(event).getUrls()} {@const relays = ctx.app.router.Event(event).getUrls()}
{@const nevent = nip19.neventEncode({id: event.id, relays})} {@const nevent = nip19.neventEncode({id: event.id, relays})}
{@const following = getListValues("p", $userFollows).includes(pubkey)} {@const following = getPubkeyTagValues(getListTags($userFollows)).includes(pubkey)}
<div class="divider" /> <div class="divider" />
<Link external class="chat chat-start" href={entityLink(nevent)}> <Link external class="chat chat-start" href={entityLink(nevent)}>
<div class="chat-bubble"> <div class="chat-bubble">
<Content hideMedia={!following} {event} /> <Content hideMedia={!following} {event} />
<p class="text-xs text-right">{formatTimestamp(event.created_at)}</p> <p class="text-right text-xs">{formatTimestamp(event.created_at)}</p>
</div> </div>
</Link> </Link>
<div class="flex gap-2"> <div class="flex gap-2">
<div class="badge badge-neutral">{roots.length} recent {roots.length === 1 ? 'note' : 'notes'}</div> <div class="badge badge-neutral">
{roots.length} recent {roots.length === 1 ? "note" : "notes"}
</div>
<div class="badge badge-neutral">Last posted {formatTimestampRelative(event.created_at)}</div> <div class="badge badge-neutral">Last posted {formatTimestampRelative(event.created_at)}</div>
</div> </div>
{/if} {/if}
-1
View File
@@ -11,7 +11,6 @@
import {quintOut} from "svelte/easing" import {quintOut} from "svelte/easing"
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import {userProfile} from "@welshman/app" import {userProfile} from "@welshman/app"
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"
+17 -11
View File
@@ -1,12 +1,18 @@
<script lang="ts"> <script lang="ts">
import {nip19} from 'nostr-tools' import {nip19} from "nostr-tools"
import {derived} from 'svelte/store' import {displayPubkey, getPubkeyTagValues, getListTags} from "@welshman/util"
import {displayPubkey, getListValues} from "@welshman/util" import {
import {userFollows, deriveUserWotScore, deriveProfile, deriveHandleForPubkey, displayHandle, deriveProfileDisplay, formatTimestamp, getUserWotScore, followsByPubkey} from "@welshman/app" userFollows,
deriveUserWotScore,
deriveProfile,
deriveHandleForPubkey,
displayHandle,
deriveProfileDisplay,
} from "@welshman/app"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import Avatar from "@lib/components/Avatar.svelte" import Avatar from "@lib/components/Avatar.svelte"
import WotScore from "@lib/components/WotScore.svelte" import WotScore from "@lib/components/WotScore.svelte"
import {entityLink} from '@app/state' import {entityLink} from "@app/state"
export let pubkey export let pubkey
@@ -16,21 +22,21 @@
const handle = deriveHandleForPubkey(pubkey) const handle = deriveHandleForPubkey(pubkey)
const score = deriveUserWotScore(pubkey) const score = deriveUserWotScore(pubkey)
$: following = getListValues("p", $userFollows).includes(pubkey) $: following = getPubkeyTagValues(getListTags($userFollows)).includes(pubkey)
</script> </script>
<div class="flex gap-3 max-w-full"> <div class="flex max-w-full gap-3">
<Link external href={entityLink(npub)} class="py-1"> <Link external href={entityLink(npub)} class="py-1">
<Avatar src={$profile?.picture} size={10} /> <Avatar src={$profile?.picture} size={10} />
</Link> </Link>
<div class="flex flex-col min-w-0"> <div class="flex min-w-0 flex-col">
<div class="flex gap-2 items-center"> <div class="flex items-center gap-2">
<Link external class="text-bold text-ellipsis overflow-hidden" href={entityLink(npub)}> <Link external class="text-bold overflow-hidden text-ellipsis" href={entityLink(npub)}>
{$profileDisplay} {$profileDisplay}
</Link> </Link>
<WotScore score={$score} active={following} /> <WotScore score={$score} active={following} />
</div> </div>
<div class="text-sm opacity-75 text-ellipsis overflow-hidden"> <div class="overflow-hidden text-ellipsis text-sm opacity-75">
{$handle ? displayHandle($handle) : displayPubkey(pubkey)} {$handle ? displayHandle($handle) : displayPubkey(pubkey)}
</div> </div>
</div> </div>
+2 -2
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {deriveProfile} from '@welshman/app' import {deriveProfile} from "@welshman/app"
import Avatar from '@lib/components/Avatar.svelte' import Avatar from "@lib/components/Avatar.svelte"
export let pubkey export let pubkey
+1 -2
View File
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import {deriveProfile} from '@welshman/app'
import ProfileCircle from "@app/components/ProfileCircle.svelte" import ProfileCircle from "@app/components/ProfileCircle.svelte"
export let pubkeys export let pubkeys
@@ -8,7 +7,7 @@
<div class="flex pr-3"> <div class="flex pr-3">
{#each pubkeys.slice(0, 15) as pubkey (pubkey)} {#each pubkeys.slice(0, 15) as pubkey (pubkey)}
<div class="z-feature -mr-3 inline-block"> <div class="z-feature -mr-3 inline-block">
<ProfileCircle class="w-8 h-8" {pubkey} {...$$props} /> <ProfileCircle class="h-8 w-8" {pubkey} {...$$props} />
</div> </div>
{/each} {/each}
</div> </div>
+23 -23
View File
@@ -1,21 +1,17 @@
<script lang="ts"> <script lang="ts">
import {nip19} from 'nostr-tools' import {nip19} from "nostr-tools"
import type {SvelteComponent} from 'svelte' import type {SvelteComponent} from "svelte"
import tippy, {type Instance} from "tippy.js" import {type Instance} from "tippy.js"
import {append, always, remove, uniq} from '@welshman/lib' import {append, remove, uniq} from "@welshman/lib"
import {getListValues, MUTES} from '@welshman/util' import {profileSearch} from "@welshman/app"
import {userMutes, profileSearch, tagPubkey} from '@welshman/app' import Icon from "@lib/components/Icon.svelte"
import Icon from '@lib/components/Icon.svelte' import Tippy from "@lib/components/Tippy.svelte"
import Field from '@lib/components/Field.svelte' import Link from "@lib/components/Link.svelte"
import Tippy from '@lib/components/Tippy.svelte' import Button from "@lib/components/Button.svelte"
import Link from '@lib/components/Link.svelte' import Suggestions from "@lib/editor/Suggestions.svelte"
import Button from '@lib/components/Button.svelte' import SuggestionProfile from "@lib/editor/SuggestionProfile.svelte"
import Suggestions from '@lib/editor/Suggestions.svelte' import Name from "@app/components/Name.svelte"
import SuggestionProfile from '@lib/editor/SuggestionProfile.svelte' import {entityLink} from "@app/state"
import Name from '@app/components/Name.svelte'
import {entityLink} from '@app/state'
import {updateList} from '@app/commands'
import {pushToast} from '@app/toast'
export let value: string[] export let value: string[]
@@ -52,7 +48,7 @@
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div> <div>
{#each value as pubkey (pubkey)} {#each value as pubkey (pubkey)}
<div class="badge badge-neutral mr-1 flex-inline gap-1"> <div class="flex-inline badge badge-neutral mr-1 gap-1">
<Button class="flex items-center" on:click={() => removePubkey(pubkey)}> <Button class="flex items-center" on:click={() => removePubkey(pubkey)}>
<Icon icon="close-circle" size={4} class="-ml-1 mt-px" /> <Icon icon="close-circle" size={4} class="-ml-1 mt-px" />
</Button> </Button>
@@ -64,7 +60,12 @@
</div> </div>
<label class="input input-bordered flex w-full items-center gap-2" bind:this={input}> <label class="input input-bordered flex w-full items-center gap-2" bind:this={input}>
<Icon icon="magnifer" /> <Icon icon="magnifer" />
<input class="grow" type="text" placeholder="Search for profiles..." bind:value={term} on:keydown={onKeyDown} /> <input
class="grow"
type="text"
placeholder="Search for profiles..."
bind:value={term}
on:keydown={onKeyDown} />
</label> </label>
<Tippy <Tippy
bind:popover bind:popover
@@ -75,14 +76,13 @@
select: selectPubkey, select: selectPubkey,
search: profileSearch, search: profileSearch,
component: SuggestionProfile, component: SuggestionProfile,
class: 'rounded-box', class: "rounded-box",
style: `left: 4px; width: ${input?.clientWidth + 12}px`, style: `left: 4px; width: ${input?.clientWidth + 12}px`,
}} }}
params={{ params={{
trigger: "manual", trigger: "manual",
interactive: true, interactive: true,
maxWidth: 'none', maxWidth: "none",
getReferenceClientRect: () => input.getBoundingClientRect(), getReferenceClientRect: () => input.getBoundingClientRect(),
}} }} />
/>
</div> </div>
+7 -4
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {onMount} from 'svelte' import {onMount} from "svelte"
import type {Readable} from 'svelte/store' import type {Readable} from "svelte/store"
import {relaySearch} from "@welshman/app" import {relaySearch} from "@welshman/app"
import {createScroller} from "@lib/html" import {createScroller} from "@lib/html"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -19,7 +19,7 @@
const sub = discoverRelays() const sub = discoverRelays()
const scroller = createScroller({ const scroller = createScroller({
delay: 300, delay: 300,
element: element.closest('.modal-box')!, element: element.closest(".modal-box")!,
onScroll: () => { onScroll: () => {
limit += 20 limit += 20
}, },
@@ -37,7 +37,10 @@
<Icon icon="magnifer" /> <Icon icon="magnifer" />
<input bind:value={term} class="grow" type="text" placeholder="Search for relays..." /> <input bind:value={term} class="grow" type="text" placeholder="Search for relays..." />
</label> </label>
{#each $relaySearch.searchValues(term).filter(url => !$relays.includes(url)).slice(0, limit) as url (url)} {#each $relaySearch
.searchValues(term)
.filter(url => !$relays.includes(url))
.slice(0, limit) as url (url)}
<RelayItem {url}> <RelayItem {url}>
<Button class="btn btn-outline btn-sm flex items-center" on:click={() => addRelay(url)}> <Button class="btn btn-outline btn-sm flex items-center" on:click={() => addRelay(url)}>
<Icon icon="add-circle" /> <Icon icon="add-circle" />
+8 -5
View File
@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import {displayUrl} from '@welshman/lib' import {displayUrl} from "@welshman/lib"
import {displayRelayUrl} from '@welshman/util' import {displayRelayUrl} from "@welshman/util"
import {deriveRelay} from '@welshman/app' import {deriveRelay} from "@welshman/app"
export let url export let url
@@ -30,11 +30,14 @@
&bull; &bull;
{/if} {/if}
{#if $relay?.profile?.supported_nips} {#if $relay?.profile?.supported_nips}
<span class="cursor-pointer underline tooltip" data-tip="NIPs supported: {$relay.profile.supported_nips.join(", ")}"> <span
class="tooltip cursor-pointer underline"
data-tip="NIPs supported: {$relay.profile.supported_nips.join(', ')}">
{$relay.profile.supported_nips.length} NIPs {$relay.profile.supported_nips.length} NIPs
</span> </span>
&bull; &bull;
{/if} {/if}
Connected {connections} {connections === 1 ? 'time' : 'times'} Connected {connections}
{connections === 1 ? "time" : "times"}
</span> </span>
</div> </div>
-1
View File
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import Button from "@lib/components/Button.svelte"
import CardButton from "@lib/components/CardButton.svelte" import CardButton from "@lib/components/CardButton.svelte"
import SpaceCreateExternal from "@app/components/SpaceCreateExternal.svelte" import SpaceCreateExternal from "@app/components/SpaceCreateExternal.svelte"
import SpaceInviteAccept from "@app/components/SpaceInviteAccept.svelte" import SpaceInviteAccept from "@app/components/SpaceInviteAccept.svelte"
+4 -4
View File
@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import {nip19} from 'nostr-tools' import {nip19} from "nostr-tools"
import {ctx} from '@welshman/lib' import {ctx} from "@welshman/lib"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import Content from "@app/components/Content.svelte" import Content from "@app/components/Content.svelte"
import NoteCard from "@app/components/NoteCard.svelte" import NoteCard from "@app/components/NoteCard.svelte"
import {entityLink} from '@app/state' import {entityLink} from "@app/state"
export let root export let root
export let replies export let replies
@@ -22,7 +22,7 @@
</NoteCard> </NoteCard>
<Link <Link
href={entityLink(nevent)} href={entityLink(nevent)}
class="flex items-center gap-2 btn btn-neutral -mt-6 mr-4 rounded-full"> class="btn btn-neutral -mt-6 mr-4 flex items-center gap-2 rounded-full">
<Icon icon="chat-round" /> <Icon icon="chat-round" />
<span>{replies.length}</span> <span>{replies.length}</span>
</Link> </Link>
+5 -1
View File
@@ -10,7 +10,11 @@ export type ModalOptions = {
drawer?: boolean drawer?: boolean
} }
export const pushModal = (component: ComponentType, props: Record<string, any> = {}, options: ModalOptions = {}) => { export const pushModal = (
component: ComponentType,
props: Record<string, any> = {},
options: ModalOptions = {},
) => {
const id = randomId() const id = randomId()
// TODO: fix memory leak here by listening to history somehow // TODO: fix memory leak here by listening to history somehow
+64 -28
View File
@@ -1,7 +1,20 @@
import {nip19} from "nostr-tools" import {nip19} from "nostr-tools"
import twColors from "tailwindcss/colors"
import {get, derived} from "svelte/store" import {get, derived} from "svelte/store"
import type {Maybe} from "@welshman/lib" import type {Maybe} from "@welshman/lib"
import {setContext, remove, assoc, sortBy, sort, uniq, partition, nth, max, pushToMapKey, nthEq} from "@welshman/lib" import {
setContext,
remove,
assoc,
sortBy,
sort,
uniq,
partition,
nth,
max,
pushToMapKey,
nthEq,
} from "@welshman/lib"
import { import {
getIdFilters, getIdFilters,
NOTE, NOTE,
@@ -29,7 +42,6 @@ import {
subscribe, subscribe,
collection, collection,
loadRelay, loadRelay,
loadProfile,
profilesByPubkey, profilesByPubkey,
getDefaultAppContext, getDefaultAppContext,
getDefaultNetContext, getDefaultNetContext,
@@ -69,7 +81,29 @@ export const IMGPROXY_URL = "https://imgproxy.coracle.social"
export const REACTION_KINDS = [REACTION, ZAP_RESPONSE] export const REACTION_KINDS = [REACTION, ZAP_RESPONSE]
export const dufflepud = (path: string) => DUFFLEPUD_URL + '/' + path export const colors = [
["amber", twColors.amber[600]],
["blue", twColors.blue[600]],
["cyan", twColors.cyan[600]],
["emerald", twColors.emerald[600]],
["fuchsia", twColors.fuchsia[600]],
["green", twColors.green[600]],
["indigo", twColors.indigo[600]],
["sky", twColors.sky[600]],
["lime", twColors.lime[600]],
["orange", twColors.orange[600]],
["pink", twColors.pink[600]],
["purple", twColors.purple[600]],
["red", twColors.red[600]],
["rose", twColors.rose[600]],
["sky", twColors.sky[600]],
["teal", twColors.teal[600]],
["violet", twColors.violet[600]],
["yellow", twColors.yellow[600]],
["zinc", twColors.zinc[600]],
]
export const dufflepud = (path: string) => DUFFLEPUD_URL + "/" + path
export const imgproxy = (url: string, {w = 640, h = 1024} = {}) => { export const imgproxy = (url: string, {w = 640, h = 1024} = {}) => {
if (!url || url.match("gif$")) { if (!url || url.match("gif$")) {
@@ -153,7 +187,7 @@ setContext({
}), }),
}) })
repository.on('update', ({added}) => { repository.on("update", ({added}) => {
for (const event of added) { for (const event of added) {
ensureUnwrapped(event) ensureUnwrapped(event)
} }
@@ -310,33 +344,36 @@ export const makeChatId = (pubkeys: string[]) => sort(uniq(pubkeys)).join(",")
export const splitChatId = (id: string) => id.split(",") export const splitChatId = (id: string) => id.split(",")
export const chats = derived([pubkey, chatMessages, profilesByPubkey], ([$pubkey, $messages, $profilesByPubkey]) => { export const chats = derived(
const messagesByChatId = new Map<string, TrustedEvent[]>() [pubkey, chatMessages, profilesByPubkey],
([$pubkey, $messages, $profilesByPubkey]) => {
const messagesByChatId = new Map<string, TrustedEvent[]>()
for (const message of $messages) { for (const message of $messages) {
const chatId = makeChatId(getPubkeyTagValues(message.tags)) const chatId = makeChatId(getPubkeyTagValues(message.tags))
pushToMapKey(messagesByChatId, chatId, message) pushToMapKey(messagesByChatId, chatId, message)
} }
return sortBy( return sortBy(
c => -c.last_activity, c => -c.last_activity,
Array.from(messagesByChatId.entries()).map(([id, events]): Chat => { Array.from(messagesByChatId.entries()).map(([id, events]): Chat => {
const pubkeys = splitChatId(id) const pubkeys = splitChatId(id)
const messages = sortBy(e => -e.created_at, events) const messages = sortBy(e => -e.created_at, events)
const last_activity = messages[0].created_at const last_activity = messages[0].created_at
const search_text = remove($pubkey as string, pubkeys) const search_text = remove($pubkey as string, pubkeys)
.map(pubkey => { .map(pubkey => {
const profile = $profilesByPubkey.get(pubkey) const profile = $profilesByPubkey.get(pubkey)
return profile ? displayProfile(profile) : "" return profile ? displayProfile(profile) : ""
}) })
.join(' ') .join(" ")
return {id, pubkeys, messages, last_activity, search_text} return {id, pubkeys, messages, last_activity, search_text}
}) }),
) )
}) },
)
export const { export const {
indexStore: chatsById, indexStore: chatsById,
@@ -348,13 +385,12 @@ export const {
getKey: chat => chat.id, getKey: chat => chat.id,
load: async (id: string, request: Partial<SubscribeRequestWithHandlers> = {}) => { load: async (id: string, request: Partial<SubscribeRequestWithHandlers> = {}) => {
const $pubkey = pubkey.get() const $pubkey = pubkey.get()
const [url, room] = splitChatId(id)
const chat = get(chatsById).get(id) const chat = get(chatsById).get(id)
const timestamps = chat?.messages.map(e => e.created_at) || [] const timestamps = chat?.messages.map(e => e.created_at) || []
const since = Math.max(0, max(timestamps) - 3600) const since = Math.max(0, max(timestamps) - 3600)
if ($pubkey) { if ($pubkey) {
await load({...request, filters: [{kinds: [WRAP], '#p': [$pubkey], since}]}) await load({...request, filters: [{kinds: [WRAP], "#p": [$pubkey], since}]})
} }
}, },
}) })
+1 -1
View File
@@ -13,7 +13,7 @@
style={`width: ${size * 4}px; height: ${size * 4}px; min-width: ${size * 4}px; background-image: url(${src}); ${$$props.style || ""}`} /> style={`width: ${size * 4}px; height: ${size * 4}px; min-width: ${size * 4}px; background-image: url(${src}); ${$$props.style || ""}`} />
{:else} {:else}
<div <div
class={cx($$props.class, "rounded-full !flex center")} class={cx($$props.class, "center !flex rounded-full")}
style={`width: ${size * 4}px; height: ${size * 4}px; min-width: ${size * 4}px; ${$$props.style || ""}`}> style={`width: ${size * 4}px; height: ${size * 4}px; min-width: ${size * 4}px; ${$$props.style || ""}`}>
<Icon {icon} size={Math.round(size * 0.8)} /> <Icon {icon} size={Math.round(size * 0.8)} />
</div> </div>
+4 -4
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {slide} from '@lib/transition' import {slide} from "@lib/transition"
import Icon from '@lib/components/Icon.svelte' import Icon from "@lib/components/Icon.svelte"
const toggle = () => { const toggle = () => {
isOpen = !isOpen isOpen = !isOpen
@@ -9,10 +9,10 @@
let isOpen = false let isOpen = false
</script> </script>
<div class="flex flex-col gap-4 relative {$$props.class}"> <div class="relative flex flex-col gap-4 {$$props.class}">
<button <button
type="button" type="button"
class="absolute top-8 right-8 cursor-pointer w-4 h-4 transition-all" class="absolute right-8 top-8 h-4 w-4 cursor-pointer transition-all"
class:rotate-90={!isOpen} class:rotate-90={!isOpen}
on:click={toggle}> on:click={toggle}>
<Icon icon="alt-arrow-down" /> <Icon icon="alt-arrow-down" />
+3 -4
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import {randomId} from '@welshman/lib' import {randomId} from "@welshman/lib"
import Icon from '@lib/components/Icon.svelte'
const id = randomId() const id = randomId()
@@ -31,8 +30,8 @@
</div> </div>
<div class="drawer-side z-modal"> <div class="drawer-side z-modal">
<label for={id} aria-label="close sidebar" class="drawer-overlay"></label> <label for={id} aria-label="close sidebar" class="drawer-overlay"></label>
<div class="menu bg-base-200 text-base-content min-h-full w-80 p-0"> <div class="menu min-h-full w-80 bg-base-200 p-0 text-base-content">
<slot /> <slot />
</div> </div>
</div> </div>
</div> </div>
+4 -4
View File
@@ -1,14 +1,14 @@
<script lang="ts"> <script lang="ts">
import 'emoji-picker-element' import "emoji-picker-element"
import type {Emoji} from 'emoji-picker-element/shared' import type {Emoji} from "emoji-picker-element/shared"
import {onMount} from 'svelte' import {onMount} from "svelte"
export let onClick: (emoji: Emoji) => void export let onClick: (emoji: Emoji) => void
let element: Element let element: Element
onMount(() => { onMount(() => {
element.addEventListener('emoji-click', (event: any) => onClick(event.detail as Emoji)) element.addEventListener("emoji-click", (event: any) => onClick(event.detail as Emoji))
}) })
</script> </script>
+2 -2
View File
@@ -84,7 +84,7 @@
document: Document, document: Document,
earth: Earth, earth: Earth,
pen: Pen, pen: Pen,
'pen-new-square': PenNewSquare, "pen-new-square": PenNewSquare,
"headphones-round": HeadphonesRound, "headphones-round": HeadphonesRound,
"add-circle": AddCircle, "add-circle": AddCircle,
"alt-arrow-down": AltArrowDown, "alt-arrow-down": AltArrowDown,
@@ -112,7 +112,7 @@
hashtag: Hashtag, hashtag: Hashtag,
"hand-pills": HandPills, "hand-pills": HandPills,
"home-smile": HomeSmile, "home-smile": HomeSmile,
"inbox": Inbox, inbox: Inbox,
"info-circle": InfoCircle, "info-circle": InfoCircle,
"info-square": InfoSquare, "info-square": InfoSquare,
key: Key, key: Key,
+1 -1
View File
@@ -5,6 +5,6 @@
export let props = {} export let props = {}
</script> </script>
<div class="modal-box bg-alt overflow-visible" transition:fly={{duration: 100}}> <div class="bg-alt modal-box overflow-visible" transition:fly={{duration: 100}}>
<svelte:component this={component} {...props} /> <svelte:component this={component} {...props} />
</div> </div>
+1 -1
View File
@@ -1,3 +1,3 @@
<div class="flex w-60 flex-shrink-0 flex-col gap-1 bg-base-300 max-h-screen"> <div class="flex max-h-screen w-60 flex-shrink-0 flex-col gap-1 bg-base-300">
<slot /> <slot />
</div> </div>
+10 -8
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {onMount} from 'svelte' import {onMount} from "svelte"
import type {SvelteComponent, ComponentType, ComponentProps} from 'svelte' import type {SvelteComponent, ComponentType, ComponentProps} from "svelte"
import tippy, {type Instance, type Props} from "tippy.js" import tippy, {type Instance, type Props} from "tippy.js"
export let component: ComponentType export let component: ComponentType
@@ -14,15 +14,17 @@
$: instance?.$set(props) $: instance?.$set(props)
onMount(() => { onMount(() => {
const target = document.createElement("div") if (element) {
const target = document.createElement("div")
popover = tippy(element, {content: target, ...params}) popover = tippy(element, {content: target, ...params})
instance = new component({target, props}) instance = new component({target, props})
return () => { return () => {
popover?.destroy() popover?.destroy()
instance?.$destroy() instance?.$destroy()
}
} }
}) })
</script> </script>
+2 -2
View File
@@ -14,7 +14,7 @@
</style> </style>
<script lang="ts"> <script lang="ts">
import {clamp} from '@welshman/lib' import {clamp} from "@welshman/lib"
export let score export let score
export let max = 100 export let max = 100
@@ -26,7 +26,7 @@
$: normalizedScore = clamp([0, max], score) / max $: normalizedScore = clamp([0, max], score) / max
$: dashOffset = 100 - 44 * normalizedScore $: dashOffset = 100 - 44 * normalizedScore
$: style = `transform: rotate(${135 - normalizedScore * 180}deg)` $: style = `transform: rotate(${135 - normalizedScore * 180}deg)`
$: stroke = active ? 'var(--primary)' : 'var(--base-content)' $: stroke = active ? "var(--primary)" : "var(--base-content)"
</script> </script>
<div class="relative h-[14px] w-[14px]"> <div class="relative h-[14px] w-[14px]">
+9 -4
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import cx from 'classnames' import cx from "classnames"
import type {NodeViewProps} from "@tiptap/core" import type {NodeViewProps} from "@tiptap/core"
import {NodeViewWrapper} from "svelte-tiptap" import {NodeViewWrapper} from "svelte-tiptap"
import {ellipsize, nthEq} from "@welshman/lib" import {ellipsize, nthEq} from "@welshman/lib"
@@ -11,9 +11,11 @@
export let selected: NodeViewProps["selected"] export let selected: NodeViewProps["selected"]
const displayEvent = (e: TrustedEvent) => { const displayEvent = (e: TrustedEvent) => {
const content = e?.tags.find(nthEq(0, 'alt'))?.[1] || e?.content const content = e?.tags.find(nthEq(0, "alt"))?.[1] || e?.content
return content.length > 1 ? ellipsize(content, 30) : fromNostrURI(nevent || naddr).slice(0, 16) + "..." return content.length > 1
? ellipsize(content, 30)
: fromNostrURI(nevent || naddr).slice(0, 16) + "..."
} }
$: ({identifier, pubkey, kind, id, relays = [], nevent, naddr} = node.attrs) $: ({identifier, pubkey, kind, id, relays = [], nevent, naddr} = node.attrs)
@@ -21,7 +23,10 @@
</script> </script>
<NodeViewWrapper class="inline"> <NodeViewWrapper class="inline">
<Link external href={entityLink(node.attrs.nevent)} class={cx("link-content", {"link-content-selected": selected})}> <Link
external
href={entityLink(node.attrs.nevent)}
class={cx("link-content", {"link-content-selected": selected})}>
{displayEvent($event)} {displayEvent($event)}
</Link> </Link>
</NodeViewWrapper> </NodeViewWrapper>
+1 -1
View File
@@ -5,7 +5,7 @@
import {displayProfile} from "@welshman/util" import {displayProfile} from "@welshman/util"
import {deriveProfile} from "@welshman/app" import {deriveProfile} from "@welshman/app"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import {entityLink} from '@app/state' import {entityLink} from "@app/state"
export let node: NodeViewProps["node"] export let node: NodeViewProps["node"]
export let selected: NodeViewProps["selected"] export let selected: NodeViewProps["selected"]
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import Profile from '@app/components/Profile.svelte' import Profile from "@app/components/Profile.svelte"
export let value export let value
</script> </script>
+3 -3
View File
@@ -4,7 +4,7 @@
import {throttle} from "throttle-debounce" import {throttle} from "throttle-debounce"
import {fly, slide} from "svelte/transition" import {fly, slide} from "svelte/transition"
import {clamp} from "@welshman/lib" import {clamp} from "@welshman/lib"
import Icon from '@lib/components/Icon.svelte' import Icon from "@lib/components/Icon.svelte"
import {theme} from "@app/theme" import {theme} from "@app/theme"
export let term export let term
@@ -77,11 +77,11 @@
{/if} {/if}
{#each items as value, i (value)} {#each items as value, i (value)}
<button <button
class="white-space-nowrap block w-full min-w-0 cursor-pointer overflow-x-hidden text-ellipsis px-4 py-2 text-left transition-all hover:brightness-150 flex items-center" class="white-space-nowrap block flex w-full min-w-0 cursor-pointer items-center overflow-x-hidden text-ellipsis px-4 py-2 text-left transition-all hover:brightness-150"
on:mousedown|preventDefault on:mousedown|preventDefault
on:click|preventDefault={() => select(value)}> on:click|preventDefault={() => select(value)}>
{#if index === i} {#if index === i}
<div transition:slide|local={{axis: 'x'}} class="pr-2"> <div transition:slide|local={{axis: "x"}} class="pr-2">
<Icon icon="alt-arrow-right" /> <Icon icon="alt-arrow-right" />
</div> </div>
{/if} {/if}
+26 -18
View File
@@ -56,29 +56,37 @@ export const findMarks = (type: string, json: JSONContent) => {
export const getEditorTags = (editor: Editor) => { export const getEditorTags = (editor: Editor) => {
const json = editor.getJSON() const json = editor.getJSON()
const topicTags = findMarks("tag", json).map( const topicTags = findMarks("tag", json).map(({attrs}: any) => [
({attrs}: any) => ["t", attrs.tag.replace(/^#/, '').toLowerCase()], "t",
) attrs.tag.replace(/^#/, "").toLowerCase(),
])
const naddrTags = findNodes("naddr", json).map( const naddrTags = findNodes("naddr", json).map(({kind, pubkey, identifier, relays}: any) => {
({kind, pubkey, identifier, relays}: any) => { const address = new Address(kind, pubkey, identifier).toString()
const address = new Address(kind, pubkey, identifier).toString()
return ["q", address, choice(relays) || "", pubkey] return ["q", address, choice(relays) || "", pubkey]
}, })
)
const neventTags = findNodes("nevent", json).map( const neventTags = findNodes("nevent", json).map(({id, author, relays}: any) => [
({id, author, relays}: any) => ["q", id, choice(relays) || "", author || ""], "q",
) id,
choice(relays) || "",
author || "",
])
const mentionTags = findNodes("nprofile", json).map( const mentionTags = findNodes("nprofile", json).map(({pubkey, relays}: any) => [
({pubkey, relays}: any) => ["p", pubkey, choice(relays) || "", ""], "p",
) pubkey,
choice(relays) || "",
"",
])
const imetaTags = findNodes("image", json).map( const imetaTags = findNodes("image", json).map(({src, sha256}: any) => [
({src, sha256}: any) => ["imeta", `url ${src}`, `x ${sha256}`, `ox ${sha256}`], "imeta",
) `url ${src}`,
`x ${sha256}`,
`ox ${sha256}`,
])
return [...topicTags, ...naddrTags, ...neventTags, ...mentionTags, ...imetaTags] return [...topicTags, ...naddrTags, ...neventTags, ...mentionTags, ...imetaTags]
} }
+3 -1
View File
@@ -107,7 +107,9 @@
<div data-theme={$theme}> <div data-theme={$theme}>
<div class="flex h-screen overflow-hidden"> <div class="flex h-screen overflow-hidden">
<PrimaryNav /> <PrimaryNav />
<slot /> {#key $page.params}
<slot />
{/key}
</div> </div>
<dialog bind:this={dialog} class="modal modal-bottom !z-modal sm:modal-middle"> <dialog bind:this={dialog} class="modal modal-bottom !z-modal sm:modal-middle">
{#if prev && !prev.options?.drawer} {#if prev && !prev.options?.drawer}
+1 -1
View File
@@ -17,7 +17,7 @@
onMount(() => { onMount(() => {
const sub = discoverRelays() const sub = discoverRelays()
const scroller = createScroller({ const scroller = createScroller({
element: element.closest('.max-h-screen')!, element: element.closest(".max-h-screen")!,
onScroll: () => { onScroll: () => {
limit += 20 limit += 20
}, },
+2 -3
View File
@@ -19,10 +19,9 @@
</label> </label>
<div class="grid grid-cols-2 gap-4 md:grid-cols-2"> <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="card2 bg-alt shadow-xl flex flex-col justify-center gap-4" data-theme={name}> <div class="card2 bg-alt flex flex-col justify-center gap-4 shadow-xl" data-theme={name}>
<h2 class="card2 bg-alt text-center capitalize">{name}</h2> <h2 class="card2 bg-alt text-center capitalize">{name}</h2>
<button class="btn btn-primary w-full" on:click={() => theme.set(name)} <button class="btn btn-primary w-full" on:click={() => theme.set(name)}>Use Theme</button>
>Use Theme</button>
</div> </div>
{/each} {/each}
</div> </div>
+18 -14
View File
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import {onMount} from 'svelte' import {onMount} from "svelte"
import {ellipsize, ctx, ago, remove} from '@welshman/lib' import {ctx, ago, remove} from "@welshman/lib"
import {WRAP} from '@welshman/util' import {WRAP} from "@welshman/util"
import {pubkey, subscribe} from '@welshman/app' import {pubkey, subscribe} from "@welshman/app"
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 Page from "@lib/components/Page.svelte" import Page from "@lib/components/Page.svelte"
@@ -16,8 +16,8 @@
import ProfileCircle from "@app/components/ProfileCircle.svelte" import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ProfileCircles from "@app/components/ProfileCircles.svelte" import ProfileCircles from "@app/components/ProfileCircles.svelte"
import ChatStart from "@app/components/ChatStart.svelte" import ChatStart from "@app/components/ChatStart.svelte"
import {chatSearch, pullConservatively} from '@app/state' import {chatSearch, pullConservatively} from "@app/state"
import {pushModal} from '@app/modal' import {pushModal} from "@app/modal"
const startChat = () => pushModal(ChatStart) const startChat = () => pushModal(ChatStart)
@@ -26,7 +26,7 @@
$: chats = $chatSearch.searchOptions(term).filter(c => c.pubkeys.length > 1) $: chats = $chatSearch.searchOptions(term).filter(c => c.pubkeys.length > 1)
onMount(() => { onMount(() => {
const filter = {kinds: [WRAP], '#p': [$pubkey!]} const filter = {kinds: [WRAP], "#p": [$pubkey!]}
const sub = subscribe({filters: [{...filter, since: ago(30)}]}) const sub = subscribe({filters: [{...filter, since: ago(30)}]})
pullConservatively({ pullConservatively({
@@ -64,29 +64,33 @@
</SecondaryNavHeader> </SecondaryNavHeader>
</div> </div>
</SecondaryNavSection> </SecondaryNavSection>
<label class="input input-bordered input-sm flex items-center gap-2 mx-6 -mt-4" in:fly={{delay: 200}}> <label
class="input input-sm input-bordered mx-6 -mt-4 flex items-center gap-2"
in:fly={{delay: 200}}>
<Icon icon="magnifer" /> <Icon icon="magnifer" />
<input bind:value={term} class="grow" type="text" /> <input bind:value={term} class="grow" type="text" />
</label> </label>
<div class="overflow-auto"> <div class="overflow-auto">
{#each chats as {id, pubkeys, messages}, i (id)} {#each chats as { id, pubkeys, messages }, i (id)}
{@const message = messages[0]} {@const message = messages[0]}
{@const others = remove($pubkey, pubkeys)} {@const others = remove($pubkey, pubkeys)}
<div class="px-6 py-2 border-t border-base-100 border-solid hover:bg-base-100 transition-colors cursor-pointer"> <div
class="cursor-pointer border-t border-solid border-base-100 px-6 py-2 transition-colors hover:bg-base-100">
<Link class="flex flex-col justify-start gap-1" href="/home/{id}"> <Link class="flex flex-col justify-start gap-1" href="/home/{id}">
<div class="flex gap-2 items-center"> <div class="flex items-center gap-2">
{#if others.length === 1} {#if others.length === 1}
<ProfileCircle pubkey={others[0]} size={5} /> <ProfileCircle pubkey={others[0]} size={5} />
<Name pubkey={others[0]} /> <Name pubkey={others[0]} />
{:else} {:else}
<ProfileCircles pubkeys={others} size={5} /> <ProfileCircles pubkeys={others} size={5} />
<p class="whitespace-nowrap overflow-hidden text-ellipsis"> <p class="overflow-hidden text-ellipsis whitespace-nowrap">
<Name pubkey={others[0]} /> <Name pubkey={others[0]} />
and {others.length - 1} {others.length > 2 ? 'others' : 'other'} and {others.length - 1}
{others.length > 2 ? "others" : "other"}
</p> </p>
{/if} {/if}
</div> </div>
<p class="text-sm whitespace-nowrap overflow-hidden text-ellipsis"> <p class="overflow-hidden text-ellipsis whitespace-nowrap text-sm">
{message.content} {message.content}
</p> </p>
</Link> </Link>
+3 -2
View File
@@ -8,9 +8,10 @@
const browseSpaces = () => goto("/discover") const browseSpaces = () => goto("/discover")
const leaveFeedback = () => goto("/home/97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322") const leaveFeedback = () =>
goto("/home/97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322")
const donate = () => window.open('https://geyser.fund/project/flotilla') const donate = () => window.open("https://geyser.fund/project/flotilla")
</script> </script>
<div class="hero min-h-screen bg-base-200"> <div class="hero min-h-screen bg-base-200">
+136
View File
@@ -0,0 +1,136 @@
<script lang="ts" context="module">
type Element = {
id: string
type: "date" | "note"
value: string | TrustedEvent
showPubkey: boolean
}
</script>
<script lang="ts">
import {page} from "$app/stores"
import {ctx, uniq, sortBy, remove} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {createEvent, DIRECT_MESSAGE} from "@welshman/util"
import {Nip59} from "@welshman/signer"
import {
pubkey,
signer,
formatTimestampAsDate,
tagPubkey,
makeThunk,
publishThunk,
} from "@welshman/app"
import {fly} from "@lib/transition"
import Spinner from "@lib/components/Spinner.svelte"
import Divider from "@lib/components/Divider.svelte"
import Name from "@app/components/Name.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ProfileCircles from "@app/components/ProfileCircles.svelte"
import ChatMessage from "@app/components/ChatMessage.svelte"
import ChatCompose from "@app/components/ChannelCompose.svelte"
import {deriveChat, splitChatId} from "@app/state"
const {chat: id} = $page.params
const chat = deriveChat(id)
const pubkeys = splitChatId(id)
const others = remove($pubkey, pubkeys)
const assertEvent = (e: any) => e as TrustedEvent
const onSubmit = async ({content, ...params}: EventContent) => {
const tags = [...params.tags, ...pubkeys.map(pubkey => tagPubkey(pubkey))]
const template = createEvent(DIRECT_MESSAGE, {content, tags})
const nip59 = Nip59.fromSigner($signer!)
for (const recipient of uniq(pubkeys)) {
const rumor = await nip59.wrap(recipient, template)
publishThunk(
makeThunk({
event: rumor.wrap,
relays: ctx.app.router.PublishMessage(recipient).getUrls(),
}),
)
}
}
let loading = true
let elements: Element[] = []
$: {
elements = []
let previousDate
let previousPubkey
for (const event of sortBy(e => e.created_at, $chat?.messages || [])) {
const {id, pubkey, created_at} = event
const date = formatTimestampAsDate(created_at)
if (date !== previousDate) {
elements.push({type: "date", value: date, id: date, showPubkey: false})
}
elements.push({
id,
type: "note",
value: event,
showPubkey: date !== previousDate || previousPubkey !== pubkey,
})
previousDate = date
previousPubkey = pubkey
}
elements.reverse()
}
setTimeout(() => {
loading = false
}, 3000)
</script>
<div class="relative flex h-screen flex-col">
<div class="relative z-feature mx-2 rounded-xl pt-4">
<div
class="flex min-h-12 items-center justify-between gap-4 rounded-xl bg-base-100 px-4 shadow-xl">
<div class="flex items-center gap-2">
{#if others.length === 1}
<ProfileCircle pubkey={others[0]} size={5} />
<Name pubkey={others[0]} />
{:else}
<ProfileCircles pubkeys={others} size={5} />
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
<Name pubkey={others[0]} />
and {others.length - 1}
{others.length > 2 ? "others" : "other"}
</p>
{/if}
</div>
</div>
</div>
<div class="-mt-2 flex flex-grow flex-col-reverse overflow-auto py-2">
{#each elements as { type, id, value, showPubkey } (id)}
{#if type === "date"}
<Divider>{value}</Divider>
{:else}
<div in:fly>
<ChatMessage event={assertEvent(value)} {pubkeys} {showPubkey} />
</div>
{/if}
{/each}
<p class="flex h-10 items-center justify-center py-20">
<Spinner {loading}>
{#if loading}
Looking for messages...
{:else}
End of message history
{/if}
</Spinner>
</p>
</div>
<div class="shadow-top-xl border-t border-solid border-base-100 bg-base-100">
<ChatCompose {onSubmit} />
</div>
</div>
+31 -33
View File
@@ -1,48 +1,47 @@
<script lang="ts"> <script lang="ts">
import {onMount} from 'svelte' import {onMount} from "svelte"
import {derived} from 'svelte/store' import {derived} from "svelte/store"
import {createScroller} from '@lib/html' import {createScroller} from "@lib/html"
import {shuffle, sortBy, sleep, ago, DAY, HOUR, pushToMapKey} from '@welshman/lib' import {sortBy, sleep, ago, DAY, HOUR, pushToMapKey} from "@welshman/lib"
import {getListValues, getAncestorTagValues, NOTE, REACTION} from '@welshman/util' import {
import type {TrustedEvent} from '@welshman/util' getListTags,
import {deriveEvents} from '@welshman/store' getPubkeyTagValues,
import {profileSearch, repository, userFollows, load} from '@welshman/app' getAncestorTagValues,
import Spinner from '@lib/components/Spinner.svelte' NOTE,
import NoteCard from '@app/components/NoteCard.svelte' REACTION,
import Content from '@app/components/Content.svelte' } from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {deriveEvents} from "@welshman/store"
import {repository, userFollows, load} from "@welshman/app"
import Spinner from "@lib/components/Spinner.svelte"
import NoteCard from "@app/components/NoteCard.svelte"
import Content from "@app/components/Content.svelte"
let element: Element let element: Element
let loading = sleep(3000)
let events: TrustedEvent[] = [] let events: TrustedEvent[] = []
const since = ago(DAY) const since = ago(DAY)
const authors = getListValues("p", $userFollows) const loading = sleep(3000)
const authors = getPubkeyTagValues(getListTags($userFollows))
const notesFilter = {kinds: [NOTE], authors, since} const notesFilter = {kinds: [NOTE], authors, since}
const notes = deriveEvents(repository, {filters: [notesFilter]}) const notes = deriveEvents(repository, {filters: [notesFilter]})
const reactionsFilter = {kinds: [REACTION], '#p': authors, since} const reactionsFilter = {kinds: [REACTION], "#p": authors, since}
const reactions = deriveEvents(repository, {filters: [reactionsFilter]}) const reactions = deriveEvents(repository, {filters: [reactionsFilter]})
const reactionsByParent = derived( const reactionsByParent = derived(reactions, $reactions => {
reactions, const $reactionsByParent = new Map<string, TrustedEvent[]>()
$reactions => {
const $reactionsByParent = new Map<string, TrustedEvent[]>()
for (const event of $reactions) { for (const event of $reactions) {
const [parentId] = getAncestorTagValues(event.tags).replies const [parentId] = getAncestorTagValues(event.tags).replies
if (parentId) { if (parentId) {
pushToMapKey($reactionsByParent, parentId, event) pushToMapKey($reactionsByParent, parentId, event)
}
} }
return $reactionsByParent
} }
)
const isLike = (e: TrustedEvent) => return $reactionsByParent
e.kind === REACTION && ["+", ""].includes(e.content) })
const isReplyOf = (e: TrustedEvent, p: TrustedEvent) => const isLike = (e: TrustedEvent) => e.kind === REACTION && ["+", ""].includes(e.content)
getAncestorTagValues(e.tags).replies.includes(e.id)
const scoreEvent = (e: TrustedEvent) => { const scoreEvent = (e: TrustedEvent) => {
const thisReactions = $reactionsByParent.get(e.id) || [] const thisReactions = $reactionsByParent.get(e.id) || []
@@ -57,12 +56,12 @@
load({filters: [notesFilter, reactionsFilter]}) load({filters: [notesFilter, reactionsFilter]})
const scroller = createScroller({ const scroller = createScroller({
element: element.closest('.max-h-screen')!, element: element.closest(".max-h-screen")!,
onScroll: () => { onScroll: () => {
const seen = new Set(events.map(e => e.id)) const seen = new Set(events.map(e => e.id))
const eligible = sortBy( const eligible = sortBy(
scoreEvent, scoreEvent,
$notes.filter(e => !seen.has(e.id) && getAncestorTagValues(e.tags).replies.length === 0) $notes.filter(e => !seen.has(e.id) && getAncestorTagValues(e.tags).replies.length === 0),
) )
events = [...events, ...eligible.slice(0, 10)] events = [...events, ...eligible.slice(0, 10)]
@@ -73,7 +72,6 @@
}) })
</script> </script>
<div class="content column gap-4" bind:this={element}> <div class="content column gap-4" bind:this={element}>
{#await loading} {#await loading}
<div class="center my-20"> <div class="center my-20">
+10 -13
View File
@@ -1,25 +1,23 @@
<script lang="ts"> <script lang="ts">
import {onMount} from 'svelte' import {onMount} from "svelte"
import {createScroller} from '@lib/html' import {createScroller} from "@lib/html"
import Icon from '@lib/components/Icon.svelte' import Icon from "@lib/components/Icon.svelte"
import {shuffle} from '@welshman/lib' import {shuffle} from "@welshman/lib"
import {getListValues} from '@welshman/util' import {getPubkeyTagValues, getListTags} from "@welshman/util"
import {profileSearch, userFollows} from '@welshman/app' import {profileSearch, userFollows} from "@welshman/app"
import PeopleItem from '@app/components/PeopleItem.svelte' import PeopleItem from "@app/components/PeopleItem.svelte"
const defaultPubkeys = shuffle(getListValues("p", $userFollows)) const defaultPubkeys = shuffle(getPubkeyTagValues(getListTags($userFollows)))
let term = "" let term = ""
let limit = 10 let limit = 10
let element: Element let element: Element
$: pubkeys = term $: pubkeys = term ? $profileSearch.searchValues(term) : defaultPubkeys
? $profileSearch.searchValues(term)
: defaultPubkeys
onMount(() => { onMount(() => {
const scroller = createScroller({ const scroller = createScroller({
element: element.closest('.max-h-screen')!, element: element.closest(".max-h-screen")!,
onScroll: () => { onScroll: () => {
limit += 10 limit += 10
}, },
@@ -29,7 +27,6 @@
}) })
</script> </script>
<div class="content column gap-4" bind:this={element}> <div class="content column gap-4" bind:this={element}>
<h1 class="superheading mt-20">People</h1> <h1 class="superheading mt-20">People</h1>
<p class="text-center">Get the latest from people in your network</p> <p class="text-center">Get the latest from people in your network</p>
+13 -26
View File
@@ -1,26 +1,17 @@
<script lang="ts"> <script lang="ts">
import {nip19} from 'nostr-tools' import {always} from "@welshman/lib"
import type {SvelteComponent} from 'svelte' import {getListTags, getPubkeyTagValues, MUTES} from "@welshman/util"
import tippy, {type Instance} from "tippy.js" import {userMutes, tagPubkey} from "@welshman/app"
import {append, always, remove, uniq} from '@welshman/lib' import Field from "@lib/components/Field.svelte"
import {getListValues, MUTES} from '@welshman/util' import Button from "@lib/components/Button.svelte"
import {userMutes, profileSearch, tagPubkey} from '@welshman/app' import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
import Icon from '@lib/components/Icon.svelte' import {updateList} from "@app/commands"
import Field from '@lib/components/Field.svelte' import {pushToast} from "@app/toast"
import Tippy from '@lib/components/Tippy.svelte'
import Link from '@lib/components/Link.svelte'
import Button from '@lib/components/Button.svelte'
import Suggestions from '@lib/editor/Suggestions.svelte'
import SuggestionProfile from '@lib/editor/SuggestionProfile.svelte'
import ProfileMultiSelect from '@app/components/ProfileMultiSelect.svelte'
import {entityLink} from '@app/state'
import {updateList} from '@app/commands'
import {pushToast} from '@app/toast'
let mutedPubkeys = getListValues("p", $userMutes) let mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
const reset = () => { const reset = () => {
mutedPubkeys = getListValues("p", $userMutes) mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
} }
const onSubmit = async () => { const onSubmit = async () => {
@@ -38,13 +29,9 @@
<ProfileMultiSelect bind:value={mutedPubkeys} /> <ProfileMultiSelect bind:value={mutedPubkeys} />
</div> </div>
</Field> </Field>
<div class="flex flex-row items-center justify-between gap-4 mt-4"> <div class="mt-4 flex flex-row items-center justify-between gap-4">
<Button class="btn btn-neutral" on:click={reset}> <Button class="btn btn-neutral" on:click={reset}>Discard Changes</Button>
Discard Changes <Button type="submit" class="btn btn-primary">Save Changes</Button>
</Button>
<Button type="submit" class="btn btn-primary">
Save Changes
</Button>
</div> </div>
</div> </div>
</form> </form>
+4 -2
View File
@@ -2,7 +2,7 @@
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 {entityLink} from '@app/state' import {entityLink} from "@app/state"
const nprofile = const nprofile =
"nprofile1qqsf03c2gsmx5ef4c9zmxvlew04gdh7u94afnknp33qvv3c94kvwxgspz4mhxue69uhhyetvv9ujuerpd46hxtnfduhsz9rhwden5te0wfjkcctev93xcefwdaexwtcpzdmhxue69uhhqatjwpkx2urpvuhx2ue0vamm57" "nprofile1qqsf03c2gsmx5ef4c9zmxvlew04gdh7u94afnknp33qvv3c94kvwxgspz4mhxue69uhhyetvv9ujuerpd46hxtnfduhsz9rhwden5te0wfjkcctev93xcefwdaexwtcpzdmhxue69uhhqatjwpkx2urpvuhx2ue0vamm57"
@@ -22,7 +22,9 @@
<div class="card2 bg-alt flex flex-col gap-2 text-center shadow-2xl"> <div class="card2 bg-alt flex flex-col gap-2 text-center shadow-2xl">
<h3 class="text-2xl sm:h-12">Get in touch</h3> <h3 class="text-2xl sm:h-12">Get in touch</h3>
<p class="sm:h-16">Having problems? Let us know by filing an issue.</p> <p class="sm:h-16">Having problems? Let us know by filing an issue.</p>
<Link class="btn btn-primary" href="/home/97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322"> <Link
class="btn btn-primary"
href="/home/97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322">
Open an Issue Open an Issue
</Link> </Link>
</div> </div>
+33 -27
View File
@@ -1,26 +1,31 @@
<script lang="ts"> <script lang="ts">
import {nip19} from 'nostr-tools'
import {last, ctx} from "@welshman/lib" import {last, ctx} from "@welshman/lib"
import {PROFILE, createEvent, displayPubkey, displayProfile, makeProfile, editProfile, createProfile, isPublishedProfile} from "@welshman/util" import {
import {pubkey, getProfile, displayHandle, makeThunk, publishThunk} from "@welshman/app" createEvent,
import {slide} from '@lib/transition' displayPubkey,
displayProfile,
makeProfile,
editProfile,
createProfile,
isPublishedProfile,
} from "@welshman/util"
import {pubkey, profilesByPubkey, makeThunk, publishThunk} from "@welshman/app"
import {slide} from "@lib/transition"
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 Avatar from "@lib/components/Avatar.svelte" import Avatar from "@lib/components/Avatar.svelte"
import InputProfilePicture from "@lib/components/InputProfilePicture.svelte" import InputProfilePicture from "@lib/components/InputProfilePicture.svelte"
import Content from '@app/components/Content.svelte' import Content from "@app/components/Content.svelte"
import InfoHandle from '@app/components/InfoHandle.svelte' import InfoHandle from "@app/components/InfoHandle.svelte"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
const npub = nip19.npubEncode($pubkey!)
const pubkeyDisplay = displayPubkey($pubkey!) const pubkeyDisplay = displayPubkey($pubkey!)
const displayNip05 = (nip05: string) => const displayNip05 = (nip05: string) => (nip05?.startsWith("_@") ? last(nip05.split("@")) : nip05)
nip05?.startsWith("_@") ? last(nip05.split("@")) : nip05
const cloneProfile = () => ({...(getProfile($pubkey!) || makeProfile())}) const cloneProfile = () => ({...($profilesByPubkey.get($pubkey!) || makeProfile())})
const toggleEdit = () => { const toggleEdit = () => {
editing = !editing editing = !editing
@@ -52,23 +57,23 @@
<div class="content column gap-4"> <div class="content column gap-4">
<div class="card2 bg-alt shadow-xl"> <div class="card2 bg-alt shadow-xl">
<div class="flex gap-2 justify-between"> <div class="flex justify-between gap-2">
<div class="flex gap-3 max-w-full"> <div class="flex max-w-full gap-3">
<div class="py-1"> <div class="py-1">
<Avatar src={profile?.picture} size={10} /> <Avatar src={profile?.picture} size={10} />
</div> </div>
<div class="flex flex-col min-w-0"> <div class="flex min-w-0 flex-col">
<div class="flex gap-2 items-center"> <div class="flex items-center gap-2">
<div class="text-bold text-ellipsis overflow-hidden"> <div class="text-bold overflow-hidden text-ellipsis">
{displayProfile(profile, pubkeyDisplay)} {displayProfile(profile, pubkeyDisplay)}
</div> </div>
</div> </div>
<div class="text-sm opacity-75 text-ellipsis overflow-hidden"> <div class="overflow-hidden text-ellipsis text-sm opacity-75">
{profile?.nip05 ? displayNip05(profile.nip05) : pubkeyDisplay} {profile?.nip05 ? displayNip05(profile.nip05) : pubkeyDisplay}
</div> </div>
</div> </div>
</div> </div>
<Button class="btn btn-neutral btn-circle w-12 h-12 center -mt-4 -mr-4" on:click={toggleEdit}> <Button class="center btn btn-circle btn-neutral -mr-4 -mt-4 h-12 w-12" on:click={toggleEdit}>
<Icon icon="pen-new-square" /> <Icon icon="pen-new-square" />
</Button> </Button>
</div> </div>
@@ -90,7 +95,11 @@
</Field> </Field>
<Field> <Field>
<p slot="label">About You</p> <p slot="label">About You</p>
<textarea class="textarea textarea-bordered leading-4" rows="3" bind:value={profile.about} slot="input" /> <textarea
class="textarea textarea-bordered leading-4"
rows="3"
bind:value={profile.about}
slot="input" />
</Field> </Field>
<Field> <Field>
<p slot="label">Nostr Address</p> <p slot="label">Nostr Address</p>
@@ -99,16 +108,13 @@
<input bind:value={profile.nip05} class="grow" type="text" /> <input bind:value={profile.nip05} class="grow" type="text" />
</label> </label>
<p slot="info"> <p slot="info">
<Button class="link" on:click={() => pushModal(InfoHandle)}>What is a nostr address?</Button> <Button class="link" on:click={() => pushModal(InfoHandle)}
>What is a nostr address?</Button>
</p> </p>
</Field> </Field>
<div class="flex flex-row items-center justify-between gap-4 mt-4"> <div class="mt-4 flex flex-row items-center justify-between gap-4">
<Button class="btn btn-neutral" on:click={stopEdit}> <Button class="btn btn-neutral" on:click={stopEdit}>Discard Changes</Button>
Discard Changes <Button type="submit" class="btn btn-primary">Save Changes</Button>
</Button>
<Button type="submit" class="btn btn-primary">
Save Changes
</Button>
</div> </div>
</form> </form>
{/if} {/if}
+23 -19
View File
@@ -1,14 +1,19 @@
<script lang="ts"> <script lang="ts">
import {derived} from "svelte/store" import {derived} from "svelte/store"
import type {Readable} from 'svelte/store' import {
import {relaySearch, getRelayUrls, userRelaySelections, userInboxRelaySelections, getReadRelayUrls, getWriteRelayUrls} from "@welshman/app" getRelayUrls,
userRelaySelections,
userInboxRelaySelections,
getReadRelayUrls,
getWriteRelayUrls,
} from "@welshman/app"
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 Collapse from "@lib/components/Collapse.svelte" import Collapse from "@lib/components/Collapse.svelte"
import RelayItem from "@app/components/RelayItem.svelte" import RelayItem from "@app/components/RelayItem.svelte"
import RelayAdd from "@app/components/RelayAdd.svelte" import RelayAdd from "@app/components/RelayAdd.svelte"
import {pushModal} from '@app/modal' import {pushModal} from "@app/modal"
import {setRelayPolicy, setInboxRelayPolicy} from '@app/commands' import {setRelayPolicy, setInboxRelayPolicy} from "@app/commands"
const readRelayUrls = derived(userRelaySelections, getReadRelayUrls) const readRelayUrls = derived(userRelaySelections, getReadRelayUrls)
const writeRelayUrls = derived(userRelaySelections, getWriteRelayUrls) const writeRelayUrls = derived(userRelaySelections, getWriteRelayUrls)
@@ -41,20 +46,20 @@
<div class="content column gap-4"> <div class="content column gap-4">
<Collapse class="card2 bg-alt column gap-4"> <Collapse class="card2 bg-alt column gap-4">
<h2 slot="title" class="text-xl flex items-center gap-3"> <h2 slot="title" class="flex items-center gap-3 text-xl">
<Icon icon="earth" /> <Icon icon="earth" />
Broadcast Relays Broadcast Relays
</h2> </h2>
<p slot="description" class="text-sm"> <p slot="description" class="text-sm">
These relays will be advertised on your profile as places where you send your public These relays will be advertised on your profile as places where you send your public notes. Be
notes. Be sure to select relays that will accept your notes, and which will let people sure to select relays that will accept your notes, and which will let people who follow you
who follow you read them. read them.
</p> </p>
<div class="column gap-2"> <div class="column gap-2">
{#each $writeRelayUrls.sort() as url (url)} {#each $writeRelayUrls.sort() as url (url)}
<RelayItem {url}> <RelayItem {url}>
<Button <Button
class="flex items-center tooltip" class="tooltip flex items-center"
data-tip="Stop using this relay" data-tip="Stop using this relay"
on:click={() => removeWriteRelay(url)}> on:click={() => removeWriteRelay(url)}>
<Icon icon="close-circle" /> <Icon icon="close-circle" />
@@ -70,20 +75,19 @@
</div> </div>
</Collapse> </Collapse>
<Collapse class="card2 bg-alt column gap-4"> <Collapse class="card2 bg-alt column gap-4">
<h2 slot="title" class="text-xl flex items-center gap-3"> <h2 slot="title" class="flex items-center gap-3 text-xl">
<Icon icon="inbox" /> <Icon icon="inbox" />
Inbox Relays Inbox Relays
</h2> </h2>
<p slot="description" class="text-sm"> <p slot="description" class="text-sm">
These relays will be advertised on your profile as places where other people should These relays will be advertised on your profile as places where other people should send notes
send notes intended for you. Be sure to select relays that will accept notes that intended for you. Be sure to select relays that will accept notes that tag you.
tag you.
</p> </p>
<div class="column gap-2"> <div class="column gap-2">
{#each $readRelayUrls.sort() as url (url)} {#each $readRelayUrls.sort() as url (url)}
<RelayItem {url}> <RelayItem {url}>
<Button <Button
class="flex items-center tooltip" class="tooltip flex items-center"
data-tip="Stop using this relay" data-tip="Stop using this relay"
on:click={() => removeReadRelay(url)}> on:click={() => removeReadRelay(url)}>
<Icon icon="close-circle" /> <Icon icon="close-circle" />
@@ -99,20 +103,20 @@
</div> </div>
</Collapse> </Collapse>
<Collapse class="card2 bg-alt column gap-4"> <Collapse class="card2 bg-alt column gap-4">
<h2 slot="title" class="text-xl flex items-center gap-3"> <h2 slot="title" class="flex items-center gap-3 text-xl">
<Icon icon="mailbox" /> <Icon icon="mailbox" />
Messaging Relays Messaging Relays
</h2> </h2>
<p slot="description" class="text-sm"> <p slot="description" class="text-sm">
These relays will be advertised on your profile as places you use to send and These relays will be advertised on your profile as places you use to send and receive direct
receive direct messages. Be sure to select relays that will accept your messages messages. Be sure to select relays that will accept your messages and messages from people
and messages from people you'd like to be in contact with. you'd like to be in contact with.
</p> </p>
<div class="column gap-2"> <div class="column gap-2">
{#each $inboxRelayUrls.sort() as url (url)} {#each $inboxRelayUrls.sort() as url (url)}
<RelayItem {url}> <RelayItem {url}>
<Button <Button
class="flex items-center tooltip" class="tooltip flex items-center"
data-tip="Stop using this relay" data-tip="Stop using this relay"
on:click={() => removeInboxRelay(url)}> on:click={() => removeInboxRelay(url)}>
<Icon icon="close-circle" /> <Icon icon="close-circle" />
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {page} from "$app/stores" import {page} from "$app/stores"
import {sort, now} from "@welshman/lib" import {sort} from "@welshman/lib"
import {displayRelayUrl, REACTION, NOTE, EVENT_DATE, EVENT_TIME, CLASSIFIED} from "@welshman/util" import {displayRelayUrl, REACTION, NOTE, EVENT_DATE, EVENT_TIME, CLASSIFIED} from "@welshman/util"
import {subscribe} from "@welshman/app" import {subscribe} from "@welshman/app"
import {fly, slide} from "@lib/transition" import {fly, slide} from "@lib/transition"
@@ -20,7 +20,15 @@
import Divider from "@lib/components/Divider.svelte" import Divider from "@lib/components/Divider.svelte"
import ChannelMessage from "@app/components/ChannelMessage.svelte" import ChannelMessage from "@app/components/ChannelMessage.svelte"
import ChannelCompose from "@app/components/ChannelCompose.svelte" import ChannelCompose from "@app/components/ChannelCompose.svelte"
import {userMembership, decodeNRelay, makeChannelId, deriveChannel, GENERAL, tagRoom, MESSAGE} from "@app/state" import {
userMembership,
decodeNRelay,
makeChannelId,
deriveChannel,
GENERAL,
tagRoom,
MESSAGE,
} from "@app/state"
import {addRoomMembership, removeRoomMembership} from "@app/commands" import {addRoomMembership, removeRoomMembership} from "@app/commands"
const {nrelay, room = GENERAL} = $page.params const {nrelay, room = GENERAL} = $page.params