Compare commits

..

7 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
userAdityaa deb2b31466 chore: redesign threads as a linear phpBB-style forum view (#300)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-06-17 16:08:58 +00:00
Jon Staab b86632e86e Unwrap messages that are only quotes 2026-06-15 17:56:07 -07:00
Jon Staab 3f96b5547c Use direct zapping for the donate page, link to flotilla space for support 2026-06-15 14:00:15 -07:00
Jon Staab eebd764a18 Speed up feed loading 2026-06-15 13:12:46 -07:00
userAdityaa 3945685554 fix: make account selector inert during email login (#304)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-06-15 18:26:41 +00:00
194 changed files with 2085 additions and 1378 deletions
+1
View File
@@ -74,6 +74,7 @@
"@vite-pwa/sveltekit": "^1.1.0", "@vite-pwa/sveltekit": "^1.1.0",
"@welshman/app": "^0.8.16", "@welshman/app": "^0.8.16",
"@welshman/content": "^0.8.16", "@welshman/content": "^0.8.16",
"@welshman/domain": "^0.8.16",
"@welshman/editor": "^0.8.16", "@welshman/editor": "^0.8.16",
"@welshman/feeds": "^0.8.16", "@welshman/feeds": "^0.8.16",
"@welshman/lib": "^0.8.16", "@welshman/lib": "^0.8.16",
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 KiB

+12 -203
View File
@@ -2,15 +2,10 @@
@config "../tailwind.config.js"; @config "../tailwind.config.js";
@theme {
--font-sans: "Lato", ui-sans-serif, system-ui, sans-serif;
--font-display: "Baloo 2", "Lato", ui-rounded, system-ui, sans-serif;
}
/* root */ /* root */
:root { :root {
font-family: var(--font-sans); font-family: Lato;
--sait: var(--safe-area-inset-top, env(safe-area-inset-top)); --sait: var(--safe-area-inset-top, env(safe-area-inset-top));
--saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom)); --saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
--sail: var(--safe-area-inset-left, env(safe-area-inset-left)); --sail: var(--safe-area-inset-left, env(safe-area-inset-left));
@@ -158,15 +153,15 @@
} }
@utility heading { @utility heading {
@apply font-display text-center text-2xl font-bold tracking-tight; @apply text-center text-2xl;
} }
@utility brand { @utility subheading {
@apply font-display text-primary font-bold tracking-tight; @apply text-center text-xl;
} }
@utility label { @utility superheading {
@apply font-display text-sm font-semibold tracking-wider uppercase opacity-70; @apply text-center text-4xl;
} }
@utility link { @utility link {
@@ -220,19 +215,8 @@
@font-face { @font-face {
font-family: "Lato"; font-family: "Lato";
font-style: normal; font-style: bold;
font-weight: 300; font-weight: 600;
src:
local(""),
url("/fonts/Lato-Light.ttf") format("truetype");
}
/* Lato ships Regular + Bold only; map 600 (semibold) and 700 (bold) to the
Bold file so the browser never synthesizes a faux-bold. */
@font-face {
font-family: "Lato";
font-style: normal;
font-weight: 600 700;
src: src:
local(""), local(""),
url("/fonts/Lato-Bold.ttf") format("truetype"); url("/fonts/Lato-Bold.ttf") format("truetype");
@@ -244,38 +228,13 @@
font-weight: 400; font-weight: 400;
src: src:
local(""), local(""),
url("/fonts/Lato-Italic.ttf") format("truetype"); url("/fonts/Italic.ttf") format("truetype");
}
/* Baloo 2 — rounded, friendly display face (self-hosted, Latin subset). */
@font-face {
font-family: "Baloo 2";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("/fonts/Baloo2-Medium.woff2") format("woff2");
}
@font-face {
font-family: "Baloo 2";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/fonts/Baloo2-SemiBold.woff2") format("woff2");
}
@font-face {
font-family: "Baloo 2";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("/fonts/Baloo2-Bold.woff2") format("woff2");
} }
/* root */ /* root */
:root { :root {
font-family: var(--font-sans); font-family: Lato;
text-size-adjust: 100%; text-size-adjust: 100%;
--sait: var(--safe-area-inset-top, env(safe-area-inset-top)); --sait: var(--safe-area-inset-top, env(safe-area-inset-top));
--saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom)); --saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
@@ -325,7 +284,7 @@
opacity: 0.5; opacity: 0.5;
} }
/* editors */ /* tiptap */
.input-editor, .input-editor,
.chat-editor, .chat-editor,
@@ -364,11 +323,7 @@
} }
.chat-editor .tiptap { .chat-editor .tiptap {
@apply bg-base-300 rounded-[1.5rem] pr-12 transition-shadow; @apply rounded-box bg-base-300 pr-12;
}
.chat-editor:focus-within .tiptap {
box-shadow: 0 0 0 2px color-mix(in oklab, var(--color-primary), transparent 55%);
} }
.note-editor .tiptap { .note-editor .tiptap {
@@ -493,149 +448,3 @@ body.keyboard-open .chat__compose {
.chat__scroll-down { .chat__scroll-down {
@apply pb-sai z-feature fixed right-4 bottom-28 md:bottom-16; @apply pb-sai z-feature fixed right-4 bottom-28 md:bottom-16;
} }
/* shape, depth & motion */
/* Accessibility: neutralize all motion when the user asks for it. Decorative
motion is otherwise opt-in via `motion-safe:` and the guards below. */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* Soft, diffuse elevation — replaces ad-hoc hard `shadow-md` uses. */
@utility shadow-soft {
box-shadow:
0 4px 16px -4px oklch(0% 0 0 / 0.18),
0 1px 3px oklch(0% 0 0 / 0.08);
}
/* Organic "hand-drawn" avatar masks. The image (or gradient fallback) fills
the blob; three variants are chosen deterministically by pubkey hash so a
person's shape stays stable across the app. */
@utility avatar-blob {
border-radius: 42% 58% 54% 46% / 58% 46% 54% 42%;
}
@utility avatar-blob-2 {
border-radius: 60% 40% 46% 54% / 43% 57% 43% 57%;
}
@utility avatar-blob-3 {
border-radius: 47% 53% 62% 38% / 50% 62% 38% 50%;
}
/* Friendly rounded-square for space / relay / room tiles. */
@utility squircle {
border-radius: 30%;
}
/* Every DaisyUI button speaks in the rounded display voice and presses in. */
.btn {
font-family: var(--font-display);
font-weight: 600;
letter-spacing: -0.01em;
}
@media (prefers-reduced-motion: no-preference) {
.btn {
transition:
transform 150ms ease,
box-shadow 150ms ease,
background-color 150ms ease,
border-color 150ms ease;
}
.btn:active {
transform: scale(0.96);
}
}
/* ---- Motion vocabulary ---- */
@keyframes nav-button-pop {
0% {
transform: scale(0.9);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}
@keyframes button-pop {
0% {
transform: scale(0.97);
}
40% {
transform: scale(1.02);
}
100% {
transform: scale(1);
}
}
@keyframes pop {
0% {
transform: scale(0);
}
70% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
@keyframes reaction-pop {
0% {
transform: scale(0.6);
}
60% {
transform: scale(1.15);
}
100% {
transform: scale(1);
}
}
@keyframes float {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-6px);
}
}
@keyframes wiggle {
0%,
100% {
transform: rotate(0deg);
}
25% {
transform: rotate(-4deg);
}
75% {
transform: rotate(4deg);
}
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
@utility animate-pop {
animation: pop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@utility animate-reaction-pop {
animation: reaction-pop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
@utility animate-float {
animation: float 6s ease-in-out infinite;
}
@utility animate-wiggle {
animation: wiggle 0.4s ease-in-out;
}
+1 -1
View File
@@ -18,7 +18,7 @@ import {derived, get} from "svelte/store"
import {map, not, nthEq, reject, removeUndefined, uniqBy} from "@welshman/lib" import {map, not, nthEq, reject, removeUndefined, uniqBy} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util" import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
import {signer} from "@welshman/app" import {signer} from "@app/welshman"
import {load} from "@welshman/net" import {load} from "@welshman/net"
import {getLivekitEndpoint} from "$lib/livekit" import {getLivekitEndpoint} from "$lib/livekit"
import {AbortError, TimeoutError, whenAborted, whenTimeout} from "$lib/util" import {AbortError, TimeoutError, whenAborted, whenTimeout} from "$lib/util"
+4 -1
View File
@@ -2,7 +2,8 @@ import {DELETE, PROFILE, getPubkeyTagValues} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {append, call, on, reject, remove, sort, sortBy, spec, uniq, uniqBy} from "@welshman/lib" import {append, call, on, reject, remove, sort, sortBy, spec, uniq, uniqBy} from "@welshman/lib"
import type {Override} from "@welshman/lib" import type {Override} from "@welshman/lib"
import {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 {derived, readable} from "svelte/store"
import {DM_KINDS} from "@app/content" import {DM_KINDS} from "@app/content"
import type {RepositoryUpdate} from "@welshman/net" import type {RepositoryUpdate} from "@welshman/net"
@@ -35,6 +36,8 @@ export const chatsById = call(() => {
const chatsByPubkey = new Map<string, string[]>() const chatsByPubkey = new Map<string, string[]>()
const addSearchText = (chat: Override<Chat, {search_text?: string}>) => { const addSearchText = (chat: Override<Chat, {search_text?: string}>) => {
const displayProfileByPubkey = (pk: string) => app.use(Profiles).display(pk).get()
chat.search_text = chat.search_text =
chat.pubkeys.length === 1 chat.pubkeys.length === 1
? displayProfileByPubkey(chat.pubkeys[0]) + " note to self" ? displayProfileByPubkey(chat.pubkeys[0]) + " note to self"
+4 -3
View File
@@ -1,6 +1,7 @@
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {COMMENT, makeEvent} from "@welshman/util" import {COMMENT, makeEvent} from "@welshman/util"
import {publishThunk, tagEventForComment} from "@welshman/app" import {Thunks, Tags} from "@welshman/app"
import {app} from "@app/welshman"
export type CommentParams = { export type CommentParams = {
event: TrustedEvent event: TrustedEvent
@@ -10,7 +11,7 @@ export type CommentParams = {
} }
export const makeComment = ({url, event, content, tags = []}: CommentParams) => export const makeComment = ({url, event, content, tags = []}: CommentParams) =>
makeEvent(COMMENT, {content, tags: [...tags, ...tagEventForComment(event, url)]}) makeEvent(COMMENT, {content, tags: [...tags, ...app.use(Tags).tagEventForComment(event, url)]})
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) => 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"> <script lang="ts">
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {pubkey} from "@welshman/app" import {pubkey} from "@app/welshman"
import Dialog from "@lib/components/Dialog.svelte" import Dialog from "@lib/components/Dialog.svelte"
import Landing from "@app/components/Landing.svelte" import Landing from "@app/components/Landing.svelte"
import Toast from "@app/components/Toast.svelte" import Toast from "@app/components/Toast.svelte"
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import {getTagValue, getAddress} from "@welshman/util" import {getTagValue, getAddress} from "@welshman/util"
import {pubkey} from "@welshman/app" import {pubkey} from "@app/welshman"
import Pen2 from "@assets/icons/pen-2.svg?dataurl" import Pen2 from "@assets/icons/pen-2.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
+4 -3
View File
@@ -3,7 +3,8 @@
import {writable} from "svelte/store" import {writable} from "svelte/store"
import {randomId, HOUR} from "@welshman/lib" import {randomId, HOUR} from "@welshman/lib"
import {makeEvent, EVENT_TIME} from "@welshman/util" import {makeEvent, EVENT_TIME} from "@welshman/util"
import {publishThunk, waitForThunkError} from "@welshman/app" import {app} from "@app/welshman"
import {Thunks} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import {daysBetween} from "@lib/util" import {daysBetween} from "@lib/util"
import GallerySend from "@assets/icons/gallery-send.svg?dataurl" import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
@@ -107,8 +108,8 @@
} }
const event = makeEvent(EVENT_TIME, {content, tags}) const event = makeEvent(EVENT_TIME, {content, tags})
const calendarThunk = publishThunk({event, relays: [url]}) const calendarThunk = app.use(Thunks).publish({event, relays: [url]})
const error = await waitForThunkError(calendarThunk) const error = await calendarThunk.waitForError()
if (error) { if (error) {
return pushToast({theme: "error", message: error}) return pushToast({theme: "error", message: error})
+9 -13
View File
@@ -26,14 +26,10 @@
DIRECT_MESSAGE, DIRECT_MESSAGE,
DIRECT_MESSAGE_FILE, DIRECT_MESSAGE_FILE,
} from "@welshman/util" } from "@welshman/util"
import { import {app, pubkey} from "@app/welshman"
pubkey, import {Tags, Wraps, Thunks, MessagingRelayLists} from "@welshman/app"
tagPubkey,
sendWrapped, const messagingRelayListsByPubkey = app.use(MessagingRelayLists).index.$
mergeThunks,
loadMessagingRelayList,
messagingRelayListsByPubkey,
} from "@welshman/app"
import Danger from "@assets/icons/danger-triangle.svg?dataurl" import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import ArrowLeft from "@assets/icons/arrow-left.svg?dataurl" import ArrowLeft from "@assets/icons/arrow-left.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -96,7 +92,7 @@
const onSubmit = async (params: EventContent) => { const onSubmit = async (params: EventContent) => {
try { 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 // Remove p tags since they result in forking the conversation
params.tags = params.tags.filter(nthNe(0, "p")) params.tags = params.tags.filter(nthNe(0, "p"))
@@ -109,7 +105,7 @@
return return
} }
await sendWrapped({ await app.use(Wraps).publish({
event: makeDelete({event: eventToEdit, protect: false}), event: makeDelete({event: eventToEdit, protect: false}),
recipients: pubkeys, recipients: pubkeys,
pow: 16, pow: 16,
@@ -158,7 +154,7 @@
// Sleep 1 second between each one to make sure timestamps are distinct // Sleep 1 second between each one to make sure timestamps are distinct
const thunks = await Promise.all( const thunks = await Promise.all(
Array.from(enumerate(templates)).map(([i, event]) => Array.from(enumerate(templates)).map(([i, event]) =>
sendWrapped({ app.use(Wraps).publish({
event, event,
recipients: pubkeys, recipients: pubkeys,
delay: $userSettingsValues.send_delay + ms(i), delay: $userSettingsValues.send_delay + ms(i),
@@ -171,7 +167,7 @@
timeout: 30_000, timeout: 30_000,
children: { children: {
component: ThunkToast, component: ThunkToast,
props: {thunk: mergeThunks(thunks)}, props: {thunk: app.use(Thunks).merge(thunks)},
}, },
}) })
} finally { } finally {
@@ -234,7 +230,7 @@
onMount(() => { onMount(() => {
for (const pubkey of others) { for (const pubkey of others) {
loadMessagingRelayList(pubkey) app.use(MessagingRelayLists).load(pubkey)
} }
}) })
+1 -1
View File
@@ -127,7 +127,7 @@
</div> </div>
<Button <Button
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send" data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
class="center tooltip tooltip-left bg-primary text-primary-content absolute top-[7px] right-[7px] h-11 w-11 min-w-11 scale-90 rounded-full transition-transform motion-safe:hover:scale-100" class="center tooltip tooltip-left absolute right-4 h-10 w-10 min-w-10 rounded-full"
disabled={$uploading || disabled} disabled={$uploading || disabled}
onclick={submit}> onclick={submit}>
<Icon icon={Plane} /> <Icon icon={Plane} />
+3 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {TrustedEvent} from "@welshman/util" 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 {slide} from "@lib/transition"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -21,7 +22,7 @@
<div <div
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8" class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8"
transition:slide> transition:slide>
<p class="text-xs text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p> <p class="text-xs text-primary">{verb} @{app.use(Profiles).display(event.pubkey).get()}</p>
{#key event.id} {#key event.id}
<NoteContentMinimal trimParent {event} /> <NoteContentMinimal trimParent {event} />
{/key} {/key}
+9 -5
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {getRelaysFromList} from "@welshman/util" import {app, userRelayList} from "@app/welshman"
import {waitForThunkError, setMessagingRelays, userRelayList, setRelays} from "@welshman/app" import {RelayLists, MessagingRelayLists} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
@@ -29,8 +29,10 @@
loading = true loading = true
try { try {
if (getRelaysFromList($userRelayList).length === 0) { if (($userRelayList?.urls() ?? []).length === 0) {
const error = await waitForThunkError(await setRelays(DEFAULT_RELAYS.map(r => ["r", r]))) const error = await (
await app.use(RelayLists).setRelays(DEFAULT_RELAYS.map(r => ["r", r]))
).waitForError()
if (error) { if (error) {
pushToast({theme: "error", message: error}) pushToast({theme: "error", message: error})
@@ -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) { if (error) {
pushToast({theme: "error", message: error}) pushToast({theme: "error", message: error})
+3 -2
View File
@@ -3,7 +3,8 @@
import {page} from "$app/stores" import {page} from "$app/stores"
import {remove, uniq, formatTimestamp} from "@welshman/lib" import {remove, uniq, formatTimestamp} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {pubkey, loadMessagingRelayList} from "@welshman/app" import {app, pubkey} from "@app/welshman"
import {MessagingRelayLists} from "@welshman/app"
import {fade} from "@lib/transition" import {fade} from "@lib/transition"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileName from "@app/components/ProfileName.svelte" import ProfileName from "@app/components/ProfileName.svelte"
@@ -28,7 +29,7 @@
onMount(() => { onMount(() => {
for (const pk of others) { for (const pk of others) {
loadMessagingRelayList(pk) app.use(MessagingRelayLists).load(pk)
} }
}) })
</script> </script>
+11 -12
View File
@@ -1,8 +1,9 @@
<script lang="ts"> <script lang="ts">
import {type Instance} from "tippy.js" import {type Instance} from "tippy.js"
import {formatTimestampAsTime} from "@welshman/lib" import {hash, formatTimestampAsTime} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import {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 {isMobile} from "@lib/html"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl" import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -16,7 +17,7 @@
import ProfileDetail from "@app/components/ProfileDetail.svelte" import ProfileDetail from "@app/components/ProfileDetail.svelte"
import ChatMessageMenu from "@app/components/ChatMessageMenu.svelte" import ChatMessageMenu from "@app/components/ChatMessageMenu.svelte"
import ChatMessageMenuMobile from "@app/components/ChatMessageMenuMobile.svelte" import ChatMessageMenuMobile from "@app/components/ChatMessageMenuMobile.svelte"
import {getColor} from "@app/theme" import {colors} from "@app/theme"
import {makeDelete} from "@app/deletes" import {makeDelete} from "@app/deletes"
import {makeReaction} from "@app/reactions" import {makeReaction} from "@app/reactions"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
@@ -33,18 +34,18 @@
const {event, replyTo, canEdit, onEdit, pubkeys, showPubkey = false}: Props = $props() const {event, replyTo, canEdit, onEdit, pubkeys, showPubkey = false}: Props = $props()
const isOwn = event.pubkey === $pubkey const isOwn = event.pubkey === $pubkey
const profileDisplay = deriveProfileDisplay(event.pubkey) const profileDisplay = app.use(Profiles).display(event.pubkey).$
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id)) const thunk = app.use(Thunks).merge($thunks.filter(t => t.event.id === event.id))
const colorValue = getColor(event.pubkey) const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
const reply = () => replyTo(event) const reply = () => replyTo(event)
const edit = canEdit?.(event) ? () => onEdit?.(event) : undefined const edit = canEdit?.(event) ? () => onEdit?.(event) : undefined
const deleteReaction = (event: TrustedEvent) => const deleteReaction = (event: TrustedEvent) =>
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) => const createReaction = (template: EventContent) =>
sendWrapped({ app.use(Wraps).publish({
event: makeReaction({event, protect: false, ...template}), event: makeReaction({event, protect: false, ...template}),
recipients: pubkeys, recipients: pubkeys,
pow: 16, pow: 16,
@@ -101,9 +102,7 @@
{/if} {/if}
<div class="flex min-w-0 flex-col" class:items-end={isOwn}> <div class="flex min-w-0 flex-col" class:items-end={isOwn}>
<TapTarget <TapTarget
class="chat-bubble shadow-soft mx-1 mb-2 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl min-w-[100px] {isOwn class="bg-alt chat-bubble mx-1 mb-2 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl min-w-[100px]"
? 'bg-primary text-primary-content'
: 'bg-base-100'}"
onTap={showMobileMenu}> onTap={showMobileMenu}>
{#if showPubkey} {#if showPubkey}
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@@ -111,7 +110,7 @@
<Button onclick={openProfile} class="flex items-center gap-1"> <Button onclick={openProfile} class="flex items-center gap-1">
<ProfileCircle <ProfileCircle
pubkey={event.pubkey} pubkey={event.pubkey}
style="box-shadow: 0 0 0 1.5px {colorValue}" class="border border-solid border-base-content"
size={4} /> size={4} />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Button onclick={openProfile} class="text-sm font-bold" style="color: {colorValue}"> <Button onclick={openProfile} class="text-sm font-bold" style="color: {colorValue}">
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import type {NativeEmoji} from "emoji-picker-element/shared" import type {NativeEmoji} from "emoji-picker-element/shared"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {sendWrapped} from "@welshman/app" import {app} from "@app/welshman"
import {Wraps} from "@welshman/app"
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl" import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import EmojiButton from "@lib/components/EmojiButton.svelte" import EmojiButton from "@lib/components/EmojiButton.svelte"
@@ -15,7 +16,7 @@
const {event, pubkeys}: Props = $props() const {event, pubkeys}: Props = $props()
const onEmoji = (emoji: NativeEmoji) => const onEmoji = (emoji: NativeEmoji) =>
sendWrapped({ app.use(Wraps).publish({
event: makeReaction({event, content: emoji.unicode, protect: false}), event: makeReaction({event, content: emoji.unicode, protect: false}),
recipients: pubkeys, recipients: pubkeys,
pow: 16, pow: 16,
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import type {NativeEmoji} from "emoji-picker-element/shared" import type {NativeEmoji} from "emoji-picker-element/shared"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {sendWrapped} from "@welshman/app" import {app} from "@app/welshman"
import {Wraps} from "@welshman/app"
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl" import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
import Pen from "@assets/icons/pen.svg?dataurl" import Pen from "@assets/icons/pen.svg?dataurl"
import Reply from "@assets/icons/reply-2.svg?dataurl" import Reply from "@assets/icons/reply-2.svg?dataurl"
@@ -28,7 +29,7 @@
const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => { const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => {
history.back() history.back()
sendWrapped({ app.use(Wraps).publish({
event: makeReaction({event, content: emoji.unicode, protect: false}), event: makeReaction({event, content: emoji.unicode, protect: false}),
recipients: pubkeys, recipients: pubkeys,
pow: 16, pow: 16,
+3 -2
View File
@@ -4,7 +4,8 @@
import {writable} from "svelte/store" import {writable} from "svelte/store"
import {tryCatch, uniq} from "@welshman/lib" import {tryCatch, uniq} from "@welshman/lib"
import {fromNostrURI} from "@welshman/util" import {fromNostrURI} from "@welshman/util"
import {loadMessagingRelayList} from "@welshman/app" import {app} from "@app/welshman"
import {MessagingRelayLists} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import Field from "@lib/components/Field.svelte" import Field from "@lib/components/Field.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -34,7 +35,7 @@
let pubkeys: string[] = $state([]) let pubkeys: string[] = $state([])
$effect(() => { $effect(() => {
pubkeys.forEach(pubkey => loadMessagingRelayList(pubkey)) pubkeys.forEach(pubkey => app.use(MessagingRelayLists).load(pubkey))
}) })
onMount(() => { onMount(() => {
+1 -1
View File
@@ -2,7 +2,7 @@
import {uniq} from "@welshman/lib" import {uniq} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import {getTagValue, getTagValues, getAddress} from "@welshman/util" import {getTagValue, getTagValues, getAddress} from "@welshman/util"
import {pubkey} from "@welshman/app" import {pubkey} from "@app/welshman"
import Pen2 from "@assets/icons/pen-2.svg?dataurl" import Pen2 from "@assets/icons/pen-2.svg?dataurl"
import {normalizeTopic} from "@lib/util" import {normalizeTopic} from "@lib/util"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
+4 -3
View File
@@ -2,7 +2,8 @@
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {removeUndefined, randomId, uniq} from "@welshman/lib" import {removeUndefined, randomId, uniq} from "@welshman/lib"
import {makeEvent, CLASSIFIED} from "@welshman/util" import {makeEvent, CLASSIFIED} from "@welshman/util"
import {publishThunk, waitForThunkError} from "@welshman/app" import {app} from "@app/welshman"
import {Thunks} from "@welshman/app"
import {isMobile, preventDefault} from "@lib/html" import {isMobile, preventDefault} from "@lib/html"
import {normalizeTopic} from "@lib/util" import {normalizeTopic} from "@lib/util"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -118,12 +119,12 @@
} }
} }
const classifiedThunk = publishThunk({ const classifiedThunk = app.use(Thunks).publish({
relays: [url], relays: [url],
event: makeEvent(CLASSIFIED, {content, tags}), event: makeEvent(CLASSIFIED, {content, tags}),
}) })
const error = await waitForThunkError(classifiedThunk) const error = await classifiedThunk.waitForError()
if (error) { if (error) {
return pushToast({theme: "error", message: error}) return pushToast({theme: "error", message: error})
@@ -9,7 +9,7 @@
tagsFromIMeta, tagsFromIMeta,
makeBlossomAuthEvent, makeBlossomAuthEvent,
} from "@welshman/util" } from "@welshman/util"
import {signer} from "@welshman/app" import {signer} from "@app/welshman"
import LinkRound from "@assets/icons/link-round.svg?dataurl" import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
+3 -2
View File
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import {removeUndefined} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import type {ProfilePointer} from "@welshman/content" import type {ProfilePointer} from "@welshman/content"
import {deriveProfileDisplay} from "@welshman/app" import {app} from "@app/welshman"
import {Profiles} from "@welshman/app"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte" import ProfileDetail from "@app/components/ProfileDetail.svelte"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
@@ -13,7 +14,7 @@
const {value, url}: Props = $props() 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}) const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url})
</script> </script>
+4 -1
View File
@@ -6,6 +6,7 @@
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import NoteCard from "@app/components/NoteCard.svelte" import NoteCard from "@app/components/NoteCard.svelte"
import NoteContent from "@app/components/NoteContent.svelte"
import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte" import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
import {deriveEvent} from "@app/repository" import {deriveEvent} from "@app/repository"
import {entityLink} from "@app/env" import {entityLink} from "@app/env"
@@ -43,7 +44,9 @@
<Button class="my-2 block w-full max-w-full text-left" {onclick}> <Button class="my-2 block w-full max-w-full text-left" {onclick}>
{#if $quote} {#if $quote}
{#if $quote.kind === MESSAGE} {#if $quote.content.trim().match(/^(nostr:)?nevent1[a-z0-9]+$/)}
<NoteContent {url} event={$quote} />
{:else if $quote.kind === MESSAGE}
<div <div
class="border-l-2 border-solid border-l-primary py-1 pl-2 opacity-90" class="border-l-2 border-solid border-l-primary py-1 pl-2 opacity-90"
style="background-color: color-mix(in srgb, var(--color-primary) 10%, var(--color-base-300) 90%);"> style="background-color: color-mix(in srgb, var(--color-primary) 10%, var(--color-base-300) 90%);">
+1 -1
View File
@@ -4,7 +4,7 @@
import type {Writable} from "svelte/store" import type {Writable} from "svelte/store"
import type {Instance} from "tippy.js" import type {Instance} from "tippy.js"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import {createSearch} from "@welshman/app" import {createSearch} from "@app/welshman"
import {currencyOptions, displayCurrency} from "@lib/currency" import {currencyOptions, displayCurrency} from "@lib/currency"
import Suggestions from "@lib/components/Suggestions.svelte" import Suggestions from "@lib/components/Suggestions.svelte"
import CurrencySuggestion from "@app/components/CurrencySuggestion.svelte" import CurrencySuggestion from "@app/components/CurrencySuggestion.svelte"
-31
View File
@@ -1,31 +0,0 @@
<script lang="ts">
import type {Snippet} from "svelte"
import {fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte"
type Props = {
icon?: string
title: string
children?: Snippet
action?: Snippet
}
const {icon, title, children, action}: Props = $props()
</script>
<div
class="m-auto flex max-w-sm flex-col items-center gap-3 px-4 py-12 text-center"
in:fly={{y: 16}}>
{#if icon}
<div class="bg-primary/10 text-primary center size-16 rounded-full motion-safe:animate-float">
<Icon {icon} size={8} />
</div>
{/if}
<h3 class="font-display text-xl font-bold tracking-tight">{title}</h3>
{#if children}
<p class="text-sm opacity-70">{@render children?.()}</p>
{/if}
{#if action}
<div class="mt-1">{@render action?.()}</div>
{/if}
</div>
+1 -1
View File
@@ -5,7 +5,7 @@
import {load} from "@welshman/net" import {load} from "@welshman/net"
import {deriveArray, deriveEventsById} from "@welshman/store" import {deriveArray, deriveEventsById} from "@welshman/store"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {repository} from "@welshman/app" import {repository} from "@app/welshman"
import {deriveChecked} from "@app/notifications" import {deriveChecked} from "@app/notifications"
import Reply from "@assets/icons/reply-2.svg?dataurl" import Reply from "@assets/icons/reply-2.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
+1 -1
View File
@@ -4,7 +4,7 @@
import {LOCALE, secondsToDate} from "@welshman/lib" import {LOCALE, secondsToDate} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import {tracker} from "@welshman/app" import {tracker} from "@app/welshman"
import FileText from "@assets/icons/file-text.svg?dataurl" import FileText from "@assets/icons/file-text.svg?dataurl"
import Copy from "@assets/icons/copy.svg?dataurl" import Copy from "@assets/icons/copy.svg?dataurl"
import UserCircle from "@assets/icons/user-circle.svg?dataurl" import UserCircle from "@assets/icons/user-circle.svg?dataurl"
+3 -2
View File
@@ -4,7 +4,8 @@
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {COMMENT, ManagementMethod} from "@welshman/util" import {COMMENT, ManagementMethod} from "@welshman/util"
import {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 ShareCircle from "@assets/icons/share-circle.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl" import Code2 from "@assets/icons/code-2.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl" import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
@@ -56,7 +57,7 @@
title: `Delete ${noun}`, title: `Delete ${noun}`,
message: `Are you sure you want to delete this ${noun.toLowerCase()} from the space?`, message: `Are you sure you want to delete this ${noun.toLowerCase()} from the space?`,
confirm: async () => { confirm: async () => {
const {error} = await manageRelay(url, { const {error} = await app.use(RelayManagement).post(url, {
method: ManagementMethod.BanEvent, method: ManagementMethod.BanEvent,
params: [event.id], params: [event.id],
}) })
+23 -5
View File
@@ -1,15 +1,17 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {writable} from "svelte/store" import {writable} from "svelte/store"
import type {TrustedEvent} from "@welshman/util"
import {isMobile, preventDefault} from "@lib/html" import {isMobile, preventDefault} from "@lib/html"
import {fly} from "@lib/transition" import {fly} from "@lib/transition"
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl" import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import EditorContent from "@app/editor/EditorContent.svelte" import EditorContent from "@app/editor/EditorContent.svelte"
import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
import {publishComment} from "@app/comments" import {publishComment} from "@app/comments"
import {canEnforceNip70} from "@app/relays" import {canEnforceNip70} from "@app/relays"
import {PROTECTED} from "@app/groups" import {PROTECTED, prependParent} from "@app/groups"
import {makeEditor} from "@app/editor" import {makeEditor} from "@app/editor"
import {DraftKey} from "@app/drafts" import {DraftKey} from "@app/drafts"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
@@ -18,8 +20,17 @@
content?: string | object content?: string | object
} }
const {url, event, onClose, onSubmit} = $props() type Props = {
const draftKey = new DraftKey<Values>(`reply:${event.id}`) url: string
event: TrustedEvent
parent?: TrustedEvent
onClose: () => void
onClearParent?: () => void
onSubmit: (thunk: unknown) => void
}
const {url, event, parent, onClose, onClearParent, onSubmit}: Props = $props()
const draftKey = new DraftKey<Values>(`reply:${event.id}:${parent?.id || ""}`)
const initialValues = draftKey.get() const initialValues = draftKey.get()
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
const uploading = writable(false) const uploading = writable(false)
@@ -31,8 +42,8 @@
if ($uploading) return if ($uploading) return
const ed = await editor const ed = await editor
const content = ed.getText({blockSeparator: "\n"}).trim() let content = ed.getText({blockSeparator: "\n"}).trim()
const tags = ed.storage.nostr.getEditorTags() let tags = ed.storage.nostr.getEditorTags()
if (await shouldProtect) { if (await shouldProtect) {
tags.push(PROTECTED) tags.push(PROTECTED)
@@ -45,6 +56,10 @@
}) })
} }
if (parent) {
;({content, tags} = prependParent(parent, {content, tags}, url))
}
draftKey.clear() draftKey.clear()
onSubmit(publishComment({event, content, tags, relays: [url]})) onSubmit(publishComment({event, content, tags, relays: [url]}))
} }
@@ -87,6 +102,9 @@
onsubmit={preventDefault(submit)} onsubmit={preventDefault(submit)}
class="left-content bottom-sai right-sai fixed z-feature mb-14 md:mb-0 w-full md:w-auto pr-2"> class="left-content bottom-sai right-sai fixed z-feature mb-14 md:mb-0 w-full md:w-auto pr-2">
<div class="card2 mx-2 my-2 bg-alt shadow-md"> <div class="card2 mx-2 my-2 bg-alt shadow-md">
{#if parent}
<ChatComposeParent event={parent} clear={() => onClearParent?.()} verb="Replying to" />
{/if}
<div class="relative"> <div class="relative">
<div class="note-editor grow overflow-hidden"> <div class="note-editor grow overflow-hidden">
<EditorContent {autofocus} {editor} /> <EditorContent {autofocus} {editor} />
+4 -3
View File
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import {writable} from "svelte/store" import {writable} from "svelte/store"
import {makeEvent, ZAP_GOAL} from "@welshman/util" import {makeEvent, ZAP_GOAL} from "@welshman/util"
import {publishThunk, waitForThunkError} from "@welshman/app" import {app} from "@app/welshman"
import {Thunks} from "@welshman/app"
import {isMobile, preventDefault} from "@lib/html" import {isMobile, preventDefault} from "@lib/html"
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl" import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
import Bolt from "@assets/icons/bolt.svg?dataurl" import Bolt from "@assets/icons/bolt.svg?dataurl"
@@ -93,12 +94,12 @@
tags.push(["h", h]) tags.push(["h", h])
} }
const goalThunk = publishThunk({ const goalThunk = app.use(Thunks).publish({
relays: [url], relays: [url],
event: makeEvent(ZAP_GOAL, {content: title, tags}), event: makeEvent(ZAP_GOAL, {content: title, tags}),
}) })
const error = await waitForThunkError(goalThunk) const error = await goalThunk.waitForError()
if (error) { if (error) {
return pushToast({theme: "error", message: error}) return pushToast({theme: "error", message: error})
+13 -9
View File
@@ -2,8 +2,10 @@
import {now, DAY, uniq, sum} from "@welshman/lib" import {now, DAY, uniq, sum} from "@welshman/lib"
import type {Zap, TrustedEvent} from "@welshman/util" import type {Zap, TrustedEvent} from "@welshman/util"
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util" import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
import {deriveItemsByKey, deriveArray} from "@welshman/store" import {derived} from "svelte/store"
import {repository, getValidZap} from "@welshman/app" 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 Bolt from "@assets/icons/bolt.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import ZapButton from "@app/components/ZapButton.svelte" import ZapButton from "@app/components/ZapButton.svelte"
@@ -16,13 +18,15 @@
const {url, event, ...props}: Props = $props() const {url, event, ...props}: Props = $props()
const zaps = deriveArray( // Validated zaps for this goal. `validZapReceipts` is a reactive Projection
deriveItemsByKey<Zap>({ // (resolves recipient zappers from loaded profiles); we re-derive it whenever
repository, // the set of ZAP_RESPONSE events in the repository changes.
getKey: zap => zap.response.id, const zapReceipts = deriveEvents({repository, filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}]})
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}], const zaps = derived(
eventToItem: (response: TrustedEvent) => getValidZap(response, event), zapReceipts,
}), ($receipts: TrustedEvent[], set) =>
app.use(Zappers).validZapReceipts($receipts, event).$.subscribe(set),
[] as Zap[],
) )
const goalAmount = parseInt(getTagValue("amount", event.tags) || "0") const goalAmount = parseInt(getTagValue("amount", event.tags) || "0")
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {createSearch} from "@welshman/app" import {createSearch} from "@app/welshman"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {session} from "@welshman/app" import {session} from "@app/welshman"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import CheckCircle from "@assets/icons/check-circle.svg?dataurl" import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
+3 -2
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <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 AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -12,7 +13,7 @@
const {pubkey} = $props() const {pubkey} = $props()
const zapper = deriveZapperForPubkey(pubkey) const zapper = app.use(Zappers).forPubkey(pubkey).$
const back = () => history.back() const back = () => history.back()
</script> </script>
@@ -80,9 +80,8 @@
for={id} for={id}
aria-label="Drag and drop files here." aria-label="Drag and drop files here."
style="background-image: url({url});" style="background-image: url({url});"
class="avatar-blob relative flex h-24 w-24 shrink-0 cursor-pointer items-center justify-center border-2 border-dashed border-primary/40 bg-base-300 bg-cover bg-center transition-all motion-safe:hover:rotate-1 motion-safe:hover:scale-[1.02]" class="relative flex h-24 w-24 shrink-0 cursor-pointer items-center justify-center rounded-full border-2 border-solid border-base-content bg-base-300 bg-cover bg-center transition-all"
class:transparent={!url} class:transparent={!url}
class:border-solid={url || active}
class:border-primary={active} class:border-primary={active}
ondragenter={stopPropagation(preventDefault(onDragEnter))} ondragenter={stopPropagation(preventDefault(onDragEnter))}
ondragover={stopPropagation(preventDefault(onDragOver))} ondragover={stopPropagation(preventDefault(onDragOver))}
+8 -2
View File
@@ -1,8 +1,14 @@
<script lang="ts"> <script lang="ts">
import {Client} from "@pomade/core" import {Client} from "@pomade/core"
import {getPubkey} from "@welshman/util" import {getPubkey} from "@welshman/util"
import type {SessionPomade} from "@welshman/app" import {session} from "@app/welshman"
import {session} from "@welshman/app" // 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 {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
+7 -2
View File
@@ -1,7 +1,12 @@
<script lang="ts"> <script lang="ts">
import {Client} from "@pomade/core" import {Client} from "@pomade/core"
import type {SessionPomade} from "@welshman/app" import {session} from "@app/welshman"
import {session} from "@welshman/app" // 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 {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
+4 -10
View File
@@ -9,7 +9,7 @@
import CardButton from "@lib/components/CardButton.svelte" import CardButton from "@lib/components/CardButton.svelte"
import LogIn from "@app/components/LogIn.svelte" import LogIn from "@app/components/LogIn.svelte"
import SignUp from "@app/components/SignUp.svelte" import SignUp from "@app/components/SignUp.svelte"
import {PLATFORM_TERMS, PLATFORM_PRIVACY, PLATFORM_NAME, PLATFORM_LOGO} from "@app/env" import {PLATFORM_TERMS, PLATFORM_PRIVACY, PLATFORM_NAME} from "@app/env"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
const logIn = () => pushModal(LogIn) const logIn = () => pushModal(LogIn)
@@ -19,15 +19,9 @@
<Modal> <Modal>
<ModalBody> <ModalBody>
<div class="flex flex-col items-center gap-3 py-2"> <div class="py-2">
<img <h1 class="heading">Welcome to {PLATFORM_NAME}!</h1>
src={PLATFORM_LOGO} <p class="text-center">The chat app built for self-hosted communities.</p>
alt={PLATFORM_NAME}
class="shadow-soft ring-primary/20 size-16 rounded-2xl object-cover ring-4 motion-safe:animate-float" />
<h1 class="heading">Welcome to <span class="brand">{PLATFORM_NAME}</span>!</h1>
<p class="max-w-sm text-center opacity-80">
A cozy home for your community — chat, connect, and own your little corner of the internet.
</p>
</div> </div>
<Button onclick={logIn}> <Button onclick={logIn}>
<CardButton class="btn-primary"> <CardButton class="btn-primary">
+5 -5
View File
@@ -2,7 +2,7 @@
import {onMount} from "svelte" import {onMount} from "svelte"
import {Capacitor} from "@capacitor/core" import {Capacitor} from "@capacitor/core"
import {getNip07, getNip55, Nip55Signer} from "@welshman/signer" import {getNip07, getNip55, Nip55Signer} from "@welshman/signer"
import {addSession, type 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 Widget from "@assets/icons/widget-4.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl" import Letter from "@assets/icons/letter.svg?dataurl"
import Cpu from "@assets/icons/cpu-bolt.svg?dataurl" import Cpu from "@assets/icons/cpu-bolt.svg?dataurl"
@@ -32,8 +32,8 @@
const signUp = () => pushModal(SignUp) const signUp = () => pushModal(SignUp)
const onSuccess = async (session: Session) => { const onSuccess = async (session: FlotillaSession, pubkey: string) => {
addSession(session) addSession({...session, pubkey})
setChecked("*") setChecked("*")
clearModals() clearModals()
} }
@@ -45,7 +45,7 @@
const pubkey = await getNip07()?.getPublicKey() const pubkey = await getNip07()?.getPublicKey()
if (pubkey) { if (pubkey) {
await onSuccess(makeNip07Session(pubkey)) await onSuccess(makeNip07Session(pubkey), pubkey)
} else { } else {
pushToast({ pushToast({
theme: "error", theme: "error",
@@ -65,7 +65,7 @@
const pubkey = await signer.getPubkey() const pubkey = await signer.getPubkey()
if (pubkey) { if (pubkey) {
await onSuccess(makeNip55Session(pubkey, app.packageName)) await onSuccess(makeNip55Session(pubkey, app.packageName), pubkey)
} else { } else {
pushToast({ pushToast({
theme: "error", theme: "error",
+1 -1
View File
@@ -4,7 +4,7 @@
import type {Nip46ResponseWithResult} from "@welshman/signer" import type {Nip46ResponseWithResult} from "@welshman/signer"
import {Nip46Broker} from "@welshman/signer" import {Nip46Broker} from "@welshman/signer"
import {makeSecret} from "@welshman/util" import {makeSecret} from "@welshman/util"
import {loginWithNip01, loginWithNip46} from "@welshman/app" import {loginWithNip01, loginWithNip46} from "@app/welshman"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
+2 -2
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {bytesToHex} from "@welshman/lib" import {bytesToHex} from "@welshman/lib"
import {loginWithNip01} from "@welshman/app" import {loginWithNip01} from "@app/welshman"
import {decrypt} from "nostr-tools/nip49" import {decrypt} from "nostr-tools/nip49"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import {nsecDecode} from "@lib/util" import {nsecDecode} from "@lib/util"
@@ -56,7 +56,7 @@
}) })
} }
loginWithNip01(secret) await loginWithNip01(secret)
setChecked("*") setChecked("*")
clearModals() clearModals()
} catch (e) { } catch (e) {
+1 -1
View File
@@ -76,7 +76,7 @@
onclick={() => selectAccount(option)} onclick={() => selectAccount(option)}
disabled={loading} disabled={loading}
class="card2 bg-alt flex w-full items-center p-3 text-left"> class="card2 bg-alt flex w-full items-center p-3 text-left">
<Profile pubkey={option.pubkey} /> <Profile inert pubkey={option.pubkey} />
</Button> </Button>
{/each} {/each}
</div> </div>
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {Capacitor} from "@capacitor/core" import {Capacitor} from "@capacitor/core"
import {pubkey} from "@welshman/app" import {pubkey} from "@app/welshman"
import Server from "@assets/icons/server.svg?dataurl" import Server from "@assets/icons/server.svg?dataurl"
import GalleryMinimalistic from "@assets/icons/gallery-minimalistic.svg?dataurl" import GalleryMinimalistic from "@assets/icons/gallery-minimalistic.svg?dataurl"
import Shield from "@assets/icons/shield-minimalistic.svg?dataurl" import Shield from "@assets/icons/shield-minimalistic.svg?dataurl"
@@ -3,8 +3,10 @@
import {sum} from "@welshman/lib" import {sum} from "@welshman/lib"
import type {Zap, TrustedEvent} from "@welshman/util" import type {Zap, TrustedEvent} from "@welshman/util"
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util" import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
import {deriveItemsByKey, deriveArray} from "@welshman/store" import {derived} from "svelte/store"
import {repository, getValidZap} from "@welshman/app" 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 Bolt from "@assets/icons/bolt.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import ContentMinimal from "@app/components/ContentMinimal.svelte" import ContentMinimal from "@app/components/ContentMinimal.svelte"
@@ -14,13 +16,14 @@
const content = getTagValue("summary", props.event.tags) const content = getTagValue("summary", props.event.tags)
const fakeEvent = {content, tags: props.event.tags} const fakeEvent = {content, tags: props.event.tags}
const zaps = deriveArray( // Validated zaps for this goal (reactive Projection, re-derived as the set of
deriveItemsByKey<Zap>({ // ZAP_RESPONSE events in the repository changes).
repository, const zapReceipts = deriveEvents({repository, filters: [{kinds: [ZAP_RESPONSE], "#e": [props.event.id]}]})
getKey: zap => zap.response.id, const zaps = derived(
filters: [{kinds: [ZAP_RESPONSE], "#e": [props.event.id]}], zapReceipts,
eventToItem: (response: TrustedEvent) => getValidZap(response, props.event), ($receipts: TrustedEvent[], set) =>
}), app.use(Zappers).validZapReceipts($receipts, props.event).$.subscribe(set),
[] as Zap[],
) )
const goalAmount = parseInt(getTagValue("amount", props.event.tags) || "0") const goalAmount = parseInt(getTagValue("amount", props.event.tags) || "0")
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import {Client} from "@pomade/core" import {Client} from "@pomade/core"
import type {SessionPomade} from "@welshman/app" import type {SessionPomade} from "@welshman/app"
import {session} from "@welshman/app" import {session} from "@app/welshman"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
@@ -1,7 +1,12 @@
<script lang="ts"> <script lang="ts">
import {Client} from "@pomade/core" import {Client} from "@pomade/core"
import {session} from "@welshman/app" import {session} from "@app/welshman"
import type {SessionPomade} from "@welshman/app" // 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 {preventDefault} from "@lib/html"
import Key from "@assets/icons/key.svg?dataurl" import Key from "@assets/icons/key.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
+4 -3
View File
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import {insertAt, now, randomId, removeAt, removeUndefined} from "@welshman/lib" import {insertAt, now, randomId, removeAt, removeUndefined} from "@welshman/lib"
import {makeEvent, POLL} from "@welshman/util" import {makeEvent, POLL} from "@welshman/util"
import {publishThunk, waitForThunkError} from "@welshman/app" import {Thunks} from "@welshman/app"
import {app} from "@app/welshman"
import {isMobile, preventDefault} from "@lib/html" import {isMobile, preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import HamburgerMenu from "@assets/icons/hamburger-menu.svg?dataurl" import HamburgerMenu from "@assets/icons/hamburger-menu.svg?dataurl"
@@ -142,12 +143,12 @@
tags.push(PROTECTED) tags.push(PROTECTED)
} }
const pollThunk = publishThunk({ const pollThunk = app.use(Thunks).publish({
relays: [url], relays: [url],
event: makeEvent(POLL, {content: title.trim(), tags}), event: makeEvent(POLL, {content: title.trim(), tags}),
}) })
const error = await waitForThunkError(pollThunk) const error = await pollThunk.waitForError()
if (error) { if (error) {
return pushToast({theme: "error", message: error}) return pushToast({theme: "error", message: error})
+7 -5
View File
@@ -2,7 +2,9 @@
import {onDestroy} from "svelte" import {onDestroy} from "svelte"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {POLL_RESPONSE} from "@welshman/util" import {POLL_RESPONSE} from "@welshman/util"
import {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 {formatTimestampRelative} from "@welshman/lib"
import {deriveEvents} from "@app/repository" import {deriveEvents} from "@app/repository"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
@@ -50,7 +52,7 @@
const publishSelection = (selection: string[]) => { const publishSelection = (selection: string[]) => {
if (activeThunk) { if (activeThunk) {
abortThunk(activeThunk) activeThunk.abort()
} }
if (selection.length === 0) { if (selection.length === 0) {
@@ -58,7 +60,7 @@
return return
} }
activeThunk = publishThunk({ activeThunk = app.use(Thunks).publish({
relays: [url], relays: [url],
event: makePollResponse({event, selectedIds: selection}), event: makePollResponse({event, selectedIds: selection}),
delay: publishDelay, delay: publishDelay,
@@ -92,7 +94,7 @@
} }
let selectedIds = $state<string[]>([]) let selectedIds = $state<string[]>([])
let activeThunk: ReturnType<typeof publishThunk> | undefined let activeThunk: Thunk | undefined
$effect(() => { $effect(() => {
if (ownResponse) { if (ownResponse) {
@@ -102,7 +104,7 @@
onDestroy(() => { onDestroy(() => {
if (activeThunk) { if (activeThunk) {
abortThunk(activeThunk) activeThunk.abort()
} }
}) })
</script> </script>
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {Client} from "@pomade/core" import {Client} from "@pomade/core"
import {session, isPomadeSession} from "@welshman/app" import {session, isPomadeSession} from "@app/welshman"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl" import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import {fly} from "@lib/transition" import {fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
+5 -5
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {userProfile} from "@welshman/app" import {userProfile} from "@app/welshman"
import Letter from "@assets/icons/letter.svg?dataurl" import Letter from "@assets/icons/letter.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import Widget from "@assets/icons/widget-4.svg?dataurl" import Widget from "@assets/icons/widget-4.svg?dataurl"
@@ -41,8 +41,8 @@
{/if} {/if}
<div class="flex flex-col"> <div class="flex flex-col">
<PrimaryNavItem title="Settings" href="/settings/profile" prefix="/settings"> <PrimaryNavItem title="Settings" href="/settings/profile" prefix="/settings">
{#if $userProfile?.picture} {#if $userProfile?.picture()}
<ImageIcon alt="Settings" src={$userProfile?.picture} class="rounded-full" size={10} /> <ImageIcon alt="Settings" src={$userProfile?.picture()} class="rounded-full" size={10} />
{:else} {:else}
<ImageIcon alt="Settings" src={UserRounded} class="rounded-full" size={8} /> <ImageIcon alt="Settings" src={UserRounded} class="rounded-full" size={8} />
{/if} {/if}
@@ -86,8 +86,8 @@
{/if} {/if}
</div> </div>
<PrimaryNavItem title="Settings" onclick={showSettingsMenu}> <PrimaryNavItem title="Settings" onclick={showSettingsMenu}>
{#if $userProfile?.picture} {#if $userProfile?.picture()}
<ImageIcon alt="Settings" src={$userProfile?.picture} size={10} class="rounded-full" /> <ImageIcon alt="Settings" src={$userProfile?.picture()} size={10} class="rounded-full" />
{:else} {:else}
<ImageIcon alt="Settings" src={Settings} size={8} class="rounded-full" /> <ImageIcon alt="Settings" src={Settings} size={8} class="rounded-full" />
{/if} {/if}
@@ -1,5 +1,6 @@
<script lang="ts"> <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 PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
import RelayIcon from "@app/components/RelayIcon.svelte" import RelayIcon from "@app/components/RelayIcon.svelte"
import {makeSpacePath, goToSpace} from "@app/routes" import {makeSpacePath, goToSpace} from "@app/routes"
@@ -15,7 +16,7 @@
const path = makeSpacePath(url) const path = makeSpacePath(url)
const display = $derived(deriveRelayDisplay(url)) const display = $derived(app.use(Relays).display(url).$)
</script> </script>
<PrimaryNavItem <PrimaryNavItem
+6 -5
View File
@@ -1,8 +1,9 @@
<script lang="ts"> <script lang="ts">
import * as nip19 from "nostr-tools/nip19" import * as nip19 from "nostr-tools/nip19"
import {removeUndefined} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {displayPubkey} from "@welshman/util" import {displayPubkey, displayNip05} from "@welshman/util"
import {deriveHandleForPubkey, displayHandle, deriveProfileDisplay} from "@welshman/app" import {Handles, Profiles} from "@welshman/app"
import {app} from "@app/welshman"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte" import ProfileCircle from "@app/components/ProfileCircle.svelte"
@@ -23,8 +24,8 @@
const {pubkey, url, showPubkey, inert, avatarSize = 10}: Props = $props() const {pubkey, url, showPubkey, inert, avatarSize = 10}: Props = $props()
const relays = removeUndefined([url]) const relays = removeUndefined([url])
const profileDisplay = deriveProfileDisplay(pubkey, relays) const profileDisplay = app.use(Profiles).display(pubkey, relays).$
const handle = deriveHandleForPubkey(pubkey) const handle = app.use(Handles).forPubkey(pubkey).$
const openProfile = () => { const openProfile = () => {
pushModal(ProfileDetail, {pubkey, url}) pushModal(ProfileDetail, {pubkey, url})
@@ -58,7 +59,7 @@
</div> </div>
{#if $handle} {#if $handle}
<div class="overflow-hidden text-ellipsis text-sm opacity-75"> <div class="overflow-hidden text-ellipsis text-sm opacity-75">
{displayHandle($handle)} {displayNip05($handle?.nip05)}
</div> </div>
{/if} {/if}
{#if showPubkey} {#if showPubkey}
+3 -2
View File
@@ -6,7 +6,8 @@
import {deriveEventsDesc, deriveEventsById} from "@welshman/store" import {deriveEventsDesc, deriveEventsById} from "@welshman/store"
import {formatTimestampRelative} from "@welshman/lib" import {formatTimestampRelative} from "@welshman/lib"
import {NOTE, ROOMS, COMMENT, MESSAGE} from "@welshman/util" import {NOTE, ROOMS, COMMENT, MESSAGE} from "@welshman/util"
import {repository, loadRelayList} from "@welshman/app" import {RelayLists} from "@welshman/app"
import {repository, app} from "@app/welshman"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileSpaces from "@app/components/ProfileSpaces.svelte" import ProfileSpaces from "@app/components/ProfileSpaces.svelte"
import {deriveGroupList, getSpaceUrlsFromGroupList} from "@app/groups" import {deriveGroupList, getSpaceUrlsFromGroupList} from "@app/groups"
@@ -30,7 +31,7 @@
onMount(async () => { onMount(async () => {
// Make sure we have their relay selections before we load their posts // Make sure we have their relay selections before we load their posts
await loadRelayList(pubkey) await app.use(RelayLists).load(pubkey)
// Load groups and at least one note, regardless of time frame // Load groups and at least one note, regardless of time frame
load({ load({
+10 -33
View File
@@ -1,8 +1,9 @@
<script lang="ts"> <script lang="ts">
import cx from "classnames" import cx from "classnames"
import {removeUndefined} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {deriveProfile, deriveProfileDisplay} from "@welshman/app" import {Profiles} from "@welshman/app"
import {getColor, getBlobVariant} from "@app/theme" import {app} from "@app/welshman"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import ImageIcon from "@lib/components/ImageIcon.svelte" import ImageIcon from "@lib/components/ImageIcon.svelte"
type Props = { type Props = {
@@ -10,39 +11,15 @@
class?: string class?: string
size?: number size?: number
url?: string url?: string
shape?: "blob" | "circle"
style?: string
} }
const {pubkey, url, size = 7, shape = "blob", style = "", ...props}: Props = $props() const {pubkey, url, size = 7, ...props}: Props = $props()
const profile = deriveProfile(pubkey, removeUndefined([url])) const profile = app.use(Profiles).one(pubkey, removeUndefined([url]))
const display = deriveProfileDisplay(pubkey)
// Organic, hand-drawn-feeling mask. The variant is stable per pubkey so a
// person's silhouette never changes; `shape="circle"` opts back into a disc.
const shapeClass =
shape === "circle"
? "rounded-full"
: ["avatar-blob", "avatar-blob-2", "avatar-blob-3"][getBlobVariant(pubkey) - 1]
const color = getColor(pubkey)
const px = $derived(size * 4)
const initial = $derived([...($display || "")].find(c => c.trim()) || "?")
</script> </script>
{#if $profile?.picture} <ImageIcon
<ImageIcon {size} alt="" {style} class={cx(props.class, shapeClass)} src={$profile.picture} /> {size}
{:else} alt=""
<!-- Fallback: a subtle gradient derived from the pubkey + the person's initial. --> class={cx(props.class, "rounded-full")}
<div src={$profile?.picture() || UserRounded} />
class={cx(
props.class,
shapeClass,
"font-display flex shrink-0 items-center justify-center font-bold text-white uppercase select-none",
)}
style="width:{px}px;height:{px}px;font-size:{px *
0.45}px;background-image:linear-gradient(135deg,{color},color-mix(in oklab,{color},#000 28%));{style}">
{initial}
</div>
{/if}
+11 -13
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import cx from "classnames" import cx from "classnames"
import {getProfile, loadProfile} from "@welshman/app" import {Profiles} from "@welshman/app"
import {app} from "@app/welshman"
import {isMobile} from "@lib/html" import {isMobile} from "@lib/html"
import ProfileCircle from "@app/components/ProfileCircle.svelte" import ProfileCircle from "@app/components/ProfileCircle.svelte"
@@ -14,22 +15,20 @@
const {pubkeys, size = 7, limit, class: className}: Props = $props() const {pubkeys, size = 7, limit, class: className}: Props = $props()
const effectiveLimit = $derived(limit ?? (isMobile ? 7 : 10)) const effectiveLimit = $derived(limit ?? (isMobile ? 7 : 10))
// circle is one step smaller than box so the bg-base-100 wrapper reads as a
// thin separating ring between overlapping avatars (Discord-style stack).
const dimensions = $derived( const dimensions = $derived(
size <= 5 size <= 5
? {box: "h-5 w-5", circle: 4, overlap: "-mr-2", overflow: "text-[9px]"} ? {box: "h-5 w-5", overlap: "-mr-2", overflow: "text-[9px]"}
: size <= 6 : size <= 6
? {box: "h-6 w-6", circle: 5, overlap: "-mr-2.5", overflow: "text-[10px]"} ? {box: "h-6 w-6", overlap: "-mr-2.5", overflow: "text-[10px]"}
: {box: "h-8 w-8", circle: 7, overlap: "-mr-3", overflow: "text-xs"}, : {box: "h-8 w-8", overlap: "-mr-3", overflow: "text-xs"},
) )
for (const pubkey of pubkeys) { for (const pubkey of pubkeys) {
loadProfile(pubkey) app.use(Profiles).load(pubkey)
} }
const visiblePubkeys = $derived.by(() => { const visiblePubkeys = $derived.by(() => {
const filtered = pubkeys.filter(pubkey => getProfile(pubkey)?.picture) const filtered = pubkeys.filter(pubkey => app.use(Profiles).get(pubkey)?.picture())
return filtered.length > 0 ? filtered : pubkeys.slice(0, 1) return filtered.length > 0 ? filtered : pubkeys.slice(0, 1)
}) })
@@ -39,21 +38,20 @@
</script> </script>
<div class={cx("flex", size <= 5 ? "pr-2" : "pr-3", className)}> <div class={cx("flex", size <= 5 ? "pr-2" : "pr-3", className)}>
{#each displayPubkeys as pubkey, i (pubkey)} {#each displayPubkeys as pubkey (pubkey)}
<div <div
class={cx( class={cx(
"z-feature inline-flex items-center justify-center rounded-full bg-base-100 transition-transform", "z-feature inline-block flex items-center justify-center rounded-full bg-base-100",
dimensions.box, dimensions.box,
dimensions.overlap, dimensions.overlap,
i % 2 === 0 ? "rotate-2" : "-rotate-2",
)}> )}>
<ProfileCircle class="bg-base-300" shape="circle" {pubkey} size={dimensions.circle} /> <ProfileCircle class={cx(dimensions.box, "bg-base-300")} {pubkey} {size} />
</div> </div>
{/each} {/each}
{#if overflowCount > 0} {#if overflowCount > 0}
<div <div
class={cx( class={cx(
"z-feature bg-primary text-primary-content shadow-soft font-display inline-flex rotate-2 items-center justify-center rounded-full font-bold", "z-feature inline-flex items-center justify-center rounded-full bg-neutral font-medium text-neutral-content",
dimensions.box, dimensions.box,
dimensions.overlap, dimensions.overlap,
dimensions.overflow, dimensions.overflow,
+9 -15
View File
@@ -1,15 +1,9 @@
<script lang="ts"> <script lang="ts">
import {chunk, sleep, uniq} from "@welshman/lib" import {chunk, sleep, uniq} from "@welshman/lib"
import { import {makeEvent, DELETE, isReplaceable, getAddress} from "@welshman/util"
makeEvent, import {ProfileBuilder} from "@welshman/domain"
createProfile, import {Thunks, RelayLists} from "@welshman/app"
PROFILE, import {pubkey, repository, app} from "@app/welshman"
DELETE,
isReplaceable,
getAddress,
RelayMode,
} from "@welshman/util"
import {pubkey, publishThunk, repository, derivePubkeyRelays} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
@@ -31,7 +25,7 @@
let confirmText = $state("") let confirmText = $state("")
const CONFIRM_TEXT = "permanently delete my nostr account" const CONFIRM_TEXT = "permanently delete my nostr account"
const userWriteRelays = derivePubkeyRelays($pubkey!, RelayMode.Write) const userWriteRelays = app.use(RelayLists).writeUrls($pubkey!).$
const confirmOk = $derived(confirmText.toLowerCase().trim() === CONFIRM_TEXT) const confirmOk = $derived(confirmText.toLowerCase().trim() === CONFIRM_TEXT)
const showProgress = $derived(progress !== undefined) const showProgress = $derived(progress !== undefined)
@@ -44,7 +38,7 @@
} }
const chunks = chunk(500, repository.query([{authors: [$pubkey!]}])) 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 vanishEvent = makeEvent(62, {tags: [["relay", "ALL_RELAYS"]]})
const denominator = chunks.length + 2 const denominator = chunks.length + 2
const relays = uniq([...INDEXER_RELAYS, ...$userWriteRelays, ...$userSpaceUrls]) const relays = uniq([...INDEXER_RELAYS, ...$userWriteRelays, ...$userSpaceUrls])
@@ -58,12 +52,12 @@
} }
// First, blank out their profile in case relays don't support deletion by address // First, blank out their profile in case relays don't support deletion by address
await publishThunk({relays, event: profileEvent}) await app.use(Thunks).publish({relays, event: profileEvent})
await incrementProgress() await incrementProgress()
// Next, send a "right to vanish" event to all relays // Next, send a "right to vanish" event to all relays
await publishThunk({relays, event: vanishEvent}) await app.use(Thunks).publish({relays, event: vanishEvent})
await incrementProgress() 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() await incrementProgress()
} }
+6 -10
View File
@@ -2,12 +2,8 @@
import {onMount} from "svelte" import {onMount} from "svelte"
import {removeUndefined} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {ManagementMethod} from "@welshman/util" import {ManagementMethod} from "@welshman/util"
import { import {RelayManagement, Profiles, MessagingRelayLists} from "@welshman/app"
manageRelay, import {app} from "@app/welshman"
deriveProfile,
displayProfileByPubkey,
loadMessagingRelayList,
} from "@welshman/app"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl" import Code2 from "@assets/icons/code-2.svg?dataurl"
import Letter from "@assets/icons/letter-opened.svg?dataurl" import Letter from "@assets/icons/letter-opened.svg?dataurl"
@@ -41,7 +37,7 @@
const {pubkey, url}: Props = $props() const {pubkey, url}: Props = $props()
const profile = deriveProfile(pubkey, removeUndefined([url])) const profile = app.use(Profiles).one(pubkey, removeUndefined([url]))
const userIsAdmin = deriveUserIsSpaceAdmin(url) const userIsAdmin = deriveUserIsSpaceAdmin(url)
@@ -66,9 +62,9 @@
const banMember = () => const banMember = () =>
pushModal(Confirm, { pushModal(Confirm, {
title: "Ban User", 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 () => { confirm: async () => {
const {error} = await manageRelay(url!, { const {error} = await app.use(RelayManagement).post(url!, {
method: ManagementMethod.BanPubkey, method: ManagementMethod.BanPubkey,
params: [pubkey], params: [pubkey],
}) })
@@ -96,7 +92,7 @@
let showMenu = $state(false) let showMenu = $state(false)
onMount(() => { onMount(() => {
loadMessagingRelayList(pubkey) app.use(MessagingRelayLists).load(pubkey)
}) })
</script> </script>
+16 -5
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import type {Profile} from "@welshman/util" import {Profile, ProfileBuilder} from "@welshman/domain"
import {makeProfile} from "@welshman/util" import {pubkey, profilesByPubkey} from "@app/welshman"
import {pubkey, profilesByPubkey, waitForThunkError} from "@welshman/app"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import {errorMessage} from "@lib/util" import {errorMessage} from "@lib/util"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -12,16 +11,28 @@
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
import {updateProfile} from "@app/profiles" 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 initialValues = {profile}
const back = () => history.back() const back = () => history.back()
// TODO(welshman-migration): `profile` here is a plain values object (the form binds
// mutable fields), not a Profile Reader. It is typed Profile to match ProfileEditForm's
// Values type and updateProfile's signature, both of which still say Profile; updateProfile
// routes a non-Reader through `new ProfileBuilder().update(profile)`. Confirm the intended
// values vs Reader contract once the shared types settle.
const onsubmit = async ({profile}: {profile: Profile}) => { const onsubmit = async ({profile}: {profile: Profile}) => {
loading = true loading = true
try { try {
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) { if (error) {
pushToast({ pushToast({
+3 -2
View File
@@ -4,7 +4,8 @@
import {feedFromFilter, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds" import {feedFromFilter, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
import {NOTE, getReplyTags} from "@welshman/util" import {NOTE, getReplyTags} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {makeFeedController} from "@welshman/app" import {Feeds} from "@welshman/app"
import {app} from "@app/welshman"
import {createScroller} from "@lib/html" import {createScroller} from "@lib/html"
import {fly} from "@lib/transition" import {fly} from "@lib/transition"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
@@ -19,7 +20,7 @@
let {url, pubkey, events = $bindable([]), hideLoading = false}: Props = $props() let {url, pubkey, events = $bindable([]), hideLoading = false}: Props = $props()
const ctrl = makeFeedController({ const ctrl = app.use(Feeds).makeFeedController({
useWindowing: true, useWindowing: true,
feed: makeIntersectionFeed( feed: makeIntersectionFeed(
makeRelayFeed(url), makeRelayFeed(url),
+4 -3
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import {removeUndefined} from "@welshman/lib" 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" import ContentMinimal from "@app/components/ContentMinimal.svelte"
export type Props = { export type Props = {
@@ -10,9 +11,9 @@
const {pubkey, url}: Props = $props() const {pubkey, url}: Props = $props()
const profile = deriveProfile(pubkey, removeUndefined([url])) const profile = app.use(Profiles).one(pubkey, removeUndefined([url]))
</script> </script>
{#if $profile} {#if $profile}
<ContentMinimal event={{content: $profile.about || "", tags: []}} /> <ContentMinimal event={{content: $profile.about() || "", tags: []}} />
{/if} {/if}
+1 -1
View File
@@ -4,7 +4,7 @@
import type {Writable} from "svelte/store" import type {Writable} from "svelte/store"
import {type Instance} from "tippy.js" import {type Instance} from "tippy.js"
import {append, remove, uniq} from "@welshman/lib" import {append, remove, uniq} from "@welshman/lib"
import {profileSearch} from "@welshman/app" import {profileSearch} from "@app/welshman"
import Suggestions from "@lib/components/Suggestions.svelte" import Suggestions from "@lib/components/Suggestions.svelte"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
+3 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import {removeUndefined} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {deriveProfileDisplay} from "@welshman/app" import {Profiles} from "@welshman/app"
import {app} from "@app/welshman"
type Props = { type Props = {
pubkey: string pubkey: string
@@ -9,7 +10,7 @@
const {pubkey, url}: Props = $props() const {pubkey, url}: Props = $props()
const profileDisplay = deriveProfileDisplay(pubkey, removeUndefined([url])) const profileDisplay = app.use(Profiles).display(pubkey, removeUndefined([url])).$
</script> </script>
{$profileDisplay} {$profileDisplay}
+2 -8
View File
@@ -2,14 +2,8 @@
const {current, total}: {current: number; total: number} = $props() const {current, total}: {current: number; total: number} = $props()
</script> </script>
<div class="flex w-full gap-1.5"> <div class="flex w-full">
{#each Array(total) as _, i} {#each Array(total) as _, i}
<div <div class="h-1 flex-1 transition-colors {i < current ? 'bg-primary' : 'bg-base-300'}"></div>
class="h-2 flex-1 rounded-full transition-colors duration-300 {i < current
? 'bg-primary'
: i === current
? 'bg-primary/40 motion-safe:animate-pulse'
: 'bg-base-300'}">
</div>
{/each} {/each}
</div> </div>
+15 -14
View File
@@ -17,7 +17,8 @@
import type {TrustedEvent, EventContent, Zap} from "@welshman/util" import type {TrustedEvent, EventContent, Zap} from "@welshman/util"
import {deriveArray, deriveEventsById, deriveItemsByKey} from "@welshman/store" import {deriveArray, deriveEventsById, deriveItemsByKey} from "@welshman/store"
import {load} from "@welshman/net" import {load} from "@welshman/net"
import {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 {isMobile, preventDefault, stopPropagation} from "@lib/html"
import Danger from "@assets/icons/danger-triangle.svg?dataurl" import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -64,15 +65,15 @@
repository, repository,
getKey: zap => zap.response.id, getKey: zap => zap.response.id,
filters: [{kinds: [ZAP_RESPONSE], "#e": eventIds}], filters: [{kinds: [ZAP_RESPONSE], "#e": eventIds}],
eventToItem: (response: TrustedEvent) => { eventToItem: async (response: TrustedEvent) => {
const zap = getValidZap(response, event) const zap = await app.use(Zappers).validateZapReceipt(response, event)
if (zap) { if (zap) {
return zap return zap
} }
if (innerEvent) { if (innerEvent) {
return getValidZap(response, innerEvent) return await app.use(Zappers).validateZapReceipt(response, innerEvent)
} }
}, },
}), }),
@@ -150,28 +151,28 @@
{@const amount = fromMsats(sum(zaps.map(zap => zap.invoiceAmount)))} {@const amount = fromMsats(sum(zaps.map(zap => zap.invoiceAmount)))}
{@const pubkeys = uniq(zaps.map(zap => zap.request.pubkey))} {@const pubkeys = uniq(zaps.map(zap => zap.request.pubkey))}
{@const isOwn = $pubkey && pubkeys.includes($pubkey)} {@const isOwn = $pubkey && pubkeys.includes($pubkey)}
{@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))} {@const info = displayList(pubkeys.map(pubkey => app.use(Profiles).display(pubkey).get()))}
{@const tooltip = `${info} zapped`} {@const tooltip = `${info} zapped`}
<button <button
type="button" type="button"
data-tip={tooltip} data-tip={tooltip}
class={cx( class={cx(
reactionClass, reactionClass,
"flex-inline btn btn-xs flex items-center gap-1 rounded-full border text-xs font-normal transition-transform motion-safe:hover:scale-110 motion-safe:active:scale-95", "flex-inline btn btn-outline btn-neutral btn-xs flex items-center gap-1 rounded-full text-xs font-normal bg-alt",
{ {
tooltip: !noTooltip && !isMobile, tooltip: !noTooltip && !isMobile,
"bg-alt border-base-content/15": !isOwn, "border-neutral-content/20": !isOwn,
"border-primary/50 bg-primary/15 text-primary": isOwn, "btn-primary": isOwn,
}, },
)}> )}>
<Reaction event={zaps[0].request} /> <Reaction event={zaps[0].request} />
<span class="font-semibold">{amount}</span> <span>{amount}</span>
</button> </button>
{/each} {/each}
{#each groupedReactions.entries() as [key, events]} {#each groupedReactions.entries() as [key, events]}
{@const pubkeys = events.map(e => e.pubkey)} {@const pubkeys = events.map(e => e.pubkey)}
{@const isOwn = $pubkey && pubkeys.includes($pubkey)} {@const isOwn = $pubkey && pubkeys.includes($pubkey)}
{@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))} {@const info = displayList(pubkeys.map(pubkey => app.use(Profiles).display(pubkey).get()))}
{@const tooltip = `${info} reacted`} {@const tooltip = `${info} reacted`}
{@const onClick = () => onReactionClick(events)} {@const onClick = () => onReactionClick(events)}
<button <button
@@ -179,17 +180,17 @@
data-tip={tooltip} data-tip={tooltip}
class={cx( class={cx(
reactionClass, reactionClass,
"flex-inline btn btn-xs gap-1 rounded-full border font-normal transition-transform motion-safe:hover:scale-110 motion-safe:active:scale-95", "flex-inline btn btn-outline btn-neutral btn-xs gap-1 rounded-full font-normal bg-alt",
{ {
tooltip: !noTooltip && !isMobile, tooltip: !noTooltip && !isMobile,
"bg-alt border-base-content/15": !isOwn, "border-neutral-content/20": !isOwn,
"border-primary/50 bg-primary/15 text-primary": isOwn, "btn-primary": isOwn,
}, },
)} )}
onclick={stopPropagation(preventDefault(onClick))}> onclick={stopPropagation(preventDefault(onClick))}>
<Reaction event={events[0]} /> <Reaction event={events[0]} />
{#if events.length > 1} {#if events.length > 1}
<span class="font-semibold">{events.length}</span> <span>{events.length}</span>
{/if} {/if}
</button> </button>
{/each} {/each}
+2 -8
View File
@@ -10,7 +10,6 @@
import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte" import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte" import ProfileCircle from "@app/components/ProfileCircle.svelte"
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte" import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
import {getColor} from "@app/theme"
import {makeRoomPath, makeSpaceChatPath} from "@app/routes" import {makeRoomPath, makeSpaceChatPath} from "@app/routes"
type Props = { type Props = {
@@ -26,9 +25,7 @@
const onClick = () => goto(h ? makeRoomPath(url, h) : makeSpaceChatPath(url)) const onClick = () => goto(h ? makeRoomPath(url, h) : makeSpaceChatPath(url))
</script> </script>
<Button <Button class="cv card2 bg-alt shadow-md" onclick={onClick}>
class="cv card2 bg-alt shadow-soft block w-full transition-all motion-safe:hover:-translate-y-0.5"
onclick={onClick}>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
<div class="flex items-center gap-2 text-sm"> <div class="flex items-center gap-2 text-sm">
{#if h} {#if h}
@@ -42,10 +39,7 @@
</span> </span>
</div> </div>
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">
<ProfileCircle <ProfileCircle pubkey={event.pubkey} size={10} />
pubkey={event.pubkey}
size={10}
style="box-shadow: 0 0 0 2px {getColor(event.pubkey)}" />
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<NoteContentMinimal {event} /> <NoteContentMinimal {event} />
</div> </div>
+2 -2
View File
@@ -5,7 +5,7 @@
import {tryCatch} from "@welshman/lib" import {tryCatch} from "@welshman/lib"
import {isShareableRelayUrl, isIPAddress, normalizeRelayUrl} from "@welshman/util" import {isShareableRelayUrl, isIPAddress, normalizeRelayUrl} from "@welshman/util"
import type {Thunk} from "@welshman/app" import type {Thunk} from "@welshman/app"
import {waitForThunkError, relaySearch} from "@welshman/app" import {relaySearch} from "@app/welshman"
import {createScroller} from "@lib/html" import {createScroller} from "@lib/html"
import {errorMessage} from "@lib/util" import {errorMessage} from "@lib/util"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
@@ -34,7 +34,7 @@
loading.add(url) loading.add(url)
try { try {
const error = await waitForThunkError(await addRelay(url)) const error = await (await addRelay(url)).waitForError()
if (error) { if (error) {
pushToast({ pushToast({
+3 -2
View File
@@ -1,9 +1,10 @@
<script lang="ts"> <script lang="ts">
import {deriveRelay} from "@welshman/app" import {Relays} from "@welshman/app"
import {app} from "@app/welshman"
const {...props} = $props() const {...props} = $props()
const relay = deriveRelay(props.url) const relay = app.use(Relays).one(props.url)
</script> </script>
{#if $relay?.description} {#if $relay?.description}
+6 -18
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import cx from "classnames" import {Relays} from "@welshman/app"
import {deriveRelay} from "@welshman/app" import {app} from "@app/welshman"
import {getColor} from "@app/theme" import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
import ImageIcon from "@lib/components/ImageIcon.svelte" import ImageIcon from "@lib/components/ImageIcon.svelte"
type Props = { type Props = {
@@ -12,23 +12,11 @@
const {url, size = 7, ...props}: Props = $props() const {url, size = 7, ...props}: Props = $props()
const relay = deriveRelay(url) const relay = app.use(Relays).one(url)
const px = size * 4
const color = getColor(url)
const letter = (url.replace(/^wss?:\/\//, "").replace(/^www\./, "")[0] || "?").toUpperCase()
</script> </script>
{#if $relay?.icon} {#if $relay?.icon}
<ImageIcon {size} alt="" src={$relay.icon} class={cx(props.class, "squircle")} /> <ImageIcon {size} alt="" src={$relay?.icon} class={props.class} />
{:else} {:else}
<!-- Lettered workspace tile (Slack/Discord-style) colored by the relay url. --> <ImageIcon size={size - 2} alt="" src={RemoteControllerMinimalistic} class={props.class} />
<div
class={cx(
props.class,
"squircle font-display flex shrink-0 items-center justify-center font-bold text-white uppercase",
)}
style="width:{px}px;height:{px}px;font-size:{px *
0.42}px;background-image:linear-gradient(135deg,{color},color-mix(in oklab,{color},#000 28%))">
{letter}
</div>
{/if} {/if}
+4 -3
View File
@@ -4,12 +4,13 @@
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import {displayUrl} from "@welshman/lib" import {displayUrl} from "@welshman/lib"
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import {deriveRelay, deriveRelayStats} from "@welshman/app" import {Relays, RelayStats} from "@welshman/app"
import {app} from "@app/welshman"
const {url, children} = $props() const {url, children} = $props()
const relay = deriveRelay(url) const relay = app.use(Relays).one(url)
const relayStats = deriveRelayStats(url) const relayStats = app.use(RelayStats).one(url)
const connections = $derived($relayStats?.open_count || 0) const connections = $derived($relayStats?.open_count || 0)
</script> </script>
+1 -2
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import type {Readable} from "svelte/store" import type {Readable} from "svelte/store"
import {SvelteSet} from "svelte/reactivity" import {SvelteSet} from "svelte/reactivity"
import {waitForThunkError} from "@welshman/app"
import type {Thunk} from "@welshman/app" import type {Thunk} from "@welshman/app"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -37,7 +36,7 @@
loading.add(url) loading.add(url)
try { try {
const error = await waitForThunkError(await removeRelay(url)) const error = await (await removeRelay(url)).waitForError()
if (error) { if (error) {
pushToast({ pushToast({
+3 -2
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import {deriveRelayDisplay} from "@welshman/app" import {Relays} from "@welshman/app"
import {app} from "@app/welshman"
type Props = { type Props = {
url: string url: string
@@ -8,7 +9,7 @@
const {url, ...props}: Props = $props() const {url, ...props}: Props = $props()
const display = $derived(deriveRelayDisplay(url)) const display = $derived(app.use(Relays).display(url).$)
</script> </script>
<span class={props.class}> <span class={props.class}>
+1 -1
View File
@@ -2,7 +2,7 @@
import {REPORT} from "@welshman/util" import {REPORT} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {deriveEventsById} from "@welshman/store" import {deriveEventsById} from "@welshman/store"
import {repository} from "@welshman/app" import {repository} from "@app/welshman"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte" import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
+6 -5
View File
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import {getTag, ManagementMethod} from "@welshman/util" import {getTag, ManagementMethod} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {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 InboxOut from "@assets/icons/inbox-out.svg?dataurl"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl" import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl" import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
@@ -45,7 +46,7 @@
} }
const dismissReport = async () => { const dismissReport = async () => {
const {error} = await manageRelay(url, { const {error} = await app.use(RelayManagement).post(url, {
method: ManagementMethod.BanEvent, method: ManagementMethod.BanEvent,
params: [event.id, "Dismissed by admin"], params: [event.id, "Dismissed by admin"],
}) })
@@ -66,7 +67,7 @@
title: `Remove Content`, title: `Remove Content`,
message: `Are you sure you want to delete this content from the space?`, message: `Are you sure you want to delete this content from the space?`,
confirm: async () => { confirm: async () => {
const {error} = await manageRelay(url, { const {error} = await app.use(RelayManagement).post(url, {
method: ManagementMethod.BanEvent, method: ManagementMethod.BanEvent,
params: [id, reason], params: [id, reason],
}) })
@@ -89,9 +90,9 @@
pushModal(Confirm, { pushModal(Confirm, {
title: "Ban User", 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 () => { confirm: async () => {
const {error} = await manageRelay(url, { const {error} = await app.use(RelayManagement).post(url, {
method: ManagementMethod.BanPubkey, method: ManagementMethod.BanPubkey,
params: [pubkey, reason], params: [pubkey, reason],
}) })
+1 -1
View File
@@ -135,7 +135,7 @@
</div> </div>
<Button <Button
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send" data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
class="center tooltip tooltip-left bg-primary text-primary-content absolute top-[7px] right-[7px] h-11 w-11 min-w-11 scale-90 rounded-full transition-transform motion-safe:hover:scale-100" class="center tooltip tooltip-left absolute right-4 h-10 w-10 min-w-10 rounded-full"
disabled={$uploading} disabled={$uploading}
onclick={submit}> onclick={submit}>
<Icon icon={Plane} /> <Icon icon={Plane} />
+3 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {TrustedEvent} from "@welshman/util" 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 {slide} from "@lib/transition"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -21,7 +22,7 @@
<div <div
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8" class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8"
transition:slide> transition:slide>
<p class="text-xs text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p> <p class="text-xs text-primary">{verb} @{app.use(Profiles).display(event.pubkey).get()}</p>
{#key event.id} {#key event.id}
<NoteContentMinimal trimParent {event} /> <NoteContentMinimal trimParent {event} />
{/key} {/key}
+11 -9
View File
@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import type {RoomMeta} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import {displayRelayUrl, makeRoomMeta} from "@welshman/util"
import type {Thunk} from "@welshman/app" 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 Pen from "@assets/icons/pen.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl" import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import Login3 from "@assets/icons/login-3.svg?dataurl" import Login3 from "@assets/icons/login-3.svg?dataurl"
@@ -71,11 +71,13 @@
const startEdit = () => pushModal(RoomEdit, {url, h}) 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 loading = true
try { try {
const message = await waitForThunkError(f(url, makeRoomMeta({h}))) const message = await (await f(url, {h})).waitForError()
if (message && !message.startsWith("duplicate:")) { if (message && !message.startsWith("duplicate:")) {
pushToast({theme: "error", message}) 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}) const showMembers = () => pushModal(RoomMembers, {url, h})
@@ -109,8 +111,8 @@
message: message:
"This room will no longer be accessible to space members, and all messages posted to it will be deleted.", "This room will no longer be accessible to space members, and all messages posted to it will be deleted.",
confirm: async () => { confirm: async () => {
const thunk = deleteRoom(url, $room) const thunk = await app.use(Rooms).delete(url, $room)
const message = await waitForThunkError(thunk) const message = await thunk.waitForError()
if (message) { if (message) {
repository.removeEvent(thunk.event.id) repository.removeEvent(thunk.event.id)
+58 -6
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import type {RoomMeta} from "@welshman/util" import {Rooms} from "@welshman/app"
import {makeRoomMeta} from "@welshman/util" import {app} from "@app/welshman"
import {waitForThunkError, createRoom, editRoom, joinRoom} from "@welshman/app"
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl" import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
import Hashtag from "@assets/icons/hashtag.svg?dataurl" import Hashtag from "@assets/icons/hashtag.svg?dataurl"
import Volume from "@assets/icons/volume.svg?dataurl" import Volume from "@assets/icons/volume.svg?dataurl"
@@ -19,6 +18,56 @@
import {deriveHasLivekit} from "@app/relays" import {deriveHasLivekit} from "@app/relays"
import {getRoomType, RoomType} from "@app/groups" import {getRoomType, RoomType} from "@app/groups"
// Plain mutable form object (the old @welshman/util `RoomMeta` plain-object
// type, removed in the migration). The new domain `RoomMeta` is an async
// method-accessor Reader, which doesn't fit a `$state` object bound to inputs,
// so we keep a plain object here and let `app.use(Rooms).create/edit/join`
// (which accept a plain object) build the events at submit time.
type RoomMeta = {
h: string
name?: string
about?: string
picture?: string
pictureMeta?: string[]
isClosed?: boolean
isHidden?: boolean
isPrivate?: boolean
isRestricted?: boolean
livekit?: boolean
}
// TODO(welshman-migration): reimplemented inline from the removed
// @welshman/util `generateH`/`makeRoomMeta` (room-id generator). Verify the
// generated id still matches the expected `^[a-z]+[1-9]$` shape and that no
// shared generator should be used instead.
const vowels = "a,e,i,o,u,ay,ey,oy,ou,ia,ea,ough,oo,ee,argh".split(",")
const consonants =
"p,b,t,d,k,g,ch,sh,th,f,v,s,z,l,r,m,n,pl,bl,cl,gl,pr,br,tr,dr,kr,gr,fl,sl,fr,thr,str,sk,sp,st".split(
",",
)
const generateH = () => {
const n = (6 + Math.random() * 2) | 0
const s = [consonants.slice(), vowels.slice()]
if (Math.random() < 0.5) {
s.reverse()
}
return (
Array.from({length: n}, (_, i) =>
s[i % 2].splice((Math.random() * s[i % 2].length) | 0, 1),
).join("") +
(1 + Math.floor(Math.random() * 9))
)
}
const makeRoomMeta = (room: Partial<RoomMeta> = {}): RoomMeta => ({
h: room.h ?? generateH(),
...room,
})
type Props = { type Props = {
url: string url: string
header: Snippet header: Snippet
@@ -58,19 +107,22 @@
room.pictureMeta = result.tags 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")) { if (createMessage && !createMessage.includes("already")) {
return pushToast({theme: "error", message: createMessage}) 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) { if (editMessage) {
return pushToast({theme: "error", message: 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")) { if (joinMessage && !joinMessage.includes("already")) {
return pushToast({theme: "error", message: joinMessage}) return pushToast({theme: "error", message: joinMessage})
+4 -13
View File
@@ -3,7 +3,6 @@
import Volume from "@assets/icons/volume.svg?dataurl" import Volume from "@assets/icons/volume.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import ImageIcon from "@lib/components/ImageIcon.svelte" import ImageIcon from "@lib/components/ImageIcon.svelte"
import {getColor} from "@app/theme"
import {deriveRoom} from "@app/groups" import {deriveRoom} from "@app/groups"
interface Props { interface Props {
@@ -17,26 +16,18 @@
const room = deriveRoom(url, h) const room = deriveRoom(url, h)
const isVoiceRoom = $derived($room.livekit) const isVoiceRoom = $derived($room.livekit)
const px = size * 4
// Voice rooms read warm/orange; text rooms get a per-room identity color.
const color = $derived(isVoiceRoom ? "var(--color-secondary)" : getColor(h))
</script> </script>
{#if isVoiceRoom} {#if isVoiceRoom}
<div class="flex shrink-0 items-center gap-1.5"> <div class="flex shrink-0 items-center gap-1.5">
<Icon size={size + 1} icon={Volume} class="text-secondary" /> <Icon size={size + 1} icon={Volume} />
{#if $room.picture} {#if $room.picture}
<span class="text-base">/</span> <span class="text-base">/</span>
<ImageIcon src={$room.picture} {size} alt="" class="squircle shadow-sm" /> <ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" />
{/if} {/if}
</div> </div>
{:else if $room.picture} {:else if $room.picture}
<ImageIcon src={$room.picture} {size} alt="" class="squircle shadow-sm" /> <ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" />
{:else} {:else}
<!-- Colored room tile with the type glyph in white. --> <Icon icon={fallbackIcon} {size} />
<div
class="squircle flex shrink-0 items-center justify-center text-white"
style="width:{px}px;height:{px}px;background-image:linear-gradient(135deg,{color},color-mix(in oklab,{color},#000 28%))">
<Icon icon={fallbackIcon} size={Math.max(3, size - 1)} />
</div>
{/if} {/if}
+21 -17
View File
@@ -1,16 +1,18 @@
<script lang="ts"> <script lang="ts">
import cx from "classnames" import cx from "classnames"
import {readable} from "svelte/store" import {readable} from "svelte/store"
import {gte, now, displayList, formatTimestampAsTime, formatTimestampAsDate} from "@welshman/lib" import {
hash,
gte,
now,
displayList,
formatTimestampAsTime,
formatTimestampAsDate,
} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import {MESSAGE, COMMENT, getTag} from "@welshman/util" import {MESSAGE, COMMENT, getTag} from "@welshman/util"
import { import {Thunks, Profiles} from "@welshman/app"
thunks, import {thunks, pubkey, app} from "@app/welshman"
pubkey,
mergeThunks,
deriveProfileDisplay,
displayProfileByPubkey,
} from "@welshman/app"
import {isMobile} from "@lib/html" import {isMobile} from "@lib/html"
import Pen from "@assets/icons/pen.svg?dataurl" import Pen from "@assets/icons/pen.svg?dataurl"
import Reply from "@assets/icons/reply-2.svg?dataurl" import Reply from "@assets/icons/reply-2.svg?dataurl"
@@ -28,7 +30,7 @@
import RoomItemMenuButton from "@app/components/RoomItemMenuButton.svelte" import RoomItemMenuButton from "@app/components/RoomItemMenuButton.svelte"
import RoomItemMenuMobile from "@app/components/RoomItemMenuMobile.svelte" import RoomItemMenuMobile from "@app/components/RoomItemMenuMobile.svelte"
import RoomItemContent from "@app/components/RoomItemContent.svelte" import RoomItemContent from "@app/components/RoomItemContent.svelte"
import {getColor} from "@app/theme" import {colors} from "@app/theme"
import {ENABLE_ZAPS} from "@app/env" import {ENABLE_ZAPS} from "@app/env"
import {deriveEventsForUrl, deriveEvent} from "@app/repository" import {deriveEventsForUrl, deriveEvent} from "@app/repository"
import {publishDelete} from "@app/deletes" import {publishDelete} from "@app/deletes"
@@ -51,9 +53,9 @@
const path = getRoomItemPath(url, event) const path = getRoomItemPath(url, event)
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
const today = formatTimestampAsDate(now()) const today = formatTimestampAsDate(now())
const profileDisplay = deriveProfileDisplay(event.pubkey, [url]) const profileDisplay = app.use(Profiles).display(event.pubkey, [url]).$
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id)) const thunk = app.use(Thunks).merge($thunks.filter(t => t.event.id === event.id))
const colorValue = getColor(event.pubkey) const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
const qTag = getTag("q", event.tags) const qTag = getTag("q", event.tags)
const isQuoteOnly = Boolean( const isQuoteOnly = Boolean(
@@ -88,7 +90,10 @@
<div class="flex w-full gap-3 overflow-auto"> <div class="flex w-full gap-3 overflow-auto">
{#if showPubkey} {#if showPubkey}
<Button onclick={openProfile} class="flex items-start pt-1.5 justify-center w-8 shrink-0"> <Button onclick={openProfile} class="flex items-start pt-1.5 justify-center w-8 shrink-0">
<ProfileCircle pubkey={event.pubkey} style="box-shadow: 0 0 0 2px {colorValue}" size={8} /> <ProfileCircle
pubkey={event.pubkey}
class="border border-solid border-base-content"
size={8} />
</Button> </Button>
{:else} {:else}
<div class="w-8 shrink-0"></div> <div class="w-8 shrink-0"></div>
@@ -128,7 +133,7 @@
{#if path && $innerComments.length > 0} {#if path && $innerComments.length > 0}
{@const pubkeys = $innerComments.map(e => e.pubkey)} {@const pubkeys = $innerComments.map(e => e.pubkey)}
{@const isOwn = $pubkey && pubkeys.includes($pubkey)} {@const isOwn = $pubkey && pubkeys.includes($pubkey)}
{@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))} {@const info = displayList(pubkeys.map(pubkey => app.use(Profiles).display(pubkey).get()))}
{@const tooltip = `${info} commented`} {@const tooltip = `${info} commented`}
<div data-tip={tooltip} class="tooltip tooltip-right flex"> <div data-tip={tooltip} class="tooltip tooltip-right flex">
<Link <Link
@@ -145,9 +150,8 @@
</div> </div>
{#if !isMobile} {#if !isMobile}
<button <button
class="join bg-base-100 shadow-soft absolute right-2 top-0.5 translate-y-1 rounded-full p-0.5 text-xs opacity-0 transition-all" class="join absolute right-2 top-0.5 border border-solid border-neutral text-xs opacity-0 transition-all pr-2"
class:group-hover:opacity-100={!isMobile} class:group-hover:opacity-100={!isMobile}>
class:group-hover:translate-y-0={!isMobile}>
{#if ENABLE_ZAPS} {#if ENABLE_ZAPS}
<RoomItemZapButton {url} {event} /> <RoomItemZapButton {url} {event} />
{/if} {/if}
+3 -2
View File
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {ManagementMethod} from "@welshman/util" import {ManagementMethod} from "@welshman/util"
import {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 Code2 from "@assets/icons/code-2.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl" import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import Danger from "@assets/icons/danger.svg?dataurl" import Danger from "@assets/icons/danger.svg?dataurl"
@@ -45,7 +46,7 @@
title: `Delete Message`, title: `Delete Message`,
message: `Are you sure you want to delete this message from the space?`, message: `Are you sure you want to delete this message from the space?`,
confirm: async () => { confirm: async () => {
const {error} = await manageRelay(url, { const {error} = await app.use(RelayManagement).post(url, {
method: ManagementMethod.BanEvent, method: ManagementMethod.BanEvent,
params: [event.id], params: [event.id],
}) })
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {NativeEmoji} from "emoji-picker-element/shared" import type {NativeEmoji} from "emoji-picker-element/shared"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {pubkey} from "@welshman/app" import {pubkey} from "@app/welshman"
import Bolt from "@assets/icons/bolt.svg?dataurl" import Bolt from "@assets/icons/bolt.svg?dataurl"
import Reply from "@assets/icons/reply-2.svg?dataurl" import Reply from "@assets/icons/reply-2.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl" import Code2 from "@assets/icons/code-2.svg?dataurl"
+9 -4
View File
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import {getTagValue, ManagementMethod} from "@welshman/util" import {getTagValue, ManagementMethod} from "@welshman/util"
import type {TrustedEvent, PublishedRoomMeta} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {repository, manageRelay} from "@welshman/app" import {RelayManagement} from "@welshman/app"
import {app, repository} from "@app/welshman"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileName from "@app/components/ProfileName.svelte" import ProfileName from "@app/components/ProfileName.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte" import ProfileDetail from "@app/components/ProfileDetail.svelte"
@@ -28,7 +29,7 @@
loading = true loading = true
try { try {
const {error} = await manageRelay(url, { const {error} = await app.use(RelayManagement).post(url, {
method: ManagementMethod.BanEvent, method: ManagementMethod.BanEvent,
params: [event.id, "Join request dismissed"], params: [event.id, "Join request dismissed"],
}) })
@@ -49,7 +50,11 @@
loading = true loading = true
try { 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) { if (error) {
pushToast({theme: "error", message: error}) pushToast({theme: "error", message: error})
+4 -2
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <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 MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl" import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl" import AddCircle from "@assets/icons/add-circle.svg?dataurl"
@@ -51,7 +52,8 @@
title: "Remove Member", title: "Remove Member",
message: "Are you sure you want to remove this user from the room?", message: "Are you sure you want to remove this user from the room?",
confirm: async () => { confirm: async () => {
const error = await waitForThunkError(removeRoomMember(url, $room, pubkey)) const thunk = await app.use(Rooms).removeMember(url, $room, pubkey)
const error = await thunk.waitForError()
if (error) { if (error) {
pushToast({theme: "error", message: error}) pushToast({theme: "error", message: error})
+8 -4
View File
@@ -2,8 +2,8 @@
import {onMount} from "svelte" import {onMount} from "svelte"
import {setKey, popKey} from "@lib/implicit" import {setKey, popKey} from "@lib/implicit"
import {sleep} from "@welshman/lib" import {sleep} from "@welshman/lib"
import {displayProfileByPubkey} from "@welshman/app" import {Profiles} from "@welshman/app"
import type {PublishedRoomMeta} from "@welshman/util" import {app} from "@app/welshman"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -42,7 +42,11 @@
// Show loading for auto submit callback // Show loading for auto submit callback
await sleep(500) 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) { if (error) {
pushToast({theme: "error", message: error}) pushToast({theme: "error", message: error})
@@ -72,7 +76,7 @@
subtitle: "Automatically add members to space", subtitle: "Automatically add members to space",
message: message:
nonSpaceMembers.length === 1 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?`, : `${nonSpaceMembers.length} people are not members of this space. Add them?`,
confirm: async () => { confirm: async () => {
setKey("RoomMembersAdd.confirm", true) setKey("RoomMembersAdd.confirm", true)
+13 -13
View File
@@ -1,8 +1,10 @@
<script lang="ts"> <script lang="ts">
import type {ClientOptions} from "@pomade/core" import type {ClientOptions} from "@pomade/core"
import type {Profile} from "@welshman/util" import type {Profile} from "@welshman/domain"
import {makeProfile, makeSecret, RELAYS, MESSAGING_RELAYS, makeEvent} from "@welshman/util" import {makeSecret, RELAYS, MESSAGING_RELAYS, makeEvent} from "@welshman/util"
import {loginWithNip01, publishThunk} from "@welshman/app" 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 Key from "@assets/icons/key-minimalistic.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl" import Letter from "@assets/icons/letter.svg?dataurl"
import {getKey, setKey} from "@lib/implicit" import {getKey, setKey} from "@lib/implicit"
@@ -31,7 +33,7 @@
setKey("signup.email", "") setKey("signup.email", "")
setKey("signup.secret", makeSecret()) setKey("signup.secret", makeSecret())
setKey("signup.profile", makeProfile()) setKey("signup.profile", new ProfileBuilder().values)
setKey("signup.clientOptions", undefined) setKey("signup.clientOptions", undefined)
const hasPomade = POMADE_SIGNERS.length >= 3 const hasPomade = POMADE_SIGNERS.length >= 3
@@ -40,13 +42,13 @@
const completeSignup = () => { const completeSignup = () => {
// Add default outbox/inbox relays // Add default outbox/inbox relays
publishThunk({ app.use(Thunks).publish({
event: makeEvent(RELAYS, {tags: DEFAULT_RELAYS.map(url => ["r", url])}), event: makeEvent(RELAYS, {tags: DEFAULT_RELAYS.map(url => ["r", url])}),
relays: [...INDEXER_RELAYS, ...DEFAULT_RELAYS], relays: [...INDEXER_RELAYS, ...DEFAULT_RELAYS],
}) })
// Add default messaging relays // Add default messaging relays
publishThunk({ app.use(Thunks).publish({
event: makeEvent(MESSAGING_RELAYS, {tags: DEFAULT_MESSAGING_RELAYS.map(url => ["r", url])}), event: makeEvent(MESSAGING_RELAYS, {tags: DEFAULT_MESSAGING_RELAYS.map(url => ["r", url])}),
relays: DEFAULT_RELAYS, relays: DEFAULT_RELAYS,
}) })
@@ -83,10 +85,10 @@
key: () => pushModal(SignUpKey, {next: flows.nostr.complete, step: 2, totalSteps: 3}), key: () => pushModal(SignUpKey, {next: flows.nostr.complete, step: 2, totalSteps: 3}),
complete: () => complete: () =>
pushModal(SignUpComplete, {next: flows.nostr.finalize, step: 3, totalSteps: 3}), pushModal(SignUpComplete, {next: flows.nostr.finalize, step: 3, totalSteps: 3}),
finalize: () => { finalize: async () => {
const secret = getKey<string>("signup.secret")! const secret = getKey<string>("signup.secret")!
loginWithNip01(secret) await loginWithNip01(secret)
completeSignup() completeSignup()
}, },
}, },
@@ -95,19 +97,17 @@
<Modal> <Modal>
<ModalBody> <ModalBody>
<h1 class="heading">Join <span class="brand">{PLATFORM_NAME}</span></h1> <h1 class="heading">Join {PLATFORM_NAME}</h1>
<p class="m-auto max-w-sm text-center"> <p class="m-auto max-w-sm text-center">
Censorship resistant digital spaces for communities. Meet new people, own your identity. Censorship resistant digital spaces for communities. Meet new people, own your identity.
</p> </p>
{#if hasPomade} {#if hasPomade}
<Button onclick={flows.email.start} class="btn btn-primary rounded-full"> <Button onclick={flows.email.start} class="btn btn-primary">
<Icon icon={Letter} /> <Icon icon={Letter} />
Sign up with email Sign up with email
</Button> </Button>
{/if} {/if}
<Button <Button onclick={flows.nostr.start} class="btn {hasPomade ? 'btn-neutral' : 'btn-primary'}">
onclick={flows.nostr.start}
class="btn rounded-full {hasPomade ? 'btn-neutral' : 'btn-primary'}">
<Icon icon={Key} /> <Icon icon={Key} />
Generate a key Generate a key
</Button> </Button>
+5 -7
View File
@@ -2,11 +2,12 @@
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import HomeSmile from "@assets/icons/home-smile.svg?dataurl" import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import Modal from "@lib/components/Modal.svelte" import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte" import ModalBody from "@lib/components/ModalBody.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import ProgressBar from "@app/components/ProgressBar.svelte" import ProgressBar from "@app/components/ProgressBar.svelte"
@@ -23,12 +24,9 @@
<Modal tag="form" onsubmit={preventDefault(next)}> <Modal tag="form" onsubmit={preventDefault(next)}>
<ModalBody> <ModalBody>
<div class="flex flex-col items-center gap-3"> <ModalHeader>
<div class="center bg-primary/15 text-primary size-16 rounded-full motion-safe:animate-pop"> <ModalTitle>You're all set!</ModalTitle>
<Icon icon={CheckCircle} size={9} /> </ModalHeader>
</div>
<h1 class="heading">You're all set!</h1>
</div>
<p> <p>
You've created your profile, saved your keys, and now you're ready to start chatting — all You've created your profile, saved your keys, and now you're ready to start chatting — all
without asking permission! without asking permission!
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {spec, avg} from "@welshman/lib" import {spec, avg} from "@welshman/lib"
import {session, SessionMethod, signerLog} from "@welshman/app" import {session, SessionMethod, signerLog} from "@app/welshman"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Danger from "@assets/icons/danger-triangle.svg?dataurl" import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl" import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
+10 -8
View File
@@ -29,20 +29,22 @@
<Button onclick={back} class="place-self-start pr-3 md:hidden"> <Button onclick={back} class="place-self-start pr-3 md:hidden">
<Icon icon={ArrowLeft} size={7} /> <Icon icon={ArrowLeft} size={7} />
</Button> </Button>
<div class="ellipsize whitespace-nowrap flex grow items-center justify-between gap-4"> <div class="flex grow items-center justify-between gap-4">
<div class="flex flex-col"> <div class="flex min-w-0 flex-col">
<div class="flex gap-2 items-center"> <div class="flex min-w-0 items-start gap-2">
<RelayIcon {url} size={5} class="rounded-full md:hidden" /> <RelayIcon {url} size={5} class="shrink-0 rounded-full md:hidden" />
<div class="hidden md:contents"> <div class="hidden shrink-0 md:flex md:items-center">
{@render leading?.()} {@render leading?.()}
</div> </div>
{@render title?.()} <div class="min-w-0">
{@render title?.()}
</div>
</div> </div>
<div class="text-xs text-primary md:hidden"> <div class="text-xs text-primary pl-7 md:hidden">
{displayRelayUrl(url)} {displayRelayUrl(url)}
</div> </div>
</div> </div>
<div class="flex gap-2 items-start"> <div class="flex shrink-0 items-center gap-2">
{@render action?.()} {@render action?.()}
</div> </div>
</div> </div>
+3 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import {displayRelayUrl} from "@welshman/util" 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 UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Pen from "@assets/icons/pen.svg?dataurl" import Pen from "@assets/icons/pen.svg?dataurl"
@@ -26,7 +27,7 @@
} }
const {url}: Props = $props() const {url}: Props = $props()
const relay = deriveRelay(url) const relay = app.use(Relays).one(url)
const owner = $derived($relay?.pubkey) const owner = $derived($relay?.pubkey)
const userIsAdmin = deriveUserIsSpaceAdmin(url) const userIsAdmin = deriveUserIsSpaceAdmin(url)
+6 -5
View File
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import type {RelayProfile} from "@welshman/util" import type {RelayProfile} from "@welshman/util"
import {displayRelayUrl, ManagementMethod} from "@welshman/util" import {displayRelayUrl, ManagementMethod} from "@welshman/util"
import {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 StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
import Widget from "@assets/icons/widget-4.svg?dataurl" import Widget from "@assets/icons/widget-4.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -36,7 +37,7 @@
const submit = async () => { const submit = async () => {
if (values.name != initialValues.name) { if (values.name != initialValues.name) {
const res = await manageRelay(url, { const res = await app.use(RelayManagement).post(url, {
method: ManagementMethod.ChangeRelayName, method: ManagementMethod.ChangeRelayName,
params: [values.name || ""], params: [values.name || ""],
}) })
@@ -47,7 +48,7 @@
} }
if (values.description != initialValues.description) { if (values.description != initialValues.description) {
const res = await manageRelay(url, { const res = await app.use(RelayManagement).post(url, {
method: ManagementMethod.ChangeRelayDescription, method: ManagementMethod.ChangeRelayDescription,
params: [values.description || ""], params: [values.description || ""],
}) })
@@ -64,7 +65,7 @@
return pushToast({theme: "error", message: error}) return pushToast({theme: "error", message: error})
} }
const res = await manageRelay(url, { const res = await app.use(RelayManagement).post(url, {
method: ManagementMethod.ChangeRelayIcon, method: ManagementMethod.ChangeRelayIcon,
params: [result.url], params: [result.url],
}) })
@@ -75,7 +76,7 @@
} }
pushToast({message: "Your changes have been saved!"}) pushToast({message: "Your changes have been saved!"})
forceLoadRelay(url) app.use(Relays).forceLoad(url)
clearModals() clearModals()
} }
+2 -3
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import {debounce} from "throttle-debounce" import {debounce} from "throttle-debounce"
import {dissoc, maybe} from "@welshman/lib" import {dissoc, maybe} from "@welshman/lib"
import {goto} from "$app/navigation"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import {slideAndFade} from "@lib/transition" import {slideAndFade} from "@lib/transition"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
@@ -22,7 +21,7 @@
import RelaySummary from "@app/components/RelaySummary.svelte" import RelaySummary from "@app/components/RelaySummary.svelte"
import SpaceJoinSettings from "@app/components/SpaceJoinSettings.svelte" import SpaceJoinSettings from "@app/components/SpaceJoinSettings.svelte"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
import {makeSpacePath} from "@app/routes" import {goToSpace} from "@app/routes"
import {relaysMostlyRestricted} from "@app/policies" import {relaysMostlyRestricted} from "@app/policies"
import {notificationSettings, setSpaceNotifications} from "@app/settings" import {notificationSettings, setSpaceNotifications} from "@app/settings"
import {parseInviteLink} from "@app/invites" import {parseInviteLink} from "@app/invites"
@@ -68,7 +67,7 @@
} }
await addSpace(url) await addSpace(url)
await goto(makeSpacePath(url), {replaceState: true}) await goToSpace(url)
broadcastUserData([url]) broadcastUserData([url])
relaysMostlyRestricted.update(dissoc(url)) relaysMostlyRestricted.update(dissoc(url))
+2 -3
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {goto} from "$app/navigation"
import {dissoc, maybe} from "@welshman/lib" import {dissoc, maybe} from "@welshman/lib"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -22,7 +21,7 @@
import {notificationSettings} from "@app/settings" import {notificationSettings} from "@app/settings"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
import {makeSpacePath} from "@app/routes" import {goToSpace} from "@app/routes"
import {Push} from "@app/push" import {Push} from "@app/push"
type Props = { type Props = {
@@ -56,7 +55,7 @@
} }
await addSpace(url) await addSpace(url)
await goto(makeSpacePath(url), {replaceState: true}) await goToSpace(url)
broadcastUserData([url]) broadcastUserData([url])
relaysMostlyRestricted.update(dissoc(url)) relaysMostlyRestricted.update(dissoc(url))
+6 -5
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import {ManagementMethod} from "@welshman/util" 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 MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import UserMinus from "@assets/icons/user-minus.svg?dataurl" import UserMinus from "@assets/icons/user-minus.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl" import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
@@ -60,9 +61,9 @@
const unallowMember = (pubkey: string) => const unallowMember = (pubkey: string) =>
pushModal(Confirm, { pushModal(Confirm, {
title: "Remove User", title: "Remove User",
message: `Are you sure you want to remove @${displayProfileByPubkey(pubkey)} from the space?`, message: `Are you sure you want to remove @${app.use(Profiles).display(pubkey).get()} from the space?`,
confirm: async () => { confirm: async () => {
const {error} = await manageRelay(url, { const {error} = await app.use(RelayManagement).post(url, {
method: ManagementMethod.UnallowPubkey, method: ManagementMethod.UnallowPubkey,
params: [pubkey], params: [pubkey],
}) })
@@ -79,9 +80,9 @@
const banMember = (pubkey: string) => const banMember = (pubkey: string) =>
pushModal(Confirm, { pushModal(Confirm, {
title: "Ban User", title: "Ban User",
message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`, message: `Are you sure you want to ban @${app.use(Profiles).display(pubkey).get()} from the space?`,
confirm: async () => { confirm: async () => {
const {error} = await manageRelay(url, { const {error} = await app.use(RelayManagement).post(url, {
method: ManagementMethod.BanPubkey, method: ManagementMethod.BanPubkey,
params: [pubkey], params: [pubkey],
}) })
+3 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import {displayRelayUrl, ManagementMethod} from "@welshman/util" 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 MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import Restart from "@assets/icons/restart.svg?dataurl" import Restart from "@assets/icons/restart.svg?dataurl"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
@@ -42,7 +43,7 @@
} }
const unbanMember = async (pubkey: string) => { const unbanMember = async (pubkey: string) => {
const {error} = await manageRelay(url, { const {error} = await app.use(RelayManagement).post(url, {
method: ManagementMethod.UnbanPubkey, method: ManagementMethod.UnbanPubkey,
params: [pubkey], params: [pubkey],
}) })
+5 -4
View File
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import {derived} from "svelte/store" import {derived} from "svelte/store"
import {displayRelayUrl, EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, POLL} from "@welshman/util" import {displayRelayUrl, EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, POLL} from "@welshman/util"
import {deriveRelay, createSearch, pubkey} from "@welshman/app" import {Relays} from "@welshman/app"
import {app, createSearch, pubkey} from "@app/welshman"
import {fly} from "@lib/transition" import {fly} from "@lib/transition"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl" import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
@@ -60,7 +61,7 @@
const {url} = $props() const {url} = $props()
const relay = deriveRelay(url) const relay = app.use(Relays).one(url)
const chatPath = makeSpacePath(url, "chat") const chatPath = makeSpacePath(url, "chat")
const goalsPath = makeSpacePath(url, "goals") const goalsPath = makeSpacePath(url, "goals")
const threadsPath = makeSpacePath(url, "threads") const threadsPath = makeSpacePath(url, "threads")
@@ -139,7 +140,7 @@
class="relative flex w-full flex-col rounded-xl p-3 transition-all hover:bg-base-100" class="relative flex w-full flex-col rounded-xl p-3 transition-all hover:bg-base-100"
onclick={openMenu}> onclick={openMenu}>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<strong class="font-display relative flex items-center gap-1"> <strong class="flex items-center gap-1 relative">
<RelayName {url} class="ellipsize" /> <RelayName {url} class="ellipsize" />
<div <div
class="absolute -right-3 top-0 h-2 w-2 rounded-full bg-primary transition-all opacity-0" class="absolute -right-3 top-0 h-2 w-2 rounded-full bg-primary transition-all opacity-0"
@@ -311,7 +312,7 @@
<div <div
class="flex shrink-0 flex-col gap-2 p-2 pt-0 -mt-4 pb-[calc(var(--saib)+0.25rem)] md:pb-2 z-nav"> class="flex shrink-0 flex-col gap-2 p-2 pt-0 -mt-4 pb-[calc(var(--saib)+0.25rem)] md:pb-2 z-nav">
<VoiceWidget /> <VoiceWidget />
<Button class="btn btn-ghost btn-sm bg-base-100 h-10 rounded-full" onclick={showDetail}> <Button class="btn btn-neutral btn-sm h-10" onclick={showDetail}>
<SocketStatusIndicator {url} /> <SocketStatusIndicator {url} />
</Button> </Button>
</div> </div>
+3 -2
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <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 Server from "@assets/icons/server.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import ProfileLink from "@app/components/ProfileLink.svelte" import ProfileLink from "@app/components/ProfileLink.svelte"
@@ -11,7 +12,7 @@
const {url}: Props = $props() const {url}: Props = $props()
const relay = deriveRelay(url) const relay = app.use(Relays).one(url)
</script> </script>
<div class="card2 bg-alt flex flex-col gap-4"> <div class="card2 bg-alt flex flex-col gap-4">
+1 -1
View File
@@ -2,7 +2,7 @@
import {tick} from "svelte" import {tick} from "svelte"
import {debounce} from "throttle-debounce" import {debounce} from "throttle-debounce"
import {request} from "@welshman/net" import {request} from "@welshman/net"
import {repository, tracker} from "@welshman/app" import {repository, tracker} from "@app/welshman"
import {formatTimestampAsDate, groupBy, uniqBy, now, MINUTE, HOUR, DAY, WEEK} from "@welshman/lib" import {formatTimestampAsDate, groupBy, uniqBy, now, MINUTE, HOUR, DAY, WEEK} from "@welshman/lib"
import type {TrustedEvent, Filter} from "@welshman/util" import type {TrustedEvent, Filter} from "@welshman/util"
import {MESSAGE, sortEventsDesc} from "@welshman/util" import {MESSAGE, sortEventsDesc} from "@welshman/util"
+40
View File
@@ -0,0 +1,40 @@
<script lang="ts">
import type {TrustedEvent} from "@welshman/util"
import RoomName from "@app/components/RoomName.svelte"
import ThreadBoardItem from "@app/components/ThreadBoardItem.svelte"
type Props = {
url: string
h: string
threads: TrustedEvent[]
}
const {url, h, threads}: Props = $props()
</script>
<section class="overflow-hidden rounded-box border border-base-content/15 bg-base-100 shadow-sm">
<header
class="flex items-center justify-between gap-2 border-b border-base-content/15 bg-base-200/70 px-4 py-2.5">
<h2 class="text-sm font-bold sm:text-base">
{#if h}
#<RoomName {url} {h} />
{:else}
General
{/if}
</h2>
<span class="text-xs opacity-60">
{threads.length}
{threads.length === 1 ? "topic" : "topics"}
</span>
</header>
<div
class="hidden border-b border-base-content/10 bg-base-200/40 px-4 py-2 text-xs font-bold uppercase tracking-wide opacity-60 sm:grid sm:grid-cols-[1fr_8rem_5rem_8rem] sm:gap-x-4">
<span>Topic</span>
<span>Author</span>
<span class="text-center">Replies</span>
<span class="text-right">Last post</span>
</div>
{#each threads as event (event.id)}
<ThreadBoardItem {url} {event} />
{/each}
</section>
+48
View File
@@ -0,0 +1,48 @@
<script lang="ts">
import {formatTimestamp, max} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {COMMENT, getTagValue} from "@welshman/util"
import Link from "@lib/components/Link.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ProfileName from "@app/components/ProfileName.svelte"
import {deriveEventsForUrl} from "@app/repository"
import {makeThreadPath} from "@app/routes"
type Props = {
url: string
event: TrustedEvent
}
const {url, event}: Props = $props()
const filters = [{kinds: [COMMENT], "#E": [event.id]}]
const replies = deriveEventsForUrl(url, filters)
const replyCount = $derived($replies.length)
const lastActive = $derived(max([...$replies, event].map(e => e.created_at)))
const title = getTagValue("title", event.tags)
</script>
<Link
href={makeThreadPath(url, event.id)}
class="grid grid-cols-[1fr_auto] gap-x-3 gap-y-1 border-b border-base-content/10 px-3 py-3 transition-colors hover:bg-base-200/50 sm:grid-cols-[1fr_8rem_5rem_8rem] sm:items-center sm:gap-x-4 sm:px-4">
<div class="col-span-2 min-w-0 sm:col-span-1">
<p class="ellipsize text-sm font-bold sm:text-base">{title || "Untitled thread"}</p>
<p class="ellipsize mt-0.5 text-xs opacity-60 sm:hidden">
by <ProfileName pubkey={event.pubkey} {url} />
</p>
</div>
<div class="hidden items-center gap-2 sm:flex">
<ProfileCircle pubkey={event.pubkey} {url} size={6} />
<span class="ellipsize text-sm">
<ProfileName pubkey={event.pubkey} {url} />
</span>
</div>
<p class="text-right text-xs opacity-75 sm:text-center sm:text-sm">
<span class="opacity-60 sm:hidden">Replies · </span>
{replyCount}
</p>
<p class="text-right text-xs opacity-75 sm:text-sm">
<span class="opacity-60 sm:hidden">Last · </span>
{formatTimestamp(lastActive)}
</p>
</Link>
+4 -3
View File
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import {writable} from "svelte/store" import {writable} from "svelte/store"
import {makeEvent, THREAD} from "@welshman/util" import {makeEvent, THREAD} from "@welshman/util"
import {publishThunk, waitForThunkError} from "@welshman/app" import {Thunks} from "@welshman/app"
import {app} from "@app/welshman"
import {isMobile, preventDefault} from "@lib/html" import {isMobile, preventDefault} from "@lib/html"
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl" import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -79,12 +80,12 @@
tags.push(["h", h]) tags.push(["h", h])
} }
const threadThunk = publishThunk({ const threadThunk = app.use(Thunks).publish({
relays: [url], relays: [url],
event: makeEvent(THREAD, {content, tags}), event: makeEvent(THREAD, {content, tags}),
}) })
const error = await waitForThunkError(threadThunk) const error = await threadThunk.waitForError()
if (error) { if (error) {
return pushToast({theme: "error", message: error}) return pushToast({theme: "error", message: error})

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