Compare commits

..

2 Commits

Author SHA1 Message Date
hodlbod 9df8cee501 Migrate to new @welshman/domain + instance-based @welshman/app API
Adopts the rewritten welshman API: the removed @welshman/util helpers
(Profile/List/Room/Handler/Encryptable) are now Reader/Builder classes in
@welshman/domain, and @welshman/app dropped its global singletons for an App
instance + app.use(Plugin) registry.

- src/app/welshman.ts is now the app bootstrap + session-state module (one shared
  App instance, multi-account sessions/login, app-wide reactive views) rather than
  a compat shim re-exporting the old globals.
- Rewrote ~100 callers to use app.use(Plugin) directly (thunks, profiles, relays,
  rooms, zaps, tags, wot, feeds, sync); thunk helpers are now thunk methods.
- Added @welshman/domain dependency.
- Resolved residual gaps (storage hydration via plugin.onItem/wrapManager/Plaintext,
  relay-list mutators, search-relay list, outbox #d filter).

Best-effort: no toolchain/linking available, so this is not build- or
type-checked. Remaining judgment calls are flagged with TODO(welshman-migration).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BsMjvv7krpZeHK1Njeneru
2026-06-20 14:55:06 +00:00
Jon Staab fd4e7a9f2d Fix doubled side rail and some space navigation 2026-06-19 21:59:05 -07:00
165 changed files with 1487 additions and 1748 deletions
+1
View File
@@ -74,6 +74,7 @@
"@vite-pwa/sveltekit": "^1.1.0",
"@welshman/app": "^0.8.16",
"@welshman/content": "^0.8.16",
"@welshman/domain": "^0.8.16",
"@welshman/editor": "^0.8.16",
"@welshman/feeds": "^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 type {TrustedEvent} from "@welshman/util"
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
import {signer} from "@welshman/app"
import {signer} from "@app/welshman"
import {load} from "@welshman/net"
import {getLivekitEndpoint} from "$lib/livekit"
import {AbortError, TimeoutError, whenAborted, whenTimeout} from "$lib/util"
+4 -1
View File
@@ -2,7 +2,8 @@ import {DELETE, PROFILE, getPubkeyTagValues} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {append, call, on, reject, remove, sort, sortBy, spec, uniq, uniqBy} from "@welshman/lib"
import type {Override} from "@welshman/lib"
import {createSearch, displayProfileByPubkey, pubkey, repository} from "@welshman/app"
import {Profiles} from "@welshman/app"
import {app, createSearch, pubkey, repository} from "@app/welshman"
import {derived, readable} from "svelte/store"
import {DM_KINDS} from "@app/content"
import type {RepositoryUpdate} from "@welshman/net"
@@ -35,6 +36,8 @@ export const chatsById = call(() => {
const chatsByPubkey = new Map<string, string[]>()
const addSearchText = (chat: Override<Chat, {search_text?: string}>) => {
const displayProfileByPubkey = (pk: string) => app.use(Profiles).display(pk).get()
chat.search_text =
chat.pubkeys.length === 1
? displayProfileByPubkey(chat.pubkeys[0]) + " note to self"
+4 -3
View File
@@ -1,6 +1,7 @@
import type {TrustedEvent} from "@welshman/util"
import {COMMENT, makeEvent} from "@welshman/util"
import {publishThunk, tagEventForComment} from "@welshman/app"
import {Thunks, Tags} from "@welshman/app"
import {app} from "@app/welshman"
export type CommentParams = {
event: TrustedEvent
@@ -10,7 +11,7 @@ export type CommentParams = {
}
export const makeComment = ({url, event, content, tags = []}: CommentParams) =>
makeEvent(COMMENT, {content, tags: [...tags, ...tagEventForComment(event, url)]})
makeEvent(COMMENT, {content, tags: [...tags, ...app.use(Tags).tagEventForComment(event, url)]})
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
publishThunk({event: makeComment({url: relays[0], ...params}), relays})
app.use(Thunks).publish({event: makeComment({url: relays[0], ...params}), relays})
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import type {Snippet} from "svelte"
import {pubkey} from "@welshman/app"
import {pubkey} from "@app/welshman"
import Dialog from "@lib/components/Dialog.svelte"
import Landing from "@app/components/Landing.svelte"
import Toast from "@app/components/Toast.svelte"
@@ -1,7 +1,7 @@
<script lang="ts">
import type {TrustedEvent, EventContent} from "@welshman/util"
import {getTagValue, getAddress} from "@welshman/util"
import {pubkey} from "@welshman/app"
import {pubkey} from "@app/welshman"
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
+4 -3
View File
@@ -3,7 +3,8 @@
import {writable} from "svelte/store"
import {randomId, HOUR} from "@welshman/lib"
import {makeEvent, EVENT_TIME} from "@welshman/util"
import {publishThunk, waitForThunkError} from "@welshman/app"
import {app} from "@app/welshman"
import {Thunks} from "@welshman/app"
import {preventDefault} from "@lib/html"
import {daysBetween} from "@lib/util"
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
@@ -107,8 +108,8 @@
}
const event = makeEvent(EVENT_TIME, {content, tags})
const calendarThunk = publishThunk({event, relays: [url]})
const error = await waitForThunkError(calendarThunk)
const calendarThunk = app.use(Thunks).publish({event, relays: [url]})
const error = await calendarThunk.waitForError()
if (error) {
return pushToast({theme: "error", message: error})
+9 -13
View File
@@ -26,14 +26,10 @@
DIRECT_MESSAGE,
DIRECT_MESSAGE_FILE,
} from "@welshman/util"
import {
pubkey,
tagPubkey,
sendWrapped,
mergeThunks,
loadMessagingRelayList,
messagingRelayListsByPubkey,
} from "@welshman/app"
import {app, pubkey} from "@app/welshman"
import {Tags, Wraps, Thunks, MessagingRelayLists} from "@welshman/app"
const messagingRelayListsByPubkey = app.use(MessagingRelayLists).index.$
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import ArrowLeft from "@assets/icons/arrow-left.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
@@ -96,7 +92,7 @@
const onSubmit = async (params: EventContent) => {
try {
const ptags = remove($pubkey!, pubkeys).map(tagPubkey)
const ptags = remove($pubkey!, pubkeys).map(pk => app.use(Tags).tagPubkey(pk))
// Remove p tags since they result in forking the conversation
params.tags = params.tags.filter(nthNe(0, "p"))
@@ -109,7 +105,7 @@
return
}
await sendWrapped({
await app.use(Wraps).publish({
event: makeDelete({event: eventToEdit, protect: false}),
recipients: pubkeys,
pow: 16,
@@ -158,7 +154,7 @@
// Sleep 1 second between each one to make sure timestamps are distinct
const thunks = await Promise.all(
Array.from(enumerate(templates)).map(([i, event]) =>
sendWrapped({
app.use(Wraps).publish({
event,
recipients: pubkeys,
delay: $userSettingsValues.send_delay + ms(i),
@@ -171,7 +167,7 @@
timeout: 30_000,
children: {
component: ThunkToast,
props: {thunk: mergeThunks(thunks)},
props: {thunk: app.use(Thunks).merge(thunks)},
},
})
} finally {
@@ -234,7 +230,7 @@
onMount(() => {
for (const pubkey of others) {
loadMessagingRelayList(pubkey)
app.use(MessagingRelayLists).load(pubkey)
}
})
+3 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import type {TrustedEvent} from "@welshman/util"
import {displayProfileByPubkey} from "@welshman/app"
import {app} from "@app/welshman"
import {Profiles} from "@welshman/app"
import {slide} from "@lib/transition"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
@@ -21,7 +22,7 @@
<div
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8"
transition:slide>
<p class="text-xs text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
<p class="text-xs text-primary">{verb} @{app.use(Profiles).display(event.pubkey).get()}</p>
{#key event.id}
<NoteContentMinimal trimParent {event} />
{/key}
+9 -5
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import {getRelaysFromList} from "@welshman/util"
import {waitForThunkError, setMessagingRelays, userRelayList, setRelays} from "@welshman/app"
import {app, userRelayList} from "@app/welshman"
import {RelayLists, MessagingRelayLists} from "@welshman/app"
import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
@@ -29,8 +29,10 @@
loading = true
try {
if (getRelaysFromList($userRelayList).length === 0) {
const error = await waitForThunkError(await setRelays(DEFAULT_RELAYS.map(r => ["r", r])))
if (($userRelayList?.urls() ?? []).length === 0) {
const error = await (
await app.use(RelayLists).setRelays(DEFAULT_RELAYS.map(r => ["r", r]))
).waitForError()
if (error) {
pushToast({theme: "error", message: error})
@@ -38,7 +40,9 @@
}
}
const error = await waitForThunkError(await setMessagingRelays(DEFAULT_MESSAGING_RELAYS))
const error = await (
await app.use(MessagingRelayLists).setUrls(DEFAULT_MESSAGING_RELAYS)
).waitForError()
if (error) {
pushToast({theme: "error", message: error})
+3 -2
View File
@@ -3,7 +3,8 @@
import {page} from "$app/stores"
import {remove, uniq, formatTimestamp} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {pubkey, loadMessagingRelayList} from "@welshman/app"
import {app, pubkey} from "@app/welshman"
import {MessagingRelayLists} from "@welshman/app"
import {fade} from "@lib/transition"
import Button from "@lib/components/Button.svelte"
import ProfileName from "@app/components/ProfileName.svelte"
@@ -28,7 +29,7 @@
onMount(() => {
for (const pk of others) {
loadMessagingRelayList(pk)
app.use(MessagingRelayLists).load(pk)
}
})
</script>
+6 -5
View File
@@ -2,7 +2,8 @@
import {type Instance} from "tippy.js"
import {hash, formatTimestampAsTime} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {thunks, mergeThunks, pubkey, deriveProfileDisplay, sendWrapped} from "@welshman/app"
import {app, thunks, pubkey} from "@app/welshman"
import {Thunks, Profiles, Wraps} from "@welshman/app"
import {isMobile} from "@lib/html"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
@@ -33,18 +34,18 @@
const {event, replyTo, canEdit, onEdit, pubkeys, showPubkey = false}: Props = $props()
const isOwn = event.pubkey === $pubkey
const profileDisplay = deriveProfileDisplay(event.pubkey)
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
const profileDisplay = app.use(Profiles).display(event.pubkey).$
const thunk = app.use(Thunks).merge($thunks.filter(t => t.event.id === event.id))
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
const reply = () => replyTo(event)
const edit = canEdit?.(event) ? () => onEdit?.(event) : undefined
const deleteReaction = (event: TrustedEvent) =>
sendWrapped({event: makeDelete({event, protect: false}), recipients: pubkeys, pow: 16})
app.use(Wraps).publish({event: makeDelete({event, protect: false}), recipients: pubkeys, pow: 16})
const createReaction = (template: EventContent) =>
sendWrapped({
app.use(Wraps).publish({
event: makeReaction({event, protect: false, ...template}),
recipients: pubkeys,
pow: 16,
@@ -1,7 +1,8 @@
<script lang="ts">
import type {NativeEmoji} from "emoji-picker-element/shared"
import type {TrustedEvent} from "@welshman/util"
import {sendWrapped} from "@welshman/app"
import {app} from "@app/welshman"
import {Wraps} from "@welshman/app"
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import EmojiButton from "@lib/components/EmojiButton.svelte"
@@ -15,7 +16,7 @@
const {event, pubkeys}: Props = $props()
const onEmoji = (emoji: NativeEmoji) =>
sendWrapped({
app.use(Wraps).publish({
event: makeReaction({event, content: emoji.unicode, protect: false}),
recipients: pubkeys,
pow: 16,
@@ -1,7 +1,8 @@
<script lang="ts">
import type {NativeEmoji} from "emoji-picker-element/shared"
import type {TrustedEvent} from "@welshman/util"
import {sendWrapped} from "@welshman/app"
import {app} from "@app/welshman"
import {Wraps} from "@welshman/app"
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
import Pen from "@assets/icons/pen.svg?dataurl"
import Reply from "@assets/icons/reply-2.svg?dataurl"
@@ -28,7 +29,7 @@
const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => {
history.back()
sendWrapped({
app.use(Wraps).publish({
event: makeReaction({event, content: emoji.unicode, protect: false}),
recipients: pubkeys,
pow: 16,
+3 -2
View File
@@ -4,7 +4,8 @@
import {writable} from "svelte/store"
import {tryCatch, uniq} from "@welshman/lib"
import {fromNostrURI} from "@welshman/util"
import {loadMessagingRelayList} from "@welshman/app"
import {app} from "@app/welshman"
import {MessagingRelayLists} from "@welshman/app"
import {preventDefault} from "@lib/html"
import Field from "@lib/components/Field.svelte"
import Button from "@lib/components/Button.svelte"
@@ -34,7 +35,7 @@
let pubkeys: string[] = $state([])
$effect(() => {
pubkeys.forEach(pubkey => loadMessagingRelayList(pubkey))
pubkeys.forEach(pubkey => app.use(MessagingRelayLists).load(pubkey))
})
onMount(() => {
+1 -1
View File
@@ -2,7 +2,7 @@
import {uniq} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {getTagValue, getTagValues, getAddress} from "@welshman/util"
import {pubkey} from "@welshman/app"
import {pubkey} from "@app/welshman"
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
import {normalizeTopic} from "@lib/util"
import Link from "@lib/components/Link.svelte"
+4 -3
View File
@@ -2,7 +2,8 @@
import type {Snippet} from "svelte"
import {removeUndefined, randomId, uniq} from "@welshman/lib"
import {makeEvent, CLASSIFIED} from "@welshman/util"
import {publishThunk, waitForThunkError} from "@welshman/app"
import {app} from "@app/welshman"
import {Thunks} from "@welshman/app"
import {isMobile, preventDefault} from "@lib/html"
import {normalizeTopic} from "@lib/util"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -118,12 +119,12 @@
}
}
const classifiedThunk = publishThunk({
const classifiedThunk = app.use(Thunks).publish({
relays: [url],
event: makeEvent(CLASSIFIED, {content, tags}),
})
const error = await waitForThunkError(classifiedThunk)
const error = await classifiedThunk.waitForError()
if (error) {
return pushToast({theme: "error", message: error})
@@ -9,7 +9,7 @@
tagsFromIMeta,
makeBlossomAuthEvent,
} from "@welshman/util"
import {signer} from "@welshman/app"
import {signer} from "@app/welshman"
import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
+3 -2
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import {removeUndefined} from "@welshman/lib"
import type {ProfilePointer} from "@welshman/content"
import {deriveProfileDisplay} from "@welshman/app"
import {app} from "@app/welshman"
import {Profiles} from "@welshman/app"
import Button from "@lib/components/Button.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte"
import {pushModal} from "@app/modal"
@@ -13,7 +14,7 @@
const {value, url}: Props = $props()
const display = deriveProfileDisplay(value.pubkey, removeUndefined([url]))
const display = app.use(Profiles).display(value.pubkey, removeUndefined([url])).$
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url})
</script>
+1 -1
View File
@@ -4,7 +4,7 @@
import type {Writable} from "svelte/store"
import type {Instance} from "tippy.js"
import {preventDefault} from "@lib/html"
import {createSearch} from "@welshman/app"
import {createSearch} from "@app/welshman"
import {currencyOptions, displayCurrency} from "@lib/currency"
import Suggestions from "@lib/components/Suggestions.svelte"
import CurrencySuggestion from "@app/components/CurrencySuggestion.svelte"
+1 -1
View File
@@ -5,7 +5,7 @@
import {load} from "@welshman/net"
import {deriveArray, deriveEventsById} from "@welshman/store"
import type {TrustedEvent} from "@welshman/util"
import {repository} from "@welshman/app"
import {repository} from "@app/welshman"
import {deriveChecked} from "@app/notifications"
import Reply from "@assets/icons/reply-2.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
+1 -1
View File
@@ -4,7 +4,7 @@
import {LOCALE, secondsToDate} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {displayRelayUrl} from "@welshman/util"
import {tracker} from "@welshman/app"
import {tracker} from "@app/welshman"
import FileText from "@assets/icons/file-text.svg?dataurl"
import Copy from "@assets/icons/copy.svg?dataurl"
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
+3 -2
View File
@@ -4,7 +4,8 @@
import {goto} from "$app/navigation"
import type {TrustedEvent} from "@welshman/util"
import {COMMENT, ManagementMethod} from "@welshman/util"
import {pubkey, repository, relaysByUrl, manageRelay} from "@welshman/app"
import {app, pubkey, repository, relaysByUrl} from "@app/welshman"
import {RelayManagement} from "@welshman/app"
import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
@@ -56,7 +57,7 @@
title: `Delete ${noun}`,
message: `Are you sure you want to delete this ${noun.toLowerCase()} from the space?`,
confirm: async () => {
const {error} = await manageRelay(url, {
const {error} = await app.use(RelayManagement).post(url, {
method: ManagementMethod.BanEvent,
params: [event.id],
})
+4 -3
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import {writable} from "svelte/store"
import {makeEvent, ZAP_GOAL} from "@welshman/util"
import {publishThunk, waitForThunkError} from "@welshman/app"
import {app} from "@app/welshman"
import {Thunks} from "@welshman/app"
import {isMobile, preventDefault} from "@lib/html"
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
import Bolt from "@assets/icons/bolt.svg?dataurl"
@@ -93,12 +94,12 @@
tags.push(["h", h])
}
const goalThunk = publishThunk({
const goalThunk = app.use(Thunks).publish({
relays: [url],
event: makeEvent(ZAP_GOAL, {content: title, tags}),
})
const error = await waitForThunkError(goalThunk)
const error = await goalThunk.waitForError()
if (error) {
return pushToast({theme: "error", message: error})
+13 -9
View File
@@ -2,8 +2,10 @@
import {now, DAY, uniq, sum} from "@welshman/lib"
import type {Zap, TrustedEvent} from "@welshman/util"
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
import {deriveItemsByKey, deriveArray} from "@welshman/store"
import {repository, getValidZap} from "@welshman/app"
import {derived} from "svelte/store"
import {deriveEvents} from "@welshman/store"
import {app, repository} from "@app/welshman"
import {Zappers} from "@welshman/app"
import Bolt from "@assets/icons/bolt.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import ZapButton from "@app/components/ZapButton.svelte"
@@ -16,13 +18,15 @@
const {url, event, ...props}: Props = $props()
const zaps = deriveArray(
deriveItemsByKey<Zap>({
repository,
getKey: zap => zap.response.id,
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
eventToItem: (response: TrustedEvent) => getValidZap(response, event),
}),
// Validated zaps for this goal. `validZapReceipts` is a reactive Projection
// (resolves recipient zappers from loaded profiles); we re-derive it whenever
// the set of ZAP_RESPONSE events in the repository changes.
const zapReceipts = deriveEvents({repository, filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}]})
const zaps = derived(
zapReceipts,
($receipts: TrustedEvent[], set) =>
app.use(Zappers).validZapReceipts($receipts, event).$.subscribe(set),
[] as Zap[],
)
const goalAmount = parseInt(getTagValue("amount", event.tags) || "0")
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import {createSearch} from "@welshman/app"
import {createSearch} from "@app/welshman"
import Icon from "@lib/components/Icon.svelte"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import {session} from "@welshman/app"
import {session} from "@app/welshman"
import Link from "@lib/components/Link.svelte"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
+3 -2
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import {deriveZapperForPubkey} from "@welshman/app"
import {app} from "@app/welshman"
import {Zappers} from "@welshman/app"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
@@ -12,7 +13,7 @@
const {pubkey} = $props()
const zapper = deriveZapperForPubkey(pubkey)
const zapper = app.use(Zappers).forPubkey(pubkey).$
const back = () => history.back()
</script>
+8 -2
View File
@@ -1,8 +1,14 @@
<script lang="ts">
import {Client} from "@pomade/core"
import {getPubkey} from "@welshman/util"
import type {SessionPomade} from "@welshman/app"
import {session} from "@welshman/app"
import {session} from "@app/welshman"
// TODO(welshman-migration): `SessionPomade` was removed from @welshman/app; the
// 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 AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
+7 -2
View File
@@ -1,7 +1,12 @@
<script lang="ts">
import {Client} from "@pomade/core"
import type {SessionPomade} from "@welshman/app"
import {session} from "@welshman/app"
import {session} from "@app/welshman"
// TODO(welshman-migration): `SessionPomade` was removed from @welshman/app; the
// 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 AltArrowLeft from "@assets/icons/alt-arrow-left.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 {Capacitor} from "@capacitor/core"
import {getNip07, getNip55, Nip55Signer} from "@welshman/signer"
import {addSession, type Session, makeNip07Session, makeNip55Session} from "@welshman/app"
import {addSession, type FlotillaSession, makeNip07Session, makeNip55Session} from "@app/welshman"
import Widget from "@assets/icons/widget-4.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl"
import Cpu from "@assets/icons/cpu-bolt.svg?dataurl"
@@ -32,8 +32,8 @@
const signUp = () => pushModal(SignUp)
const onSuccess = async (session: Session) => {
addSession(session)
const onSuccess = async (session: FlotillaSession, pubkey: string) => {
addSession({...session, pubkey})
setChecked("*")
clearModals()
}
@@ -45,7 +45,7 @@
const pubkey = await getNip07()?.getPublicKey()
if (pubkey) {
await onSuccess(makeNip07Session(pubkey))
await onSuccess(makeNip07Session(pubkey), pubkey)
} else {
pushToast({
theme: "error",
@@ -65,7 +65,7 @@
const pubkey = await signer.getPubkey()
if (pubkey) {
await onSuccess(makeNip55Session(pubkey, app.packageName))
await onSuccess(makeNip55Session(pubkey, app.packageName), pubkey)
} else {
pushToast({
theme: "error",
+1 -1
View File
@@ -4,7 +4,7 @@
import type {Nip46ResponseWithResult} from "@welshman/signer"
import {Nip46Broker} from "@welshman/signer"
import {makeSecret} from "@welshman/util"
import {loginWithNip01, loginWithNip46} from "@welshman/app"
import {loginWithNip01, loginWithNip46} from "@app/welshman"
import {preventDefault} from "@lib/html"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
+2 -2
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import {bytesToHex} from "@welshman/lib"
import {loginWithNip01} from "@welshman/app"
import {loginWithNip01} from "@app/welshman"
import {decrypt} from "nostr-tools/nip49"
import {preventDefault} from "@lib/html"
import {nsecDecode} from "@lib/util"
@@ -56,7 +56,7 @@
})
}
loginWithNip01(secret)
await loginWithNip01(secret)
setChecked("*")
clearModals()
} catch (e) {
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import {Capacitor} from "@capacitor/core"
import {pubkey} from "@welshman/app"
import {pubkey} from "@app/welshman"
import Server from "@assets/icons/server.svg?dataurl"
import GalleryMinimalistic from "@assets/icons/gallery-minimalistic.svg?dataurl"
import Shield from "@assets/icons/shield-minimalistic.svg?dataurl"
@@ -3,8 +3,10 @@
import {sum} from "@welshman/lib"
import type {Zap, TrustedEvent} from "@welshman/util"
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
import {deriveItemsByKey, deriveArray} from "@welshman/store"
import {repository, getValidZap} from "@welshman/app"
import {derived} from "svelte/store"
import {deriveEvents} from "@welshman/store"
import {app, repository} from "@app/welshman"
import {Zappers} from "@welshman/app"
import Bolt from "@assets/icons/bolt.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import ContentMinimal from "@app/components/ContentMinimal.svelte"
@@ -14,13 +16,14 @@
const content = getTagValue("summary", props.event.tags)
const fakeEvent = {content, tags: props.event.tags}
const zaps = deriveArray(
deriveItemsByKey<Zap>({
repository,
getKey: zap => zap.response.id,
filters: [{kinds: [ZAP_RESPONSE], "#e": [props.event.id]}],
eventToItem: (response: TrustedEvent) => getValidZap(response, props.event),
}),
// Validated zaps for this goal (reactive Projection, re-derived as the set of
// ZAP_RESPONSE events in the repository changes).
const zapReceipts = deriveEvents({repository, filters: [{kinds: [ZAP_RESPONSE], "#e": [props.event.id]}]})
const zaps = derived(
zapReceipts,
($receipts: TrustedEvent[], set) =>
app.use(Zappers).validZapReceipts($receipts, props.event).$.subscribe(set),
[] as Zap[],
)
const goalAmount = parseInt(getTagValue("amount", props.event.tags) || "0")
+1 -33
View File
@@ -4,16 +4,11 @@
import type {TrustedEvent, EventContent} from "@welshman/util"
import {Router} from "@welshman/router"
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 Button from "@lib/components/Button.svelte"
import Popover from "@lib/components/Popover.svelte"
import EmojiButton from "@lib/components/EmojiButton.svelte"
import NoteContent from "@app/components/NoteContent.svelte"
import NoteCard from "@app/components/NoteCard.svelte"
import ReactionSummary from "@app/components/ReactionSummary.svelte"
import ProfileNoteMenu from "@app/components/ProfileNoteMenu.svelte"
import {publishDelete} from "@app/deletes"
import {publishReaction} from "@app/reactions"
import {canEnforceNip70} from "@app/relays"
@@ -22,10 +17,9 @@
event: TrustedEvent
children?: Snippet
url?: string
editable?: boolean
}
const {url, event, children, editable = false}: Props = $props()
const {url, event, children}: Props = $props()
const relays = url ? [url] : Router.get().Event(event).getUrls()
@@ -44,35 +38,9 @@
content: emoji.unicode,
protect: await shouldProtect,
})
const toggleMenu = () => {
showMenu = !showMenu
}
const closeMenu = () => {
showMenu = false
}
let showMenu = $state(false)
</script>
<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" />
<div class="flex w-full justify-between gap-2">
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-right">
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import {Client} from "@pomade/core"
import type {SessionPomade} from "@welshman/app"
import {session} from "@welshman/app"
import {session} from "@app/welshman"
import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
@@ -1,7 +1,12 @@
<script lang="ts">
import {Client} from "@pomade/core"
import {session} from "@welshman/app"
import type {SessionPomade} from "@welshman/app"
import {session} from "@app/welshman"
// TODO(welshman-migration): `SessionPomade` was removed from @welshman/app; the
// 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 Key from "@assets/icons/key.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
+9 -6
View File
@@ -1,11 +1,12 @@
<script lang="ts">
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
import Profile from "@app/components/Profile.svelte"
import ProfileInfo from "@app/components/ProfileInfo.svelte"
import ProfileBadges from "@app/components/ProfileBadges.svelte"
import {makeProfilePath} from "@app/routes"
import ProfileDetail from "@app/components/ProfileDetail.svelte"
import {pushModal} from "@app/modal"
type Props = {
pubkey: string
@@ -13,20 +14,22 @@
}
const {pubkey, url}: Props = $props()
const openProfile = () => pushModal(ProfileDetail, {pubkey, url})
</script>
<div class="card2 bg-alt flex flex-col gap-4 shadow-md">
<div class="flex justify-between">
<Profile {pubkey} {url} />
<Link href={makeProfilePath(pubkey)} class="btn btn-primary hidden sm:flex">
<Button onclick={openProfile} class="btn btn-primary hidden sm:flex">
<Icon icon={UserCircle} />
View Profile
</Link>
</Button>
</div>
<ProfileInfo {pubkey} {url} />
<ProfileBadges {pubkey} {url} />
<Link href={makeProfilePath(pubkey)} class="btn btn-primary sm:hidden">
<Button onclick={openProfile} class="btn btn-primary sm:hidden">
<Icon icon={UserCircle} />
View Profile
</Link>
</Button>
</div>
+4 -3
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import {insertAt, now, randomId, removeAt, removeUndefined} from "@welshman/lib"
import {makeEvent, POLL} from "@welshman/util"
import {publishThunk, waitForThunkError} from "@welshman/app"
import {Thunks} from "@welshman/app"
import {app} from "@app/welshman"
import {isMobile, preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import HamburgerMenu from "@assets/icons/hamburger-menu.svg?dataurl"
@@ -142,12 +143,12 @@
tags.push(PROTECTED)
}
const pollThunk = publishThunk({
const pollThunk = app.use(Thunks).publish({
relays: [url],
event: makeEvent(POLL, {content: title.trim(), tags}),
})
const error = await waitForThunkError(pollThunk)
const error = await pollThunk.waitForError()
if (error) {
return pushToast({theme: "error", message: error})
+7 -5
View File
@@ -2,7 +2,9 @@
import {onDestroy} from "svelte"
import type {TrustedEvent} from "@welshman/util"
import {POLL_RESPONSE} from "@welshman/util"
import {pubkey, publishThunk, abortThunk} from "@welshman/app"
import type {Thunk} from "@welshman/app"
import {Thunks} from "@welshman/app"
import {pubkey, app} from "@app/welshman"
import {formatTimestampRelative} from "@welshman/lib"
import {deriveEvents} from "@app/repository"
import {pushToast} from "@app/toast"
@@ -50,7 +52,7 @@
const publishSelection = (selection: string[]) => {
if (activeThunk) {
abortThunk(activeThunk)
activeThunk.abort()
}
if (selection.length === 0) {
@@ -58,7 +60,7 @@
return
}
activeThunk = publishThunk({
activeThunk = app.use(Thunks).publish({
relays: [url],
event: makePollResponse({event, selectedIds: selection}),
delay: publishDelay,
@@ -92,7 +94,7 @@
}
let selectedIds = $state<string[]>([])
let activeThunk: ReturnType<typeof publishThunk> | undefined
let activeThunk: Thunk | undefined
$effect(() => {
if (ownResponse) {
@@ -102,7 +104,7 @@
onDestroy(() => {
if (activeThunk) {
abortThunk(activeThunk)
activeThunk.abort()
}
})
</script>
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import {onMount} from "svelte"
import {Client} from "@pomade/core"
import {session, isPomadeSession} from "@welshman/app"
import {session, isPomadeSession} from "@app/welshman"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import {fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte"
+5 -5
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import type {Snippet} from "svelte"
import {userProfile} from "@welshman/app"
import {userProfile} from "@app/welshman"
import Letter from "@assets/icons/letter.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import Widget from "@assets/icons/widget-4.svg?dataurl"
@@ -41,8 +41,8 @@
{/if}
<div class="flex flex-col">
<PrimaryNavItem title="Settings" href="/settings/profile" prefix="/settings">
{#if $userProfile?.picture}
<ImageIcon alt="Settings" src={$userProfile?.picture} class="rounded-full" size={10} />
{#if $userProfile?.picture()}
<ImageIcon alt="Settings" src={$userProfile?.picture()} class="rounded-full" size={10} />
{:else}
<ImageIcon alt="Settings" src={UserRounded} class="rounded-full" size={8} />
{/if}
@@ -86,8 +86,8 @@
{/if}
</div>
<PrimaryNavItem title="Settings" onclick={showSettingsMenu}>
{#if $userProfile?.picture}
<ImageIcon alt="Settings" src={$userProfile?.picture} size={10} class="rounded-full" />
{#if $userProfile?.picture()}
<ImageIcon alt="Settings" src={$userProfile?.picture()} size={10} class="rounded-full" />
{:else}
<ImageIcon alt="Settings" src={Settings} size={8} class="rounded-full" />
{/if}
@@ -1,5 +1,6 @@
<script lang="ts">
import {deriveRelayDisplay} from "@welshman/app"
import {Relays} from "@welshman/app"
import {app} from "@app/welshman"
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
import RelayIcon from "@app/components/RelayIcon.svelte"
import {makeSpacePath, goToSpace} from "@app/routes"
@@ -15,7 +16,7 @@
const path = makeSpacePath(url)
const display = $derived(deriveRelayDisplay(url))
const display = $derived(app.use(Relays).display(url).$)
</script>
<PrimaryNavItem
+6 -5
View File
@@ -1,8 +1,9 @@
<script lang="ts">
import * as nip19 from "nostr-tools/nip19"
import {removeUndefined} from "@welshman/lib"
import {displayPubkey} from "@welshman/util"
import {deriveHandleForPubkey, displayHandle, deriveProfileDisplay} from "@welshman/app"
import {displayPubkey, displayNip05} from "@welshman/util"
import {Handles, Profiles} from "@welshman/app"
import {app} from "@app/welshman"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
@@ -23,8 +24,8 @@
const {pubkey, url, showPubkey, inert, avatarSize = 10}: Props = $props()
const relays = removeUndefined([url])
const profileDisplay = deriveProfileDisplay(pubkey, relays)
const handle = deriveHandleForPubkey(pubkey)
const profileDisplay = app.use(Profiles).display(pubkey, relays).$
const handle = app.use(Handles).forPubkey(pubkey).$
const openProfile = () => {
pushModal(ProfileDetail, {pubkey, url})
@@ -58,7 +59,7 @@
</div>
{#if $handle}
<div class="overflow-hidden text-ellipsis text-sm opacity-75">
{displayHandle($handle)}
{displayNip05($handle?.nip05)}
</div>
{/if}
{#if showPubkey}
+5 -2
View File
@@ -6,7 +6,8 @@
import {deriveEventsDesc, deriveEventsById} from "@welshman/store"
import {formatTimestampRelative} from "@welshman/lib"
import {NOTE, ROOMS, COMMENT, MESSAGE} from "@welshman/util"
import {repository, loadRelayList} from "@welshman/app"
import {RelayLists} from "@welshman/app"
import {repository, app} from "@app/welshman"
import Button from "@lib/components/Button.svelte"
import ProfileSpaces from "@app/components/ProfileSpaces.svelte"
import {deriveGroupList, getSpaceUrlsFromGroupList} from "@app/groups"
@@ -29,8 +30,10 @@
const openSpaces = () => pushModal(ProfileSpaces, {pubkey, url})
onMount(async () => {
await loadRelayList(pubkey)
// Make sure we have their relay selections before we load their posts
await app.use(RelayLists).load(pubkey)
// Load groups and at least one note, regardless of time frame
load({
filters: [
{authors: [pubkey], kinds: [ROOMS]},
+4 -3
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import cx from "classnames"
import {removeUndefined} from "@welshman/lib"
import {deriveProfile} from "@welshman/app"
import {Profiles} from "@welshman/app"
import {app} from "@app/welshman"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import ImageIcon from "@lib/components/ImageIcon.svelte"
@@ -14,11 +15,11 @@
const {pubkey, url, size = 7, ...props}: Props = $props()
const profile = deriveProfile(pubkey, removeUndefined([url]))
const profile = app.use(Profiles).one(pubkey, removeUndefined([url]))
</script>
<ImageIcon
{size}
alt=""
class={cx(props.class, "rounded-full")}
src={$profile?.picture || UserRounded} />
src={$profile?.picture() || UserRounded} />
+11 -6
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import cx from "classnames"
import {loadProfile} from "@welshman/app"
import {Profiles} from "@welshman/app"
import {app} from "@app/welshman"
import {isMobile} from "@lib/html"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
@@ -22,13 +23,17 @@
: {box: "h-8 w-8", overlap: "-mr-3", overflow: "text-xs"},
)
$effect(() => {
for (const pk of pubkeys) {
loadProfile(pk)
}
for (const pubkey of pubkeys) {
app.use(Profiles).load(pubkey)
}
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([...pubkeys].toSorted().slice(0, effectiveLimit))
const displayPubkeys = $derived(visiblePubkeys.toSorted().slice(0, effectiveLimit))
const overflowCount = $derived(Math.max(0, pubkeys.length - effectiveLimit))
</script>
+9 -15
View File
@@ -1,15 +1,9 @@
<script lang="ts">
import {chunk, sleep, uniq} from "@welshman/lib"
import {
makeEvent,
createProfile,
PROFILE,
DELETE,
isReplaceable,
getAddress,
RelayMode,
} from "@welshman/util"
import {pubkey, publishThunk, repository, derivePubkeyRelays} from "@welshman/app"
import {makeEvent, DELETE, isReplaceable, getAddress} from "@welshman/util"
import {ProfileBuilder} from "@welshman/domain"
import {Thunks, RelayLists} from "@welshman/app"
import {pubkey, repository, app} from "@app/welshman"
import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
@@ -31,7 +25,7 @@
let confirmText = $state("")
const CONFIRM_TEXT = "permanently delete my nostr account"
const userWriteRelays = derivePubkeyRelays($pubkey!, RelayMode.Write)
const userWriteRelays = app.use(RelayLists).writeUrls($pubkey!).$
const confirmOk = $derived(confirmText.toLowerCase().trim() === CONFIRM_TEXT)
const showProgress = $derived(progress !== undefined)
@@ -44,7 +38,7 @@
}
const chunks = chunk(500, repository.query([{authors: [$pubkey!]}]))
const profileEvent = makeEvent(PROFILE, createProfile({name: "[deleted]"}))
const profileEvent = await new ProfileBuilder().update({name: "[deleted]"}).toTemplate()
const vanishEvent = makeEvent(62, {tags: [["relay", "ALL_RELAYS"]]})
const denominator = chunks.length + 2
const relays = uniq([...INDEXER_RELAYS, ...$userWriteRelays, ...$userSpaceUrls])
@@ -58,12 +52,12 @@
}
// First, blank out their profile in case relays don't support deletion by address
await publishThunk({relays, event: profileEvent})
await app.use(Thunks).publish({relays, event: profileEvent})
await incrementProgress()
// Next, send a "right to vanish" event to all relays
await publishThunk({relays, event: vanishEvent})
await app.use(Thunks).publish({relays, event: vanishEvent})
await incrementProgress()
@@ -79,7 +73,7 @@
}
}
await publishThunk({relays, event: makeEvent(DELETE, {tags})})
await app.use(Thunks).publish({relays, event: makeEvent(DELETE, {tags})})
await incrementProgress()
}
+21 -18
View File
@@ -2,20 +2,18 @@
import {onMount} from "svelte"
import {removeUndefined} from "@welshman/lib"
import {ManagementMethod} from "@welshman/util"
import {
manageRelay,
deriveProfile,
displayProfileByPubkey,
loadMessagingRelayList,
} from "@welshman/app"
import {RelayManagement, Profiles, MessagingRelayLists} from "@welshman/app"
import {app} from "@app/welshman"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl"
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
import Letter from "@assets/icons/letter-opened.svg?dataurl"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
import Restart from "@assets/icons/restart.svg?dataurl"
import {fly} from "@lib/transition"
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 Button from "@lib/components/Button.svelte"
import Popover from "@lib/components/Popover.svelte"
@@ -26,10 +24,11 @@
import ProfileInfo from "@app/components/ProfileInfo.svelte"
import EventInfo from "@app/components/EventInfo.svelte"
import ProfileBadges from "@app/components/ProfileBadges.svelte"
import {pubkeyLink} from "@app/env"
import {deriveUserIsSpaceAdmin, deriveSpaceBannedPubkeyItems, addSpaceMembers} from "@app/members"
import {pushModal} from "@app/modal"
import {pushToast} from "@app/toast"
import {goToProfile} from "@app/routes"
import {goToChat} from "@app/routes"
export type Props = {
pubkey: string
@@ -38,7 +37,7 @@
const {pubkey, url}: Props = $props()
const profile = deriveProfile(pubkey, removeUndefined([url]))
const profile = app.use(Profiles).one(pubkey, removeUndefined([url]))
const userIsAdmin = deriveUserIsSpaceAdmin(url)
@@ -50,9 +49,9 @@
const showInfo = () => pushModal(EventInfo, {url, event: $profile!.event})
const viewProfile = () => goToProfile(pubkey)
const openChat = () => goToChat([pubkey])
const toggleMenu = () => {
const toggleMenu = (pubkey: string) => {
showMenu = !showMenu
}
@@ -63,9 +62,9 @@
const banMember = () =>
pushModal(Confirm, {
title: "Ban User",
message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
message: `Are you sure you want to ban @${app.use(Profiles).display(pubkey).get()} from the space?`,
confirm: async () => {
const {error} = await manageRelay(url!, {
const {error} = await app.use(RelayManagement).post(url!, {
method: ManagementMethod.BanPubkey,
params: [pubkey],
})
@@ -93,7 +92,7 @@
let showMenu = $state(false)
onMount(() => {
loadMessagingRelayList(pubkey)
app.use(MessagingRelayLists).load(pubkey)
})
</script>
@@ -104,7 +103,7 @@
<Profile showPubkey avatarSize={14} {pubkey} {url} />
{#if $profile || $userIsAdmin}
<div class="relative">
<Button class="btn btn-circle btn-ghost btn-sm" onclick={toggleMenu}>
<Button class="btn btn-circle btn-ghost btn-sm" onclick={() => toggleMenu(pubkey)}>
<Icon icon={MenuDots} />
</Button>
{#if showMenu}
@@ -153,9 +152,13 @@
Go back
</Button>
<div class="flex gap-2">
<Button onclick={viewProfile} class="btn btn-primary">
<Icon icon={UserCircle} />
View Full Profile
<Link external href={pubkeyLink(pubkey)} class="btn btn-neutral">
<ImageIcon alt="" src="/coracle.png" />
Open in Coracle
</Link>
<Button onclick={openChat} class="btn btn-primary">
<Icon icon={Letter} />
Message
</Button>
</div>
</ModalFooter>
+16 -5
View File
@@ -1,7 +1,6 @@
<script lang="ts">
import type {Profile} from "@welshman/util"
import {makeProfile} from "@welshman/util"
import {pubkey, profilesByPubkey, waitForThunkError} from "@welshman/app"
import {Profile, ProfileBuilder} from "@welshman/domain"
import {pubkey, profilesByPubkey} from "@app/welshman"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import {errorMessage} from "@lib/util"
import Icon from "@lib/components/Icon.svelte"
@@ -12,16 +11,28 @@
import {pushToast} from "@app/toast"
import {updateProfile} from "@app/profiles"
const profile = $profilesByPubkey.get($pubkey!) || makeProfile()
// The edit form binds to plain mutable fields (name/about/nip05/picture), so we
// 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 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}) => {
loading = true
try {
const error = await waitForThunkError(updateProfile({profile}))
// TODO(welshman-migration): updateProfile is async (returns Promise<Thunk>); the old
// 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) {
pushToast({
-19
View File
@@ -4,7 +4,6 @@
import {preventDefault} from "@lib/html"
import UserCircle from "@assets/icons/user-circle.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 Field from "@lib/components/Field.svelte"
import Button from "@lib/components/Button.svelte"
@@ -86,24 +85,6 @@
{/snippet}
</Field>
{#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>
{#snippet label()}
<p>Nostr Address</p>
+3 -2
View File
@@ -4,7 +4,8 @@
import {feedFromFilter, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
import {NOTE, getReplyTags} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {makeFeedController} from "@welshman/app"
import {Feeds} from "@welshman/app"
import {app} from "@app/welshman"
import {createScroller} from "@lib/html"
import {fly} from "@lib/transition"
import Spinner from "@lib/components/Spinner.svelte"
@@ -19,7 +20,7 @@
let {url, pubkey, events = $bindable([]), hideLoading = false}: Props = $props()
const ctrl = makeFeedController({
const ctrl = app.use(Feeds).makeFeedController({
useWindowing: true,
feed: makeIntersectionFeed(
makeRelayFeed(url),
+4 -3
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import {removeUndefined} from "@welshman/lib"
import {deriveProfile} from "@welshman/app"
import {Profiles} from "@welshman/app"
import {app} from "@app/welshman"
import ContentMinimal from "@app/components/ContentMinimal.svelte"
export type Props = {
@@ -10,9 +11,9 @@
const {pubkey, url}: Props = $props()
const profile = deriveProfile(pubkey, removeUndefined([url]))
const profile = app.use(Profiles).one(pubkey, removeUndefined([url]))
</script>
{#if $profile}
<ContentMinimal event={{content: $profile.about || "", tags: []}} />
<ContentMinimal event={{content: $profile.about() || "", tags: []}} />
{/if}
+1 -1
View File
@@ -4,7 +4,7 @@
import type {Writable} from "svelte/store"
import {type Instance} from "tippy.js"
import {append, remove, uniq} from "@welshman/lib"
import {profileSearch} from "@welshman/app"
import {profileSearch} from "@app/welshman"
import Suggestions from "@lib/components/Suggestions.svelte"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
+3 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import {removeUndefined} from "@welshman/lib"
import {deriveProfileDisplay} from "@welshman/app"
import {Profiles} from "@welshman/app"
import {app} from "@app/welshman"
type Props = {
pubkey: string
@@ -9,7 +10,7 @@
const {pubkey, url}: Props = $props()
const profileDisplay = deriveProfileDisplay(pubkey, removeUndefined([url]))
const profileDisplay = app.use(Profiles).display(pubkey, removeUndefined([url])).$
</script>
{$profileDisplay}
-69
View File
@@ -1,69 +0,0 @@
<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>
-382
View File
@@ -1,382 +0,0 @@
<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,
getFollows,
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 Settings from "@assets/icons/settings.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 ProfileEdit from "@app/components/ProfileEdit.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 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(Math.round(clamp([0, 100], $score)))
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 startEdit = () => pushModal(ProfileEdit)
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">
{#if isSelf}
<Button
class="w-fit rounded-full sm:border-4 sm:border-base-200 sm:bg-base-200"
onclick={startEdit}>
<ProfileCircle pubkey={target} size={16} class="sm:hidden" />
<ProfileCircle pubkey={target} size={20} class="hidden sm:block" />
</Button>
{:else}
<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>
{/if}
</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={startEdit}>
<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>
{#if isSelf}
<li>
<Button onclick={openSettings}>
<Icon icon={Settings} />
Account Settings
</Button>
</li>
{/if}
</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} />
Trust score {displayScore}
</span>
{#if sharedSpaceUrls.length > 0}
<button
class="badge badge-neutral inline-flex h-6 items-center gap-1.5 border-0"
onclick={showSpacesTab}>
<Icon icon={UsersGroup} size={3} />
{sharedSpaceUrls.length} shared {sharedSpaceUrls.length === 1
? "space"
: "spaces"}
</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">
<ProfileTrust pubkey={target} />
<ProfileSharedSpaces pubkey={target} limit={3} onViewAll={showSpacesTab} />
</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">
<ProfileTrust pubkey={target} />
<ProfileSharedSpaces pubkey={target} limit={3} onViewAll={showSpacesTab} />
</div>
</aside>
</div>
</div>
@@ -1,68 +0,0 @@
<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()
const ctrl = makeFeedController({
useWindowing: true,
feed: feedFromFilter({kinds: [NOTE], authors: [pubkey]}),
onEvent: (event: TrustedEvent) => {
if (getReplyTags(event.tags).replies.length === 0) {
buffer.push(event)
}
},
})
let element: Element | undefined = $state()
let events: TrustedEvent[] = $state([])
let buffer: TrustedEvent[] = []
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>
{/each}
<p class="center my-12 flex">
<Spinner loading />
</p>
</div>
</div>
@@ -1,47 +0,0 @@
<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>
@@ -1,88 +0,0 @@
<script lang="ts">
import {onMount} from "svelte"
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 {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(getEventTagValues(getListTags($pinList)))
const displayIds = $derived(limit ? pinnedIds.slice(0, limit) : pinnedIds)
const pinnedEvents = $derived.by(() => {
return sortBy(
e => -pinnedIds.indexOf(e.id),
displayIds
.map(id => repository.getEvent(id))
.filter((event): event is TrustedEvent => Boolean(event)),
)
})
let loading = $state(pinnedIds.length > 0)
onMount(() => {
if (pinnedIds.length === 0) {
loading = false
return
}
const missing = pinnedIds.filter(id => !repository.getEvent(id))
if (missing.length === 0) {
loading = false
return
}
load({
relays: Router.get().FromPubkeys([pubkey]).getUrls(),
filters: [{ids: missing}],
onEvent: () => {
loading = !pinnedIds.every(id => repository.getEvent(id))
},
onClose: () => {
loading = false
},
})
})
</script>
{#if pinnedIds.length > 0}
<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}
<div class="col-2">
{#each pinnedEvents as event (event.id)}
<div in:fly>
<NoteItem {event} {editable} />
</div>
{/each}
</div>
{#if onViewAll && pinnedIds.length > (limit || pinnedIds.length)}
<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
@@ -1,35 +0,0 @@
<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>
@@ -1,74 +0,0 @@
<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
limit?: number
onViewAll?: () => void
class?: string
}
const {pubkey, limit, onViewAll, ...props}: Props = $props()
const groupList = deriveGroupList(pubkey)
const spaceUrls = $derived(getSpaceUrlsFromGroupList($groupList))
const sharedSpaceUrls = $derived($userSpaceUrls.filter(url => spaceUrls.includes(url)))
const displayUrls = $derived(limit ? sharedSpaceUrls.slice(0, limit) : sharedSpaceUrls)
</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>Shared spaces</strong>
</div>
<span class="badge badge-neutral">{sharedSpaceUrls.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 && sharedSpaceUrls.length > (limit || sharedSpaceUrls.length)}
<button
class="link link-primary row-2 border-t border-base-300 pt-4 text-sm max-sm:pt-4"
onclick={onViewAll}>
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">
No shared spaces yet.
</p>
{/if}
</div>
-72
View File
@@ -1,72 +0,0 @@
<script lang="ts">
import {clamp, uniq} from "@welshman/lib"
import {
pubkey,
followLists,
deriveUserWotScore,
deriveProfileDisplay,
deriveFollowList,
getFollows,
} 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
}
const {pubkey: target}: Props = $props()
const score = deriveUserWotScore(target)
const profileDisplay = deriveProfileDisplay(target)
const targetFollowList = deriveFollowList(target)
const mutualFollows = $derived.by(() => {
const viewer = $pubkey
void $followLists
void $targetFollowList
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(Math.round(clamp([0, 100], $score)))
const progress = $derived(displayScore)
const trustMessage = $derived.by(() => {
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">Trust score</span>
<span class="text-lg font-semibold">{displayScore} / 100</span>
</div>
<progress class="progress progress-primary w-full" value={progress} max="100"></progress>
<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>
+7 -6
View File
@@ -17,7 +17,8 @@
import type {TrustedEvent, EventContent, Zap} from "@welshman/util"
import {deriveArray, deriveEventsById, deriveItemsByKey} from "@welshman/store"
import {load} from "@welshman/net"
import {pubkey, repository, getValidZap, displayProfileByPubkey} from "@welshman/app"
import {Zappers, Profiles} from "@welshman/app"
import {pubkey, repository, app} from "@app/welshman"
import {isMobile, preventDefault, stopPropagation} from "@lib/html"
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
@@ -64,15 +65,15 @@
repository,
getKey: zap => zap.response.id,
filters: [{kinds: [ZAP_RESPONSE], "#e": eventIds}],
eventToItem: (response: TrustedEvent) => {
const zap = getValidZap(response, event)
eventToItem: async (response: TrustedEvent) => {
const zap = await app.use(Zappers).validateZapReceipt(response, event)
if (zap) {
return zap
}
if (innerEvent) {
return getValidZap(response, innerEvent)
return await app.use(Zappers).validateZapReceipt(response, innerEvent)
}
},
}),
@@ -150,7 +151,7 @@
{@const amount = fromMsats(sum(zaps.map(zap => zap.invoiceAmount)))}
{@const pubkeys = uniq(zaps.map(zap => zap.request.pubkey))}
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
{@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
{@const info = displayList(pubkeys.map(pubkey => app.use(Profiles).display(pubkey).get()))}
{@const tooltip = `${info} zapped`}
<button
type="button"
@@ -171,7 +172,7 @@
{#each groupedReactions.entries() as [key, events]}
{@const pubkeys = events.map(e => e.pubkey)}
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
{@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
{@const info = displayList(pubkeys.map(pubkey => app.use(Profiles).display(pubkey).get()))}
{@const tooltip = `${info} reacted`}
{@const onClick = () => onReactionClick(events)}
<button
+2 -2
View File
@@ -5,7 +5,7 @@
import {tryCatch} from "@welshman/lib"
import {isShareableRelayUrl, isIPAddress, normalizeRelayUrl} from "@welshman/util"
import type {Thunk} from "@welshman/app"
import {waitForThunkError, relaySearch} from "@welshman/app"
import {relaySearch} from "@app/welshman"
import {createScroller} from "@lib/html"
import {errorMessage} from "@lib/util"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
@@ -34,7 +34,7 @@
loading.add(url)
try {
const error = await waitForThunkError(await addRelay(url))
const error = await (await addRelay(url)).waitForError()
if (error) {
pushToast({
+3 -2
View File
@@ -1,9 +1,10 @@
<script lang="ts">
import {deriveRelay} from "@welshman/app"
import {Relays} from "@welshman/app"
import {app} from "@app/welshman"
const {...props} = $props()
const relay = deriveRelay(props.url)
const relay = app.use(Relays).one(props.url)
</script>
{#if $relay?.description}
+3 -2
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import {deriveRelay} from "@welshman/app"
import {Relays} from "@welshman/app"
import {app} from "@app/welshman"
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
import ImageIcon from "@lib/components/ImageIcon.svelte"
@@ -11,7 +12,7 @@
const {url, size = 7, ...props}: Props = $props()
const relay = deriveRelay(url)
const relay = app.use(Relays).one(url)
</script>
{#if $relay?.icon}
+4 -3
View File
@@ -4,12 +4,13 @@
import Link from "@lib/components/Link.svelte"
import {displayUrl} from "@welshman/lib"
import {displayRelayUrl} from "@welshman/util"
import {deriveRelay, deriveRelayStats} from "@welshman/app"
import {Relays, RelayStats} from "@welshman/app"
import {app} from "@app/welshman"
const {url, children} = $props()
const relay = deriveRelay(url)
const relayStats = deriveRelayStats(url)
const relay = app.use(Relays).one(url)
const relayStats = app.use(RelayStats).one(url)
const connections = $derived($relayStats?.open_count || 0)
</script>
+1 -2
View File
@@ -1,7 +1,6 @@
<script lang="ts">
import type {Readable} from "svelte/store"
import {SvelteSet} from "svelte/reactivity"
import {waitForThunkError} from "@welshman/app"
import type {Thunk} from "@welshman/app"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -37,7 +36,7 @@
loading.add(url)
try {
const error = await waitForThunkError(await removeRelay(url))
const error = await (await removeRelay(url)).waitForError()
if (error) {
pushToast({
+3 -2
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import {deriveRelayDisplay} from "@welshman/app"
import {Relays} from "@welshman/app"
import {app} from "@app/welshman"
type Props = {
url: string
@@ -8,7 +9,7 @@
const {url, ...props}: Props = $props()
const display = $derived(deriveRelayDisplay(url))
const display = $derived(app.use(Relays).display(url).$)
</script>
<span class={props.class}>
+1 -1
View File
@@ -2,7 +2,7 @@
import {REPORT} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {deriveEventsById} from "@welshman/store"
import {repository} from "@welshman/app"
import {repository} from "@app/welshman"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
+6 -5
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import {getTag, ManagementMethod} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {pubkey, manageRelay, repository, displayProfileByPubkey} from "@welshman/app"
import {RelayManagement, Profiles} from "@welshman/app"
import {pubkey, repository, app} from "@app/welshman"
import InboxOut from "@assets/icons/inbox-out.svg?dataurl"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
@@ -45,7 +46,7 @@
}
const dismissReport = async () => {
const {error} = await manageRelay(url, {
const {error} = await app.use(RelayManagement).post(url, {
method: ManagementMethod.BanEvent,
params: [event.id, "Dismissed by admin"],
})
@@ -66,7 +67,7 @@
title: `Remove Content`,
message: `Are you sure you want to delete this content from the space?`,
confirm: async () => {
const {error} = await manageRelay(url, {
const {error} = await app.use(RelayManagement).post(url, {
method: ManagementMethod.BanEvent,
params: [id, reason],
})
@@ -89,9 +90,9 @@
pushModal(Confirm, {
title: "Ban User",
message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
message: `Are you sure you want to ban @${app.use(Profiles).display(pubkey).get()} from the space?`,
confirm: async () => {
const {error} = await manageRelay(url, {
const {error} = await app.use(RelayManagement).post(url, {
method: ManagementMethod.BanPubkey,
params: [pubkey, reason],
})
+3 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import type {TrustedEvent} from "@welshman/util"
import {displayProfileByPubkey} from "@welshman/app"
import {Profiles} from "@welshman/app"
import {app} from "@app/welshman"
import {slide} from "@lib/transition"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
@@ -21,7 +22,7 @@
<div
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8"
transition:slide>
<p class="text-xs text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
<p class="text-xs text-primary">{verb} @{app.use(Profiles).display(event.pubkey).get()}</p>
{#key event.id}
<NoteContentMinimal trimParent {event} />
{/key}
+11 -9
View File
@@ -1,9 +1,9 @@
<script lang="ts">
import {goto} from "$app/navigation"
import type {RoomMeta} from "@welshman/util"
import {displayRelayUrl, makeRoomMeta} from "@welshman/util"
import {displayRelayUrl} from "@welshman/util"
import type {Thunk} from "@welshman/app"
import {deleteRoom, waitForThunkError, repository, joinRoom, leaveRoom} from "@welshman/app"
import {Rooms} from "@welshman/app"
import {repository, app} from "@app/welshman"
import Pen from "@assets/icons/pen.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import Login3 from "@assets/icons/login-3.svg?dataurl"
@@ -71,11 +71,13 @@
const startEdit = () => pushModal(RoomEdit, {url, h})
const handleLoading = async (f: (url: string, room: RoomMeta) => Thunk) => {
const handleLoading = async (
f: (url: string, room: {h: string}) => Promise<Thunk>,
) => {
loading = true
try {
const message = await waitForThunkError(f(url, makeRoomMeta({h})))
const message = await (await f(url, {h})).waitForError()
if (message && !message.startsWith("duplicate:")) {
pushToast({theme: "error", message})
@@ -85,9 +87,9 @@
}
}
const join = () => handleLoading(joinRoom)
const join = () => handleLoading((url, room) => app.use(Rooms).join(url, room))
const leave = () => handleLoading(leaveRoom)
const leave = () => handleLoading((url, room) => app.use(Rooms).leave(url, room))
const showMembers = () => pushModal(RoomMembers, {url, h})
@@ -109,8 +111,8 @@
message:
"This room will no longer be accessible to space members, and all messages posted to it will be deleted.",
confirm: async () => {
const thunk = deleteRoom(url, $room)
const message = await waitForThunkError(thunk)
const thunk = await app.use(Rooms).delete(url, $room)
const message = await thunk.waitForError()
if (message) {
repository.removeEvent(thunk.event.id)
+58 -6
View File
@@ -1,8 +1,7 @@
<script lang="ts">
import type {Snippet} from "svelte"
import type {RoomMeta} from "@welshman/util"
import {makeRoomMeta} from "@welshman/util"
import {waitForThunkError, createRoom, editRoom, joinRoom} from "@welshman/app"
import {Rooms} from "@welshman/app"
import {app} from "@app/welshman"
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
import Volume from "@assets/icons/volume.svg?dataurl"
@@ -19,6 +18,56 @@
import {deriveHasLivekit} from "@app/relays"
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 = {
url: string
header: Snippet
@@ -58,19 +107,22 @@
room.pictureMeta = result.tags
}
const createMessage = await waitForThunkError(createRoom(url, room))
// TODO(welshman-migration): app.use(Rooms).create/edit/join are async
// (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")) {
return pushToast({theme: "error", message: createMessage})
}
const editMessage = await waitForThunkError(editRoom(url, room))
const editMessage = await (await app.use(Rooms).edit(url, room)).waitForError()
if (editMessage) {
return pushToast({theme: "error", message: editMessage})
}
const joinMessage = await waitForThunkError(joinRoom(url, room))
const joinMessage = await (await app.use(Rooms).join(url, room)).waitForError()
if (joinMessage && !joinMessage.includes("already")) {
return pushToast({theme: "error", message: joinMessage})
+5 -10
View File
@@ -11,13 +11,8 @@
} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {MESSAGE, COMMENT, getTag} from "@welshman/util"
import {
thunks,
pubkey,
mergeThunks,
deriveProfileDisplay,
displayProfileByPubkey,
} from "@welshman/app"
import {Thunks, Profiles} from "@welshman/app"
import {thunks, pubkey, app} from "@app/welshman"
import {isMobile} from "@lib/html"
import Pen from "@assets/icons/pen.svg?dataurl"
import Reply from "@assets/icons/reply-2.svg?dataurl"
@@ -58,8 +53,8 @@
const path = getRoomItemPath(url, event)
const shouldProtect = canEnforceNip70(url)
const today = formatTimestampAsDate(now())
const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
const profileDisplay = app.use(Profiles).display(event.pubkey, [url]).$
const thunk = app.use(Thunks).merge($thunks.filter(t => t.event.id === event.id))
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
const qTag = getTag("q", event.tags)
@@ -138,7 +133,7 @@
{#if path && $innerComments.length > 0}
{@const pubkeys = $innerComments.map(e => e.pubkey)}
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
{@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
{@const info = displayList(pubkeys.map(pubkey => app.use(Profiles).display(pubkey).get()))}
{@const tooltip = `${info} commented`}
<div data-tip={tooltip} class="tooltip tooltip-right flex">
<Link
+3 -2
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import type {TrustedEvent} from "@welshman/util"
import {ManagementMethod} from "@welshman/util"
import {pubkey, manageRelay, repository} from "@welshman/app"
import {RelayManagement} from "@welshman/app"
import {pubkey, repository, app} from "@app/welshman"
import Code2 from "@assets/icons/code-2.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import Danger from "@assets/icons/danger.svg?dataurl"
@@ -45,7 +46,7 @@
title: `Delete Message`,
message: `Are you sure you want to delete this message from the space?`,
confirm: async () => {
const {error} = await manageRelay(url, {
const {error} = await app.use(RelayManagement).post(url, {
method: ManagementMethod.BanEvent,
params: [event.id],
})
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import type {NativeEmoji} from "emoji-picker-element/shared"
import type {TrustedEvent} from "@welshman/util"
import {pubkey} from "@welshman/app"
import {pubkey} from "@app/welshman"
import Bolt from "@assets/icons/bolt.svg?dataurl"
import Reply from "@assets/icons/reply-2.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl"
+9 -4
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import {getTagValue, ManagementMethod} from "@welshman/util"
import type {TrustedEvent, PublishedRoomMeta} from "@welshman/util"
import {repository, manageRelay} from "@welshman/app"
import type {TrustedEvent} from "@welshman/util"
import {RelayManagement} from "@welshman/app"
import {app, repository} from "@app/welshman"
import Button from "@lib/components/Button.svelte"
import ProfileName from "@app/components/ProfileName.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte"
@@ -28,7 +29,7 @@
loading = true
try {
const {error} = await manageRelay(url, {
const {error} = await app.use(RelayManagement).post(url, {
method: ManagementMethod.BanEvent,
params: [event.id, "Join request dismissed"],
})
@@ -49,7 +50,11 @@
loading = true
try {
const error = await addRoomMembers(url, $room as PublishedRoomMeta, [event.pubkey])
// TODO(welshman-migration): addRoomMembers now expects a RoomMeta domain
// 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) {
pushToast({theme: "error", message: error})
+4 -2
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import {waitForThunkError, removeRoomMember} from "@welshman/app"
import {Rooms} from "@welshman/app"
import {app} from "@app/welshman"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
@@ -51,7 +52,8 @@
title: "Remove Member",
message: "Are you sure you want to remove this user from the room?",
confirm: async () => {
const error = await waitForThunkError(removeRoomMember(url, $room, pubkey))
const thunk = await app.use(Rooms).removeMember(url, $room, pubkey)
const error = await thunk.waitForError()
if (error) {
pushToast({theme: "error", message: error})
+8 -4
View File
@@ -2,8 +2,8 @@
import {onMount} from "svelte"
import {setKey, popKey} from "@lib/implicit"
import {sleep} from "@welshman/lib"
import {displayProfileByPubkey} from "@welshman/app"
import type {PublishedRoomMeta} from "@welshman/util"
import {Profiles} from "@welshman/app"
import {app} from "@app/welshman"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
@@ -42,7 +42,11 @@
// Show loading for auto submit callback
await sleep(500)
const error = await addRoomMembers(url, $room as PublishedRoomMeta, pubkeys)
// TODO(welshman-migration): addRoomMembers now expects a RoomMeta domain
// 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) {
pushToast({theme: "error", message: error})
@@ -72,7 +76,7 @@
subtitle: "Automatically add members to space",
message:
nonSpaceMembers.length === 1
? `${displayProfileByPubkey(nonSpaceMembers[0])} is not a member of this space. Add them?`
? `${app.use(Profiles).display(nonSpaceMembers[0]).get()} is not a member of this space. Add them?`
: `${nonSpaceMembers.length} people are not members of this space. Add them?`,
confirm: async () => {
setKey("RoomMembersAdd.confirm", true)
+10 -8
View File
@@ -1,8 +1,10 @@
<script lang="ts">
import type {ClientOptions} from "@pomade/core"
import type {Profile} from "@welshman/util"
import {makeProfile, makeSecret, RELAYS, MESSAGING_RELAYS, makeEvent} from "@welshman/util"
import {loginWithNip01, publishThunk} from "@welshman/app"
import type {Profile} from "@welshman/domain"
import {makeSecret, RELAYS, MESSAGING_RELAYS, makeEvent} from "@welshman/util"
import {ProfileBuilder} from "@welshman/domain"
import {Thunks} from "@welshman/app"
import {app, loginWithNip01} from "@app/welshman"
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl"
import {getKey, setKey} from "@lib/implicit"
@@ -31,7 +33,7 @@
setKey("signup.email", "")
setKey("signup.secret", makeSecret())
setKey("signup.profile", makeProfile())
setKey("signup.profile", new ProfileBuilder().values)
setKey("signup.clientOptions", undefined)
const hasPomade = POMADE_SIGNERS.length >= 3
@@ -40,13 +42,13 @@
const completeSignup = () => {
// Add default outbox/inbox relays
publishThunk({
app.use(Thunks).publish({
event: makeEvent(RELAYS, {tags: DEFAULT_RELAYS.map(url => ["r", url])}),
relays: [...INDEXER_RELAYS, ...DEFAULT_RELAYS],
})
// Add default messaging relays
publishThunk({
app.use(Thunks).publish({
event: makeEvent(MESSAGING_RELAYS, {tags: DEFAULT_MESSAGING_RELAYS.map(url => ["r", url])}),
relays: DEFAULT_RELAYS,
})
@@ -83,10 +85,10 @@
key: () => pushModal(SignUpKey, {next: flows.nostr.complete, step: 2, totalSteps: 3}),
complete: () =>
pushModal(SignUpComplete, {next: flows.nostr.finalize, step: 3, totalSteps: 3}),
finalize: () => {
finalize: async () => {
const secret = getKey<string>("signup.secret")!
loginWithNip01(secret)
await loginWithNip01(secret)
completeSignup()
},
},
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import {spec, avg} from "@welshman/lib"
import {session, SessionMethod, signerLog} from "@welshman/app"
import {session, SessionMethod, signerLog} from "@app/welshman"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
+3 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import {displayRelayUrl} from "@welshman/util"
import {deriveRelay} from "@welshman/app"
import {Relays} from "@welshman/app"
import {app} from "@app/welshman"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Pen from "@assets/icons/pen.svg?dataurl"
@@ -26,7 +27,7 @@
}
const {url}: Props = $props()
const relay = deriveRelay(url)
const relay = app.use(Relays).one(url)
const owner = $derived($relay?.pubkey)
const userIsAdmin = deriveUserIsSpaceAdmin(url)
+6 -5
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import type {RelayProfile} from "@welshman/util"
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
import {manageRelay, forceLoadRelay} from "@welshman/app"
import {RelayManagement, Relays} from "@welshman/app"
import {app} from "@app/welshman"
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
import Widget from "@assets/icons/widget-4.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -36,7 +37,7 @@
const submit = async () => {
if (values.name != initialValues.name) {
const res = await manageRelay(url, {
const res = await app.use(RelayManagement).post(url, {
method: ManagementMethod.ChangeRelayName,
params: [values.name || ""],
})
@@ -47,7 +48,7 @@
}
if (values.description != initialValues.description) {
const res = await manageRelay(url, {
const res = await app.use(RelayManagement).post(url, {
method: ManagementMethod.ChangeRelayDescription,
params: [values.description || ""],
})
@@ -64,7 +65,7 @@
return pushToast({theme: "error", message: error})
}
const res = await manageRelay(url, {
const res = await app.use(RelayManagement).post(url, {
method: ManagementMethod.ChangeRelayIcon,
params: [result.url],
})
@@ -75,7 +76,7 @@
}
pushToast({message: "Your changes have been saved!"})
forceLoadRelay(url)
app.use(Relays).forceLoad(url)
clearModals()
}
+2 -3
View File
@@ -1,7 +1,6 @@
<script lang="ts">
import {debounce} from "throttle-debounce"
import {dissoc, maybe} from "@welshman/lib"
import {goto} from "$app/navigation"
import {preventDefault} from "@lib/html"
import {slideAndFade} from "@lib/transition"
import Spinner from "@lib/components/Spinner.svelte"
@@ -22,7 +21,7 @@
import RelaySummary from "@app/components/RelaySummary.svelte"
import SpaceJoinSettings from "@app/components/SpaceJoinSettings.svelte"
import {pushToast} from "@app/toast"
import {makeSpacePath} from "@app/routes"
import {goToSpace} from "@app/routes"
import {relaysMostlyRestricted} from "@app/policies"
import {notificationSettings, setSpaceNotifications} from "@app/settings"
import {parseInviteLink} from "@app/invites"
@@ -68,7 +67,7 @@
}
await addSpace(url)
await goto(makeSpacePath(url), {replaceState: true})
await goToSpace(url)
broadcastUserData([url])
relaysMostlyRestricted.update(dissoc(url))
+2 -3
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import {onMount} from "svelte"
import {goto} from "$app/navigation"
import {dissoc, maybe} from "@welshman/lib"
import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -22,7 +21,7 @@
import {notificationSettings} from "@app/settings"
import {pushModal} from "@app/modal"
import {pushToast} from "@app/toast"
import {makeSpacePath} from "@app/routes"
import {goToSpace} from "@app/routes"
import {Push} from "@app/push"
type Props = {
@@ -56,7 +55,7 @@
}
await addSpace(url)
await goto(makeSpacePath(url), {replaceState: true})
await goToSpace(url)
broadcastUserData([url])
relaysMostlyRestricted.update(dissoc(url))
+6 -5
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import {ManagementMethod} from "@welshman/util"
import {manageRelay, displayProfileByPubkey} from "@welshman/app"
import {RelayManagement, Profiles} from "@welshman/app"
import {app} from "@app/welshman"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import UserMinus from "@assets/icons/user-minus.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
@@ -60,9 +61,9 @@
const unallowMember = (pubkey: string) =>
pushModal(Confirm, {
title: "Remove User",
message: `Are you sure you want to remove @${displayProfileByPubkey(pubkey)} from the space?`,
message: `Are you sure you want to remove @${app.use(Profiles).display(pubkey).get()} from the space?`,
confirm: async () => {
const {error} = await manageRelay(url, {
const {error} = await app.use(RelayManagement).post(url, {
method: ManagementMethod.UnallowPubkey,
params: [pubkey],
})
@@ -79,9 +80,9 @@
const banMember = (pubkey: string) =>
pushModal(Confirm, {
title: "Ban User",
message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
message: `Are you sure you want to ban @${app.use(Profiles).display(pubkey).get()} from the space?`,
confirm: async () => {
const {error} = await manageRelay(url, {
const {error} = await app.use(RelayManagement).post(url, {
method: ManagementMethod.BanPubkey,
params: [pubkey],
})
+3 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
import {manageRelay} from "@welshman/app"
import {RelayManagement} from "@welshman/app"
import {app} from "@app/welshman"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import Restart from "@assets/icons/restart.svg?dataurl"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
@@ -42,7 +43,7 @@
}
const unbanMember = async (pubkey: string) => {
const {error} = await manageRelay(url, {
const {error} = await app.use(RelayManagement).post(url, {
method: ManagementMethod.UnbanPubkey,
params: [pubkey],
})
+3 -2
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import {derived} from "svelte/store"
import {displayRelayUrl, EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, POLL} from "@welshman/util"
import {deriveRelay, createSearch, pubkey} from "@welshman/app"
import {Relays} from "@welshman/app"
import {app, createSearch, pubkey} from "@app/welshman"
import {fly} from "@lib/transition"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
@@ -60,7 +61,7 @@
const {url} = $props()
const relay = deriveRelay(url)
const relay = app.use(Relays).one(url)
const chatPath = makeSpacePath(url, "chat")
const goalsPath = makeSpacePath(url, "goals")
const threadsPath = makeSpacePath(url, "threads")
+3 -2
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import {deriveRelay} from "@welshman/app"
import {Relays} from "@welshman/app"
import {app} from "@app/welshman"
import Server from "@assets/icons/server.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import ProfileLink from "@app/components/ProfileLink.svelte"
@@ -11,7 +12,7 @@
const {url}: Props = $props()
const relay = deriveRelay(url)
const relay = app.use(Relays).one(url)
</script>
<div class="card2 bg-alt flex flex-col gap-4">
+1 -1
View File
@@ -2,7 +2,7 @@
import {tick} from "svelte"
import {debounce} from "throttle-debounce"
import {request} from "@welshman/net"
import {repository, tracker} from "@welshman/app"
import {repository, tracker} from "@app/welshman"
import {formatTimestampAsDate, groupBy, uniqBy, now, MINUTE, HOUR, DAY, WEEK} from "@welshman/lib"
import type {TrustedEvent, Filter} from "@welshman/util"
import {MESSAGE, sortEventsDesc} from "@welshman/util"
+4 -3
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import {writable} from "svelte/store"
import {makeEvent, THREAD} from "@welshman/util"
import {publishThunk, waitForThunkError} from "@welshman/app"
import {Thunks} from "@welshman/app"
import {app} from "@app/welshman"
import {isMobile, preventDefault} from "@lib/html"
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -79,12 +80,12 @@
tags.push(["h", h])
}
const threadThunk = publishThunk({
const threadThunk = app.use(Thunks).publish({
relays: [url],
event: makeEvent(THREAD, {content, tags}),
})
const error = await waitForThunkError(threadThunk)
const error = await threadThunk.waitForError()
if (error) {
return pushToast({theme: "error", message: error})
+6 -5
View File
@@ -1,8 +1,9 @@
<script lang="ts">
import {formatTimestamp} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {COMMENT} from "@welshman/util"
import {deriveHandleForPubkey, deriveProfileDisplay, displayHandle} from "@welshman/app"
import {COMMENT, displayNip05} from "@welshman/util"
import {Handles, Profiles} from "@welshman/app"
import {app} from "@app/welshman"
import Reply from "@assets/icons/reply-2.svg?dataurl"
import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
@@ -26,8 +27,8 @@
const {url, event, threadPubkey, onReply}: Props = $props()
const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
const handle = deriveHandleForPubkey(event.pubkey)
const profileDisplay = app.use(Profiles).display(event.pubkey, [url]).$
const handle = app.use(Handles).forPubkey(event.pubkey).$
const isOp = event.pubkey === threadPubkey
const isComment = event.kind === COMMENT
@@ -53,7 +54,7 @@
{$profileDisplay}
</Button>
{#if $handle}
<span class="ellipsize text-xs opacity-75">{displayHandle($handle)}</span>
<span class="ellipsize text-xs opacity-75">{displayNip05($handle?.nip05)}</span>
{/if}
{#if isOp}
<span class="badge badge-primary badge-sm">OP</span>
+5 -4
View File
@@ -2,7 +2,8 @@
import {stopPropagation} from "svelte/legacy"
import {noop} from "@welshman/lib"
import type {AbstractThunk} from "@welshman/app"
import {flattenThunks, getFailedThunkUrls, publishThunk, thunkIsComplete} from "@welshman/app"
import {Thunks} from "@welshman/app"
import {app} from "@app/welshman"
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Tippy from "@lib/components/Tippy.svelte"
@@ -18,15 +19,15 @@
const {thunk, showToastOnRetry, ...restProps}: Props = $props()
const showFailure = $derived(thunkIsComplete($thunk) && getFailedThunkUrls($thunk).length > 0)
const showFailure = $derived($thunk.isComplete() && $thunk.getFailedUrls().length > 0)
const retry = (url: string) => {
for (const child of flattenThunks([thunk])) {
for (const child of app.use(Thunks).flatten([thunk])) {
if (!child.options.relays.includes(url)) {
continue
}
const retried = publishThunk({...child.options, relays: [url]})
const retried = app.use(Thunks).publish({...child.options, relays: [url]})
if (showToastOnRetry) {
pushToast({
+2 -3
View File
@@ -2,7 +2,6 @@
import {stopPropagation} from "svelte/legacy"
import {PublishStatus} from "@welshman/net"
import type {AbstractThunk} from "@welshman/app"
import {abortThunk, thunkHasStatus} from "@welshman/app"
interface Props {
thunk: AbstractThunk
@@ -11,9 +10,9 @@
const {thunk, ...restProps}: Props = $props()
const abort = () => abortThunk(thunk)
const abort = () => thunk.abort()
const isSending = $derived(thunkHasStatus(PublishStatus.Sending, $thunk))
const isSending = $derived($thunk.hasStatus(PublishStatus.Sending))
</script>
<div class="flex w-full justify-end px-1 text-xs {restProps.class}">
+2 -3
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import type {AbstractThunk} from "@welshman/app"
import {thunkIsComplete, getFailedThunkUrls} from "@welshman/app"
import ThunkFailure from "@app/components/ThunkFailure.svelte"
import ThunkPending from "@app/components/ThunkPending.svelte"
@@ -12,8 +11,8 @@
const {thunk, showToastOnRetry, ...restProps}: Props = $props()
const showFailure = $derived(thunkIsComplete($thunk) && getFailedThunkUrls($thunk).length > 0)
const showPending = $derived(!thunkIsComplete($thunk))
const showFailure = $derived($thunk.isComplete() && $thunk.getFailedUrls().length > 0)
const showPending = $derived(!$thunk.isComplete())
</script>
{#if showFailure}
+2 -3
View File
@@ -1,7 +1,6 @@
<script lang="ts">
import {stopPropagation} from "svelte/legacy"
import type {AbstractThunk} from "@welshman/app"
import {getFailedThunkUrls, getThunkUrlsWithStatus} from "@welshman/app"
import {PublishStatus} from "@welshman/net"
import {displayRelayUrl} from "@welshman/util"
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
@@ -17,8 +16,8 @@
const {thunk, retry}: Props = $props()
const successUrls = $derived(getThunkUrlsWithStatus(PublishStatus.Success, $thunk))
const failedUrls = $derived(getFailedThunkUrls($thunk))
const successUrls = $derived($thunk.getUrlsWithStatus(PublishStatus.Success))
const failedUrls = $derived($thunk.getFailedUrls())
const total = $derived(successUrls.length + failedUrls.length)
const isPartial = $derived(successUrls.length > 0 && failedUrls.length > 0)
@@ -3,7 +3,8 @@
import type {TrustedEvent} from "@welshman/util"
import {PublishStatus} from "@welshman/net"
import {deriveIsDeleted} from "@welshman/store"
import {thunks, mergeThunks, thunkHasStatus, repository} from "@welshman/app"
import {Thunks} from "@welshman/app"
import {app, thunks, repository} from "@app/welshman"
import ThunkStatus from "@app/components/ThunkStatus.svelte"
type Props = {
@@ -14,12 +15,12 @@
const {event, children}: Props = $props()
const deleted = deriveIsDeleted(repository, event)
const thunk = $derived(mergeThunks($thunks.filter(t => t.event.id === event.id)))
const thunk = $derived(app.use(Thunks).merge($thunks.filter(t => t.event.id === event.id)))
</script>
{#if $deleted}
<div class="btn btn-error btn-xs rounded-full">Deleted</div>
{:else if thunk.thunks.length > 0 && !thunkHasStatus(PublishStatus.Success, thunk)}
{:else if thunk.thunks.length > 0 && !thunk.hasStatus(PublishStatus.Success)}
<ThunkStatus {thunk} />
{:else if children}
{@render children?.()}

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