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
- [ ] Relay settings
------
---
- [ ] 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
+5 -5
View File
@@ -48,13 +48,13 @@
"@tiptap/extension-text": "^2.6.6",
"@tiptap/suggestion": "^2.6.4",
"@types/throttle-debounce": "^5.0.2",
"@welshman/app": "^0.0.7",
"@welshman/content": "^0.0.11",
"@welshman/lib": "^0.0.19",
"@welshman/net": "^0.0.23",
"@welshman/signer": "^0.0.6",
"@welshman/store": "^0.0.8",
"@welshman/util": "^0.0.34",
"@welshman/util": "^0.0.36",
"@welshman/store": "^0.0.9",
"@welshman/net": "^0.0.24",
"@welshman/signer": "^0.0.7",
"@welshman/app": "^0.0.11",
"daisyui": "^4.12.10",
"date-picker-svelte": "^2.13.0",
"emoji-picker-element": "^1.22.8",
+6 -3
View File
@@ -51,11 +51,13 @@
--secondary: oklch(var(--s));
}
.bg-alt, .bg-alt .bg-alt .bg-alt {
.bg-alt,
.bg-alt .bg-alt .bg-alt {
@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;
}
@@ -139,7 +141,8 @@
@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;
}
+68 -24
View File
@@ -1,9 +1,24 @@
import {uniqBy, sleep, chunk, equals, nthNe, choice, append} from "@welshman/lib"
import {DELETE, PROFILE, INBOX_RELAYS, RELAYS, MUTES, FOLLOWS, REACTION, isSignedEvent, getPubkeyTagValues, createEvent, displayProfile, normalizeRelayUrl} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {get} from "svelte/store"
import {ctx, uniqBy, uniq, sleep, chunk, equals, choice, append} from "@welshman/lib"
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 {Nip59, stamp} from "@welshman/signer"
import {
pubkey,
signer,
repository,
makeThunk,
publishThunk,
@@ -19,7 +34,7 @@ import {
tagPubkey,
tagReactionTo,
getRelayUrls,
getInboxRelaySelections,
userInboxRelaySelections,
} from "@welshman/app"
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) => {
const urls = getRelayUrls(getInboxRelaySelections(pubkey.get()!))
const urls = getRelayUrls(get(userInboxRelaySelections))
// Only update inbox policies if they already exist or we're adding them
if (enabled || urls.includes(url)) {
@@ -179,7 +194,7 @@ export const joinRelay = async (url: string, claim?: string) => {
makeThunk({
event: createEvent(28934, {tags: [["claim", claim]]}),
relays: [url],
})
}),
)
}
@@ -189,27 +204,56 @@ export const joinRelay = async (url: string, claim?: string) => {
// Actions
export const publishReaction = ({relays, event, content, tags = []}: {
relays: string[]
event: TrustedEvent,
content: string,
tags?: string[][]
export const sendWrapped = async ({
template,
pubkeys,
}: {
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,
tags: [
...tags,
...tagReactionTo(event),
],
tags: [...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}) => {
const deleteEvent = createEvent(DELETE, {
tags: [["k", String(event.kind)], ...tagEvent(event)],
})
export const makeDelete = ({event}: {event: TrustedEvent}) =>
createEvent(DELETE, {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 {writable} from "svelte/store"
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 Button from "@lib/components/Button.svelte"
import {getEditorOptions, getEditorTags, addFile} from "@lib/editor"
import {MESSAGE} from "@app/state"
import {getPubkeyHints} from "@app/commands"
export let onSubmit
@@ -27,7 +24,9 @@
}
onMount(() => {
editor = createEditor(getEditorOptions({submit, loading, getPubkeyHints, submitOnEnter: true, autofocus: true}))
editor = createEditor(
getEditorOptions({submit, loading, getPubkeyHints, submitOnEnter: true, autofocus: true}),
)
})
</script>
+9 -40
View File
@@ -1,9 +1,4 @@
<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 {hash, ellipsize, uniqBy, groupBy, now} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
@@ -14,24 +9,19 @@
deriveProfile,
deriveProfileDisplay,
formatTimestampAsTime,
tagReactionTo,
tagEvent,
makeThunk,
publishThunk,
pubkey,
} 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 Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Avatar from "@lib/components/Avatar.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 {tagRoom, REPLY, deriveEvent, displayReaction} from "@app/state"
import {colors, tagRoom, deriveEvent, displayReaction} from "@app/state"
import {publishDelete, publishReaction} from "@app/commands"
import {pushModal} from '@app/modal'
import {pushModal} from "@app/modal"
export let url
export let room
@@ -39,28 +29,6 @@
export let showPubkey = 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 profileDisplay = deriveProfileDisplay(event.pubkey)
const reactions = deriveEvents(repository, {filters: [{kinds: [REACTION], "#e": [event.id]}]})
@@ -96,8 +64,6 @@
}
}
let drawer: SvelteComponent
$: rootPubkey = $rootEvent?.pubkey || rootTag?.[4]
$: rootProfile = deriveProfile(rootPubkey || "")
$: rootProfileDisplay = deriveProfileDisplay(rootPubkey || "")
@@ -107,7 +73,10 @@
!isPending && !isPublished && findStatus($ps, [PublishStatus.Failure, PublishStatus.Timeout])
</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}
<div class="flex items-center gap-1 pl-12 text-xs">
<Icon icon="square-share-line" size={3} />
@@ -154,7 +123,7 @@
</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]}
{#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
@@ -1,13 +1,13 @@
<script lang="ts">
import tippy, {type Instance} from "tippy.js"
import type {NativeEmoji} from 'emoji-picker-element/shared'
import {between} from '@welshman/lib'
import {type Instance} from "tippy.js"
import type {NativeEmoji} from "emoji-picker-element/shared"
import {between} from "@welshman/lib"
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 {tagRoom} from '@app/state'
import {publishReaction} from '@app/commands'
import {tagRoom} from "@app/state"
import {publishReaction} from "@app/commands"
export let url, room, event
@@ -48,6 +48,5 @@
params={{
trigger: "manual",
interactive: true,
}}
/>
}} />
</div>
+14 -14
View File
@@ -1,17 +1,17 @@
<script lang="ts">
import {sortBy, append, range} from '@welshman/lib'
import {createEvent} from '@welshman/util'
import type {EventContent, TrustedEvent} from '@welshman/util'
import {repository, makeThunk, publishThunk} from '@welshman/app'
import {deriveEvents} from '@welshman/store'
import ChannelMessage from '@app/components/ChannelMessage.svelte'
import ChannelCompose from '@app/components/ChannelCompose.svelte'
import {tagRoom, REPLY} from '@app/state'
import {sortBy, append} from "@welshman/lib"
import {createEvent} from "@welshman/util"
import type {EventContent, TrustedEvent} from "@welshman/util"
import {repository, makeThunk, publishThunk} from "@welshman/app"
import {deriveEvents} from "@welshman/store"
import ChannelMessage from "@app/components/ChannelMessage.svelte"
import ChannelCompose from "@app/components/ChannelCompose.svelte"
import {tagRoom, REPLY} from "@app/state"
export let url, room, event: TrustedEvent
const replies = deriveEvents(repository, {
filters: [{kinds: [REPLY], '#E': [event.id]}],
filters: [{kinds: [REPLY], "#E": [event.id]}],
})
const onSubmit = ({content, tags}: EventContent) => {
@@ -30,11 +30,11 @@
}
if (seenRoots.size === 0) {
tags.push(['K', String(event.kind)])
tags.push(['E', event.id])
tags.push(["K", String(event.kind)])
tags.push(["E", event.id])
} else {
tags.push(['k', String(event.kind)])
tags.push(['e', event.id])
tags.push(["k", String(event.kind)])
tags.push(["e", event.id])
}
const reply = createEvent(REPLY, {content, tags: append(tagRoom(room, url), tags)})
@@ -43,7 +43,7 @@
}
</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">
<ChannelMessage {url} {room} {event} showPubkey />
{#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">
import {goto} from "$app/navigation"
import {displayRelayUrl} from "@welshman/util"
import Field from "@lib/components/Field.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
@@ -19,9 +17,7 @@
<form class="column gap-4" on:submit|preventDefault={onSubmit}>
<h1 class="heading">Start a Chat</h1>
<p class="text-center">
Create an encrypted chat room for private conversations.
</p>
<p class="text-center">Create an encrypted chat room for private conversations.</p>
<Field>
<p slot="label">Members</p>
<div slot="input">
+11 -13
View File
@@ -16,16 +16,16 @@
isAddress,
isNewline,
} from "@welshman/content"
import Link from '@lib/components/Link.svelte'
import ContentToken from '@app/components/ContentToken.svelte'
import ContentCode from '@app/components/ContentCode.svelte'
import ContentLinkInline from '@app/components/ContentLinkInline.svelte'
import ContentLinkBlock from '@app/components/ContentLinkBlock.svelte'
import ContentNewline from '@app/components/ContentNewline.svelte'
import ContentQuote from '@app/components/ContentQuote.svelte'
import ContentTopic from '@app/components/ContentTopic.svelte'
import ContentMention from '@app/components/ContentMention.svelte'
import {entityLink} from '@app/state'
import Link from "@lib/components/Link.svelte"
import ContentToken from "@app/components/ContentToken.svelte"
import ContentCode from "@app/components/ContentCode.svelte"
import ContentLinkInline from "@app/components/ContentLinkInline.svelte"
import ContentLinkBlock from "@app/components/ContentLinkBlock.svelte"
import ContentNewline from "@app/components/ContentNewline.svelte"
import ContentQuote from "@app/components/ContentQuote.svelte"
import ContentTopic from "@app/components/ContentTopic.svelte"
import ContentMention from "@app/components/ContentMention.svelte"
import {entityLink} from "@app/state"
export let event
export let minLength = 500
@@ -50,8 +50,6 @@
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 isBlock = (i: number) => {
@@ -115,7 +113,7 @@
{/each}
</div>
{#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}>
See more
</button>
+3 -6
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import {ellipsize, displayUrl, postJson} from "@welshman/lib"
import {ellipsize, postJson} from "@welshman/lib"
import {dufflepud, imgproxy} from "@app/state"
import Link from "@lib/components/Link.svelte"
@@ -22,16 +22,13 @@
external
href={url}
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)$/)}
<video controls src={url} class="max-h-96 object-contain object-center">
<track kind="captions" />
</video>
{:else if url.match(/\.(jpe?g|png|gif|webp)$/)}
<img
alt="Link preview"
src={imgproxy(url)}
class="object-cover object-center max-h-96" />
<img alt="Link preview" src={imgproxy(url)} class="max-h-96 object-cover object-center" />
{:else}
{#await loadPreview()}
<div class="center my-12 w-full">
+2 -2
View File
@@ -1,9 +1,9 @@
<script lang="ts">
import {nip19} from 'nostr-tools'
import {nip19} from "nostr-tools"
import {displayProfile} from "@welshman/util"
import {deriveProfile} from "@welshman/app"
import Link from "@lib/components/Link.svelte"
import {entityLink} from '@app/state'
import {entityLink} from "@app/state"
export let value
+4 -7
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import {getAddress, Address} from "@welshman/util"
import {Address} from "@welshman/util"
import Spinner from "@lib/components/Spinner.svelte"
import NoteCard from "@app/components/NoteCard.svelte"
import {deriveEvent} from "@app/state"
@@ -12,18 +12,15 @@
const event = deriveEvent(idOrAddress, relays)
let element: Element
$: address = $event ? getAddress($event) : ""
$: isGroup = address.match(/^(34550|35834):/)
</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}
<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} />
</NoteCard>
{:else}
<div class="p-4 rounded-box">
<div class="rounded-box p-4">
<Spinner loading>Loading event...</Spinner>
</div>
{/if}
-3
View File
@@ -1,7 +1,4 @@
<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 Button from "@lib/components/Button.svelte"
import {clip} from "@app/toast"
+9 -4
View File
@@ -8,13 +8,18 @@
<h1 class="heading">What is a nostr address?</h1>
</div>
<p>
Flotilla hosts spaces on the <Link external href="https://nostr.com/" class="underline">Nostr protocol</Link>.
Nostr uses "nostr addresses" to make it easier for people to find you, without having to
Flotilla hosts spaces on the <Link external href="https://nostr.com/" class="underline"
>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).
</p>
<p>
There are several providers of nostr addresses, including several clients. You can find a
list and more information on <Link external href="https://nostr.how/en/guides/get-verified" class="underline">nostr.how</Link>.
There are several providers of nostr addresses, including several clients. You can find a list
and more information on <Link
external
href="https://nostr.how/en/guides/get-verified"
class="underline">nostr.how</Link
>.
</p>
<Button class="btn btn-primary" on:click={() => history.back()}>Got it</Button>
</div>
+5 -3
View File
@@ -8,9 +8,11 @@
<h1 class="heading">What is a relay?</h1>
</div>
<p>
Flotilla hosts spaces on the <Link external href="https://nostr.com/" class="underline">Nostr protocol</Link>.
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.
Flotilla hosts spaces on the <Link external href="https://nostr.com/" class="underline"
>Nostr protocol</Link
>. 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>
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">
import {nip19} from 'nostr-tools'
import {nip19} from "nostr-tools"
import {ctx} from "@welshman/lib"
import {displayPubkey} from "@welshman/util"
import {deriveProfile, deriveProfileDisplay, formatTimestamp} from "@welshman/app"
import {formatTimestamp} from "@welshman/app"
import Link from "@lib/components/Link.svelte"
import Avatar from "@lib/components/Avatar.svelte"
import Profile from "@app/components/Profile.svelte"
import {entityLink} from '@app/state'
import {entityLink} from "@app/state"
export let event
+21 -12
View File
@@ -1,16 +1,23 @@
<script lang="ts">
import {onMount} from 'svelte'
import {nip19} from 'nostr-tools'
import {ago, append, first, sortBy, max, WEEK, ctx} from '@welshman/lib'
import {NOTE, getAncestorTags, getListValues} from '@welshman/util'
import type {Filter} from '@welshman/util'
import {deriveEvents} from '@welshman/store'
import {repository, load, loadRelaySelections, userFollows, formatTimestamp, formatTimestampRelative} from '@welshman/app'
import Link from '@lib/components/Link.svelte'
import {onMount} from "svelte"
import {nip19} from "nostr-tools"
import {ago, append, first, sortBy, WEEK, ctx} from "@welshman/lib"
import {NOTE, getAncestorTags, getListTags, getPubkeyTagValues} from "@welshman/util"
import type {Filter} from "@welshman/util"
import {deriveEvents} from "@welshman/store"
import {
repository,
load,
loadRelaySelections,
userFollows,
formatTimestamp,
formatTimestampRelative,
} from "@welshman/app"
import Link from "@lib/components/Link.svelte"
import Profile from "@app/components/Profile.svelte"
import ProfileInfo from "@app/components/ProfileInfo.svelte"
import Content from "@app/components/Content.svelte"
import {entityLink} from '@app/state'
import {entityLink} from "@app/state"
export let pubkey
@@ -38,16 +45,18 @@
{@const event = first(sortBy(e => -e.created_at, roots))}
{@const relays = ctx.app.router.Event(event).getUrls()}
{@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" />
<Link external class="chat chat-start" href={entityLink(nevent)}>
<div class="chat-bubble">
<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>
</Link>
<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>
{/if}
-1
View File
@@ -11,7 +11,6 @@
import {quintOut} from "svelte/easing"
import {displayRelayUrl} from "@welshman/util"
import {userProfile} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
import Avatar from "@lib/components/Avatar.svelte"
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
import SpaceAdd from "@app/components/SpaceAdd.svelte"
+17 -11
View File
@@ -1,12 +1,18 @@
<script lang="ts">
import {nip19} from 'nostr-tools'
import {derived} from 'svelte/store'
import {displayPubkey, getListValues} from "@welshman/util"
import {userFollows, deriveUserWotScore, deriveProfile, deriveHandleForPubkey, displayHandle, deriveProfileDisplay, formatTimestamp, getUserWotScore, followsByPubkey} from "@welshman/app"
import {nip19} from "nostr-tools"
import {displayPubkey, getPubkeyTagValues, getListTags} from "@welshman/util"
import {
userFollows,
deriveUserWotScore,
deriveProfile,
deriveHandleForPubkey,
displayHandle,
deriveProfileDisplay,
} from "@welshman/app"
import Link from "@lib/components/Link.svelte"
import Avatar from "@lib/components/Avatar.svelte"
import WotScore from "@lib/components/WotScore.svelte"
import {entityLink} from '@app/state'
import {entityLink} from "@app/state"
export let pubkey
@@ -16,21 +22,21 @@
const handle = deriveHandleForPubkey(pubkey)
const score = deriveUserWotScore(pubkey)
$: following = getListValues("p", $userFollows).includes(pubkey)
$: following = getPubkeyTagValues(getListTags($userFollows)).includes(pubkey)
</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">
<Avatar src={$profile?.picture} size={10} />
</Link>
<div class="flex flex-col min-w-0">
<div class="flex gap-2 items-center">
<Link external class="text-bold text-ellipsis overflow-hidden" href={entityLink(npub)}>
<div class="flex min-w-0 flex-col">
<div class="flex items-center gap-2">
<Link external class="text-bold overflow-hidden text-ellipsis" href={entityLink(npub)}>
{$profileDisplay}
</Link>
<WotScore score={$score} active={following} />
</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)}
</div>
</div>
+2 -2
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import {deriveProfile} from '@welshman/app'
import Avatar from '@lib/components/Avatar.svelte'
import {deriveProfile} from "@welshman/app"
import Avatar from "@lib/components/Avatar.svelte"
export let pubkey
+1 -2
View File
@@ -1,5 +1,4 @@
<script lang="ts">
import {deriveProfile} from '@welshman/app'
import ProfileCircle from "@app/components/ProfileCircle.svelte"
export let pubkeys
@@ -8,7 +7,7 @@
<div class="flex pr-3">
{#each pubkeys.slice(0, 15) as pubkey (pubkey)}
<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>
{/each}
</div>
+23 -23
View File
@@ -1,21 +1,17 @@
<script lang="ts">
import {nip19} from 'nostr-tools'
import type {SvelteComponent} from 'svelte'
import tippy, {type Instance} from "tippy.js"
import {append, always, remove, uniq} from '@welshman/lib'
import {getListValues, MUTES} from '@welshman/util'
import {userMutes, profileSearch, tagPubkey} from '@welshman/app'
import Icon from '@lib/components/Icon.svelte'
import Field from '@lib/components/Field.svelte'
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 Name from '@app/components/Name.svelte'
import {entityLink} from '@app/state'
import {updateList} from '@app/commands'
import {pushToast} from '@app/toast'
import {nip19} from "nostr-tools"
import type {SvelteComponent} from "svelte"
import {type Instance} from "tippy.js"
import {append, remove, uniq} from "@welshman/lib"
import {profileSearch} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
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 Name from "@app/components/Name.svelte"
import {entityLink} from "@app/state"
export let value: string[]
@@ -52,7 +48,7 @@
<div class="flex flex-col gap-2">
<div>
{#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)}>
<Icon icon="close-circle" size={4} class="-ml-1 mt-px" />
</Button>
@@ -64,7 +60,12 @@
</div>
<label class="input input-bordered flex w-full items-center gap-2" bind:this={input}>
<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>
<Tippy
bind:popover
@@ -75,14 +76,13 @@
select: selectPubkey,
search: profileSearch,
component: SuggestionProfile,
class: 'rounded-box',
class: "rounded-box",
style: `left: 4px; width: ${input?.clientWidth + 12}px`,
}}
params={{
trigger: "manual",
interactive: true,
maxWidth: 'none',
maxWidth: "none",
getReferenceClientRect: () => input.getBoundingClientRect(),
}}
/>
}} />
</div>
+7 -4
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import {onMount} from 'svelte'
import type {Readable} from 'svelte/store'
import {onMount} from "svelte"
import type {Readable} from "svelte/store"
import {relaySearch} from "@welshman/app"
import {createScroller} from "@lib/html"
import Icon from "@lib/components/Icon.svelte"
@@ -19,7 +19,7 @@
const sub = discoverRelays()
const scroller = createScroller({
delay: 300,
element: element.closest('.modal-box')!,
element: element.closest(".modal-box")!,
onScroll: () => {
limit += 20
},
@@ -37,7 +37,10 @@
<Icon icon="magnifer" />
<input bind:value={term} class="grow" type="text" placeholder="Search for relays..." />
</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}>
<Button class="btn btn-outline btn-sm flex items-center" on:click={() => addRelay(url)}>
<Icon icon="add-circle" />
+8 -5
View File
@@ -1,9 +1,9 @@
<script lang="ts">
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import {displayUrl} from '@welshman/lib'
import {displayRelayUrl} from '@welshman/util'
import {deriveRelay} from '@welshman/app'
import {displayUrl} from "@welshman/lib"
import {displayRelayUrl} from "@welshman/util"
import {deriveRelay} from "@welshman/app"
export let url
@@ -30,11 +30,14 @@
&bull;
{/if}
{#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
</span>
&bull;
{/if}
Connected {connections} {connections === 1 ? 'time' : 'times'}
Connected {connections}
{connections === 1 ? "time" : "times"}
</span>
</div>
-1
View File
@@ -1,5 +1,4 @@
<script lang="ts">
import Button from "@lib/components/Button.svelte"
import CardButton from "@lib/components/CardButton.svelte"
import SpaceCreateExternal from "@app/components/SpaceCreateExternal.svelte"
import SpaceInviteAccept from "@app/components/SpaceInviteAccept.svelte"
+4 -4
View File
@@ -1,11 +1,11 @@
<script lang="ts">
import {nip19} from 'nostr-tools'
import {ctx} from '@welshman/lib'
import {nip19} from "nostr-tools"
import {ctx} from "@welshman/lib"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Content from "@app/components/Content.svelte"
import NoteCard from "@app/components/NoteCard.svelte"
import {entityLink} from '@app/state'
import {entityLink} from "@app/state"
export let root
export let replies
@@ -22,7 +22,7 @@
</NoteCard>
<Link
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" />
<span>{replies.length}</span>
</Link>
+5 -1
View File
@@ -10,7 +10,11 @@ export type ModalOptions = {
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()
// TODO: fix memory leak here by listening to history somehow
+64 -28
View File
@@ -1,7 +1,20 @@
import {nip19} from "nostr-tools"
import twColors from "tailwindcss/colors"
import {get, derived} from "svelte/store"
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 {
getIdFilters,
NOTE,
@@ -29,7 +42,6 @@ import {
subscribe,
collection,
loadRelay,
loadProfile,
profilesByPubkey,
getDefaultAppContext,
getDefaultNetContext,
@@ -69,7 +81,29 @@ export const IMGPROXY_URL = "https://imgproxy.coracle.social"
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} = {}) => {
if (!url || url.match("gif$")) {
@@ -153,7 +187,7 @@ setContext({
}),
})
repository.on('update', ({added}) => {
repository.on("update", ({added}) => {
for (const event of added) {
ensureUnwrapped(event)
}
@@ -310,33 +344,36 @@ export const makeChatId = (pubkeys: string[]) => sort(uniq(pubkeys)).join(",")
export const splitChatId = (id: string) => id.split(",")
export const chats = derived([pubkey, chatMessages, profilesByPubkey], ([$pubkey, $messages, $profilesByPubkey]) => {
const messagesByChatId = new Map<string, TrustedEvent[]>()
export const chats = derived(
[pubkey, chatMessages, profilesByPubkey],
([$pubkey, $messages, $profilesByPubkey]) => {
const messagesByChatId = new Map<string, TrustedEvent[]>()
for (const message of $messages) {
const chatId = makeChatId(getPubkeyTagValues(message.tags))
for (const message of $messages) {
const chatId = makeChatId(getPubkeyTagValues(message.tags))
pushToMapKey(messagesByChatId, chatId, message)
}
pushToMapKey(messagesByChatId, chatId, message)
}
return sortBy(
c => -c.last_activity,
Array.from(messagesByChatId.entries()).map(([id, events]): Chat => {
const pubkeys = splitChatId(id)
const messages = sortBy(e => -e.created_at, events)
const last_activity = messages[0].created_at
const search_text = remove($pubkey as string, pubkeys)
.map(pubkey => {
const profile = $profilesByPubkey.get(pubkey)
return sortBy(
c => -c.last_activity,
Array.from(messagesByChatId.entries()).map(([id, events]): Chat => {
const pubkeys = splitChatId(id)
const messages = sortBy(e => -e.created_at, events)
const last_activity = messages[0].created_at
const search_text = remove($pubkey as string, pubkeys)
.map(pubkey => {
const profile = $profilesByPubkey.get(pubkey)
return profile ? displayProfile(profile) : ""
})
.join(' ')
return profile ? displayProfile(profile) : ""
})
.join(" ")
return {id, pubkeys, messages, last_activity, search_text}
})
)
})
return {id, pubkeys, messages, last_activity, search_text}
}),
)
},
)
export const {
indexStore: chatsById,
@@ -348,13 +385,12 @@ export const {
getKey: chat => chat.id,
load: async (id: string, request: Partial<SubscribeRequestWithHandlers> = {}) => {
const $pubkey = pubkey.get()
const [url, room] = splitChatId(id)
const chat = get(chatsById).get(id)
const timestamps = chat?.messages.map(e => e.created_at) || []
const since = Math.max(0, max(timestamps) - 3600)
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 || ""}`} />
{:else}
<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 || ""}`}>
<Icon {icon} size={Math.round(size * 0.8)} />
</div>
+4 -4
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import {slide} from '@lib/transition'
import Icon from '@lib/components/Icon.svelte'
import {slide} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte"
const toggle = () => {
isOpen = !isOpen
@@ -9,10 +9,10 @@
let isOpen = false
</script>
<div class="flex flex-col gap-4 relative {$$props.class}">
<div class="relative flex flex-col gap-4 {$$props.class}">
<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}
on:click={toggle}>
<Icon icon="alt-arrow-down" />
+3 -4
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import {randomId} from '@welshman/lib'
import Icon from '@lib/components/Icon.svelte'
import {randomId} from "@welshman/lib"
const id = randomId()
@@ -31,8 +30,8 @@
</div>
<div class="drawer-side z-modal">
<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 />
</div>
</div>
</div>
</div>
+4 -4
View File
@@ -1,14 +1,14 @@
<script lang="ts">
import 'emoji-picker-element'
import type {Emoji} from 'emoji-picker-element/shared'
import {onMount} from 'svelte'
import "emoji-picker-element"
import type {Emoji} from "emoji-picker-element/shared"
import {onMount} from "svelte"
export let onClick: (emoji: Emoji) => void
let element: Element
onMount(() => {
element.addEventListener('emoji-click', (event: any) => onClick(event.detail as Emoji))
element.addEventListener("emoji-click", (event: any) => onClick(event.detail as Emoji))
})
</script>
+2 -2
View File
@@ -84,7 +84,7 @@
document: Document,
earth: Earth,
pen: Pen,
'pen-new-square': PenNewSquare,
"pen-new-square": PenNewSquare,
"headphones-round": HeadphonesRound,
"add-circle": AddCircle,
"alt-arrow-down": AltArrowDown,
@@ -112,7 +112,7 @@
hashtag: Hashtag,
"hand-pills": HandPills,
"home-smile": HomeSmile,
"inbox": Inbox,
inbox: Inbox,
"info-circle": InfoCircle,
"info-square": InfoSquare,
key: Key,
+1 -1
View File
@@ -5,6 +5,6 @@
export let props = {}
</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} />
</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 />
</div>
+10 -8
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import {onMount} from 'svelte'
import type {SvelteComponent, ComponentType, ComponentProps} from 'svelte'
import {onMount} from "svelte"
import type {SvelteComponent, ComponentType, ComponentProps} from "svelte"
import tippy, {type Instance, type Props} from "tippy.js"
export let component: ComponentType
@@ -14,15 +14,17 @@
$: instance?.$set(props)
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 () => {
popover?.destroy()
instance?.$destroy()
return () => {
popover?.destroy()
instance?.$destroy()
}
}
})
</script>
+2 -2
View File
@@ -14,7 +14,7 @@
</style>
<script lang="ts">
import {clamp} from '@welshman/lib'
import {clamp} from "@welshman/lib"
export let score
export let max = 100
@@ -26,7 +26,7 @@
$: normalizedScore = clamp([0, max], score) / max
$: dashOffset = 100 - 44 * normalizedScore
$: style = `transform: rotate(${135 - normalizedScore * 180}deg)`
$: stroke = active ? 'var(--primary)' : 'var(--base-content)'
$: stroke = active ? "var(--primary)" : "var(--base-content)"
</script>
<div class="relative h-[14px] w-[14px]">
+9 -4
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import cx from 'classnames'
import cx from "classnames"
import type {NodeViewProps} from "@tiptap/core"
import {NodeViewWrapper} from "svelte-tiptap"
import {ellipsize, nthEq} from "@welshman/lib"
@@ -11,9 +11,11 @@
export let selected: NodeViewProps["selected"]
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)
@@ -21,7 +23,10 @@
</script>
<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)}
</Link>
</NodeViewWrapper>
+1 -1
View File
@@ -5,7 +5,7 @@
import {displayProfile} from "@welshman/util"
import {deriveProfile} from "@welshman/app"
import Link from "@lib/components/Link.svelte"
import {entityLink} from '@app/state'
import {entityLink} from "@app/state"
export let node: NodeViewProps["node"]
export let selected: NodeViewProps["selected"]
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import Profile from '@app/components/Profile.svelte'
import Profile from "@app/components/Profile.svelte"
export let value
</script>
+3 -3
View File
@@ -4,7 +4,7 @@
import {throttle} from "throttle-debounce"
import {fly, slide} from "svelte/transition"
import {clamp} from "@welshman/lib"
import Icon from '@lib/components/Icon.svelte'
import Icon from "@lib/components/Icon.svelte"
import {theme} from "@app/theme"
export let term
@@ -77,11 +77,11 @@
{/if}
{#each items as value, i (value)}
<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:click|preventDefault={() => select(value)}>
{#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" />
</div>
{/if}
+26 -18
View File
@@ -56,29 +56,37 @@ export const findMarks = (type: string, json: JSONContent) => {
export const getEditorTags = (editor: Editor) => {
const json = editor.getJSON()
const topicTags = findMarks("tag", json).map(
({attrs}: any) => ["t", attrs.tag.replace(/^#/, '').toLowerCase()],
)
const topicTags = findMarks("tag", json).map(({attrs}: any) => [
"t",
attrs.tag.replace(/^#/, "").toLowerCase(),
])
const naddrTags = findNodes("naddr", json).map(
({kind, pubkey, identifier, relays}: any) => {
const address = new Address(kind, pubkey, identifier).toString()
const naddrTags = findNodes("naddr", json).map(({kind, pubkey, identifier, relays}: any) => {
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(
({id, author, relays}: any) => ["q", id, choice(relays) || "", author || ""],
)
const neventTags = findNodes("nevent", json).map(({id, author, relays}: any) => [
"q",
id,
choice(relays) || "",
author || "",
])
const mentionTags = findNodes("nprofile", json).map(
({pubkey, relays}: any) => ["p", pubkey, choice(relays) || "", ""],
)
const mentionTags = findNodes("nprofile", json).map(({pubkey, relays}: any) => [
"p",
pubkey,
choice(relays) || "",
"",
])
const imetaTags = findNodes("image", json).map(
({src, sha256}: any) => ["imeta", `url ${src}`, `x ${sha256}`, `ox ${sha256}`],
)
const imetaTags = findNodes("image", json).map(({src, sha256}: any) => [
"imeta",
`url ${src}`,
`x ${sha256}`,
`ox ${sha256}`,
])
return [...topicTags, ...naddrTags, ...neventTags, ...mentionTags, ...imetaTags]
}
+3 -1
View File
@@ -107,7 +107,9 @@
<div data-theme={$theme}>
<div class="flex h-screen overflow-hidden">
<PrimaryNav />
<slot />
{#key $page.params}
<slot />
{/key}
</div>
<dialog bind:this={dialog} class="modal modal-bottom !z-modal sm:modal-middle">
{#if prev && !prev.options?.drawer}
+1 -1
View File
@@ -17,7 +17,7 @@
onMount(() => {
const sub = discoverRelays()
const scroller = createScroller({
element: element.closest('.max-h-screen')!,
element: element.closest(".max-h-screen")!,
onScroll: () => {
limit += 20
},
+2 -3
View File
@@ -19,10 +19,9 @@
</label>
<div class="grid grid-cols-2 gap-4 md:grid-cols-2">
{#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>
<button class="btn btn-primary w-full" on:click={() => theme.set(name)}
>Use Theme</button>
<button class="btn btn-primary w-full" on:click={() => theme.set(name)}>Use Theme</button>
</div>
{/each}
</div>
+18 -14
View File
@@ -1,8 +1,8 @@
<script lang="ts">
import {onMount} from 'svelte'
import {ellipsize, ctx, ago, remove} from '@welshman/lib'
import {WRAP} from '@welshman/util'
import {pubkey, subscribe} from '@welshman/app'
import {onMount} from "svelte"
import {ctx, ago, remove} from "@welshman/lib"
import {WRAP} from "@welshman/util"
import {pubkey, subscribe} from "@welshman/app"
import {fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte"
import Page from "@lib/components/Page.svelte"
@@ -16,8 +16,8 @@
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ProfileCircles from "@app/components/ProfileCircles.svelte"
import ChatStart from "@app/components/ChatStart.svelte"
import {chatSearch, pullConservatively} from '@app/state'
import {pushModal} from '@app/modal'
import {chatSearch, pullConservatively} from "@app/state"
import {pushModal} from "@app/modal"
const startChat = () => pushModal(ChatStart)
@@ -26,7 +26,7 @@
$: chats = $chatSearch.searchOptions(term).filter(c => c.pubkeys.length > 1)
onMount(() => {
const filter = {kinds: [WRAP], '#p': [$pubkey!]}
const filter = {kinds: [WRAP], "#p": [$pubkey!]}
const sub = subscribe({filters: [{...filter, since: ago(30)}]})
pullConservatively({
@@ -64,29 +64,33 @@
</SecondaryNavHeader>
</div>
</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" />
<input bind:value={term} class="grow" type="text" />
</label>
<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 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}">
<div class="flex gap-2 items-center">
<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="whitespace-nowrap overflow-hidden text-ellipsis">
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
<Name pubkey={others[0]} />
and {others.length - 1} {others.length > 2 ? 'others' : 'other'}
and {others.length - 1}
{others.length > 2 ? "others" : "other"}
</p>
{/if}
</div>
<p class="text-sm whitespace-nowrap overflow-hidden text-ellipsis">
<p class="overflow-hidden text-ellipsis whitespace-nowrap text-sm">
{message.content}
</p>
</Link>
+3 -2
View File
@@ -8,9 +8,10 @@
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>
<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">
import {onMount} from 'svelte'
import {derived} from 'svelte/store'
import {createScroller} from '@lib/html'
import {shuffle, sortBy, sleep, ago, DAY, HOUR, pushToMapKey} from '@welshman/lib'
import {getListValues, getAncestorTagValues, NOTE, REACTION} from '@welshman/util'
import type {TrustedEvent} from '@welshman/util'
import {deriveEvents} from '@welshman/store'
import {profileSearch, 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'
import {onMount} from "svelte"
import {derived} from "svelte/store"
import {createScroller} from "@lib/html"
import {sortBy, sleep, ago, DAY, HOUR, pushToMapKey} from "@welshman/lib"
import {
getListTags,
getPubkeyTagValues,
getAncestorTagValues,
NOTE,
REACTION,
} 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 loading = sleep(3000)
let events: TrustedEvent[] = []
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 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 reactionsByParent = derived(
reactions,
$reactions => {
const $reactionsByParent = new Map<string, TrustedEvent[]>()
const reactionsByParent = derived(reactions, $reactions => {
const $reactionsByParent = new Map<string, TrustedEvent[]>()
for (const event of $reactions) {
const [parentId] = getAncestorTagValues(event.tags).replies
for (const event of $reactions) {
const [parentId] = getAncestorTagValues(event.tags).replies
if (parentId) {
pushToMapKey($reactionsByParent, parentId, event)
}
if (parentId) {
pushToMapKey($reactionsByParent, parentId, event)
}
return $reactionsByParent
}
)
const isLike = (e: TrustedEvent) =>
e.kind === REACTION && ["+", ""].includes(e.content)
return $reactionsByParent
})
const isReplyOf = (e: TrustedEvent, p: TrustedEvent) =>
getAncestorTagValues(e.tags).replies.includes(e.id)
const isLike = (e: TrustedEvent) => e.kind === REACTION && ["+", ""].includes(e.content)
const scoreEvent = (e: TrustedEvent) => {
const thisReactions = $reactionsByParent.get(e.id) || []
@@ -57,12 +56,12 @@
load({filters: [notesFilter, reactionsFilter]})
const scroller = createScroller({
element: element.closest('.max-h-screen')!,
element: element.closest(".max-h-screen")!,
onScroll: () => {
const seen = new Set(events.map(e => e.id))
const eligible = sortBy(
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)]
@@ -73,7 +72,6 @@
})
</script>
<div class="content column gap-4" bind:this={element}>
{#await loading}
<div class="center my-20">
+10 -13
View File
@@ -1,25 +1,23 @@
<script lang="ts">
import {onMount} from 'svelte'
import {createScroller} from '@lib/html'
import Icon from '@lib/components/Icon.svelte'
import {shuffle} from '@welshman/lib'
import {getListValues} from '@welshman/util'
import {profileSearch, userFollows} from '@welshman/app'
import PeopleItem from '@app/components/PeopleItem.svelte'
import {onMount} from "svelte"
import {createScroller} from "@lib/html"
import Icon from "@lib/components/Icon.svelte"
import {shuffle} from "@welshman/lib"
import {getPubkeyTagValues, getListTags} from "@welshman/util"
import {profileSearch, userFollows} from "@welshman/app"
import PeopleItem from "@app/components/PeopleItem.svelte"
const defaultPubkeys = shuffle(getListValues("p", $userFollows))
const defaultPubkeys = shuffle(getPubkeyTagValues(getListTags($userFollows)))
let term = ""
let limit = 10
let element: Element
$: pubkeys = term
? $profileSearch.searchValues(term)
: defaultPubkeys
$: pubkeys = term ? $profileSearch.searchValues(term) : defaultPubkeys
onMount(() => {
const scroller = createScroller({
element: element.closest('.max-h-screen')!,
element: element.closest(".max-h-screen")!,
onScroll: () => {
limit += 10
},
@@ -29,7 +27,6 @@
})
</script>
<div class="content column gap-4" bind:this={element}>
<h1 class="superheading mt-20">People</h1>
<p class="text-center">Get the latest from people in your network</p>
+13 -26
View File
@@ -1,26 +1,17 @@
<script lang="ts">
import {nip19} from 'nostr-tools'
import type {SvelteComponent} from 'svelte'
import tippy, {type Instance} from "tippy.js"
import {append, always, remove, uniq} from '@welshman/lib'
import {getListValues, MUTES} from '@welshman/util'
import {userMutes, profileSearch, tagPubkey} from '@welshman/app'
import Icon from '@lib/components/Icon.svelte'
import Field from '@lib/components/Field.svelte'
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'
import {always} from "@welshman/lib"
import {getListTags, getPubkeyTagValues, MUTES} from "@welshman/util"
import {userMutes, tagPubkey} from "@welshman/app"
import Field from "@lib/components/Field.svelte"
import Button from "@lib/components/Button.svelte"
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
import {updateList} from "@app/commands"
import {pushToast} from "@app/toast"
let mutedPubkeys = getListValues("p", $userMutes)
let mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
const reset = () => {
mutedPubkeys = getListValues("p", $userMutes)
mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
}
const onSubmit = async () => {
@@ -38,13 +29,9 @@
<ProfileMultiSelect bind:value={mutedPubkeys} />
</div>
</Field>
<div class="flex flex-row items-center justify-between gap-4 mt-4">
<Button class="btn btn-neutral" on:click={reset}>
Discard Changes
</Button>
<Button type="submit" class="btn btn-primary">
Save Changes
</Button>
<div class="mt-4 flex flex-row items-center justify-between gap-4">
<Button class="btn btn-neutral" on:click={reset}>Discard Changes</Button>
<Button type="submit" class="btn btn-primary">Save Changes</Button>
</div>
</div>
</form>
+4 -2
View File
@@ -2,7 +2,7 @@
import Link from "@lib/components/Link.svelte"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import {entityLink} from '@app/state'
import {entityLink} from "@app/state"
const nprofile =
"nprofile1qqsf03c2gsmx5ef4c9zmxvlew04gdh7u94afnknp33qvv3c94kvwxgspz4mhxue69uhhyetvv9ujuerpd46hxtnfduhsz9rhwden5te0wfjkcctev93xcefwdaexwtcpzdmhxue69uhhqatjwpkx2urpvuhx2ue0vamm57"
@@ -22,7 +22,9 @@
<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>
<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
</Link>
</div>
+33 -27
View File
@@ -1,26 +1,31 @@
<script lang="ts">
import {nip19} from 'nostr-tools'
import {last, ctx} from "@welshman/lib"
import {PROFILE, createEvent, displayPubkey, displayProfile, makeProfile, editProfile, createProfile, isPublishedProfile} from "@welshman/util"
import {pubkey, getProfile, displayHandle, makeThunk, publishThunk} from "@welshman/app"
import {slide} from '@lib/transition'
import {
createEvent,
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 Field from '@lib/components/Field.svelte'
import Field from "@lib/components/Field.svelte"
import Button from "@lib/components/Button.svelte"
import Avatar from "@lib/components/Avatar.svelte"
import InputProfilePicture from "@lib/components/InputProfilePicture.svelte"
import Content from '@app/components/Content.svelte'
import InfoHandle from '@app/components/InfoHandle.svelte'
import Content from "@app/components/Content.svelte"
import InfoHandle from "@app/components/InfoHandle.svelte"
import {pushModal} from "@app/modal"
import {pushToast} from "@app/toast"
const npub = nip19.npubEncode($pubkey!)
const pubkeyDisplay = displayPubkey($pubkey!)
const displayNip05 = (nip05: string) =>
nip05?.startsWith("_@") ? last(nip05.split("@")) : nip05
const displayNip05 = (nip05: string) => (nip05?.startsWith("_@") ? last(nip05.split("@")) : nip05)
const cloneProfile = () => ({...(getProfile($pubkey!) || makeProfile())})
const cloneProfile = () => ({...($profilesByPubkey.get($pubkey!) || makeProfile())})
const toggleEdit = () => {
editing = !editing
@@ -52,23 +57,23 @@
<div class="content column gap-4">
<div class="card2 bg-alt shadow-xl">
<div class="flex gap-2 justify-between">
<div class="flex gap-3 max-w-full">
<div class="flex justify-between gap-2">
<div class="flex max-w-full gap-3">
<div class="py-1">
<Avatar src={profile?.picture} size={10} />
</div>
<div class="flex flex-col min-w-0">
<div class="flex gap-2 items-center">
<div class="text-bold text-ellipsis overflow-hidden">
<div class="flex min-w-0 flex-col">
<div class="flex items-center gap-2">
<div class="text-bold overflow-hidden text-ellipsis">
{displayProfile(profile, pubkeyDisplay)}
</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}
</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" />
</Button>
</div>
@@ -90,7 +95,11 @@
</Field>
<Field>
<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>
<p slot="label">Nostr Address</p>
@@ -99,16 +108,13 @@
<input bind:value={profile.nip05} class="grow" type="text" />
</label>
<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>
</Field>
<div class="flex flex-row items-center justify-between gap-4 mt-4">
<Button class="btn btn-neutral" on:click={stopEdit}>
Discard Changes
</Button>
<Button type="submit" class="btn btn-primary">
Save Changes
</Button>
<div class="mt-4 flex flex-row items-center justify-between gap-4">
<Button class="btn btn-neutral" on:click={stopEdit}>Discard Changes</Button>
<Button type="submit" class="btn btn-primary">Save Changes</Button>
</div>
</form>
{/if}
+23 -19
View File
@@ -1,14 +1,19 @@
<script lang="ts">
import {derived} from "svelte/store"
import type {Readable} from 'svelte/store'
import {relaySearch, getRelayUrls, userRelaySelections, userInboxRelaySelections, getReadRelayUrls, getWriteRelayUrls} from "@welshman/app"
import {
getRelayUrls,
userRelaySelections,
userInboxRelaySelections,
getReadRelayUrls,
getWriteRelayUrls,
} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Collapse from "@lib/components/Collapse.svelte"
import RelayItem from "@app/components/RelayItem.svelte"
import RelayAdd from "@app/components/RelayAdd.svelte"
import {pushModal} from '@app/modal'
import {setRelayPolicy, setInboxRelayPolicy} from '@app/commands'
import {pushModal} from "@app/modal"
import {setRelayPolicy, setInboxRelayPolicy} from "@app/commands"
const readRelayUrls = derived(userRelaySelections, getReadRelayUrls)
const writeRelayUrls = derived(userRelaySelections, getWriteRelayUrls)
@@ -41,20 +46,20 @@
<div class="content 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" />
Broadcast Relays
</h2>
<p slot="description" class="text-sm">
These relays will be advertised on your profile as places where you send your public
notes. Be sure to select relays that will accept your notes, and which will let people
who follow you read them.
These relays will be advertised on your profile as places where you send your public notes. Be
sure to select relays that will accept your notes, and which will let people who follow you
read them.
</p>
<div class="column gap-2">
{#each $writeRelayUrls.sort() as url (url)}
<RelayItem {url}>
<Button
class="flex items-center tooltip"
class="tooltip flex items-center"
data-tip="Stop using this relay"
on:click={() => removeWriteRelay(url)}>
<Icon icon="close-circle" />
@@ -70,20 +75,19 @@
</div>
</Collapse>
<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" />
Inbox Relays
</h2>
<p slot="description" class="text-sm">
These relays will be advertised on your profile as places where other people should
send notes intended for you. Be sure to select relays that will accept notes that
tag you.
These relays will be advertised on your profile as places where other people should send notes
intended for you. Be sure to select relays that will accept notes that tag you.
</p>
<div class="column gap-2">
{#each $readRelayUrls.sort() as url (url)}
<RelayItem {url}>
<Button
class="flex items-center tooltip"
class="tooltip flex items-center"
data-tip="Stop using this relay"
on:click={() => removeReadRelay(url)}>
<Icon icon="close-circle" />
@@ -99,20 +103,20 @@
</div>
</Collapse>
<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" />
Messaging Relays
</h2>
<p slot="description" class="text-sm">
These relays will be advertised on your profile as places you use to send and
receive direct messages. Be sure to select relays that will accept your messages
and messages from people you'd like to be in contact with.
These relays will be advertised on your profile as places you use to send and receive direct
messages. Be sure to select relays that will accept your messages and messages from people
you'd like to be in contact with.
</p>
<div class="column gap-2">
{#each $inboxRelayUrls.sort() as url (url)}
<RelayItem {url}>
<Button
class="flex items-center tooltip"
class="tooltip flex items-center"
data-tip="Stop using this relay"
on:click={() => removeInboxRelay(url)}>
<Icon icon="close-circle" />
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import {onMount} from "svelte"
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 {subscribe} from "@welshman/app"
import {fly, slide} from "@lib/transition"
@@ -20,7 +20,15 @@
import Divider from "@lib/components/Divider.svelte"
import ChannelMessage from "@app/components/ChannelMessage.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"
const {nrelay, room = GENERAL} = $page.params