Compare commits

..

1 Commits

Author SHA1 Message Date
userAdityaa 2df890f7c5 feat: add full profile page at /people/[npub] 2026-06-21 23:10:04 +05:30
165 changed files with 1832 additions and 1486 deletions
-1
View File
@@ -74,7 +74,6 @@
"@vite-pwa/sveltekit": "^1.1.0", "@vite-pwa/sveltekit": "^1.1.0",
"@welshman/app": "^0.8.16", "@welshman/app": "^0.8.16",
"@welshman/content": "^0.8.16", "@welshman/content": "^0.8.16",
"@welshman/domain": "^0.8.16",
"@welshman/editor": "^0.8.16", "@welshman/editor": "^0.8.16",
"@welshman/feeds": "^0.8.16", "@welshman/feeds": "^0.8.16",
"@welshman/lib": "^0.8.16", "@welshman/lib": "^0.8.16",
+1 -1
View File
@@ -18,7 +18,7 @@ import {derived, get} from "svelte/store"
import {map, not, nthEq, reject, removeUndefined, uniqBy} from "@welshman/lib" import {map, not, nthEq, reject, removeUndefined, uniqBy} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util" import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
import {signer} from "@app/welshman" import {signer} from "@welshman/app"
import {load} from "@welshman/net" import {load} from "@welshman/net"
import {getLivekitEndpoint} from "$lib/livekit" import {getLivekitEndpoint} from "$lib/livekit"
import {AbortError, TimeoutError, whenAborted, whenTimeout} from "$lib/util" import {AbortError, TimeoutError, whenAborted, whenTimeout} from "$lib/util"
+1 -4
View File
@@ -2,8 +2,7 @@ import {DELETE, PROFILE, getPubkeyTagValues} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {append, call, on, reject, remove, sort, sortBy, spec, uniq, uniqBy} from "@welshman/lib" import {append, call, on, reject, remove, sort, sortBy, spec, uniq, uniqBy} from "@welshman/lib"
import type {Override} from "@welshman/lib" import type {Override} from "@welshman/lib"
import {Profiles} from "@welshman/app" import {createSearch, displayProfileByPubkey, pubkey, repository} from "@welshman/app"
import {app, createSearch, pubkey, repository} from "@app/welshman"
import {derived, readable} from "svelte/store" import {derived, readable} from "svelte/store"
import {DM_KINDS} from "@app/content" import {DM_KINDS} from "@app/content"
import type {RepositoryUpdate} from "@welshman/net" import type {RepositoryUpdate} from "@welshman/net"
@@ -36,8 +35,6 @@ export const chatsById = call(() => {
const chatsByPubkey = new Map<string, string[]>() const chatsByPubkey = new Map<string, string[]>()
const addSearchText = (chat: Override<Chat, {search_text?: string}>) => { const addSearchText = (chat: Override<Chat, {search_text?: string}>) => {
const displayProfileByPubkey = (pk: string) => app.use(Profiles).display(pk).get()
chat.search_text = chat.search_text =
chat.pubkeys.length === 1 chat.pubkeys.length === 1
? displayProfileByPubkey(chat.pubkeys[0]) + " note to self" ? displayProfileByPubkey(chat.pubkeys[0]) + " note to self"
+3 -4
View File
@@ -1,7 +1,6 @@
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {COMMENT, makeEvent} from "@welshman/util" import {COMMENT, makeEvent} from "@welshman/util"
import {Thunks, Tags} from "@welshman/app" import {publishThunk, tagEventForComment} from "@welshman/app"
import {app} from "@app/welshman"
export type CommentParams = { export type CommentParams = {
event: TrustedEvent event: TrustedEvent
@@ -11,7 +10,7 @@ export type CommentParams = {
} }
export const makeComment = ({url, event, content, tags = []}: CommentParams) => export const makeComment = ({url, event, content, tags = []}: CommentParams) =>
makeEvent(COMMENT, {content, tags: [...tags, ...app.use(Tags).tagEventForComment(event, url)]}) makeEvent(COMMENT, {content, tags: [...tags, ...tagEventForComment(event, url)]})
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) => export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
app.use(Thunks).publish({event: makeComment({url: relays[0], ...params}), relays}) publishThunk({event: makeComment({url: relays[0], ...params}), relays})
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {pubkey} from "@app/welshman" import {pubkey} from "@welshman/app"
import Dialog from "@lib/components/Dialog.svelte" import Dialog from "@lib/components/Dialog.svelte"
import Landing from "@app/components/Landing.svelte" import Landing from "@app/components/Landing.svelte"
import Toast from "@app/components/Toast.svelte" import Toast from "@app/components/Toast.svelte"
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import {getTagValue, getAddress} from "@welshman/util" import {getTagValue, getAddress} from "@welshman/util"
import {pubkey} from "@app/welshman" import {pubkey} from "@welshman/app"
import Pen2 from "@assets/icons/pen-2.svg?dataurl" import Pen2 from "@assets/icons/pen-2.svg?dataurl"
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"
+3 -4
View File
@@ -3,8 +3,7 @@
import {writable} from "svelte/store" import {writable} from "svelte/store"
import {randomId, HOUR} from "@welshman/lib" import {randomId, HOUR} from "@welshman/lib"
import {makeEvent, EVENT_TIME} from "@welshman/util" import {makeEvent, EVENT_TIME} from "@welshman/util"
import {app} from "@app/welshman" import {publishThunk, waitForThunkError} from "@welshman/app"
import {Thunks} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import {daysBetween} from "@lib/util" import {daysBetween} from "@lib/util"
import GallerySend from "@assets/icons/gallery-send.svg?dataurl" import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
@@ -108,8 +107,8 @@
} }
const event = makeEvent(EVENT_TIME, {content, tags}) const event = makeEvent(EVENT_TIME, {content, tags})
const calendarThunk = app.use(Thunks).publish({event, relays: [url]}) const calendarThunk = publishThunk({event, relays: [url]})
const error = await calendarThunk.waitForError() const error = await waitForThunkError(calendarThunk)
if (error) { if (error) {
return pushToast({theme: "error", message: error}) return pushToast({theme: "error", message: error})
+13 -9
View File
@@ -26,10 +26,14 @@
DIRECT_MESSAGE, DIRECT_MESSAGE,
DIRECT_MESSAGE_FILE, DIRECT_MESSAGE_FILE,
} from "@welshman/util" } from "@welshman/util"
import {app, pubkey} from "@app/welshman" import {
import {Tags, Wraps, Thunks, MessagingRelayLists} from "@welshman/app" pubkey,
tagPubkey,
const messagingRelayListsByPubkey = app.use(MessagingRelayLists).index.$ sendWrapped,
mergeThunks,
loadMessagingRelayList,
messagingRelayListsByPubkey,
} from "@welshman/app"
import Danger from "@assets/icons/danger-triangle.svg?dataurl" import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import ArrowLeft from "@assets/icons/arrow-left.svg?dataurl" import ArrowLeft from "@assets/icons/arrow-left.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -92,7 +96,7 @@
const onSubmit = async (params: EventContent) => { const onSubmit = async (params: EventContent) => {
try { try {
const ptags = remove($pubkey!, pubkeys).map(pk => app.use(Tags).tagPubkey(pk)) const ptags = remove($pubkey!, pubkeys).map(tagPubkey)
// Remove p tags since they result in forking the conversation // Remove p tags since they result in forking the conversation
params.tags = params.tags.filter(nthNe(0, "p")) params.tags = params.tags.filter(nthNe(0, "p"))
@@ -105,7 +109,7 @@
return return
} }
await app.use(Wraps).publish({ await sendWrapped({
event: makeDelete({event: eventToEdit, protect: false}), event: makeDelete({event: eventToEdit, protect: false}),
recipients: pubkeys, recipients: pubkeys,
pow: 16, pow: 16,
@@ -154,7 +158,7 @@
// Sleep 1 second between each one to make sure timestamps are distinct // Sleep 1 second between each one to make sure timestamps are distinct
const thunks = await Promise.all( const thunks = await Promise.all(
Array.from(enumerate(templates)).map(([i, event]) => Array.from(enumerate(templates)).map(([i, event]) =>
app.use(Wraps).publish({ sendWrapped({
event, event,
recipients: pubkeys, recipients: pubkeys,
delay: $userSettingsValues.send_delay + ms(i), delay: $userSettingsValues.send_delay + ms(i),
@@ -167,7 +171,7 @@
timeout: 30_000, timeout: 30_000,
children: { children: {
component: ThunkToast, component: ThunkToast,
props: {thunk: app.use(Thunks).merge(thunks)}, props: {thunk: mergeThunks(thunks)},
}, },
}) })
} finally { } finally {
@@ -230,7 +234,7 @@
onMount(() => { onMount(() => {
for (const pubkey of others) { for (const pubkey of others) {
app.use(MessagingRelayLists).load(pubkey) loadMessagingRelayList(pubkey)
} }
}) })
+2 -3
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {app} from "@app/welshman" import {displayProfileByPubkey} from "@welshman/app"
import {Profiles} from "@welshman/app"
import {slide} from "@lib/transition" import {slide} from "@lib/transition"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -22,7 +21,7 @@
<div <div
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8" class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8"
transition:slide> transition:slide>
<p class="text-xs text-primary">{verb} @{app.use(Profiles).display(event.pubkey).get()}</p> <p class="text-xs text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
{#key event.id} {#key event.id}
<NoteContentMinimal trimParent {event} /> <NoteContentMinimal trimParent {event} />
{/key} {/key}
+5 -9
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {app, userRelayList} from "@app/welshman" import {getRelaysFromList} from "@welshman/util"
import {RelayLists, MessagingRelayLists} from "@welshman/app" import {waitForThunkError, setMessagingRelays, userRelayList, setRelays} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
@@ -29,10 +29,8 @@
loading = true loading = true
try { try {
if (($userRelayList?.urls() ?? []).length === 0) { if (getRelaysFromList($userRelayList).length === 0) {
const error = await ( const error = await waitForThunkError(await setRelays(DEFAULT_RELAYS.map(r => ["r", r])))
await app.use(RelayLists).setRelays(DEFAULT_RELAYS.map(r => ["r", r]))
).waitForError()
if (error) { if (error) {
pushToast({theme: "error", message: error}) pushToast({theme: "error", message: error})
@@ -40,9 +38,7 @@
} }
} }
const error = await ( const error = await waitForThunkError(await setMessagingRelays(DEFAULT_MESSAGING_RELAYS))
await app.use(MessagingRelayLists).setUrls(DEFAULT_MESSAGING_RELAYS)
).waitForError()
if (error) { if (error) {
pushToast({theme: "error", message: error}) pushToast({theme: "error", message: error})
+2 -3
View File
@@ -3,8 +3,7 @@
import {page} from "$app/stores" import {page} from "$app/stores"
import {remove, uniq, formatTimestamp} from "@welshman/lib" import {remove, uniq, formatTimestamp} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {app, pubkey} from "@app/welshman" import {pubkey, loadMessagingRelayList} from "@welshman/app"
import {MessagingRelayLists} from "@welshman/app"
import {fade} from "@lib/transition" import {fade} from "@lib/transition"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileName from "@app/components/ProfileName.svelte" import ProfileName from "@app/components/ProfileName.svelte"
@@ -29,7 +28,7 @@
onMount(() => { onMount(() => {
for (const pk of others) { for (const pk of others) {
app.use(MessagingRelayLists).load(pk) loadMessagingRelayList(pk)
} }
}) })
</script> </script>
+5 -6
View File
@@ -2,8 +2,7 @@
import {type Instance} from "tippy.js" import {type Instance} from "tippy.js"
import {hash, formatTimestampAsTime} from "@welshman/lib" import {hash, formatTimestampAsTime} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import {app, thunks, pubkey} from "@app/welshman" import {thunks, mergeThunks, pubkey, deriveProfileDisplay, sendWrapped} from "@welshman/app"
import {Thunks, Profiles, Wraps} from "@welshman/app"
import {isMobile} from "@lib/html" import {isMobile} from "@lib/html"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl" import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -34,18 +33,18 @@
const {event, replyTo, canEdit, onEdit, pubkeys, showPubkey = false}: Props = $props() const {event, replyTo, canEdit, onEdit, pubkeys, showPubkey = false}: Props = $props()
const isOwn = event.pubkey === $pubkey const isOwn = event.pubkey === $pubkey
const profileDisplay = app.use(Profiles).display(event.pubkey).$ const profileDisplay = deriveProfileDisplay(event.pubkey)
const thunk = app.use(Thunks).merge($thunks.filter(t => t.event.id === event.id)) const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
const [_, colorValue] = colors[hash(event.pubkey) % colors.length] const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
const reply = () => replyTo(event) const reply = () => replyTo(event)
const edit = canEdit?.(event) ? () => onEdit?.(event) : undefined const edit = canEdit?.(event) ? () => onEdit?.(event) : undefined
const deleteReaction = (event: TrustedEvent) => const deleteReaction = (event: TrustedEvent) =>
app.use(Wraps).publish({event: makeDelete({event, protect: false}), recipients: pubkeys, pow: 16}) sendWrapped({event: makeDelete({event, protect: false}), recipients: pubkeys, pow: 16})
const createReaction = (template: EventContent) => const createReaction = (template: EventContent) =>
app.use(Wraps).publish({ sendWrapped({
event: makeReaction({event, protect: false, ...template}), event: makeReaction({event, protect: false, ...template}),
recipients: pubkeys, recipients: pubkeys,
pow: 16, pow: 16,
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {NativeEmoji} from "emoji-picker-element/shared" import type {NativeEmoji} from "emoji-picker-element/shared"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {app} from "@app/welshman" import {sendWrapped} from "@welshman/app"
import {Wraps} from "@welshman/app"
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl" import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import EmojiButton from "@lib/components/EmojiButton.svelte" import EmojiButton from "@lib/components/EmojiButton.svelte"
@@ -16,7 +15,7 @@
const {event, pubkeys}: Props = $props() const {event, pubkeys}: Props = $props()
const onEmoji = (emoji: NativeEmoji) => const onEmoji = (emoji: NativeEmoji) =>
app.use(Wraps).publish({ sendWrapped({
event: makeReaction({event, content: emoji.unicode, protect: false}), event: makeReaction({event, content: emoji.unicode, protect: false}),
recipients: pubkeys, recipients: pubkeys,
pow: 16, pow: 16,
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {NativeEmoji} from "emoji-picker-element/shared" import type {NativeEmoji} from "emoji-picker-element/shared"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {app} from "@app/welshman" import {sendWrapped} from "@welshman/app"
import {Wraps} from "@welshman/app"
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl" import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
import Pen from "@assets/icons/pen.svg?dataurl" import Pen from "@assets/icons/pen.svg?dataurl"
import Reply from "@assets/icons/reply-2.svg?dataurl" import Reply from "@assets/icons/reply-2.svg?dataurl"
@@ -29,7 +28,7 @@
const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => { const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => {
history.back() history.back()
app.use(Wraps).publish({ sendWrapped({
event: makeReaction({event, content: emoji.unicode, protect: false}), event: makeReaction({event, content: emoji.unicode, protect: false}),
recipients: pubkeys, recipients: pubkeys,
pow: 16, pow: 16,
+2 -3
View File
@@ -4,8 +4,7 @@
import {writable} from "svelte/store" import {writable} from "svelte/store"
import {tryCatch, uniq} from "@welshman/lib" import {tryCatch, uniq} from "@welshman/lib"
import {fromNostrURI} from "@welshman/util" import {fromNostrURI} from "@welshman/util"
import {app} from "@app/welshman" import {loadMessagingRelayList} from "@welshman/app"
import {MessagingRelayLists} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
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"
@@ -35,7 +34,7 @@
let pubkeys: string[] = $state([]) let pubkeys: string[] = $state([])
$effect(() => { $effect(() => {
pubkeys.forEach(pubkey => app.use(MessagingRelayLists).load(pubkey)) pubkeys.forEach(pubkey => loadMessagingRelayList(pubkey))
}) })
onMount(() => { onMount(() => {
+1 -1
View File
@@ -2,7 +2,7 @@
import {uniq} from "@welshman/lib" import {uniq} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import {getTagValue, getTagValues, getAddress} from "@welshman/util" import {getTagValue, getTagValues, getAddress} from "@welshman/util"
import {pubkey} from "@app/welshman" import {pubkey} from "@welshman/app"
import Pen2 from "@assets/icons/pen-2.svg?dataurl" import Pen2 from "@assets/icons/pen-2.svg?dataurl"
import {normalizeTopic} from "@lib/util" import {normalizeTopic} from "@lib/util"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
+3 -4
View File
@@ -2,8 +2,7 @@
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {removeUndefined, randomId, uniq} from "@welshman/lib" import {removeUndefined, randomId, uniq} from "@welshman/lib"
import {makeEvent, CLASSIFIED} from "@welshman/util" import {makeEvent, CLASSIFIED} from "@welshman/util"
import {app} from "@app/welshman" import {publishThunk, waitForThunkError} from "@welshman/app"
import {Thunks} from "@welshman/app"
import {isMobile, preventDefault} from "@lib/html" import {isMobile, preventDefault} from "@lib/html"
import {normalizeTopic} from "@lib/util" import {normalizeTopic} from "@lib/util"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -119,12 +118,12 @@
} }
} }
const classifiedThunk = app.use(Thunks).publish({ const classifiedThunk = publishThunk({
relays: [url], relays: [url],
event: makeEvent(CLASSIFIED, {content, tags}), event: makeEvent(CLASSIFIED, {content, tags}),
}) })
const error = await classifiedThunk.waitForError() const error = await waitForThunkError(classifiedThunk)
if (error) { if (error) {
return pushToast({theme: "error", message: error}) return pushToast({theme: "error", message: error})
@@ -9,7 +9,7 @@
tagsFromIMeta, tagsFromIMeta,
makeBlossomAuthEvent, makeBlossomAuthEvent,
} from "@welshman/util" } from "@welshman/util"
import {signer} from "@app/welshman" import {signer} from "@welshman/app"
import LinkRound from "@assets/icons/link-round.svg?dataurl" import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
+2 -3
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import {removeUndefined} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import type {ProfilePointer} from "@welshman/content" import type {ProfilePointer} from "@welshman/content"
import {app} from "@app/welshman" import {deriveProfileDisplay} from "@welshman/app"
import {Profiles} from "@welshman/app"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte" import ProfileDetail from "@app/components/ProfileDetail.svelte"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
@@ -14,7 +13,7 @@
const {value, url}: Props = $props() const {value, url}: Props = $props()
const display = app.use(Profiles).display(value.pubkey, removeUndefined([url])).$ const display = deriveProfileDisplay(value.pubkey, removeUndefined([url]))
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url}) const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url})
</script> </script>
+1 -1
View File
@@ -4,7 +4,7 @@
import type {Writable} from "svelte/store" import type {Writable} from "svelte/store"
import type {Instance} from "tippy.js" import type {Instance} from "tippy.js"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import {createSearch} from "@app/welshman" import {createSearch} from "@welshman/app"
import {currencyOptions, displayCurrency} from "@lib/currency" import {currencyOptions, displayCurrency} from "@lib/currency"
import Suggestions from "@lib/components/Suggestions.svelte" import Suggestions from "@lib/components/Suggestions.svelte"
import CurrencySuggestion from "@app/components/CurrencySuggestion.svelte" import CurrencySuggestion from "@app/components/CurrencySuggestion.svelte"
+1 -1
View File
@@ -5,7 +5,7 @@
import {load} from "@welshman/net" import {load} from "@welshman/net"
import {deriveArray, deriveEventsById} from "@welshman/store" import {deriveArray, deriveEventsById} from "@welshman/store"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {repository} from "@app/welshman" import {repository} from "@welshman/app"
import {deriveChecked} from "@app/notifications" import {deriveChecked} from "@app/notifications"
import Reply from "@assets/icons/reply-2.svg?dataurl" import Reply from "@assets/icons/reply-2.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
+1 -1
View File
@@ -4,7 +4,7 @@
import {LOCALE, secondsToDate} from "@welshman/lib" import {LOCALE, secondsToDate} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import {tracker} from "@app/welshman" import {tracker} from "@welshman/app"
import FileText from "@assets/icons/file-text.svg?dataurl" import FileText from "@assets/icons/file-text.svg?dataurl"
import Copy from "@assets/icons/copy.svg?dataurl" import Copy from "@assets/icons/copy.svg?dataurl"
import UserCircle from "@assets/icons/user-circle.svg?dataurl" import UserCircle from "@assets/icons/user-circle.svg?dataurl"
+2 -3
View File
@@ -4,8 +4,7 @@
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {COMMENT, ManagementMethod} from "@welshman/util" import {COMMENT, ManagementMethod} from "@welshman/util"
import {app, pubkey, repository, relaysByUrl} from "@app/welshman" import {pubkey, repository, relaysByUrl, manageRelay} from "@welshman/app"
import {RelayManagement} from "@welshman/app"
import ShareCircle from "@assets/icons/share-circle.svg?dataurl" import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl" import Code2 from "@assets/icons/code-2.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl" import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
@@ -57,7 +56,7 @@
title: `Delete ${noun}`, title: `Delete ${noun}`,
message: `Are you sure you want to delete this ${noun.toLowerCase()} from the space?`, message: `Are you sure you want to delete this ${noun.toLowerCase()} from the space?`,
confirm: async () => { confirm: async () => {
const {error} = await app.use(RelayManagement).post(url, { const {error} = await manageRelay(url, {
method: ManagementMethod.BanEvent, method: ManagementMethod.BanEvent,
params: [event.id], params: [event.id],
}) })
+3 -4
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import {writable} from "svelte/store" import {writable} from "svelte/store"
import {makeEvent, ZAP_GOAL} from "@welshman/util" import {makeEvent, ZAP_GOAL} from "@welshman/util"
import {app} from "@app/welshman" import {publishThunk, waitForThunkError} from "@welshman/app"
import {Thunks} from "@welshman/app"
import {isMobile, preventDefault} from "@lib/html" import {isMobile, preventDefault} from "@lib/html"
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl" import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
import Bolt from "@assets/icons/bolt.svg?dataurl" import Bolt from "@assets/icons/bolt.svg?dataurl"
@@ -94,12 +93,12 @@
tags.push(["h", h]) tags.push(["h", h])
} }
const goalThunk = app.use(Thunks).publish({ const goalThunk = publishThunk({
relays: [url], relays: [url],
event: makeEvent(ZAP_GOAL, {content: title, tags}), event: makeEvent(ZAP_GOAL, {content: title, tags}),
}) })
const error = await goalThunk.waitForError() const error = await waitForThunkError(goalThunk)
if (error) { if (error) {
return pushToast({theme: "error", message: error}) return pushToast({theme: "error", message: error})
+9 -13
View File
@@ -2,10 +2,8 @@
import {now, DAY, uniq, sum} from "@welshman/lib" import {now, DAY, uniq, sum} from "@welshman/lib"
import type {Zap, TrustedEvent} from "@welshman/util" import type {Zap, TrustedEvent} from "@welshman/util"
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util" import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
import {derived} from "svelte/store" import {deriveItemsByKey, deriveArray} from "@welshman/store"
import {deriveEvents} from "@welshman/store" import {repository, getValidZap} from "@welshman/app"
import {app, repository} from "@app/welshman"
import {Zappers} from "@welshman/app"
import Bolt from "@assets/icons/bolt.svg?dataurl" import Bolt from "@assets/icons/bolt.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import ZapButton from "@app/components/ZapButton.svelte" import ZapButton from "@app/components/ZapButton.svelte"
@@ -18,15 +16,13 @@
const {url, event, ...props}: Props = $props() const {url, event, ...props}: Props = $props()
// Validated zaps for this goal. `validZapReceipts` is a reactive Projection const zaps = deriveArray(
// (resolves recipient zappers from loaded profiles); we re-derive it whenever deriveItemsByKey<Zap>({
// the set of ZAP_RESPONSE events in the repository changes. repository,
const zapReceipts = deriveEvents({repository, filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}]}) getKey: zap => zap.response.id,
const zaps = derived( filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
zapReceipts, eventToItem: (response: TrustedEvent) => getValidZap(response, event),
($receipts: TrustedEvent[], set) => }),
app.use(Zappers).validZapReceipts($receipts, event).$.subscribe(set),
[] as Zap[],
) )
const goalAmount = parseInt(getTagValue("amount", event.tags) || "0") const goalAmount = parseInt(getTagValue("amount", event.tags) || "0")
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {createSearch} from "@app/welshman" import {createSearch} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {session} from "@app/welshman" import {session} from "@welshman/app"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import CheckCircle from "@assets/icons/check-circle.svg?dataurl" import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
+2 -3
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import {app} from "@app/welshman" import {deriveZapperForPubkey} from "@welshman/app"
import {Zappers} from "@welshman/app"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
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"
@@ -13,7 +12,7 @@
const {pubkey} = $props() const {pubkey} = $props()
const zapper = app.use(Zappers).forPubkey(pubkey).$ const zapper = deriveZapperForPubkey(pubkey)
const back = () => history.back() const back = () => history.back()
</script> </script>
+2 -8
View File
@@ -1,14 +1,8 @@
<script lang="ts"> <script lang="ts">
import {Client} from "@pomade/core" import {Client} from "@pomade/core"
import {getPubkey} from "@welshman/util" import {getPubkey} from "@welshman/util"
import {session} from "@app/welshman" import type {SessionPomade} from "@welshman/app"
// TODO(welshman-migration): `SessionPomade` was removed from @welshman/app; the import {session} from "@welshman/app"
// new generic Session nests its descriptor under `.data` ({clientOptions, email}),
// whereas this code destructures `email`/`clientOptions` from the top level.
// Confirm whether flotilla's FlotillaSession still surfaces these at the top
// level (loginWithPomade stores them under `.data` via toSession) and adjust the
// destructuring accordingly. Local type kept loose to avoid a broken import.
type SessionPomade = {email: string; clientOptions: {secret: string; peers: any}}
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
+2 -7
View File
@@ -1,12 +1,7 @@
<script lang="ts"> <script lang="ts">
import {Client} from "@pomade/core" import {Client} from "@pomade/core"
import {session} from "@app/welshman" import type {SessionPomade} from "@welshman/app"
// TODO(welshman-migration): `SessionPomade` was removed from @welshman/app; the import {session} from "@welshman/app"
// new generic Session nests its descriptor under `.data` ({clientOptions, email}),
// whereas this code destructures from the top level. Confirm whether flotilla's
// FlotillaSession still surfaces these at the top level and adjust accordingly.
// Local type kept loose to avoid a broken import.
type SessionPomade = {email: string; clientOptions: {peers: any}}
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
+5 -5
View File
@@ -2,7 +2,7 @@
import {onMount} from "svelte" import {onMount} from "svelte"
import {Capacitor} from "@capacitor/core" import {Capacitor} from "@capacitor/core"
import {getNip07, getNip55, Nip55Signer} from "@welshman/signer" import {getNip07, getNip55, Nip55Signer} from "@welshman/signer"
import {addSession, type FlotillaSession, makeNip07Session, makeNip55Session} from "@app/welshman" import {addSession, type Session, makeNip07Session, makeNip55Session} from "@welshman/app"
import Widget from "@assets/icons/widget-4.svg?dataurl" import Widget from "@assets/icons/widget-4.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl" import Letter from "@assets/icons/letter.svg?dataurl"
import Cpu from "@assets/icons/cpu-bolt.svg?dataurl" import Cpu from "@assets/icons/cpu-bolt.svg?dataurl"
@@ -32,8 +32,8 @@
const signUp = () => pushModal(SignUp) const signUp = () => pushModal(SignUp)
const onSuccess = async (session: FlotillaSession, pubkey: string) => { const onSuccess = async (session: Session) => {
addSession({...session, pubkey}) addSession(session)
setChecked("*") setChecked("*")
clearModals() clearModals()
} }
@@ -45,7 +45,7 @@
const pubkey = await getNip07()?.getPublicKey() const pubkey = await getNip07()?.getPublicKey()
if (pubkey) { if (pubkey) {
await onSuccess(makeNip07Session(pubkey), pubkey) await onSuccess(makeNip07Session(pubkey))
} else { } else {
pushToast({ pushToast({
theme: "error", theme: "error",
@@ -65,7 +65,7 @@
const pubkey = await signer.getPubkey() const pubkey = await signer.getPubkey()
if (pubkey) { if (pubkey) {
await onSuccess(makeNip55Session(pubkey, app.packageName), pubkey) await onSuccess(makeNip55Session(pubkey, app.packageName))
} else { } else {
pushToast({ pushToast({
theme: "error", theme: "error",
+1 -1
View File
@@ -4,7 +4,7 @@
import type {Nip46ResponseWithResult} from "@welshman/signer" import type {Nip46ResponseWithResult} from "@welshman/signer"
import {Nip46Broker} from "@welshman/signer" import {Nip46Broker} from "@welshman/signer"
import {makeSecret} from "@welshman/util" import {makeSecret} from "@welshman/util"
import {loginWithNip01, loginWithNip46} from "@app/welshman" import {loginWithNip01, loginWithNip46} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
+2 -2
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {bytesToHex} from "@welshman/lib" import {bytesToHex} from "@welshman/lib"
import {loginWithNip01} from "@app/welshman" import {loginWithNip01} from "@welshman/app"
import {decrypt} from "nostr-tools/nip49" import {decrypt} from "nostr-tools/nip49"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import {nsecDecode} from "@lib/util" import {nsecDecode} from "@lib/util"
@@ -56,7 +56,7 @@
}) })
} }
await loginWithNip01(secret) loginWithNip01(secret)
setChecked("*") setChecked("*")
clearModals() clearModals()
} catch (e) { } catch (e) {
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {Capacitor} from "@capacitor/core" import {Capacitor} from "@capacitor/core"
import {pubkey} from "@app/welshman" import {pubkey} from "@welshman/app"
import Server from "@assets/icons/server.svg?dataurl" import Server from "@assets/icons/server.svg?dataurl"
import GalleryMinimalistic from "@assets/icons/gallery-minimalistic.svg?dataurl" import GalleryMinimalistic from "@assets/icons/gallery-minimalistic.svg?dataurl"
import Shield from "@assets/icons/shield-minimalistic.svg?dataurl" import Shield from "@assets/icons/shield-minimalistic.svg?dataurl"
@@ -3,10 +3,8 @@
import {sum} from "@welshman/lib" import {sum} from "@welshman/lib"
import type {Zap, TrustedEvent} from "@welshman/util" import type {Zap, TrustedEvent} from "@welshman/util"
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util" import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
import {derived} from "svelte/store" import {deriveItemsByKey, deriveArray} from "@welshman/store"
import {deriveEvents} from "@welshman/store" import {repository, getValidZap} from "@welshman/app"
import {app, repository} from "@app/welshman"
import {Zappers} from "@welshman/app"
import Bolt from "@assets/icons/bolt.svg?dataurl" import Bolt from "@assets/icons/bolt.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import ContentMinimal from "@app/components/ContentMinimal.svelte" import ContentMinimal from "@app/components/ContentMinimal.svelte"
@@ -16,14 +14,13 @@
const content = getTagValue("summary", props.event.tags) const content = getTagValue("summary", props.event.tags)
const fakeEvent = {content, tags: props.event.tags} const fakeEvent = {content, tags: props.event.tags}
// Validated zaps for this goal (reactive Projection, re-derived as the set of const zaps = deriveArray(
// ZAP_RESPONSE events in the repository changes). deriveItemsByKey<Zap>({
const zapReceipts = deriveEvents({repository, filters: [{kinds: [ZAP_RESPONSE], "#e": [props.event.id]}]}) repository,
const zaps = derived( getKey: zap => zap.response.id,
zapReceipts, filters: [{kinds: [ZAP_RESPONSE], "#e": [props.event.id]}],
($receipts: TrustedEvent[], set) => eventToItem: (response: TrustedEvent) => getValidZap(response, props.event),
app.use(Zappers).validZapReceipts($receipts, props.event).$.subscribe(set), }),
[] as Zap[],
) )
const goalAmount = parseInt(getTagValue("amount", props.event.tags) || "0") const goalAmount = parseInt(getTagValue("amount", props.event.tags) || "0")
+33 -1
View File
@@ -4,11 +4,16 @@
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import {Router} from "@welshman/router" import {Router} from "@welshman/router"
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl" import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import {fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Popover from "@lib/components/Popover.svelte"
import EmojiButton from "@lib/components/EmojiButton.svelte" import EmojiButton from "@lib/components/EmojiButton.svelte"
import NoteContent from "@app/components/NoteContent.svelte" import NoteContent from "@app/components/NoteContent.svelte"
import NoteCard from "@app/components/NoteCard.svelte" import NoteCard from "@app/components/NoteCard.svelte"
import ReactionSummary from "@app/components/ReactionSummary.svelte" import ReactionSummary from "@app/components/ReactionSummary.svelte"
import ProfileNoteMenu from "@app/components/ProfileNoteMenu.svelte"
import {publishDelete} from "@app/deletes" import {publishDelete} from "@app/deletes"
import {publishReaction} from "@app/reactions" import {publishReaction} from "@app/reactions"
import {canEnforceNip70} from "@app/relays" import {canEnforceNip70} from "@app/relays"
@@ -17,9 +22,10 @@
event: TrustedEvent event: TrustedEvent
children?: Snippet children?: Snippet
url?: string url?: string
editable?: boolean
} }
const {url, event, children}: Props = $props() const {url, event, children, editable = false}: Props = $props()
const relays = url ? [url] : Router.get().Event(event).getUrls() const relays = url ? [url] : Router.get().Event(event).getUrls()
@@ -38,9 +44,35 @@
content: emoji.unicode, content: emoji.unicode,
protect: await shouldProtect, protect: await shouldProtect,
}) })
const toggleMenu = () => {
showMenu = !showMenu
}
const closeMenu = () => {
showMenu = false
}
let showMenu = $state(false)
</script> </script>
<NoteCard {event} {url} class="cv card2 bg-alt"> <NoteCard {event} {url} class="cv card2 bg-alt">
{#if editable}
<div class="flex justify-end">
<div class="relative">
<Button class="btn btn-circle btn-ghost btn-xs" onclick={toggleMenu}>
<Icon icon={MenuDots} size={4} />
</Button>
{#if showMenu}
<Popover hideOnClick onClose={closeMenu}>
<div transition:fly class="absolute right-0 z-popover">
<ProfileNoteMenu {event} onClose={closeMenu} />
</div>
</Popover>
{/if}
</div>
</div>
{/if}
<NoteContent {event} expandMode="inline" /> <NoteContent {event} expandMode="inline" />
<div class="flex w-full justify-between gap-2"> <div class="flex w-full justify-between gap-2">
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-right"> <ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-right">
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import {Client} from "@pomade/core" import {Client} from "@pomade/core"
import type {SessionPomade} from "@welshman/app" import type {SessionPomade} from "@welshman/app"
import {session} from "@app/welshman" import {session} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
@@ -1,12 +1,7 @@
<script lang="ts"> <script lang="ts">
import {Client} from "@pomade/core" import {Client} from "@pomade/core"
import {session} from "@app/welshman" import {session} from "@welshman/app"
// TODO(welshman-migration): `SessionPomade` was removed from @welshman/app; the import type {SessionPomade} from "@welshman/app"
// new generic Session nests its descriptor under `.data` ({clientOptions, email}),
// whereas this code destructures `email` from the top level. Confirm whether
// flotilla's FlotillaSession still surfaces `email` at the top level and adjust.
// Local type kept loose to avoid a broken import.
type SessionPomade = {email: string}
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import Key from "@assets/icons/key.svg?dataurl" import Key from "@assets/icons/key.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
+6 -9
View File
@@ -1,12 +1,11 @@
<script lang="ts"> <script lang="ts">
import UserCircle from "@assets/icons/user-circle.svg?dataurl" import UserCircle from "@assets/icons/user-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" 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 ProfileBadges from "@app/components/ProfileBadges.svelte" import ProfileBadges from "@app/components/ProfileBadges.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte" import {makeProfilePath} from "@app/routes"
import {pushModal} from "@app/modal"
type Props = { type Props = {
pubkey: string pubkey: string
@@ -14,22 +13,20 @@
} }
const {pubkey, url}: Props = $props() const {pubkey, url}: Props = $props()
const openProfile = () => pushModal(ProfileDetail, {pubkey, url})
</script> </script>
<div class="card2 bg-alt flex flex-col gap-4 shadow-md"> <div class="card2 bg-alt flex flex-col gap-4 shadow-md">
<div class="flex justify-between"> <div class="flex justify-between">
<Profile {pubkey} {url} /> <Profile {pubkey} {url} />
<Button onclick={openProfile} class="btn btn-primary hidden sm:flex"> <Link href={makeProfilePath(pubkey)} class="btn btn-primary hidden sm:flex">
<Icon icon={UserCircle} /> <Icon icon={UserCircle} />
View Profile View Profile
</Button> </Link>
</div> </div>
<ProfileInfo {pubkey} {url} /> <ProfileInfo {pubkey} {url} />
<ProfileBadges {pubkey} {url} /> <ProfileBadges {pubkey} {url} />
<Button onclick={openProfile} class="btn btn-primary sm:hidden"> <Link href={makeProfilePath(pubkey)} class="btn btn-primary sm:hidden">
<Icon icon={UserCircle} /> <Icon icon={UserCircle} />
View Profile View Profile
</Button> </Link>
</div> </div>
+3 -4
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import {insertAt, now, randomId, removeAt, removeUndefined} from "@welshman/lib" import {insertAt, now, randomId, removeAt, removeUndefined} from "@welshman/lib"
import {makeEvent, POLL} from "@welshman/util" import {makeEvent, POLL} from "@welshman/util"
import {Thunks} from "@welshman/app" import {publishThunk, waitForThunkError} from "@welshman/app"
import {app} from "@app/welshman"
import {isMobile, preventDefault} from "@lib/html" import {isMobile, preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import HamburgerMenu from "@assets/icons/hamburger-menu.svg?dataurl" import HamburgerMenu from "@assets/icons/hamburger-menu.svg?dataurl"
@@ -143,12 +142,12 @@
tags.push(PROTECTED) tags.push(PROTECTED)
} }
const pollThunk = app.use(Thunks).publish({ const pollThunk = publishThunk({
relays: [url], relays: [url],
event: makeEvent(POLL, {content: title.trim(), tags}), event: makeEvent(POLL, {content: title.trim(), tags}),
}) })
const error = await pollThunk.waitForError() const error = await waitForThunkError(pollThunk)
if (error) { if (error) {
return pushToast({theme: "error", message: error}) return pushToast({theme: "error", message: error})
+5 -7
View File
@@ -2,9 +2,7 @@
import {onDestroy} from "svelte" import {onDestroy} from "svelte"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {POLL_RESPONSE} from "@welshman/util" import {POLL_RESPONSE} from "@welshman/util"
import type {Thunk} from "@welshman/app" import {pubkey, publishThunk, abortThunk} from "@welshman/app"
import {Thunks} from "@welshman/app"
import {pubkey, app} from "@app/welshman"
import {formatTimestampRelative} from "@welshman/lib" import {formatTimestampRelative} from "@welshman/lib"
import {deriveEvents} from "@app/repository" import {deriveEvents} from "@app/repository"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
@@ -52,7 +50,7 @@
const publishSelection = (selection: string[]) => { const publishSelection = (selection: string[]) => {
if (activeThunk) { if (activeThunk) {
activeThunk.abort() abortThunk(activeThunk)
} }
if (selection.length === 0) { if (selection.length === 0) {
@@ -60,7 +58,7 @@
return return
} }
activeThunk = app.use(Thunks).publish({ activeThunk = publishThunk({
relays: [url], relays: [url],
event: makePollResponse({event, selectedIds: selection}), event: makePollResponse({event, selectedIds: selection}),
delay: publishDelay, delay: publishDelay,
@@ -94,7 +92,7 @@
} }
let selectedIds = $state<string[]>([]) let selectedIds = $state<string[]>([])
let activeThunk: Thunk | undefined let activeThunk: ReturnType<typeof publishThunk> | undefined
$effect(() => { $effect(() => {
if (ownResponse) { if (ownResponse) {
@@ -104,7 +102,7 @@
onDestroy(() => { onDestroy(() => {
if (activeThunk) { if (activeThunk) {
activeThunk.abort() abortThunk(activeThunk)
} }
}) })
</script> </script>
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {Client} from "@pomade/core" import {Client} from "@pomade/core"
import {session, isPomadeSession} from "@app/welshman" import {session, isPomadeSession} from "@welshman/app"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl" import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import {fly} from "@lib/transition" import {fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
+5 -5
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {userProfile} from "@app/welshman" import {userProfile} from "@welshman/app"
import Letter from "@assets/icons/letter.svg?dataurl" import Letter from "@assets/icons/letter.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import Widget from "@assets/icons/widget-4.svg?dataurl" import Widget from "@assets/icons/widget-4.svg?dataurl"
@@ -41,8 +41,8 @@
{/if} {/if}
<div class="flex flex-col"> <div class="flex flex-col">
<PrimaryNavItem title="Settings" href="/settings/profile" prefix="/settings"> <PrimaryNavItem title="Settings" href="/settings/profile" prefix="/settings">
{#if $userProfile?.picture()} {#if $userProfile?.picture}
<ImageIcon alt="Settings" src={$userProfile?.picture()} class="rounded-full" size={10} /> <ImageIcon alt="Settings" src={$userProfile?.picture} class="rounded-full" size={10} />
{:else} {:else}
<ImageIcon alt="Settings" src={UserRounded} class="rounded-full" size={8} /> <ImageIcon alt="Settings" src={UserRounded} class="rounded-full" size={8} />
{/if} {/if}
@@ -86,8 +86,8 @@
{/if} {/if}
</div> </div>
<PrimaryNavItem title="Settings" onclick={showSettingsMenu}> <PrimaryNavItem title="Settings" onclick={showSettingsMenu}>
{#if $userProfile?.picture()} {#if $userProfile?.picture}
<ImageIcon alt="Settings" src={$userProfile?.picture()} size={10} class="rounded-full" /> <ImageIcon alt="Settings" src={$userProfile?.picture} size={10} class="rounded-full" />
{:else} {:else}
<ImageIcon alt="Settings" src={Settings} size={8} class="rounded-full" /> <ImageIcon alt="Settings" src={Settings} size={8} class="rounded-full" />
{/if} {/if}
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import {Relays} from "@welshman/app" import {deriveRelayDisplay} from "@welshman/app"
import {app} from "@app/welshman"
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte" import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
import RelayIcon from "@app/components/RelayIcon.svelte" import RelayIcon from "@app/components/RelayIcon.svelte"
import {makeSpacePath, goToSpace} from "@app/routes" import {makeSpacePath, goToSpace} from "@app/routes"
@@ -16,7 +15,7 @@
const path = makeSpacePath(url) const path = makeSpacePath(url)
const display = $derived(app.use(Relays).display(url).$) const display = $derived(deriveRelayDisplay(url))
</script> </script>
<PrimaryNavItem <PrimaryNavItem
+5 -6
View File
@@ -1,9 +1,8 @@
<script lang="ts"> <script lang="ts">
import * as nip19 from "nostr-tools/nip19" import * as nip19 from "nostr-tools/nip19"
import {removeUndefined} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {displayPubkey, displayNip05} from "@welshman/util" import {displayPubkey} from "@welshman/util"
import {Handles, Profiles} from "@welshman/app" import {deriveHandleForPubkey, displayHandle, deriveProfileDisplay} from "@welshman/app"
import {app} from "@app/welshman"
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 ProfileCircle from "@app/components/ProfileCircle.svelte" import ProfileCircle from "@app/components/ProfileCircle.svelte"
@@ -24,8 +23,8 @@
const {pubkey, url, showPubkey, inert, avatarSize = 10}: Props = $props() const {pubkey, url, showPubkey, inert, avatarSize = 10}: Props = $props()
const relays = removeUndefined([url]) const relays = removeUndefined([url])
const profileDisplay = app.use(Profiles).display(pubkey, relays).$ const profileDisplay = deriveProfileDisplay(pubkey, relays)
const handle = app.use(Handles).forPubkey(pubkey).$ const handle = deriveHandleForPubkey(pubkey)
const openProfile = () => { const openProfile = () => {
pushModal(ProfileDetail, {pubkey, url}) pushModal(ProfileDetail, {pubkey, url})
@@ -59,7 +58,7 @@
</div> </div>
{#if $handle} {#if $handle}
<div class="overflow-hidden text-ellipsis text-sm opacity-75"> <div class="overflow-hidden text-ellipsis text-sm opacity-75">
{displayNip05($handle?.nip05)} {displayHandle($handle)}
</div> </div>
{/if} {/if}
{#if showPubkey} {#if showPubkey}
+2 -5
View File
@@ -6,8 +6,7 @@
import {deriveEventsDesc, deriveEventsById} from "@welshman/store" import {deriveEventsDesc, deriveEventsById} from "@welshman/store"
import {formatTimestampRelative} from "@welshman/lib" import {formatTimestampRelative} from "@welshman/lib"
import {NOTE, ROOMS, COMMENT, MESSAGE} from "@welshman/util" import {NOTE, ROOMS, COMMENT, MESSAGE} from "@welshman/util"
import {RelayLists} from "@welshman/app" import {repository, loadRelayList} from "@welshman/app"
import {repository, app} from "@app/welshman"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileSpaces from "@app/components/ProfileSpaces.svelte" import ProfileSpaces from "@app/components/ProfileSpaces.svelte"
import {deriveGroupList, getSpaceUrlsFromGroupList} from "@app/groups" import {deriveGroupList, getSpaceUrlsFromGroupList} from "@app/groups"
@@ -30,10 +29,8 @@
const openSpaces = () => pushModal(ProfileSpaces, {pubkey, url}) const openSpaces = () => pushModal(ProfileSpaces, {pubkey, url})
onMount(async () => { onMount(async () => {
// Make sure we have their relay selections before we load their posts await loadRelayList(pubkey)
await app.use(RelayLists).load(pubkey)
// Load groups and at least one note, regardless of time frame
load({ load({
filters: [ filters: [
{authors: [pubkey], kinds: [ROOMS]}, {authors: [pubkey], kinds: [ROOMS]},
+3 -4
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import cx from "classnames" import cx from "classnames"
import {removeUndefined} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {Profiles} from "@welshman/app" import {deriveProfile} from "@welshman/app"
import {app} from "@app/welshman"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl" import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import ImageIcon from "@lib/components/ImageIcon.svelte" import ImageIcon from "@lib/components/ImageIcon.svelte"
@@ -15,11 +14,11 @@
const {pubkey, url, size = 7, ...props}: Props = $props() const {pubkey, url, size = 7, ...props}: Props = $props()
const profile = app.use(Profiles).one(pubkey, removeUndefined([url])) const profile = deriveProfile(pubkey, removeUndefined([url]))
</script> </script>
<ImageIcon <ImageIcon
{size} {size}
alt="" alt=""
class={cx(props.class, "rounded-full")} class={cx(props.class, "rounded-full")}
src={$profile?.picture() || UserRounded} /> src={$profile?.picture || UserRounded} />
+6 -11
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import cx from "classnames" import cx from "classnames"
import {Profiles} from "@welshman/app" import {loadProfile} from "@welshman/app"
import {app} from "@app/welshman"
import {isMobile} from "@lib/html" import {isMobile} from "@lib/html"
import ProfileCircle from "@app/components/ProfileCircle.svelte" import ProfileCircle from "@app/components/ProfileCircle.svelte"
@@ -23,17 +22,13 @@
: {box: "h-8 w-8", overlap: "-mr-3", overflow: "text-xs"}, : {box: "h-8 w-8", overlap: "-mr-3", overflow: "text-xs"},
) )
for (const pubkey of pubkeys) { $effect(() => {
app.use(Profiles).load(pubkey) for (const pk of pubkeys) {
} loadProfile(pk)
}
const visiblePubkeys = $derived.by(() => {
const filtered = pubkeys.filter(pubkey => app.use(Profiles).get(pubkey)?.picture())
return filtered.length > 0 ? filtered : pubkeys.slice(0, 1)
}) })
const displayPubkeys = $derived(visiblePubkeys.toSorted().slice(0, effectiveLimit)) const displayPubkeys = $derived([...pubkeys].toSorted().slice(0, effectiveLimit))
const overflowCount = $derived(Math.max(0, pubkeys.length - effectiveLimit)) const overflowCount = $derived(Math.max(0, pubkeys.length - effectiveLimit))
</script> </script>
+15 -9
View File
@@ -1,9 +1,15 @@
<script lang="ts"> <script lang="ts">
import {chunk, sleep, uniq} from "@welshman/lib" import {chunk, sleep, uniq} from "@welshman/lib"
import {makeEvent, DELETE, isReplaceable, getAddress} from "@welshman/util" import {
import {ProfileBuilder} from "@welshman/domain" makeEvent,
import {Thunks, RelayLists} from "@welshman/app" createProfile,
import {pubkey, repository, app} from "@app/welshman" PROFILE,
DELETE,
isReplaceable,
getAddress,
RelayMode,
} from "@welshman/util"
import {pubkey, publishThunk, repository, derivePubkeyRelays} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
@@ -25,7 +31,7 @@
let confirmText = $state("") let confirmText = $state("")
const CONFIRM_TEXT = "permanently delete my nostr account" const CONFIRM_TEXT = "permanently delete my nostr account"
const userWriteRelays = app.use(RelayLists).writeUrls($pubkey!).$ const userWriteRelays = derivePubkeyRelays($pubkey!, RelayMode.Write)
const confirmOk = $derived(confirmText.toLowerCase().trim() === CONFIRM_TEXT) const confirmOk = $derived(confirmText.toLowerCase().trim() === CONFIRM_TEXT)
const showProgress = $derived(progress !== undefined) const showProgress = $derived(progress !== undefined)
@@ -38,7 +44,7 @@
} }
const chunks = chunk(500, repository.query([{authors: [$pubkey!]}])) const chunks = chunk(500, repository.query([{authors: [$pubkey!]}]))
const profileEvent = await new ProfileBuilder().update({name: "[deleted]"}).toTemplate() const profileEvent = makeEvent(PROFILE, createProfile({name: "[deleted]"}))
const vanishEvent = makeEvent(62, {tags: [["relay", "ALL_RELAYS"]]}) const vanishEvent = makeEvent(62, {tags: [["relay", "ALL_RELAYS"]]})
const denominator = chunks.length + 2 const denominator = chunks.length + 2
const relays = uniq([...INDEXER_RELAYS, ...$userWriteRelays, ...$userSpaceUrls]) const relays = uniq([...INDEXER_RELAYS, ...$userWriteRelays, ...$userSpaceUrls])
@@ -52,12 +58,12 @@
} }
// First, blank out their profile in case relays don't support deletion by address // First, blank out their profile in case relays don't support deletion by address
await app.use(Thunks).publish({relays, event: profileEvent}) await publishThunk({relays, event: profileEvent})
await incrementProgress() await incrementProgress()
// Next, send a "right to vanish" event to all relays // Next, send a "right to vanish" event to all relays
await app.use(Thunks).publish({relays, event: vanishEvent}) await publishThunk({relays, event: vanishEvent})
await incrementProgress() await incrementProgress()
@@ -73,7 +79,7 @@
} }
} }
await app.use(Thunks).publish({relays, event: makeEvent(DELETE, {tags})}) await publishThunk({relays, event: makeEvent(DELETE, {tags})})
await incrementProgress() await incrementProgress()
} }
+18 -21
View File
@@ -2,18 +2,20 @@
import {onMount} from "svelte" import {onMount} from "svelte"
import {removeUndefined} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {ManagementMethod} from "@welshman/util" import {ManagementMethod} from "@welshman/util"
import {RelayManagement, Profiles, MessagingRelayLists} from "@welshman/app" import {
import {app} from "@app/welshman" manageRelay,
deriveProfile,
displayProfileByPubkey,
loadMessagingRelayList,
} from "@welshman/app"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl" import Code2 from "@assets/icons/code-2.svg?dataurl"
import Letter from "@assets/icons/letter-opened.svg?dataurl" import UserCircle from "@assets/icons/user-circle.svg?dataurl"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl" import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl" import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
import Restart from "@assets/icons/restart.svg?dataurl" import Restart from "@assets/icons/restart.svg?dataurl"
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 ImageIcon from "@lib/components/ImageIcon.svelte"
import Link from "@lib/components/Link.svelte"
import Confirm from "@lib/components/Confirm.svelte" import Confirm from "@lib/components/Confirm.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Popover from "@lib/components/Popover.svelte" import Popover from "@lib/components/Popover.svelte"
@@ -24,11 +26,10 @@
import ProfileInfo from "@app/components/ProfileInfo.svelte" import ProfileInfo from "@app/components/ProfileInfo.svelte"
import EventInfo from "@app/components/EventInfo.svelte" import EventInfo from "@app/components/EventInfo.svelte"
import ProfileBadges from "@app/components/ProfileBadges.svelte" import ProfileBadges from "@app/components/ProfileBadges.svelte"
import {pubkeyLink} from "@app/env"
import {deriveUserIsSpaceAdmin, deriveSpaceBannedPubkeyItems, addSpaceMembers} from "@app/members" import {deriveUserIsSpaceAdmin, deriveSpaceBannedPubkeyItems, addSpaceMembers} from "@app/members"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
import {goToChat} from "@app/routes" import {goToProfile} from "@app/routes"
export type Props = { export type Props = {
pubkey: string pubkey: string
@@ -37,7 +38,7 @@
const {pubkey, url}: Props = $props() const {pubkey, url}: Props = $props()
const profile = app.use(Profiles).one(pubkey, removeUndefined([url])) const profile = deriveProfile(pubkey, removeUndefined([url]))
const userIsAdmin = deriveUserIsSpaceAdmin(url) const userIsAdmin = deriveUserIsSpaceAdmin(url)
@@ -49,9 +50,9 @@
const showInfo = () => pushModal(EventInfo, {url, event: $profile!.event}) const showInfo = () => pushModal(EventInfo, {url, event: $profile!.event})
const openChat = () => goToChat([pubkey]) const viewProfile = () => goToProfile(pubkey)
const toggleMenu = (pubkey: string) => { const toggleMenu = () => {
showMenu = !showMenu showMenu = !showMenu
} }
@@ -62,9 +63,9 @@
const banMember = () => const banMember = () =>
pushModal(Confirm, { pushModal(Confirm, {
title: "Ban User", title: "Ban User",
message: `Are you sure you want to ban @${app.use(Profiles).display(pubkey).get()} from the space?`, message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
confirm: async () => { confirm: async () => {
const {error} = await app.use(RelayManagement).post(url!, { const {error} = await manageRelay(url!, {
method: ManagementMethod.BanPubkey, method: ManagementMethod.BanPubkey,
params: [pubkey], params: [pubkey],
}) })
@@ -92,7 +93,7 @@
let showMenu = $state(false) let showMenu = $state(false)
onMount(() => { onMount(() => {
app.use(MessagingRelayLists).load(pubkey) loadMessagingRelayList(pubkey)
}) })
</script> </script>
@@ -103,7 +104,7 @@
<Profile showPubkey avatarSize={14} {pubkey} {url} /> <Profile showPubkey avatarSize={14} {pubkey} {url} />
{#if $profile || $userIsAdmin} {#if $profile || $userIsAdmin}
<div class="relative"> <div class="relative">
<Button class="btn btn-circle btn-ghost btn-sm" onclick={() => toggleMenu(pubkey)}> <Button class="btn btn-circle btn-ghost btn-sm" onclick={toggleMenu}>
<Icon icon={MenuDots} /> <Icon icon={MenuDots} />
</Button> </Button>
{#if showMenu} {#if showMenu}
@@ -152,13 +153,9 @@
Go back Go back
</Button> </Button>
<div class="flex gap-2"> <div class="flex gap-2">
<Link external href={pubkeyLink(pubkey)} class="btn btn-neutral"> <Button onclick={viewProfile} class="btn btn-primary">
<ImageIcon alt="" src="/coracle.png" /> <Icon icon={UserCircle} />
Open in Coracle View Full Profile
</Link>
<Button onclick={openChat} class="btn btn-primary">
<Icon icon={Letter} />
Message
</Button> </Button>
</div> </div>
</ModalFooter> </ModalFooter>
+5 -16
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import {Profile, ProfileBuilder} from "@welshman/domain" import type {Profile} from "@welshman/util"
import {pubkey, profilesByPubkey} from "@app/welshman" import {makeProfile} from "@welshman/util"
import {pubkey, profilesByPubkey, waitForThunkError} from "@welshman/app"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import {errorMessage} from "@lib/util" import {errorMessage} from "@lib/util"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -11,28 +12,16 @@
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
import {updateProfile} from "@app/profiles" import {updateProfile} from "@app/profiles"
// The edit form binds to plain mutable fields (name/about/nip05/picture), so we const profile = $profilesByPubkey.get($pubkey!) || makeProfile()
// hand it a plain values object rather than a Profile Reader. A Reader exposes its
// raw parsed content as `.values`; a fresh profile starts from an empty builder.
const reader = $profilesByPubkey.get($pubkey!)
const profile = reader instanceof Profile ? {...reader.values} : new ProfileBuilder().update({}).values
const initialValues = {profile} const initialValues = {profile}
const back = () => history.back() const back = () => history.back()
// TODO(welshman-migration): `profile` here is a plain values object (the form binds
// mutable fields), not a Profile Reader. It is typed Profile to match ProfileEditForm's
// Values type and updateProfile's signature, both of which still say Profile; updateProfile
// routes a non-Reader through `new ProfileBuilder().update(profile)`. Confirm the intended
// values vs Reader contract once the shared types settle.
const onsubmit = async ({profile}: {profile: Profile}) => { const onsubmit = async ({profile}: {profile: Profile}) => {
loading = true loading = true
try { try {
// TODO(welshman-migration): updateProfile is async (returns Promise<Thunk>); the old const error = await waitForThunkError(updateProfile({profile}))
// waitForThunkError shim did not await its arg. Awaiting the thunk first before
// .waitForError() — confirm this matches intended behavior.
const error = await (await updateProfile({profile})).waitForError()
if (error) { if (error) {
pushToast({ pushToast({
+19
View File
@@ -4,6 +4,7 @@
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import UserCircle from "@assets/icons/user-circle.svg?dataurl" import UserCircle from "@assets/icons/user-circle.svg?dataurl"
import MapPoint from "@assets/icons/map-point.svg?dataurl" import MapPoint from "@assets/icons/map-point.svg?dataurl"
import LinkRound from "@assets/icons/link-round.svg?dataurl"
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"
@@ -85,6 +86,24 @@
{/snippet} {/snippet}
</Field> </Field>
{#if !isSignup} {#if !isSignup}
<Field>
{#snippet label()}
<p>Website</p>
{/snippet}
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
<Icon icon={LinkRound} />
<input
bind:value={values.profile.website}
class="grow"
type="text"
placeholder="https://" />
</label>
{/snippet}
{#snippet info()}
A link to your personal site or portfolio.
{/snippet}
</Field>
<Field> <Field>
{#snippet label()} {#snippet label()}
<p>Nostr Address</p> <p>Nostr Address</p>
+2 -3
View File
@@ -4,8 +4,7 @@
import {feedFromFilter, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds" import {feedFromFilter, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
import {NOTE, getReplyTags} from "@welshman/util" import {NOTE, getReplyTags} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {Feeds} from "@welshman/app" import {makeFeedController} from "@welshman/app"
import {app} from "@app/welshman"
import {createScroller} from "@lib/html" import {createScroller} from "@lib/html"
import {fly} from "@lib/transition" import {fly} from "@lib/transition"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
@@ -20,7 +19,7 @@
let {url, pubkey, events = $bindable([]), hideLoading = false}: Props = $props() let {url, pubkey, events = $bindable([]), hideLoading = false}: Props = $props()
const ctrl = app.use(Feeds).makeFeedController({ const ctrl = makeFeedController({
useWindowing: true, useWindowing: true,
feed: makeIntersectionFeed( feed: makeIntersectionFeed(
makeRelayFeed(url), makeRelayFeed(url),
+3 -4
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import {removeUndefined} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {Profiles} from "@welshman/app" import {deriveProfile} from "@welshman/app"
import {app} from "@app/welshman"
import ContentMinimal from "@app/components/ContentMinimal.svelte" import ContentMinimal from "@app/components/ContentMinimal.svelte"
export type Props = { export type Props = {
@@ -11,9 +10,9 @@
const {pubkey, url}: Props = $props() const {pubkey, url}: Props = $props()
const profile = app.use(Profiles).one(pubkey, removeUndefined([url])) const profile = deriveProfile(pubkey, removeUndefined([url]))
</script> </script>
{#if $profile} {#if $profile}
<ContentMinimal event={{content: $profile.about() || "", tags: []}} /> <ContentMinimal event={{content: $profile.about || "", tags: []}} />
{/if} {/if}
+1 -1
View File
@@ -4,7 +4,7 @@
import type {Writable} from "svelte/store" import type {Writable} from "svelte/store"
import {type Instance} from "tippy.js" import {type Instance} from "tippy.js"
import {append, remove, uniq} from "@welshman/lib" import {append, remove, uniq} from "@welshman/lib"
import {profileSearch} from "@app/welshman" import {profileSearch} from "@welshman/app"
import Suggestions from "@lib/components/Suggestions.svelte" import Suggestions from "@lib/components/Suggestions.svelte"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
+2 -3
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import {removeUndefined} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {Profiles} from "@welshman/app" import {deriveProfileDisplay} from "@welshman/app"
import {app} from "@app/welshman"
type Props = { type Props = {
pubkey: string pubkey: string
@@ -10,7 +9,7 @@
const {pubkey, url}: Props = $props() const {pubkey, url}: Props = $props()
const profileDisplay = app.use(Profiles).display(pubkey, removeUndefined([url])).$ const profileDisplay = deriveProfileDisplay(pubkey, removeUndefined([url]))
</script> </script>
{$profileDisplay} {$profileDisplay}
+69
View File
@@ -0,0 +1,69 @@
<script lang="ts">
import {nthEq} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {getListTags, getEventTagValues} from "@welshman/util"
import {pin, unpin, tagEvent, userPinList, waitForThunkError} from "@welshman/app"
import {Router} from "@welshman/router"
import Pin from "@assets/icons/pin.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import {publishDelete} from "@app/deletes"
import {pushModal} from "@app/modal"
import {pushToast} from "@app/toast"
type Props = {
event: TrustedEvent
onClose: () => void
}
const {event, onClose}: Props = $props()
const relays = Router.get().Event(event).getUrls()
const pinnedIds = $derived(getEventTagValues(getListTags($userPinList)))
const isPinned = $derived(pinnedIds.includes(event.id))
const togglePin = async () => {
onClose()
const thunk = isPinned ? await unpin(event.id) : await pin(tagEvent(event).find(nthEq(0, "e"))!)
const error = await waitForThunkError(thunk)
if (error) {
pushToast({theme: "error", message: "Failed to update pinned notes."})
} else {
pushToast({message: isPinned ? "Note unpinned." : "Note pinned to your profile."})
}
}
const confirmDelete = () => {
onClose()
pushModal(Confirm, {
title: "Delete Note",
message: "Are you sure you want to delete this note?",
confirm: async () => {
await publishDelete({event, relays, protect: false})
pushToast({message: "Delete request sent."})
},
})
}
</script>
<ul class="menu whitespace-nowrap rounded-box bg-base-100 p-2 shadow-md">
<li>
<Button onclick={togglePin}>
<Icon size={4} icon={Pin} />
{isPinned ? "Unpin from profile" : "Pin to profile"}
</Button>
</li>
<li>
<Button onclick={confirmDelete} class="text-error">
<Icon size={4} icon={TrashBin2} />
Delete note
</Button>
</li>
</ul>
+379
View File
@@ -0,0 +1,379 @@
<script lang="ts">
import * as nip19 from "nostr-tools/nip19"
import cx from "classnames"
import {goto} from "$app/navigation"
import {compressFile} from "@lib/html"
import {displayPubkey} from "@welshman/util"
import {
pubkey,
followLists,
deriveProfile,
deriveProfileDisplay,
deriveUserWotScore,
followersByPubkey,
getFollows,
getFollowers,
follow,
unfollow,
tagPubkey,
} from "@welshman/app"
import {clamp} from "@welshman/lib"
import Copy from "@assets/icons/copy.svg?dataurl"
import QrCode from "@assets/icons/qr-code.svg?dataurl"
import LinkRound from "@assets/icons/link-round.svg?dataurl"
import SquareArrowRight from "@assets/icons/square-arrow-right-up.svg?dataurl"
import Letter from "@assets/icons/letter-opened.svg?dataurl"
import UserPlus from "@assets/icons/user-plus.svg?dataurl"
import PenNewSquare from "@assets/icons/pen-new-square.svg?dataurl"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl"
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
import Shield from "@assets/icons/shield-minimalistic.svg?dataurl"
import UsersGroup from "@assets/icons/users-group-rounded.svg?dataurl"
import {fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
import Popover from "@lib/components/Popover.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ProfileInfo from "@app/components/ProfileInfo.svelte"
import ProfileTrust from "@app/components/ProfileTrust.svelte"
import ProfileSharedSpaces from "@app/components/ProfileSharedSpaces.svelte"
import ProfilePinnedNotes from "@app/components/ProfilePinnedNotes.svelte"
import ProfilePageNotes from "@app/components/ProfilePageNotes.svelte"
import ProfilePageSpaces from "@app/components/ProfilePageSpaces.svelte"
import ProfileQrCode from "@app/components/ProfileQrCode.svelte"
import EventInfo from "@app/components/EventInfo.svelte"
import {deriveGroupList, getSpaceUrlsFromGroupList, userSpaceUrls} from "@app/groups"
import {updateProfile} from "@app/profiles"
import {uploadFile} from "@app/uploads"
import {pushModal} from "@app/modal"
import {clip, pushToast} from "@app/toast"
import {goToChat} from "@app/routes"
type Tab = "about" | "notes" | "spaces"
type Props = {
pubkey: string
}
const {pubkey: target}: Props = $props()
const profile = deriveProfile(target)
const profileDisplay = deriveProfileDisplay(target)
const groupList = deriveGroupList(target)
const score = deriveUserWotScore(target)
const encodedNpub = nip19.npubEncode(target)
const isSelf = $derived($pubkey === target)
const followerCount = $derived.by(() => {
void $followersByPubkey
return getFollowers(target).length
})
const isFollowing = $derived.by(() => {
void $followLists
return $pubkey ? getFollows($pubkey).includes(target) : false
})
const spaceUrls = $derived(getSpaceUrlsFromGroupList($groupList))
const sharedSpaceUrls = $derived($userSpaceUrls.filter(url => spaceUrls.includes(url)))
const displayScore = $derived(isSelf ? followerCount : Math.round(clamp([0, 100], $score)))
const spaceBadgeCount = $derived(isSelf ? spaceUrls.length : sharedSpaceUrls.length)
const website = $derived($profile?.website?.replace(/^https?:\/\//, ""))
const websiteHref = $derived(
$profile?.website?.match(/^https?:\/\//)
? $profile.website
: `https://${$profile?.website || ""}`,
)
let tab = $state<Tab>("about")
let showMenu = $state(false)
let bannerLoading = $state(false)
let bannerInput: HTMLInputElement | undefined = $state()
const setTab = (next: Tab) => {
tab = next
}
const showAboutTab = () => setTab("about")
const showNotesTab = () => setTab("notes")
const showSpacesTab = () => setTab("spaces")
const copyNpub = () => clip(encodedNpub)
const showQr = () => pushModal(ProfileQrCode, {code: encodedNpub})
const toggleMenu = () => {
showMenu = !showMenu
}
const closeMenu = () => {
showMenu = false
}
const showInfo = () => {
closeMenu()
pushModal(EventInfo, {event: $profile!.event})
}
const openSettings = () => goto("/settings/profile")
const openSpaces = () => goto("/spaces")
const openRelaySettings = () => goto("/settings/relays")
const openChat = () => goToChat([target])
const toggleFollow = async () => {
if (!$pubkey || isSelf) return
if (isFollowing) {
await unfollow(target)
} else {
await follow(tagPubkey(target))
}
}
const openBannerPicker = () => bannerInput?.click()
const onBannerChange = async (e: Event) => {
const input = e.target as HTMLInputElement
const file = input.files?.[0]
input.value = ""
if (!file || !$profile) return
bannerLoading = true
try {
const {result} = await uploadFile(await compressFile(file))
if (result?.url) {
await updateProfile({profile: {...$profile, banner: result.url}})
pushToast({message: "Banner updated."})
}
} finally {
bannerLoading = false
}
}
</script>
<div class="col-4">
<div class="flex flex-col gap-3 xl:flex-row xl:items-start">
<div class="min-w-0 flex-1 overflow-hidden border-base-300 bg-alt md:rounded-box md:border">
<div class="relative overflow-hidden border-b border-base-300 bg-base-300">
{#if $profile?.banner}
<img src={$profile.banner} alt="" class="h-28 w-full object-cover sm:h-32 md:h-40" />
{:else}
<div class="h-28 w-full bg-linear-to-br from-base-300 to-base-100 sm:h-32 md:h-40"></div>
{/if}
{#if isSelf}
<Button
class="btn btn-neutral btn-sm absolute top-2 right-2 sm:top-3 sm:right-3"
disabled={bannerLoading}
onclick={openBannerPicker}>
<Icon icon={GallerySend} size={4} />
<span class="hidden sm:inline">Change banner</span>
</Button>
<input
bind:this={bannerInput}
type="file"
accept="image/*"
class="hidden"
onchange={onBannerChange} />
{/if}
</div>
<div class="relative border-b border-base-300 px-4 pb-4 sm:px-3 sm:pb-5">
<div class="-mt-8 sm:-mt-10">
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:gap-4">
<div class="w-fit shrink-0">
<div class="w-fit rounded-full sm:border-4 sm:border-base-200 sm:bg-base-200">
<ProfileCircle pubkey={target} size={16} class="sm:hidden" />
<ProfileCircle pubkey={target} size={20} class="hidden sm:block" />
</div>
</div>
<div class="flex min-w-0 flex-1 flex-col gap-3 sm:gap-2 sm:pt-14">
<div
class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-x-4">
<h1 class="min-w-0 text-xl leading-tight font-bold sm:text-2xl">
{$profileDisplay}
</h1>
{#if (isSelf || $pubkey) && $profile}
<div class="flex items-center gap-2">
{#if isSelf}
<Button
class="btn btn-primary btn-md flex-1 sm:btn-sm sm:flex-none"
onclick={openSettings}>
<Icon icon={PenNewSquare} size={4} />
Edit profile
</Button>
{:else}
<Button
class="btn btn-neutral btn-md flex-1 sm:btn-sm sm:flex-none"
onclick={toggleFollow}>
<Icon icon={UserPlus} size={4} />
{isFollowing ? "Unfollow" : "Follow"}
</Button>
<Button
class="btn btn-primary btn-md flex-1 sm:btn-sm sm:flex-none"
onclick={openChat}>
<Icon icon={Letter} size={4} />
Message
</Button>
{/if}
<div class="relative shrink-0">
<Button class="btn btn-circle btn-ghost btn-sm" onclick={toggleMenu}>
<Icon icon={MenuDots} />
</Button>
{#if showMenu}
<Popover hideOnClick onClose={closeMenu}>
<ul
transition:fly
class="bg-alt menu absolute right-0 z-popover w-48 gap-1 rounded-box p-2 shadow-md">
<li>
<Button onclick={showInfo}>
<Icon icon={Code2} />
User Details
</Button>
</li>
</ul>
</Popover>
{/if}
</div>
</div>
{/if}
</div>
<div class="flex items-center gap-1 text-sm leading-none opacity-75">
<span>{displayPubkey(target)}</span>
<Button onclick={copyNpub} class="btn btn-ghost btn-xs h-5 min-h-5 w-5 p-0">
<Icon size={3} icon={Copy} />
</Button>
<Button onclick={showQr} class="btn btn-ghost btn-xs h-5 min-h-5 w-5 p-0">
<Icon size={3} icon={QrCode} />
</Button>
</div>
{#if website}
<Link
external
href={websiteHref}
class="link link-primary row-2 w-fit text-sm font-medium">
<Icon icon={LinkRound} size={4} />
{website}
<Icon icon={SquareArrowRight} size={4} />
</Link>
{/if}
<div class="flex flex-wrap items-center gap-2">
<span class="badge badge-neutral inline-flex h-6 items-center gap-1.5 border-0">
<Icon icon={Shield} size={3} />
{#if isSelf}
{followerCount} {followerCount === 1 ? "follower" : "followers"}
{:else}
Trust score {displayScore}
{/if}
</span>
{#if spaceBadgeCount > 0}
<button
class="badge badge-neutral inline-flex h-6 items-center gap-1.5 border-0"
onclick={showSpacesTab}>
<Icon icon={UsersGroup} size={3} />
{spaceBadgeCount}
{#if isSelf}
{spaceBadgeCount === 1 ? "space" : "spaces"}
{:else}
shared {spaceBadgeCount === 1 ? "space" : "spaces"}
{/if}
</button>
{/if}
</div>
</div>
</div>
</div>
</div>
<div
class="sticky top-0 z-10 border-b border-base-300 bg-base-200/90 px-4 backdrop-blur-sm sm:px-3">
<div
role="tablist"
class="tabs tabs-bordered -mb-px flex w-full justify-between bg-transparent sm:justify-start">
<button
role="tab"
class={cx("tab flex-none px-0 sm:px-4", {"tab-active": tab === "about"})}
onclick={showAboutTab}>
About
</button>
<button
role="tab"
class={cx("tab flex-none px-0 sm:px-4", {"tab-active": tab === "notes"})}
onclick={showNotesTab}>
Notes
</button>
<button
role="tab"
class={cx("tab flex-none px-0 sm:px-4", {"tab-active": tab === "spaces"})}
onclick={showSpacesTab}>
Spaces
</button>
</div>
</div>
<div class="px-4 py-3 sm:px-3 sm:py-4">
<div class="sm:pl-3">
{#if tab === "about"}
<div class="col-3 sm:col-4">
<ProfileInfo pubkey={target} />
<div class="col-3 xl:hidden">
{@render profileAside()}
</div>
<ProfilePinnedNotes
pubkey={target}
limit={2}
editable={isSelf}
onViewAll={showNotesTab} />
</div>
{:else if tab === "notes"}
{#if isSelf}
<p class="mb-4 text-sm opacity-75">
Notes are public posts on your write relays. Pin notes to highlight them on your
profile, or manage relays in
<Button class="link link-primary" onclick={openRelaySettings}>relay settings</Button
>.
</p>
{/if}
<ProfilePageNotes pubkey={target} editable={isSelf} />
{:else}
{#if isSelf}
<div class="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<p class="text-sm opacity-75">Spaces come from your published group list.</p>
<Button class="btn btn-neutral btn-sm w-full sm:w-auto" onclick={openSpaces}>
Manage spaces
</Button>
</div>
{/if}
<ProfilePageSpaces pubkey={target} />
{/if}
</div>
</div>
</div>
<aside class="hidden w-80 shrink-0 xl:block xl:border-l xl:border-base-300 xl:pl-4">
<div class="col-3">
{@render profileAside()}
</div>
</aside>
</div>
</div>
{#snippet profileAside()}
<ProfileTrust pubkey={target} {isSelf} />
<ProfileSharedSpaces pubkey={target} {isSelf} limit={3} onViewAll={showSpacesTab} />
{/snippet}
@@ -0,0 +1,78 @@
<script lang="ts">
import {onMount} from "svelte"
import {sortBy, uniqBy} from "@welshman/lib"
import {feedFromFilter} from "@welshman/feeds"
import {NOTE, getReplyTags} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {makeFeedController} from "@welshman/app"
import {createScroller} from "@lib/html"
import {fly} from "@lib/transition"
import Spinner from "@lib/components/Spinner.svelte"
import NoteItem from "@app/components/NoteItem.svelte"
type Props = {
pubkey: string
editable?: boolean
}
const {pubkey, editable = false}: Props = $props()
let element: Element | undefined = $state()
let events: TrustedEvent[] = $state([])
let buffer: TrustedEvent[] = []
let exhausted = $state(false)
const ctrl = makeFeedController({
useWindowing: true,
feed: feedFromFilter({kinds: [NOTE], authors: [pubkey]}),
onEvent: (event: TrustedEvent) => {
if (getReplyTags(event.tags).replies.length === 0) {
buffer.push(event)
}
},
onExhausted: () => {
exhausted = true
},
})
onMount(() => {
const scroller = createScroller({
element: element!,
delay: 300,
threshold: 3000,
onScroll: () => {
buffer = uniqBy(
e => e.id,
sortBy(e => -e.created_at, buffer),
)
events = uniqBy(e => e.id, [...events, ...buffer.splice(0, 5)])
if (buffer.length < 50) {
ctrl.load(50)
}
},
})
return () => scroller.stop()
})
</script>
<div class="col-4" bind:this={element}>
<div class="flex flex-col gap-2">
{#each events as event (event.id)}
<div in:fly>
<NoteItem {event} {editable} />
</div>
{:else}
{#if exhausted}
<p class="py-12 text-center text-sm opacity-75">No notes found for this profile.</p>
{/if}
{/each}
{#if !exhausted}
<p class="center my-12 flex">
<Spinner loading />
</p>
{/if}
</div>
</div>
@@ -0,0 +1,47 @@
<script lang="ts">
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import RelayIcon from "@app/components/RelayIcon.svelte"
import RelayName from "@app/components/RelayName.svelte"
import {deriveGroupList, getSpaceUrlsFromGroupList, groupListPubkeysByUrl} from "@app/groups"
import {makeSpacePath} from "@app/routes"
type Props = {
pubkey: string
}
const {pubkey}: Props = $props()
const groupList = deriveGroupList(pubkey)
const spaceUrls = $derived(getSpaceUrlsFromGroupList($groupList))
</script>
<div class="col-2">
{#each spaceUrls as url (url)}
{@const count = $groupListPubkeysByUrl.get(url)?.size || 0}
<div
class="card2 card2-sm bg-alt flex flex-col gap-3 border border-base-300 sm:flex-row sm:items-center">
<RelayIcon {url} size={10} />
<div class="min-w-0 flex-1">
<RelayName {url} class="font-medium" />
<p class="text-sm opacity-75">
{#if count >= 1000}
{(count / 1000).toFixed(1).replace(/\.0$/, "")}K members
{:else}
{count} {count === 1 ? "member" : "members"}
{/if}
</p>
<p class="ellipsize text-xs opacity-60">{url}</p>
</div>
<Link class="btn btn-primary btn-sm w-full sm:w-auto" href={makeSpacePath(url)}>
Go to space
<Icon icon={AltArrowRight} />
</Link>
</div>
{:else}
<div class="card2 bg-alt border border-base-300 text-center">
<p class="opacity-75">No spaces found for this user.</p>
</div>
{/each}
</div>
@@ -0,0 +1,112 @@
<script lang="ts">
import {derived} from "svelte/store"
import {sortBy} from "@welshman/lib"
import {getListTags, getEventTagValues} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {derivePinList, repository} from "@welshman/app"
import {Router} from "@welshman/router"
import {load} from "@welshman/net"
import {deriveEventsById, deriveEventsDesc} from "@welshman/store"
import {fly} from "@lib/transition"
import Spinner from "@lib/components/Spinner.svelte"
import NoteItem from "@app/components/NoteItem.svelte"
type Props = {
pubkey: string
limit?: number
onViewAll?: () => void
editable?: boolean
}
const {pubkey, limit, onViewAll, editable = false}: Props = $props()
const pinList = derivePinList(pubkey)
const pinnedIds = derived(pinList, $pinList => getEventTagValues(getListTags($pinList)))
const displayIds = derived(pinnedIds, $pinnedIds =>
limit ? $pinnedIds.slice(0, limit) : $pinnedIds,
)
const pinnedEvents = derived(
displayIds,
($displayIds, set) => {
if ($displayIds.length === 0) {
set([])
return
}
return deriveEventsDesc(
deriveEventsById({repository, filters: [{ids: $displayIds}]}),
).subscribe(events => {
set(sortBy(event => -$displayIds.indexOf(event.id), events))
})
},
[] as TrustedEvent[],
)
let fetching = $state(false)
$effect(() => {
const ids = $displayIds
if (ids.length === 0) {
fetching = false
return
}
const missing = ids.filter(id => !repository.getEvent(id))
if (missing.length === 0) {
fetching = false
return
}
fetching = true
const controller = new AbortController()
load({
relays: Router.get().FromPubkeys([pubkey]).getUrls(),
filters: [{ids: missing}],
signal: controller.signal,
onClose: () => {
fetching = false
},
})
return () => controller.abort()
})
const loading = $derived(
fetching || ($displayIds.length > 0 && $pinnedEvents.length < $displayIds.length),
)
</script>
{#if $displayIds.length > 0 || loading}
<div class="col-4 border-t border-base-300 pt-4">
<strong>Pinned notes</strong>
{#if loading && $pinnedEvents.length === 0}
<p class="center flex py-8">
<Spinner loading />
</p>
{:else if $pinnedEvents.length > 0}
<div class="col-2">
{#each $pinnedEvents as event (event.id)}
<div in:fly>
<NoteItem {event} {editable} />
</div>
{/each}
</div>
{#if onViewAll && limit && $pinnedIds.length > limit}
<button class="link link-primary row-2 text-sm" onclick={onViewAll}>
View all pinned notes
<span aria-hidden="true"></span>
</button>
{/if}
{/if}
</div>
{/if}
+35
View File
@@ -0,0 +1,35 @@
<script lang="ts">
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import QRCode from "@app/components/QRCode.svelte"
type Props = {
code: string
}
const {code}: Props = $props()
const back = () => history.back()
</script>
<Modal>
<ModalBody>
<div class="col-4 items-center text-center">
<strong>Profile QR Code</strong>
<QRCode {code} class="max-w-64" />
<p class="break-all text-sm opacity-75">{code}</p>
<p class="text-sm opacity-75">Tap the QR code to copy this npub.</p>
</div>
</ModalBody>
<ModalFooter>
<Button onclick={back} class="hidden md:btn md:btn-link">
<Icon icon={AltArrowLeft} />
Go back
</Button>
<Button onclick={back} class="btn btn-neutral">Close</Button>
</ModalFooter>
</Modal>
@@ -0,0 +1,76 @@
<script lang="ts">
import cx from "classnames"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import UsersGroup from "@assets/icons/users-group-rounded.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import RelayIcon from "@app/components/RelayIcon.svelte"
import RelayName from "@app/components/RelayName.svelte"
import {
groupListPubkeysByUrl,
userSpaceUrls,
deriveGroupList,
getSpaceUrlsFromGroupList,
} from "@app/groups"
import {makeSpacePath} from "@app/routes"
type Props = {
pubkey: string
isSelf?: boolean
limit?: number
onViewAll?: () => void
class?: string
}
const {pubkey, isSelf = false, limit, onViewAll, ...props}: Props = $props()
const groupList = deriveGroupList(pubkey)
const spaceUrls = $derived(getSpaceUrlsFromGroupList($groupList))
const sharedSpaceUrls = $derived($userSpaceUrls.filter(url => spaceUrls.includes(url)))
const listedSpaceUrls = $derived(isSelf ? spaceUrls : sharedSpaceUrls)
const displayUrls = $derived(limit ? listedSpaceUrls.slice(0, limit) : listedSpaceUrls)
</script>
<div class={cx("card2 bg-alt col-3 border border-base-300 max-sm:p-5 sm:col-4", props.class)}>
<div class="flex items-center justify-between gap-2">
<div class="row-2">
<Icon icon={UsersGroup} size={5} />
<strong>{isSelf ? "Your spaces" : "Shared spaces"}</strong>
</div>
<span class="badge badge-neutral">{listedSpaceUrls.length}</span>
</div>
{#if displayUrls.length > 0}
<div class="col-2 border-t border-base-300 pt-4 sm:pt-4">
{#each displayUrls as url (url)}
{@const count = $groupListPubkeysByUrl.get(url)?.size || 0}
<Link
href={makeSpacePath(url)}
class="row-2 rounded-box border border-base-300 p-4 transition-colors hover:bg-base-300/30 sm:p-3">
<RelayIcon {url} size={8} />
<div class="min-w-0 flex-1">
<RelayName {url} class="ellipsize text-sm font-medium" />
<p class="text-xs opacity-75">
{#if count >= 1000}
{(count / 1000).toFixed(1).replace(/\.0$/, "")}K members
{:else}
{count} {count === 1 ? "member" : "members"}
{/if}
</p>
</div>
</Link>
{/each}
</div>
{#if onViewAll && listedSpaceUrls.length > (limit || listedSpaceUrls.length)}
<button
class="link link-primary row-2 border-t border-base-300 pt-4 text-sm max-sm:pt-4"
onclick={onViewAll}>
{isSelf ? "View all your spaces" : "View all shared spaces"}
<Icon icon={AltArrowRight} size={4} />
</button>
{/if}
{:else}
<p class="border-t border-base-300 pt-4 text-sm opacity-75 max-sm:pt-4">
{isSelf ? "You aren't in any spaces yet." : "No shared spaces yet."}
</p>
{/if}
</div>
+113
View File
@@ -0,0 +1,113 @@
<script lang="ts">
import {clamp, uniq} from "@welshman/lib"
import {
pubkey,
followLists,
userFollowList,
deriveUserWotScore,
deriveProfileDisplay,
deriveFollowList,
followersByPubkey,
loadFollowList,
getFollows,
getFollowers,
} from "@welshman/app"
import Shield from "@assets/icons/shield-minimalistic.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import ProfileCircles from "@app/components/ProfileCircles.svelte"
type Props = {
pubkey: string
isSelf?: boolean
}
const {pubkey: target, isSelf = false}: Props = $props()
const score = deriveUserWotScore(target)
const profileDisplay = deriveProfileDisplay(target)
const targetFollowList = deriveFollowList(target)
$effect(() => {
if (isSelf) return
loadFollowList(target)
const viewer = $pubkey
if (viewer) {
loadFollowList(viewer)
}
})
const followerCount = $derived.by(() => {
void $followersByPubkey
return getFollowers(target).length
})
const mutualFollows = $derived.by(() => {
if (isSelf) return []
const viewer = $pubkey
void $followLists
void $targetFollowList
void $userFollowList
if (!viewer) return []
const viewerFollows = new Set(getFollows(viewer))
return uniq(
getFollows(target).filter(pk => pk !== viewer && pk !== target && viewerFollows.has(pk)),
)
})
const displayScore = $derived(isSelf ? followerCount : Math.round(clamp([0, 100], $score)))
const progress = $derived(isSelf ? undefined : displayScore)
const trustMessage = $derived.by(() => {
if (isSelf) {
if (followerCount === 0) return "No one follows you in the network we know about yet."
return `${followerCount} ${followerCount === 1 ? "person follows" : "people follow"} you on the network we know about.`
}
if (displayScore >= 70) return "This user is highly trusted in your network."
if (displayScore >= 30) return "This user has some trust in your network."
return "This user is not well known in your network."
})
</script>
<div class="card2 bg-alt col-3 border border-base-300 max-sm:p-5 sm:col-4">
<div class="row-2">
<Icon icon={Shield} size={5} />
<strong>Reputation</strong>
</div>
<div class="col-2 border-t border-base-300 pt-4 sm:pt-4">
<div class="flex items-end justify-between gap-2">
<span class="text-sm opacity-75">{isSelf ? "Followers" : "Trust score"}</span>
<span class="text-lg font-semibold">
{#if isSelf}
{displayScore}
{:else}
{displayScore} / 100
{/if}
</span>
</div>
{#if !isSelf}
<progress class="progress progress-primary w-full" value={progress} max="100"></progress>
{/if}
<p class="text-sm opacity-75">{trustMessage}</p>
</div>
{#if mutualFollows.length > 0}
<div class="col-2 border-t border-base-300 pt-4 sm:pt-4">
<p class="text-sm font-medium">Mutual follows</p>
<ProfileCircles pubkeys={mutualFollows} limit={5} />
<p class="text-sm opacity-75">
{mutualFollows.length}
{mutualFollows.length === 1 ? "person" : "people"} you and {$profileDisplay} both follow.
</p>
</div>
{/if}
</div>
+6 -7
View File
@@ -17,8 +17,7 @@
import type {TrustedEvent, EventContent, Zap} from "@welshman/util" import type {TrustedEvent, EventContent, Zap} from "@welshman/util"
import {deriveArray, deriveEventsById, deriveItemsByKey} from "@welshman/store" import {deriveArray, deriveEventsById, deriveItemsByKey} from "@welshman/store"
import {load} from "@welshman/net" import {load} from "@welshman/net"
import {Zappers, Profiles} from "@welshman/app" import {pubkey, repository, getValidZap, displayProfileByPubkey} from "@welshman/app"
import {pubkey, repository, app} from "@app/welshman"
import {isMobile, preventDefault, stopPropagation} from "@lib/html" import {isMobile, preventDefault, stopPropagation} from "@lib/html"
import Danger from "@assets/icons/danger-triangle.svg?dataurl" import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -65,15 +64,15 @@
repository, repository,
getKey: zap => zap.response.id, getKey: zap => zap.response.id,
filters: [{kinds: [ZAP_RESPONSE], "#e": eventIds}], filters: [{kinds: [ZAP_RESPONSE], "#e": eventIds}],
eventToItem: async (response: TrustedEvent) => { eventToItem: (response: TrustedEvent) => {
const zap = await app.use(Zappers).validateZapReceipt(response, event) const zap = getValidZap(response, event)
if (zap) { if (zap) {
return zap return zap
} }
if (innerEvent) { if (innerEvent) {
return await app.use(Zappers).validateZapReceipt(response, innerEvent) return getValidZap(response, innerEvent)
} }
}, },
}), }),
@@ -151,7 +150,7 @@
{@const amount = fromMsats(sum(zaps.map(zap => zap.invoiceAmount)))} {@const amount = fromMsats(sum(zaps.map(zap => zap.invoiceAmount)))}
{@const pubkeys = uniq(zaps.map(zap => zap.request.pubkey))} {@const pubkeys = uniq(zaps.map(zap => zap.request.pubkey))}
{@const isOwn = $pubkey && pubkeys.includes($pubkey)} {@const isOwn = $pubkey && pubkeys.includes($pubkey)}
{@const info = displayList(pubkeys.map(pubkey => app.use(Profiles).display(pubkey).get()))} {@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
{@const tooltip = `${info} zapped`} {@const tooltip = `${info} zapped`}
<button <button
type="button" type="button"
@@ -172,7 +171,7 @@
{#each groupedReactions.entries() as [key, events]} {#each groupedReactions.entries() as [key, events]}
{@const pubkeys = events.map(e => e.pubkey)} {@const pubkeys = events.map(e => e.pubkey)}
{@const isOwn = $pubkey && pubkeys.includes($pubkey)} {@const isOwn = $pubkey && pubkeys.includes($pubkey)}
{@const info = displayList(pubkeys.map(pubkey => app.use(Profiles).display(pubkey).get()))} {@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
{@const tooltip = `${info} reacted`} {@const tooltip = `${info} reacted`}
{@const onClick = () => onReactionClick(events)} {@const onClick = () => onReactionClick(events)}
<button <button
+2 -2
View File
@@ -5,7 +5,7 @@
import {tryCatch} from "@welshman/lib" import {tryCatch} from "@welshman/lib"
import {isShareableRelayUrl, isIPAddress, normalizeRelayUrl} from "@welshman/util" import {isShareableRelayUrl, isIPAddress, normalizeRelayUrl} from "@welshman/util"
import type {Thunk} from "@welshman/app" import type {Thunk} from "@welshman/app"
import {relaySearch} from "@app/welshman" import {waitForThunkError, relaySearch} from "@welshman/app"
import {createScroller} from "@lib/html" import {createScroller} from "@lib/html"
import {errorMessage} from "@lib/util" import {errorMessage} from "@lib/util"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
@@ -34,7 +34,7 @@
loading.add(url) loading.add(url)
try { try {
const error = await (await addRelay(url)).waitForError() const error = await waitForThunkError(await addRelay(url))
if (error) { if (error) {
pushToast({ pushToast({
+2 -3
View File
@@ -1,10 +1,9 @@
<script lang="ts"> <script lang="ts">
import {Relays} from "@welshman/app" import {deriveRelay} from "@welshman/app"
import {app} from "@app/welshman"
const {...props} = $props() const {...props} = $props()
const relay = app.use(Relays).one(props.url) const relay = deriveRelay(props.url)
</script> </script>
{#if $relay?.description} {#if $relay?.description}
+2 -3
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import {Relays} from "@welshman/app" import {deriveRelay} from "@welshman/app"
import {app} from "@app/welshman"
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl" import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
import ImageIcon from "@lib/components/ImageIcon.svelte" import ImageIcon from "@lib/components/ImageIcon.svelte"
@@ -12,7 +11,7 @@
const {url, size = 7, ...props}: Props = $props() const {url, size = 7, ...props}: Props = $props()
const relay = app.use(Relays).one(url) const relay = deriveRelay(url)
</script> </script>
{#if $relay?.icon} {#if $relay?.icon}
+3 -4
View File
@@ -4,13 +4,12 @@
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 {Relays, RelayStats} from "@welshman/app" import {deriveRelay, deriveRelayStats} from "@welshman/app"
import {app} from "@app/welshman"
const {url, children} = $props() const {url, children} = $props()
const relay = app.use(Relays).one(url) const relay = deriveRelay(url)
const relayStats = app.use(RelayStats).one(url) const relayStats = deriveRelayStats(url)
const connections = $derived($relayStats?.open_count || 0) const connections = $derived($relayStats?.open_count || 0)
</script> </script>
+2 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {Readable} from "svelte/store" import type {Readable} from "svelte/store"
import {SvelteSet} from "svelte/reactivity" import {SvelteSet} from "svelte/reactivity"
import {waitForThunkError} from "@welshman/app"
import type {Thunk} from "@welshman/app" import type {Thunk} from "@welshman/app"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -36,7 +37,7 @@
loading.add(url) loading.add(url)
try { try {
const error = await (await removeRelay(url)).waitForError() const error = await waitForThunkError(await removeRelay(url))
if (error) { if (error) {
pushToast({ pushToast({
+2 -3
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import {Relays} from "@welshman/app" import {deriveRelayDisplay} from "@welshman/app"
import {app} from "@app/welshman"
type Props = { type Props = {
url: string url: string
@@ -9,7 +8,7 @@
const {url, ...props}: Props = $props() const {url, ...props}: Props = $props()
const display = $derived(app.use(Relays).display(url).$) const display = $derived(deriveRelayDisplay(url))
</script> </script>
<span class={props.class}> <span class={props.class}>
+1 -1
View File
@@ -2,7 +2,7 @@
import {REPORT} from "@welshman/util" import {REPORT} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {deriveEventsById} from "@welshman/store" import {deriveEventsById} from "@welshman/store"
import {repository} from "@app/welshman" import {repository} from "@welshman/app"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte" import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
+5 -6
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import {getTag, ManagementMethod} from "@welshman/util" import {getTag, ManagementMethod} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {RelayManagement, Profiles} from "@welshman/app" import {pubkey, manageRelay, repository, displayProfileByPubkey} from "@welshman/app"
import {pubkey, repository, app} from "@app/welshman"
import InboxOut from "@assets/icons/inbox-out.svg?dataurl" import InboxOut from "@assets/icons/inbox-out.svg?dataurl"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl" import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl" import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
@@ -46,7 +45,7 @@
} }
const dismissReport = async () => { const dismissReport = async () => {
const {error} = await app.use(RelayManagement).post(url, { const {error} = await manageRelay(url, {
method: ManagementMethod.BanEvent, method: ManagementMethod.BanEvent,
params: [event.id, "Dismissed by admin"], params: [event.id, "Dismissed by admin"],
}) })
@@ -67,7 +66,7 @@
title: `Remove Content`, title: `Remove Content`,
message: `Are you sure you want to delete this content from the space?`, message: `Are you sure you want to delete this content from the space?`,
confirm: async () => { confirm: async () => {
const {error} = await app.use(RelayManagement).post(url, { const {error} = await manageRelay(url, {
method: ManagementMethod.BanEvent, method: ManagementMethod.BanEvent,
params: [id, reason], params: [id, reason],
}) })
@@ -90,9 +89,9 @@
pushModal(Confirm, { pushModal(Confirm, {
title: "Ban User", title: "Ban User",
message: `Are you sure you want to ban @${app.use(Profiles).display(pubkey).get()} from the space?`, message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
confirm: async () => { confirm: async () => {
const {error} = await app.use(RelayManagement).post(url, { const {error} = await manageRelay(url, {
method: ManagementMethod.BanPubkey, method: ManagementMethod.BanPubkey,
params: [pubkey, reason], params: [pubkey, reason],
}) })
+2 -3
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {Profiles} from "@welshman/app" import {displayProfileByPubkey} from "@welshman/app"
import {app} from "@app/welshman"
import {slide} from "@lib/transition" import {slide} from "@lib/transition"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -22,7 +21,7 @@
<div <div
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8" class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8"
transition:slide> transition:slide>
<p class="text-xs text-primary">{verb} @{app.use(Profiles).display(event.pubkey).get()}</p> <p class="text-xs text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
{#key event.id} {#key event.id}
<NoteContentMinimal trimParent {event} /> <NoteContentMinimal trimParent {event} />
{/key} {/key}
+9 -11
View File
@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import {displayRelayUrl} from "@welshman/util" import type {RoomMeta} from "@welshman/util"
import {displayRelayUrl, makeRoomMeta} from "@welshman/util"
import type {Thunk} from "@welshman/app" import type {Thunk} from "@welshman/app"
import {Rooms} from "@welshman/app" import {deleteRoom, waitForThunkError, repository, joinRoom, leaveRoom} from "@welshman/app"
import {repository, app} from "@app/welshman"
import Pen from "@assets/icons/pen.svg?dataurl" import Pen from "@assets/icons/pen.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl" import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import Login3 from "@assets/icons/login-3.svg?dataurl" import Login3 from "@assets/icons/login-3.svg?dataurl"
@@ -71,13 +71,11 @@
const startEdit = () => pushModal(RoomEdit, {url, h}) const startEdit = () => pushModal(RoomEdit, {url, h})
const handleLoading = async ( const handleLoading = async (f: (url: string, room: RoomMeta) => Thunk) => {
f: (url: string, room: {h: string}) => Promise<Thunk>,
) => {
loading = true loading = true
try { try {
const message = await (await f(url, {h})).waitForError() const message = await waitForThunkError(f(url, makeRoomMeta({h})))
if (message && !message.startsWith("duplicate:")) { if (message && !message.startsWith("duplicate:")) {
pushToast({theme: "error", message}) pushToast({theme: "error", message})
@@ -87,9 +85,9 @@
} }
} }
const join = () => handleLoading((url, room) => app.use(Rooms).join(url, room)) const join = () => handleLoading(joinRoom)
const leave = () => handleLoading((url, room) => app.use(Rooms).leave(url, room)) const leave = () => handleLoading(leaveRoom)
const showMembers = () => pushModal(RoomMembers, {url, h}) const showMembers = () => pushModal(RoomMembers, {url, h})
@@ -111,8 +109,8 @@
message: message:
"This room will no longer be accessible to space members, and all messages posted to it will be deleted.", "This room will no longer be accessible to space members, and all messages posted to it will be deleted.",
confirm: async () => { confirm: async () => {
const thunk = await app.use(Rooms).delete(url, $room) const thunk = deleteRoom(url, $room)
const message = await thunk.waitForError() const message = await waitForThunkError(thunk)
if (message) { if (message) {
repository.removeEvent(thunk.event.id) repository.removeEvent(thunk.event.id)
+6 -58
View File
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {Rooms} from "@welshman/app" import type {RoomMeta} from "@welshman/util"
import {app} from "@app/welshman" import {makeRoomMeta} from "@welshman/util"
import {waitForThunkError, createRoom, editRoom, joinRoom} from "@welshman/app"
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl" import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
import Hashtag from "@assets/icons/hashtag.svg?dataurl" import Hashtag from "@assets/icons/hashtag.svg?dataurl"
import Volume from "@assets/icons/volume.svg?dataurl" import Volume from "@assets/icons/volume.svg?dataurl"
@@ -18,56 +19,6 @@
import {deriveHasLivekit} from "@app/relays" import {deriveHasLivekit} from "@app/relays"
import {getRoomType, RoomType} from "@app/groups" import {getRoomType, RoomType} from "@app/groups"
// Plain mutable form object (the old @welshman/util `RoomMeta` plain-object
// type, removed in the migration). The new domain `RoomMeta` is an async
// method-accessor Reader, which doesn't fit a `$state` object bound to inputs,
// so we keep a plain object here and let `app.use(Rooms).create/edit/join`
// (which accept a plain object) build the events at submit time.
type RoomMeta = {
h: string
name?: string
about?: string
picture?: string
pictureMeta?: string[]
isClosed?: boolean
isHidden?: boolean
isPrivate?: boolean
isRestricted?: boolean
livekit?: boolean
}
// TODO(welshman-migration): reimplemented inline from the removed
// @welshman/util `generateH`/`makeRoomMeta` (room-id generator). Verify the
// generated id still matches the expected `^[a-z]+[1-9]$` shape and that no
// shared generator should be used instead.
const vowels = "a,e,i,o,u,ay,ey,oy,ou,ia,ea,ough,oo,ee,argh".split(",")
const consonants =
"p,b,t,d,k,g,ch,sh,th,f,v,s,z,l,r,m,n,pl,bl,cl,gl,pr,br,tr,dr,kr,gr,fl,sl,fr,thr,str,sk,sp,st".split(
",",
)
const generateH = () => {
const n = (6 + Math.random() * 2) | 0
const s = [consonants.slice(), vowels.slice()]
if (Math.random() < 0.5) {
s.reverse()
}
return (
Array.from({length: n}, (_, i) =>
s[i % 2].splice((Math.random() * s[i % 2].length) | 0, 1),
).join("") +
(1 + Math.floor(Math.random() * 9))
)
}
const makeRoomMeta = (room: Partial<RoomMeta> = {}): RoomMeta => ({
h: room.h ?? generateH(),
...room,
})
type Props = { type Props = {
url: string url: string
header: Snippet header: Snippet
@@ -107,22 +58,19 @@
room.pictureMeta = result.tags room.pictureMeta = result.tags
} }
// TODO(welshman-migration): app.use(Rooms).create/edit/join are async const createMessage = await waitForThunkError(createRoom(url, room))
// (return Promise<Thunk>); the old code passed the un-awaited result to
// waitForThunkError. Awaiting the thunk first before .waitForError().
const createMessage = await (await app.use(Rooms).create(url, room)).waitForError()
if (createMessage && !createMessage.includes("already")) { if (createMessage && !createMessage.includes("already")) {
return pushToast({theme: "error", message: createMessage}) return pushToast({theme: "error", message: createMessage})
} }
const editMessage = await (await app.use(Rooms).edit(url, room)).waitForError() const editMessage = await waitForThunkError(editRoom(url, room))
if (editMessage) { if (editMessage) {
return pushToast({theme: "error", message: editMessage}) return pushToast({theme: "error", message: editMessage})
} }
const joinMessage = await (await app.use(Rooms).join(url, room)).waitForError() const joinMessage = await waitForThunkError(joinRoom(url, room))
if (joinMessage && !joinMessage.includes("already")) { if (joinMessage && !joinMessage.includes("already")) {
return pushToast({theme: "error", message: joinMessage}) return pushToast({theme: "error", message: joinMessage})
+10 -5
View File
@@ -11,8 +11,13 @@
} from "@welshman/lib" } from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import {MESSAGE, COMMENT, getTag} from "@welshman/util" import {MESSAGE, COMMENT, getTag} from "@welshman/util"
import {Thunks, Profiles} from "@welshman/app" import {
import {thunks, pubkey, app} from "@app/welshman" thunks,
pubkey,
mergeThunks,
deriveProfileDisplay,
displayProfileByPubkey,
} from "@welshman/app"
import {isMobile} from "@lib/html" import {isMobile} from "@lib/html"
import Pen from "@assets/icons/pen.svg?dataurl" import Pen from "@assets/icons/pen.svg?dataurl"
import Reply from "@assets/icons/reply-2.svg?dataurl" import Reply from "@assets/icons/reply-2.svg?dataurl"
@@ -53,8 +58,8 @@
const path = getRoomItemPath(url, event) const path = getRoomItemPath(url, event)
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
const today = formatTimestampAsDate(now()) const today = formatTimestampAsDate(now())
const profileDisplay = app.use(Profiles).display(event.pubkey, [url]).$ const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
const thunk = app.use(Thunks).merge($thunks.filter(t => t.event.id === event.id)) const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
const [_, colorValue] = colors[hash(event.pubkey) % colors.length] const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
const qTag = getTag("q", event.tags) const qTag = getTag("q", event.tags)
@@ -133,7 +138,7 @@
{#if path && $innerComments.length > 0} {#if path && $innerComments.length > 0}
{@const pubkeys = $innerComments.map(e => e.pubkey)} {@const pubkeys = $innerComments.map(e => e.pubkey)}
{@const isOwn = $pubkey && pubkeys.includes($pubkey)} {@const isOwn = $pubkey && pubkeys.includes($pubkey)}
{@const info = displayList(pubkeys.map(pubkey => app.use(Profiles).display(pubkey).get()))} {@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
{@const tooltip = `${info} commented`} {@const tooltip = `${info} commented`}
<div data-tip={tooltip} class="tooltip tooltip-right flex"> <div data-tip={tooltip} class="tooltip tooltip-right flex">
<Link <Link
+2 -3
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {ManagementMethod} from "@welshman/util" import {ManagementMethod} from "@welshman/util"
import {RelayManagement} from "@welshman/app" import {pubkey, manageRelay, repository} from "@welshman/app"
import {pubkey, repository, app} from "@app/welshman"
import Code2 from "@assets/icons/code-2.svg?dataurl" import Code2 from "@assets/icons/code-2.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl" import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import Danger from "@assets/icons/danger.svg?dataurl" import Danger from "@assets/icons/danger.svg?dataurl"
@@ -46,7 +45,7 @@
title: `Delete Message`, title: `Delete Message`,
message: `Are you sure you want to delete this message from the space?`, message: `Are you sure you want to delete this message from the space?`,
confirm: async () => { confirm: async () => {
const {error} = await app.use(RelayManagement).post(url, { const {error} = await manageRelay(url, {
method: ManagementMethod.BanEvent, method: ManagementMethod.BanEvent,
params: [event.id], params: [event.id],
}) })
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {NativeEmoji} from "emoji-picker-element/shared" import type {NativeEmoji} from "emoji-picker-element/shared"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {pubkey} from "@app/welshman" import {pubkey} from "@welshman/app"
import Bolt from "@assets/icons/bolt.svg?dataurl" import Bolt from "@assets/icons/bolt.svg?dataurl"
import Reply from "@assets/icons/reply-2.svg?dataurl" import Reply from "@assets/icons/reply-2.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl" import Code2 from "@assets/icons/code-2.svg?dataurl"
+4 -9
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import {getTagValue, ManagementMethod} from "@welshman/util" import {getTagValue, ManagementMethod} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent, PublishedRoomMeta} from "@welshman/util"
import {RelayManagement} from "@welshman/app" import {repository, manageRelay} from "@welshman/app"
import {app, repository} from "@app/welshman"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileName from "@app/components/ProfileName.svelte" import ProfileName from "@app/components/ProfileName.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte" import ProfileDetail from "@app/components/ProfileDetail.svelte"
@@ -29,7 +28,7 @@
loading = true loading = true
try { try {
const {error} = await app.use(RelayManagement).post(url, { const {error} = await manageRelay(url, {
method: ManagementMethod.BanEvent, method: ManagementMethod.BanEvent,
params: [event.id, "Join request dismissed"], params: [event.id, "Join request dismissed"],
}) })
@@ -50,11 +49,7 @@
loading = true loading = true
try { try {
// TODO(welshman-migration): addRoomMembers now expects a RoomMeta domain const error = await addRoomMembers(url, $room as PublishedRoomMeta, [event.pubkey])
// Reader, but deriveRoom yields the plain `Room` object. Passing it through
// as `any` preserves prior runtime behavior; verify Rooms.addMember accepts
// the plain Room shape (url/h/...) or resolve a real RoomMeta reader here.
const error = await addRoomMembers(url, $room as any, [event.pubkey])
if (error) { if (error) {
pushToast({theme: "error", message: error}) pushToast({theme: "error", message: error})
+2 -4
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import {Rooms} from "@welshman/app" import {waitForThunkError, removeRoomMember} from "@welshman/app"
import {app} from "@app/welshman"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl" import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl" import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl" import AddCircle from "@assets/icons/add-circle.svg?dataurl"
@@ -52,8 +51,7 @@
title: "Remove Member", title: "Remove Member",
message: "Are you sure you want to remove this user from the room?", message: "Are you sure you want to remove this user from the room?",
confirm: async () => { confirm: async () => {
const thunk = await app.use(Rooms).removeMember(url, $room, pubkey) const error = await waitForThunkError(removeRoomMember(url, $room, pubkey))
const error = await thunk.waitForError()
if (error) { if (error) {
pushToast({theme: "error", message: error}) pushToast({theme: "error", message: error})
+4 -8
View File
@@ -2,8 +2,8 @@
import {onMount} from "svelte" import {onMount} from "svelte"
import {setKey, popKey} from "@lib/implicit" import {setKey, popKey} from "@lib/implicit"
import {sleep} from "@welshman/lib" import {sleep} from "@welshman/lib"
import {Profiles} from "@welshman/app" import {displayProfileByPubkey} from "@welshman/app"
import {app} from "@app/welshman" import type {PublishedRoomMeta} from "@welshman/util"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -42,11 +42,7 @@
// Show loading for auto submit callback // Show loading for auto submit callback
await sleep(500) await sleep(500)
// TODO(welshman-migration): addRoomMembers now expects a RoomMeta domain const error = await addRoomMembers(url, $room as PublishedRoomMeta, pubkeys)
// Reader, but deriveRoom yields the plain `Room` object. Passing it through
// as `any` preserves prior runtime behavior; verify Rooms.addMember accepts
// the plain Room shape (url/h/...) or resolve a real RoomMeta reader here.
const error = await addRoomMembers(url, $room as any, pubkeys)
if (error) { if (error) {
pushToast({theme: "error", message: error}) pushToast({theme: "error", message: error})
@@ -76,7 +72,7 @@
subtitle: "Automatically add members to space", subtitle: "Automatically add members to space",
message: message:
nonSpaceMembers.length === 1 nonSpaceMembers.length === 1
? `${app.use(Profiles).display(nonSpaceMembers[0]).get()} is not a member of this space. Add them?` ? `${displayProfileByPubkey(nonSpaceMembers[0])} is not a member of this space. Add them?`
: `${nonSpaceMembers.length} people are not members of this space. Add them?`, : `${nonSpaceMembers.length} people are not members of this space. Add them?`,
confirm: async () => { confirm: async () => {
setKey("RoomMembersAdd.confirm", true) setKey("RoomMembersAdd.confirm", true)
+8 -10
View File
@@ -1,10 +1,8 @@
<script lang="ts"> <script lang="ts">
import type {ClientOptions} from "@pomade/core" import type {ClientOptions} from "@pomade/core"
import type {Profile} from "@welshman/domain" import type {Profile} from "@welshman/util"
import {makeSecret, RELAYS, MESSAGING_RELAYS, makeEvent} from "@welshman/util" import {makeProfile, makeSecret, RELAYS, MESSAGING_RELAYS, makeEvent} from "@welshman/util"
import {ProfileBuilder} from "@welshman/domain" import {loginWithNip01, publishThunk} from "@welshman/app"
import {Thunks} from "@welshman/app"
import {app, loginWithNip01} from "@app/welshman"
import Key from "@assets/icons/key-minimalistic.svg?dataurl" import Key from "@assets/icons/key-minimalistic.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl" import Letter from "@assets/icons/letter.svg?dataurl"
import {getKey, setKey} from "@lib/implicit" import {getKey, setKey} from "@lib/implicit"
@@ -33,7 +31,7 @@
setKey("signup.email", "") setKey("signup.email", "")
setKey("signup.secret", makeSecret()) setKey("signup.secret", makeSecret())
setKey("signup.profile", new ProfileBuilder().values) setKey("signup.profile", makeProfile())
setKey("signup.clientOptions", undefined) setKey("signup.clientOptions", undefined)
const hasPomade = POMADE_SIGNERS.length >= 3 const hasPomade = POMADE_SIGNERS.length >= 3
@@ -42,13 +40,13 @@
const completeSignup = () => { const completeSignup = () => {
// Add default outbox/inbox relays // Add default outbox/inbox relays
app.use(Thunks).publish({ publishThunk({
event: makeEvent(RELAYS, {tags: DEFAULT_RELAYS.map(url => ["r", url])}), event: makeEvent(RELAYS, {tags: DEFAULT_RELAYS.map(url => ["r", url])}),
relays: [...INDEXER_RELAYS, ...DEFAULT_RELAYS], relays: [...INDEXER_RELAYS, ...DEFAULT_RELAYS],
}) })
// Add default messaging relays // Add default messaging relays
app.use(Thunks).publish({ publishThunk({
event: makeEvent(MESSAGING_RELAYS, {tags: DEFAULT_MESSAGING_RELAYS.map(url => ["r", url])}), event: makeEvent(MESSAGING_RELAYS, {tags: DEFAULT_MESSAGING_RELAYS.map(url => ["r", url])}),
relays: DEFAULT_RELAYS, relays: DEFAULT_RELAYS,
}) })
@@ -85,10 +83,10 @@
key: () => pushModal(SignUpKey, {next: flows.nostr.complete, step: 2, totalSteps: 3}), key: () => pushModal(SignUpKey, {next: flows.nostr.complete, step: 2, totalSteps: 3}),
complete: () => complete: () =>
pushModal(SignUpComplete, {next: flows.nostr.finalize, step: 3, totalSteps: 3}), pushModal(SignUpComplete, {next: flows.nostr.finalize, step: 3, totalSteps: 3}),
finalize: async () => { finalize: () => {
const secret = getKey<string>("signup.secret")! const secret = getKey<string>("signup.secret")!
await loginWithNip01(secret) loginWithNip01(secret)
completeSignup() completeSignup()
}, },
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {spec, avg} from "@welshman/lib" import {spec, avg} from "@welshman/lib"
import {session, SessionMethod, signerLog} from "@app/welshman" import {session, SessionMethod, signerLog} from "@welshman/app"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Danger from "@assets/icons/danger-triangle.svg?dataurl" import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl" import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
+2 -3
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import {Relays} from "@welshman/app" import {deriveRelay} from "@welshman/app"
import {app} from "@app/welshman"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl" import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Pen from "@assets/icons/pen.svg?dataurl" import Pen from "@assets/icons/pen.svg?dataurl"
@@ -27,7 +26,7 @@
} }
const {url}: Props = $props() const {url}: Props = $props()
const relay = app.use(Relays).one(url) const relay = deriveRelay(url)
const owner = $derived($relay?.pubkey) const owner = $derived($relay?.pubkey)
const userIsAdmin = deriveUserIsSpaceAdmin(url) const userIsAdmin = deriveUserIsSpaceAdmin(url)
+5 -6
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {RelayProfile} from "@welshman/util" import type {RelayProfile} from "@welshman/util"
import {displayRelayUrl, ManagementMethod} from "@welshman/util" import {displayRelayUrl, ManagementMethod} from "@welshman/util"
import {RelayManagement, Relays} from "@welshman/app" import {manageRelay, forceLoadRelay} from "@welshman/app"
import {app} from "@app/welshman"
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl" import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
import Widget from "@assets/icons/widget-4.svg?dataurl" import Widget from "@assets/icons/widget-4.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -37,7 +36,7 @@
const submit = async () => { const submit = async () => {
if (values.name != initialValues.name) { if (values.name != initialValues.name) {
const res = await app.use(RelayManagement).post(url, { const res = await manageRelay(url, {
method: ManagementMethod.ChangeRelayName, method: ManagementMethod.ChangeRelayName,
params: [values.name || ""], params: [values.name || ""],
}) })
@@ -48,7 +47,7 @@
} }
if (values.description != initialValues.description) { if (values.description != initialValues.description) {
const res = await app.use(RelayManagement).post(url, { const res = await manageRelay(url, {
method: ManagementMethod.ChangeRelayDescription, method: ManagementMethod.ChangeRelayDescription,
params: [values.description || ""], params: [values.description || ""],
}) })
@@ -65,7 +64,7 @@
return pushToast({theme: "error", message: error}) return pushToast({theme: "error", message: error})
} }
const res = await app.use(RelayManagement).post(url, { const res = await manageRelay(url, {
method: ManagementMethod.ChangeRelayIcon, method: ManagementMethod.ChangeRelayIcon,
params: [result.url], params: [result.url],
}) })
@@ -76,7 +75,7 @@
} }
pushToast({message: "Your changes have been saved!"}) pushToast({message: "Your changes have been saved!"})
app.use(Relays).forceLoad(url) forceLoadRelay(url)
clearModals() clearModals()
} }
+3 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import {debounce} from "throttle-debounce" import {debounce} from "throttle-debounce"
import {dissoc, maybe} from "@welshman/lib" import {dissoc, maybe} from "@welshman/lib"
import {goto} from "$app/navigation"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import {slideAndFade} from "@lib/transition" import {slideAndFade} from "@lib/transition"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
@@ -21,7 +22,7 @@
import RelaySummary from "@app/components/RelaySummary.svelte" import RelaySummary from "@app/components/RelaySummary.svelte"
import SpaceJoinSettings from "@app/components/SpaceJoinSettings.svelte" import SpaceJoinSettings from "@app/components/SpaceJoinSettings.svelte"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
import {goToSpace} from "@app/routes" import {makeSpacePath} from "@app/routes"
import {relaysMostlyRestricted} from "@app/policies" import {relaysMostlyRestricted} from "@app/policies"
import {notificationSettings, setSpaceNotifications} from "@app/settings" import {notificationSettings, setSpaceNotifications} from "@app/settings"
import {parseInviteLink} from "@app/invites" import {parseInviteLink} from "@app/invites"
@@ -67,7 +68,7 @@
} }
await addSpace(url) await addSpace(url)
await goToSpace(url) await goto(makeSpacePath(url), {replaceState: true})
broadcastUserData([url]) broadcastUserData([url])
relaysMostlyRestricted.update(dissoc(url)) relaysMostlyRestricted.update(dissoc(url))
+3 -2
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {goto} from "$app/navigation"
import {dissoc, maybe} from "@welshman/lib" import {dissoc, maybe} from "@welshman/lib"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -21,7 +22,7 @@
import {notificationSettings} from "@app/settings" import {notificationSettings} from "@app/settings"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
import {goToSpace} from "@app/routes" import {makeSpacePath} from "@app/routes"
import {Push} from "@app/push" import {Push} from "@app/push"
type Props = { type Props = {
@@ -55,7 +56,7 @@
} }
await addSpace(url) await addSpace(url)
await goToSpace(url) await goto(makeSpacePath(url), {replaceState: true})
broadcastUserData([url]) broadcastUserData([url])
relaysMostlyRestricted.update(dissoc(url)) relaysMostlyRestricted.update(dissoc(url))
+5 -6
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import {ManagementMethod} from "@welshman/util" import {ManagementMethod} from "@welshman/util"
import {RelayManagement, Profiles} from "@welshman/app" import {manageRelay, displayProfileByPubkey} from "@welshman/app"
import {app} from "@app/welshman"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl" import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import UserMinus from "@assets/icons/user-minus.svg?dataurl" import UserMinus from "@assets/icons/user-minus.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl" import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
@@ -61,9 +60,9 @@
const unallowMember = (pubkey: string) => const unallowMember = (pubkey: string) =>
pushModal(Confirm, { pushModal(Confirm, {
title: "Remove User", title: "Remove User",
message: `Are you sure you want to remove @${app.use(Profiles).display(pubkey).get()} from the space?`, message: `Are you sure you want to remove @${displayProfileByPubkey(pubkey)} from the space?`,
confirm: async () => { confirm: async () => {
const {error} = await app.use(RelayManagement).post(url, { const {error} = await manageRelay(url, {
method: ManagementMethod.UnallowPubkey, method: ManagementMethod.UnallowPubkey,
params: [pubkey], params: [pubkey],
}) })
@@ -80,9 +79,9 @@
const banMember = (pubkey: string) => const banMember = (pubkey: string) =>
pushModal(Confirm, { pushModal(Confirm, {
title: "Ban User", title: "Ban User",
message: `Are you sure you want to ban @${app.use(Profiles).display(pubkey).get()} from the space?`, message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
confirm: async () => { confirm: async () => {
const {error} = await app.use(RelayManagement).post(url, { const {error} = await manageRelay(url, {
method: ManagementMethod.BanPubkey, method: ManagementMethod.BanPubkey,
params: [pubkey], params: [pubkey],
}) })
+2 -3
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import {displayRelayUrl, ManagementMethod} from "@welshman/util" import {displayRelayUrl, ManagementMethod} from "@welshman/util"
import {RelayManagement} from "@welshman/app" import {manageRelay} from "@welshman/app"
import {app} from "@app/welshman"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl" import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import Restart from "@assets/icons/restart.svg?dataurl" import Restart from "@assets/icons/restart.svg?dataurl"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
@@ -43,7 +42,7 @@
} }
const unbanMember = async (pubkey: string) => { const unbanMember = async (pubkey: string) => {
const {error} = await app.use(RelayManagement).post(url, { const {error} = await manageRelay(url, {
method: ManagementMethod.UnbanPubkey, method: ManagementMethod.UnbanPubkey,
params: [pubkey], params: [pubkey],
}) })
+2 -3
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import {derived} from "svelte/store" import {derived} from "svelte/store"
import {displayRelayUrl, EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, POLL} from "@welshman/util" import {displayRelayUrl, EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, POLL} from "@welshman/util"
import {Relays} from "@welshman/app" import {deriveRelay, createSearch, pubkey} from "@welshman/app"
import {app, createSearch, pubkey} from "@app/welshman"
import {fly} from "@lib/transition" import {fly} from "@lib/transition"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl" import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
@@ -61,7 +60,7 @@
const {url} = $props() const {url} = $props()
const relay = app.use(Relays).one(url) const relay = deriveRelay(url)
const chatPath = makeSpacePath(url, "chat") const chatPath = makeSpacePath(url, "chat")
const goalsPath = makeSpacePath(url, "goals") const goalsPath = makeSpacePath(url, "goals")
const threadsPath = makeSpacePath(url, "threads") const threadsPath = makeSpacePath(url, "threads")
+2 -3
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import {Relays} from "@welshman/app" import {deriveRelay} from "@welshman/app"
import {app} from "@app/welshman"
import Server from "@assets/icons/server.svg?dataurl" import Server from "@assets/icons/server.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import ProfileLink from "@app/components/ProfileLink.svelte" import ProfileLink from "@app/components/ProfileLink.svelte"
@@ -12,7 +11,7 @@
const {url}: Props = $props() const {url}: Props = $props()
const relay = app.use(Relays).one(url) const relay = deriveRelay(url)
</script> </script>
<div class="card2 bg-alt flex flex-col gap-4"> <div class="card2 bg-alt flex flex-col gap-4">
+1 -1
View File
@@ -2,7 +2,7 @@
import {tick} from "svelte" import {tick} from "svelte"
import {debounce} from "throttle-debounce" import {debounce} from "throttle-debounce"
import {request} from "@welshman/net" import {request} from "@welshman/net"
import {repository, tracker} from "@app/welshman" import {repository, tracker} from "@welshman/app"
import {formatTimestampAsDate, groupBy, uniqBy, now, MINUTE, HOUR, DAY, WEEK} from "@welshman/lib" import {formatTimestampAsDate, groupBy, uniqBy, now, MINUTE, HOUR, DAY, WEEK} from "@welshman/lib"
import type {TrustedEvent, Filter} from "@welshman/util" import type {TrustedEvent, Filter} from "@welshman/util"
import {MESSAGE, sortEventsDesc} from "@welshman/util" import {MESSAGE, sortEventsDesc} from "@welshman/util"
+3 -4
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import {writable} from "svelte/store" import {writable} from "svelte/store"
import {makeEvent, THREAD} from "@welshman/util" import {makeEvent, THREAD} from "@welshman/util"
import {Thunks} from "@welshman/app" import {publishThunk, waitForThunkError} from "@welshman/app"
import {app} from "@app/welshman"
import {isMobile, preventDefault} from "@lib/html" import {isMobile, preventDefault} from "@lib/html"
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl" import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -80,12 +79,12 @@
tags.push(["h", h]) tags.push(["h", h])
} }
const threadThunk = app.use(Thunks).publish({ const threadThunk = publishThunk({
relays: [url], relays: [url],
event: makeEvent(THREAD, {content, tags}), event: makeEvent(THREAD, {content, tags}),
}) })
const error = await threadThunk.waitForError() const error = await waitForThunkError(threadThunk)
if (error) { if (error) {
return pushToast({theme: "error", message: error}) return pushToast({theme: "error", message: error})
+5 -6
View File
@@ -1,9 +1,8 @@
<script lang="ts"> <script lang="ts">
import {formatTimestamp} from "@welshman/lib" import {formatTimestamp} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {COMMENT, displayNip05} from "@welshman/util" import {COMMENT} from "@welshman/util"
import {Handles, Profiles} from "@welshman/app" import {deriveHandleForPubkey, deriveProfileDisplay, displayHandle} from "@welshman/app"
import {app} from "@app/welshman"
import Reply from "@assets/icons/reply-2.svg?dataurl" import Reply from "@assets/icons/reply-2.svg?dataurl"
import LinkRound from "@assets/icons/link-round.svg?dataurl" import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -27,8 +26,8 @@
const {url, event, threadPubkey, onReply}: Props = $props() const {url, event, threadPubkey, onReply}: Props = $props()
const profileDisplay = app.use(Profiles).display(event.pubkey, [url]).$ const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
const handle = app.use(Handles).forPubkey(event.pubkey).$ const handle = deriveHandleForPubkey(event.pubkey)
const isOp = event.pubkey === threadPubkey const isOp = event.pubkey === threadPubkey
const isComment = event.kind === COMMENT const isComment = event.kind === COMMENT
@@ -54,7 +53,7 @@
{$profileDisplay} {$profileDisplay}
</Button> </Button>
{#if $handle} {#if $handle}
<span class="ellipsize text-xs opacity-75">{displayNip05($handle?.nip05)}</span> <span class="ellipsize text-xs opacity-75">{displayHandle($handle)}</span>
{/if} {/if}
{#if isOp} {#if isOp}
<span class="badge badge-primary badge-sm">OP</span> <span class="badge badge-primary badge-sm">OP</span>
+4 -5
View File
@@ -2,8 +2,7 @@
import {stopPropagation} from "svelte/legacy" import {stopPropagation} from "svelte/legacy"
import {noop} from "@welshman/lib" import {noop} from "@welshman/lib"
import type {AbstractThunk} from "@welshman/app" import type {AbstractThunk} from "@welshman/app"
import {Thunks} from "@welshman/app" import {flattenThunks, getFailedThunkUrls, publishThunk, thunkIsComplete} from "@welshman/app"
import {app} from "@app/welshman"
import Danger from "@assets/icons/danger-triangle.svg?dataurl" import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Tippy from "@lib/components/Tippy.svelte" import Tippy from "@lib/components/Tippy.svelte"
@@ -19,15 +18,15 @@
const {thunk, showToastOnRetry, ...restProps}: Props = $props() const {thunk, showToastOnRetry, ...restProps}: Props = $props()
const showFailure = $derived($thunk.isComplete() && $thunk.getFailedUrls().length > 0) const showFailure = $derived(thunkIsComplete($thunk) && getFailedThunkUrls($thunk).length > 0)
const retry = (url: string) => { const retry = (url: string) => {
for (const child of app.use(Thunks).flatten([thunk])) { for (const child of flattenThunks([thunk])) {
if (!child.options.relays.includes(url)) { if (!child.options.relays.includes(url)) {
continue continue
} }
const retried = app.use(Thunks).publish({...child.options, relays: [url]}) const retried = publishThunk({...child.options, relays: [url]})
if (showToastOnRetry) { if (showToastOnRetry) {
pushToast({ pushToast({
+3 -2
View File
@@ -2,6 +2,7 @@
import {stopPropagation} from "svelte/legacy" import {stopPropagation} from "svelte/legacy"
import {PublishStatus} from "@welshman/net" import {PublishStatus} from "@welshman/net"
import type {AbstractThunk} from "@welshman/app" import type {AbstractThunk} from "@welshman/app"
import {abortThunk, thunkHasStatus} from "@welshman/app"
interface Props { interface Props {
thunk: AbstractThunk thunk: AbstractThunk
@@ -10,9 +11,9 @@
const {thunk, ...restProps}: Props = $props() const {thunk, ...restProps}: Props = $props()
const abort = () => thunk.abort() const abort = () => abortThunk(thunk)
const isSending = $derived($thunk.hasStatus(PublishStatus.Sending)) const isSending = $derived(thunkHasStatus(PublishStatus.Sending, $thunk))
</script> </script>
<div class="flex w-full justify-end px-1 text-xs {restProps.class}"> <div class="flex w-full justify-end px-1 text-xs {restProps.class}">
+3 -2
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import type {AbstractThunk} from "@welshman/app" import type {AbstractThunk} from "@welshman/app"
import {thunkIsComplete, getFailedThunkUrls} from "@welshman/app"
import ThunkFailure from "@app/components/ThunkFailure.svelte" import ThunkFailure from "@app/components/ThunkFailure.svelte"
import ThunkPending from "@app/components/ThunkPending.svelte" import ThunkPending from "@app/components/ThunkPending.svelte"
@@ -11,8 +12,8 @@
const {thunk, showToastOnRetry, ...restProps}: Props = $props() const {thunk, showToastOnRetry, ...restProps}: Props = $props()
const showFailure = $derived($thunk.isComplete() && $thunk.getFailedUrls().length > 0) const showFailure = $derived(thunkIsComplete($thunk) && getFailedThunkUrls($thunk).length > 0)
const showPending = $derived(!$thunk.isComplete()) const showPending = $derived(!thunkIsComplete($thunk))
</script> </script>
{#if showFailure} {#if showFailure}
+3 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import {stopPropagation} from "svelte/legacy" import {stopPropagation} from "svelte/legacy"
import type {AbstractThunk} from "@welshman/app" import type {AbstractThunk} from "@welshman/app"
import {getFailedThunkUrls, getThunkUrlsWithStatus} from "@welshman/app"
import {PublishStatus} from "@welshman/net" import {PublishStatus} from "@welshman/net"
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import CheckCircle from "@assets/icons/check-circle.svg?dataurl" import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
@@ -16,8 +17,8 @@
const {thunk, retry}: Props = $props() const {thunk, retry}: Props = $props()
const successUrls = $derived($thunk.getUrlsWithStatus(PublishStatus.Success)) const successUrls = $derived(getThunkUrlsWithStatus(PublishStatus.Success, $thunk))
const failedUrls = $derived($thunk.getFailedUrls()) const failedUrls = $derived(getFailedThunkUrls($thunk))
const total = $derived(successUrls.length + failedUrls.length) const total = $derived(successUrls.length + failedUrls.length)
const isPartial = $derived(successUrls.length > 0 && failedUrls.length > 0) const isPartial = $derived(successUrls.length > 0 && failedUrls.length > 0)
@@ -3,8 +3,7 @@
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {PublishStatus} from "@welshman/net" import {PublishStatus} from "@welshman/net"
import {deriveIsDeleted} from "@welshman/store" import {deriveIsDeleted} from "@welshman/store"
import {Thunks} from "@welshman/app" import {thunks, mergeThunks, thunkHasStatus, repository} from "@welshman/app"
import {app, thunks, repository} from "@app/welshman"
import ThunkStatus from "@app/components/ThunkStatus.svelte" import ThunkStatus from "@app/components/ThunkStatus.svelte"
type Props = { type Props = {
@@ -15,12 +14,12 @@
const {event, children}: Props = $props() const {event, children}: Props = $props()
const deleted = deriveIsDeleted(repository, event) const deleted = deriveIsDeleted(repository, event)
const thunk = $derived(app.use(Thunks).merge($thunks.filter(t => t.event.id === event.id))) const thunk = $derived(mergeThunks($thunks.filter(t => t.event.id === event.id)))
</script> </script>
{#if $deleted} {#if $deleted}
<div class="btn btn-error btn-xs rounded-full">Deleted</div> <div class="btn btn-error btn-xs rounded-full">Deleted</div>
{:else if thunk.thunks.length > 0 && !thunk.hasStatus(PublishStatus.Success)} {:else if thunk.thunks.length > 0 && !thunkHasStatus(PublishStatus.Success, thunk)}
<ThunkStatus {thunk} /> <ThunkStatus {thunk} />
{:else if children} {:else if children}
{@render children?.()} {@render children?.()}

Some files were not shown because too many files have changed in this diff Show More