Compare commits

..

1 Commits

Author SHA1 Message Date
Jon Staab 0e41680fff AI pass on redesign 2026-06-15 10:39:01 -07:00
194 changed files with 1376 additions and 2083 deletions
-1
View File
@@ -74,7 +74,6 @@
"@vite-pwa/sveltekit": "^1.1.0", "@vite-pwa/sveltekit": "^1.1.0",
"@welshman/app": "^0.8.16", "@welshman/app": "^0.8.16",
"@welshman/content": "^0.8.16", "@welshman/content": "^0.8.16",
"@welshman/domain": "^0.8.16",
"@welshman/editor": "^0.8.16", "@welshman/editor": "^0.8.16",
"@welshman/feeds": "^0.8.16", "@welshman/feeds": "^0.8.16",
"@welshman/lib": "^0.8.16", "@welshman/lib": "^0.8.16",
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

+203 -12
View File
@@ -2,10 +2,15 @@
@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: Lato; font-family: var(--font-sans);
--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));
@@ -153,15 +158,15 @@
} }
@utility heading { @utility heading {
@apply text-center text-2xl; @apply font-display text-center text-2xl font-bold tracking-tight;
} }
@utility subheading { @utility brand {
@apply text-center text-xl; @apply font-display text-primary font-bold tracking-tight;
} }
@utility superheading { @utility label {
@apply text-center text-4xl; @apply font-display text-sm font-semibold tracking-wider uppercase opacity-70;
} }
@utility link { @utility link {
@@ -215,8 +220,19 @@
@font-face { @font-face {
font-family: "Lato"; font-family: "Lato";
font-style: bold; font-style: normal;
font-weight: 600; font-weight: 300;
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");
@@ -228,13 +244,38 @@
font-weight: 400; font-weight: 400;
src: src:
local(""), local(""),
url("/fonts/Italic.ttf") format("truetype"); url("/fonts/Lato-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: Lato; font-family: var(--font-sans);
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));
@@ -284,7 +325,7 @@
opacity: 0.5; opacity: 0.5;
} }
/* tiptap */ /* editors */
.input-editor, .input-editor,
.chat-editor, .chat-editor,
@@ -323,7 +364,11 @@
} }
.chat-editor .tiptap { .chat-editor .tiptap {
@apply rounded-box bg-base-300 pr-12; @apply bg-base-300 rounded-[1.5rem] pr-12 transition-shadow;
}
.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 {
@@ -448,3 +493,149 @@ 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 "@app/welshman" import {signer} from "@welshman/app"
import {load} from "@welshman/net" import {load} from "@welshman/net"
import {getLivekitEndpoint} from "$lib/livekit" import {getLivekitEndpoint} from "$lib/livekit"
import {AbortError, TimeoutError, whenAborted, whenTimeout} from "$lib/util" import {AbortError, TimeoutError, whenAborted, whenTimeout} from "$lib/util"
+1 -4
View File
@@ -2,8 +2,7 @@ import {DELETE, PROFILE, getPubkeyTagValues} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {append, call, on, reject, remove, sort, sortBy, spec, uniq, uniqBy} from "@welshman/lib" import {append, call, on, reject, remove, sort, sortBy, spec, uniq, uniqBy} from "@welshman/lib"
import type {Override} from "@welshman/lib" import type {Override} from "@welshman/lib"
import {Profiles} from "@welshman/app" import {createSearch, displayProfileByPubkey, pubkey, repository} from "@welshman/app"
import {app, createSearch, pubkey, repository} from "@app/welshman"
import {derived, readable} from "svelte/store" import {derived, readable} from "svelte/store"
import {DM_KINDS} from "@app/content" import {DM_KINDS} from "@app/content"
import type {RepositoryUpdate} from "@welshman/net" import type {RepositoryUpdate} from "@welshman/net"
@@ -36,8 +35,6 @@ export const chatsById = call(() => {
const chatsByPubkey = new Map<string, string[]>() const chatsByPubkey = new Map<string, string[]>()
const addSearchText = (chat: Override<Chat, {search_text?: string}>) => { const addSearchText = (chat: Override<Chat, {search_text?: string}>) => {
const displayProfileByPubkey = (pk: string) => app.use(Profiles).display(pk).get()
chat.search_text = chat.search_text =
chat.pubkeys.length === 1 chat.pubkeys.length === 1
? displayProfileByPubkey(chat.pubkeys[0]) + " note to self" ? displayProfileByPubkey(chat.pubkeys[0]) + " note to self"
+3 -4
View File
@@ -1,7 +1,6 @@
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {COMMENT, makeEvent} from "@welshman/util" import {COMMENT, makeEvent} from "@welshman/util"
import {Thunks, Tags} from "@welshman/app" import {publishThunk, tagEventForComment} from "@welshman/app"
import {app} from "@app/welshman"
export type CommentParams = { export type CommentParams = {
event: TrustedEvent event: TrustedEvent
@@ -11,7 +10,7 @@ export type CommentParams = {
} }
export const makeComment = ({url, event, content, tags = []}: CommentParams) => export const makeComment = ({url, event, content, tags = []}: CommentParams) =>
makeEvent(COMMENT, {content, tags: [...tags, ...app.use(Tags).tagEventForComment(event, url)]}) makeEvent(COMMENT, {content, tags: [...tags, ...tagEventForComment(event, url)]})
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) => export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
app.use(Thunks).publish({event: makeComment({url: relays[0], ...params}), relays}) publishThunk({event: makeComment({url: relays[0], ...params}), relays})
+1 -1
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {pubkey} from "@app/welshman" import {pubkey} from "@welshman/app"
import Dialog from "@lib/components/Dialog.svelte" import Dialog from "@lib/components/Dialog.svelte"
import Landing from "@app/components/Landing.svelte" import Landing from "@app/components/Landing.svelte"
import Toast from "@app/components/Toast.svelte" import Toast from "@app/components/Toast.svelte"
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import {getTagValue, getAddress} from "@welshman/util" import {getTagValue, getAddress} from "@welshman/util"
import {pubkey} from "@app/welshman" import {pubkey} from "@welshman/app"
import Pen2 from "@assets/icons/pen-2.svg?dataurl" import Pen2 from "@assets/icons/pen-2.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
+3 -4
View File
@@ -3,8 +3,7 @@
import {writable} from "svelte/store" import {writable} from "svelte/store"
import {randomId, HOUR} from "@welshman/lib" import {randomId, HOUR} from "@welshman/lib"
import {makeEvent, EVENT_TIME} from "@welshman/util" import {makeEvent, EVENT_TIME} from "@welshman/util"
import {app} from "@app/welshman" import {publishThunk, waitForThunkError} from "@welshman/app"
import {Thunks} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import {daysBetween} from "@lib/util" import {daysBetween} from "@lib/util"
import GallerySend from "@assets/icons/gallery-send.svg?dataurl" import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
@@ -108,8 +107,8 @@
} }
const event = makeEvent(EVENT_TIME, {content, tags}) const event = makeEvent(EVENT_TIME, {content, tags})
const calendarThunk = app.use(Thunks).publish({event, relays: [url]}) const calendarThunk = publishThunk({event, relays: [url]})
const error = await calendarThunk.waitForError() const error = await waitForThunkError(calendarThunk)
if (error) { if (error) {
return pushToast({theme: "error", message: error}) return pushToast({theme: "error", message: error})
+13 -9
View File
@@ -26,10 +26,14 @@
DIRECT_MESSAGE, DIRECT_MESSAGE,
DIRECT_MESSAGE_FILE, DIRECT_MESSAGE_FILE,
} from "@welshman/util" } from "@welshman/util"
import {app, pubkey} from "@app/welshman" import {
import {Tags, Wraps, Thunks, MessagingRelayLists} from "@welshman/app" pubkey,
tagPubkey,
const messagingRelayListsByPubkey = app.use(MessagingRelayLists).index.$ sendWrapped,
mergeThunks,
loadMessagingRelayList,
messagingRelayListsByPubkey,
} from "@welshman/app"
import Danger from "@assets/icons/danger-triangle.svg?dataurl" import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import ArrowLeft from "@assets/icons/arrow-left.svg?dataurl" import ArrowLeft from "@assets/icons/arrow-left.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -92,7 +96,7 @@
const onSubmit = async (params: EventContent) => { const onSubmit = async (params: EventContent) => {
try { try {
const ptags = remove($pubkey!, pubkeys).map(pk => app.use(Tags).tagPubkey(pk)) const ptags = remove($pubkey!, pubkeys).map(tagPubkey)
// Remove p tags since they result in forking the conversation // Remove p tags since they result in forking the conversation
params.tags = params.tags.filter(nthNe(0, "p")) params.tags = params.tags.filter(nthNe(0, "p"))
@@ -105,7 +109,7 @@
return return
} }
await app.use(Wraps).publish({ await sendWrapped({
event: makeDelete({event: eventToEdit, protect: false}), event: makeDelete({event: eventToEdit, protect: false}),
recipients: pubkeys, recipients: pubkeys,
pow: 16, pow: 16,
@@ -154,7 +158,7 @@
// Sleep 1 second between each one to make sure timestamps are distinct // Sleep 1 second between each one to make sure timestamps are distinct
const thunks = await Promise.all( const thunks = await Promise.all(
Array.from(enumerate(templates)).map(([i, event]) => Array.from(enumerate(templates)).map(([i, event]) =>
app.use(Wraps).publish({ sendWrapped({
event, event,
recipients: pubkeys, recipients: pubkeys,
delay: $userSettingsValues.send_delay + ms(i), delay: $userSettingsValues.send_delay + ms(i),
@@ -167,7 +171,7 @@
timeout: 30_000, timeout: 30_000,
children: { children: {
component: ThunkToast, component: ThunkToast,
props: {thunk: app.use(Thunks).merge(thunks)}, props: {thunk: mergeThunks(thunks)},
}, },
}) })
} finally { } finally {
@@ -230,7 +234,7 @@
onMount(() => { onMount(() => {
for (const pubkey of others) { for (const pubkey of others) {
app.use(MessagingRelayLists).load(pubkey) loadMessagingRelayList(pubkey)
} }
}) })
+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 absolute right-4 h-10 w-10 min-w-10 rounded-full" 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"
disabled={$uploading || disabled} disabled={$uploading || disabled}
onclick={submit}> onclick={submit}>
<Icon icon={Plane} /> <Icon icon={Plane} />
+2 -3
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {app} from "@app/welshman" import {displayProfileByPubkey} from "@welshman/app"
import {Profiles} from "@welshman/app"
import {slide} from "@lib/transition" import {slide} from "@lib/transition"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -22,7 +21,7 @@
<div <div
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8" class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8"
transition:slide> transition:slide>
<p class="text-xs text-primary">{verb} @{app.use(Profiles).display(event.pubkey).get()}</p> <p class="text-xs text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
{#key event.id} {#key event.id}
<NoteContentMinimal trimParent {event} /> <NoteContentMinimal trimParent {event} />
{/key} {/key}
+5 -9
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {app, userRelayList} from "@app/welshman" import {getRelaysFromList} from "@welshman/util"
import {RelayLists, MessagingRelayLists} from "@welshman/app" import {waitForThunkError, setMessagingRelays, userRelayList, setRelays} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
@@ -29,10 +29,8 @@
loading = true loading = true
try { try {
if (($userRelayList?.urls() ?? []).length === 0) { if (getRelaysFromList($userRelayList).length === 0) {
const error = await ( const error = await waitForThunkError(await setRelays(DEFAULT_RELAYS.map(r => ["r", r])))
await app.use(RelayLists).setRelays(DEFAULT_RELAYS.map(r => ["r", r]))
).waitForError()
if (error) { if (error) {
pushToast({theme: "error", message: error}) pushToast({theme: "error", message: error})
@@ -40,9 +38,7 @@
} }
} }
const error = await ( const error = await waitForThunkError(await setMessagingRelays(DEFAULT_MESSAGING_RELAYS))
await app.use(MessagingRelayLists).setUrls(DEFAULT_MESSAGING_RELAYS)
).waitForError()
if (error) { if (error) {
pushToast({theme: "error", message: error}) pushToast({theme: "error", message: error})
+2 -3
View File
@@ -3,8 +3,7 @@
import {page} from "$app/stores" import {page} from "$app/stores"
import {remove, uniq, formatTimestamp} from "@welshman/lib" import {remove, uniq, formatTimestamp} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {app, pubkey} from "@app/welshman" import {pubkey, loadMessagingRelayList} from "@welshman/app"
import {MessagingRelayLists} from "@welshman/app"
import {fade} from "@lib/transition" import {fade} from "@lib/transition"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileName from "@app/components/ProfileName.svelte" import ProfileName from "@app/components/ProfileName.svelte"
@@ -29,7 +28,7 @@
onMount(() => { onMount(() => {
for (const pk of others) { for (const pk of others) {
app.use(MessagingRelayLists).load(pk) loadMessagingRelayList(pk)
} }
}) })
</script> </script>
+12 -11
View File
@@ -1,9 +1,8 @@
<script lang="ts"> <script lang="ts">
import {type Instance} from "tippy.js" import {type Instance} from "tippy.js"
import {hash, formatTimestampAsTime} from "@welshman/lib" import {formatTimestampAsTime} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import {app, thunks, pubkey} from "@app/welshman" import {thunks, mergeThunks, pubkey, deriveProfileDisplay, sendWrapped} from "@welshman/app"
import {Thunks, Profiles, Wraps} from "@welshman/app"
import {isMobile} from "@lib/html" import {isMobile} from "@lib/html"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl" import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -17,7 +16,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 {colors} from "@app/theme" import {getColor} 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"
@@ -34,18 +33,18 @@
const {event, replyTo, canEdit, onEdit, pubkeys, showPubkey = false}: Props = $props() const {event, replyTo, canEdit, onEdit, pubkeys, showPubkey = false}: Props = $props()
const isOwn = event.pubkey === $pubkey const isOwn = event.pubkey === $pubkey
const profileDisplay = app.use(Profiles).display(event.pubkey).$ const profileDisplay = deriveProfileDisplay(event.pubkey)
const thunk = app.use(Thunks).merge($thunks.filter(t => t.event.id === event.id)) const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
const [_, colorValue] = colors[hash(event.pubkey) % colors.length] const colorValue = getColor(event.pubkey)
const reply = () => replyTo(event) const reply = () => replyTo(event)
const edit = canEdit?.(event) ? () => onEdit?.(event) : undefined const edit = canEdit?.(event) ? () => onEdit?.(event) : undefined
const deleteReaction = (event: TrustedEvent) => const deleteReaction = (event: TrustedEvent) =>
app.use(Wraps).publish({event: makeDelete({event, protect: false}), recipients: pubkeys, pow: 16}) sendWrapped({event: makeDelete({event, protect: false}), recipients: pubkeys, pow: 16})
const createReaction = (template: EventContent) => const createReaction = (template: EventContent) =>
app.use(Wraps).publish({ sendWrapped({
event: makeReaction({event, protect: false, ...template}), event: makeReaction({event, protect: false, ...template}),
recipients: pubkeys, recipients: pubkeys,
pow: 16, pow: 16,
@@ -102,7 +101,9 @@
{/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="bg-alt chat-bubble mx-1 mb-2 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl min-w-[100px]" 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
? '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">
@@ -110,7 +111,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}
class="border border-solid border-base-content" style="box-shadow: 0 0 0 1.5px {colorValue}"
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,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {NativeEmoji} from "emoji-picker-element/shared" import type {NativeEmoji} from "emoji-picker-element/shared"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {app} from "@app/welshman" import {sendWrapped} from "@welshman/app"
import {Wraps} from "@welshman/app"
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl" import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import EmojiButton from "@lib/components/EmojiButton.svelte" import EmojiButton from "@lib/components/EmojiButton.svelte"
@@ -16,7 +15,7 @@
const {event, pubkeys}: Props = $props() const {event, pubkeys}: Props = $props()
const onEmoji = (emoji: NativeEmoji) => const onEmoji = (emoji: NativeEmoji) =>
app.use(Wraps).publish({ sendWrapped({
event: makeReaction({event, content: emoji.unicode, protect: false}), event: makeReaction({event, content: emoji.unicode, protect: false}),
recipients: pubkeys, recipients: pubkeys,
pow: 16, pow: 16,
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {NativeEmoji} from "emoji-picker-element/shared" import type {NativeEmoji} from "emoji-picker-element/shared"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {app} from "@app/welshman" import {sendWrapped} from "@welshman/app"
import {Wraps} from "@welshman/app"
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl" import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
import Pen from "@assets/icons/pen.svg?dataurl" import Pen from "@assets/icons/pen.svg?dataurl"
import Reply from "@assets/icons/reply-2.svg?dataurl" import Reply from "@assets/icons/reply-2.svg?dataurl"
@@ -29,7 +28,7 @@
const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => { const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => {
history.back() history.back()
app.use(Wraps).publish({ sendWrapped({
event: makeReaction({event, content: emoji.unicode, protect: false}), event: makeReaction({event, content: emoji.unicode, protect: false}),
recipients: pubkeys, recipients: pubkeys,
pow: 16, pow: 16,
+2 -3
View File
@@ -4,8 +4,7 @@
import {writable} from "svelte/store" import {writable} from "svelte/store"
import {tryCatch, uniq} from "@welshman/lib" import {tryCatch, uniq} from "@welshman/lib"
import {fromNostrURI} from "@welshman/util" import {fromNostrURI} from "@welshman/util"
import {app} from "@app/welshman" import {loadMessagingRelayList} from "@welshman/app"
import {MessagingRelayLists} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import Field from "@lib/components/Field.svelte" import Field from "@lib/components/Field.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -35,7 +34,7 @@
let pubkeys: string[] = $state([]) let pubkeys: string[] = $state([])
$effect(() => { $effect(() => {
pubkeys.forEach(pubkey => app.use(MessagingRelayLists).load(pubkey)) pubkeys.forEach(pubkey => loadMessagingRelayList(pubkey))
}) })
onMount(() => { onMount(() => {
+1 -1
View File
@@ -2,7 +2,7 @@
import {uniq} from "@welshman/lib" import {uniq} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util" import type {TrustedEvent, EventContent} from "@welshman/util"
import {getTagValue, getTagValues, getAddress} from "@welshman/util" import {getTagValue, getTagValues, getAddress} from "@welshman/util"
import {pubkey} from "@app/welshman" import {pubkey} from "@welshman/app"
import Pen2 from "@assets/icons/pen-2.svg?dataurl" import Pen2 from "@assets/icons/pen-2.svg?dataurl"
import {normalizeTopic} from "@lib/util" import {normalizeTopic} from "@lib/util"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
+3 -4
View File
@@ -2,8 +2,7 @@
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {removeUndefined, randomId, uniq} from "@welshman/lib" import {removeUndefined, randomId, uniq} from "@welshman/lib"
import {makeEvent, CLASSIFIED} from "@welshman/util" import {makeEvent, CLASSIFIED} from "@welshman/util"
import {app} from "@app/welshman" import {publishThunk, waitForThunkError} from "@welshman/app"
import {Thunks} from "@welshman/app"
import {isMobile, preventDefault} from "@lib/html" import {isMobile, preventDefault} from "@lib/html"
import {normalizeTopic} from "@lib/util" import {normalizeTopic} from "@lib/util"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -119,12 +118,12 @@
} }
} }
const classifiedThunk = app.use(Thunks).publish({ const classifiedThunk = publishThunk({
relays: [url], relays: [url],
event: makeEvent(CLASSIFIED, {content, tags}), event: makeEvent(CLASSIFIED, {content, tags}),
}) })
const error = await classifiedThunk.waitForError() const error = await waitForThunkError(classifiedThunk)
if (error) { if (error) {
return pushToast({theme: "error", message: error}) return pushToast({theme: "error", message: error})
@@ -9,7 +9,7 @@
tagsFromIMeta, tagsFromIMeta,
makeBlossomAuthEvent, makeBlossomAuthEvent,
} from "@welshman/util" } from "@welshman/util"
import {signer} from "@app/welshman" import {signer} from "@welshman/app"
import LinkRound from "@assets/icons/link-round.svg?dataurl" import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
+2 -3
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import {removeUndefined} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import type {ProfilePointer} from "@welshman/content" import type {ProfilePointer} from "@welshman/content"
import {app} from "@app/welshman" import {deriveProfileDisplay} from "@welshman/app"
import {Profiles} from "@welshman/app"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte" import ProfileDetail from "@app/components/ProfileDetail.svelte"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
@@ -14,7 +13,7 @@
const {value, url}: Props = $props() const {value, url}: Props = $props()
const display = app.use(Profiles).display(value.pubkey, removeUndefined([url])).$ const display = deriveProfileDisplay(value.pubkey, removeUndefined([url]))
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url}) const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url})
</script> </script>
+1 -4
View File
@@ -6,7 +6,6 @@
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"
@@ -44,9 +43,7 @@
<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.content.trim().match(/^(nostr:)?nevent1[a-z0-9]+$/)} {#if $quote.kind === MESSAGE}
<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 "@app/welshman" import {createSearch} from "@welshman/app"
import {currencyOptions, displayCurrency} from "@lib/currency" import {currencyOptions, displayCurrency} from "@lib/currency"
import Suggestions from "@lib/components/Suggestions.svelte" import Suggestions from "@lib/components/Suggestions.svelte"
import CurrencySuggestion from "@app/components/CurrencySuggestion.svelte" import CurrencySuggestion from "@app/components/CurrencySuggestion.svelte"
+31
View File
@@ -0,0 +1,31 @@
<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 "@app/welshman" import {repository} from "@welshman/app"
import {deriveChecked} from "@app/notifications" import {deriveChecked} from "@app/notifications"
import Reply from "@assets/icons/reply-2.svg?dataurl" import Reply from "@assets/icons/reply-2.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
+1 -1
View File
@@ -4,7 +4,7 @@
import {LOCALE, secondsToDate} from "@welshman/lib" import {LOCALE, secondsToDate} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import {tracker} from "@app/welshman" import {tracker} from "@welshman/app"
import FileText from "@assets/icons/file-text.svg?dataurl" import FileText from "@assets/icons/file-text.svg?dataurl"
import Copy from "@assets/icons/copy.svg?dataurl" import Copy from "@assets/icons/copy.svg?dataurl"
import UserCircle from "@assets/icons/user-circle.svg?dataurl" import UserCircle from "@assets/icons/user-circle.svg?dataurl"
+2 -3
View File
@@ -4,8 +4,7 @@
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {COMMENT, ManagementMethod} from "@welshman/util" import {COMMENT, ManagementMethod} from "@welshman/util"
import {app, pubkey, repository, relaysByUrl} from "@app/welshman" import {pubkey, repository, relaysByUrl, manageRelay} from "@welshman/app"
import {RelayManagement} from "@welshman/app"
import ShareCircle from "@assets/icons/share-circle.svg?dataurl" import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl" import Code2 from "@assets/icons/code-2.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl" import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
@@ -57,7 +56,7 @@
title: `Delete ${noun}`, title: `Delete ${noun}`,
message: `Are you sure you want to delete this ${noun.toLowerCase()} from the space?`, message: `Are you sure you want to delete this ${noun.toLowerCase()} from the space?`,
confirm: async () => { confirm: async () => {
const {error} = await app.use(RelayManagement).post(url, { const {error} = await manageRelay(url, {
method: ManagementMethod.BanEvent, method: ManagementMethod.BanEvent,
params: [event.id], params: [event.id],
}) })
+5 -23
View File
@@ -1,17 +1,15 @@
<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, prependParent} from "@app/groups" import {PROTECTED} 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"
@@ -20,17 +18,8 @@
content?: string | object content?: string | object
} }
type Props = { const {url, event, onClose, onSubmit} = $props()
url: string const draftKey = new DraftKey<Values>(`reply:${event.id}`)
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)
@@ -42,8 +31,8 @@
if ($uploading) return if ($uploading) return
const ed = await editor const ed = await editor
let content = ed.getText({blockSeparator: "\n"}).trim() const content = ed.getText({blockSeparator: "\n"}).trim()
let tags = ed.storage.nostr.getEditorTags() const tags = ed.storage.nostr.getEditorTags()
if (await shouldProtect) { if (await shouldProtect) {
tags.push(PROTECTED) tags.push(PROTECTED)
@@ -56,10 +45,6 @@
}) })
} }
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]}))
} }
@@ -102,9 +87,6 @@
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} />
+3 -4
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import {writable} from "svelte/store" import {writable} from "svelte/store"
import {makeEvent, ZAP_GOAL} from "@welshman/util" import {makeEvent, ZAP_GOAL} from "@welshman/util"
import {app} from "@app/welshman" import {publishThunk, waitForThunkError} from "@welshman/app"
import {Thunks} from "@welshman/app"
import {isMobile, preventDefault} from "@lib/html" import {isMobile, preventDefault} from "@lib/html"
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl" import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
import Bolt from "@assets/icons/bolt.svg?dataurl" import Bolt from "@assets/icons/bolt.svg?dataurl"
@@ -94,12 +93,12 @@
tags.push(["h", h]) tags.push(["h", h])
} }
const goalThunk = app.use(Thunks).publish({ const goalThunk = publishThunk({
relays: [url], relays: [url],
event: makeEvent(ZAP_GOAL, {content: title, tags}), event: makeEvent(ZAP_GOAL, {content: title, tags}),
}) })
const error = await goalThunk.waitForError() const error = await waitForThunkError(goalThunk)
if (error) { if (error) {
return pushToast({theme: "error", message: error}) return pushToast({theme: "error", message: error})
+9 -13
View File
@@ -2,10 +2,8 @@
import {now, DAY, uniq, sum} from "@welshman/lib" import {now, DAY, uniq, sum} from "@welshman/lib"
import type {Zap, TrustedEvent} from "@welshman/util" import type {Zap, TrustedEvent} from "@welshman/util"
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util" import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
import {derived} from "svelte/store" import {deriveItemsByKey, deriveArray} from "@welshman/store"
import {deriveEvents} from "@welshman/store" import {repository, getValidZap} from "@welshman/app"
import {app, repository} from "@app/welshman"
import {Zappers} from "@welshman/app"
import Bolt from "@assets/icons/bolt.svg?dataurl" import Bolt from "@assets/icons/bolt.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import ZapButton from "@app/components/ZapButton.svelte" import ZapButton from "@app/components/ZapButton.svelte"
@@ -18,15 +16,13 @@
const {url, event, ...props}: Props = $props() const {url, event, ...props}: Props = $props()
// Validated zaps for this goal. `validZapReceipts` is a reactive Projection const zaps = deriveArray(
// (resolves recipient zappers from loaded profiles); we re-derive it whenever deriveItemsByKey<Zap>({
// the set of ZAP_RESPONSE events in the repository changes. repository,
const zapReceipts = deriveEvents({repository, filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}]}) getKey: zap => zap.response.id,
const zaps = derived( filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
zapReceipts, eventToItem: (response: TrustedEvent) => getValidZap(response, event),
($receipts: TrustedEvent[], set) => }),
app.use(Zappers).validZapReceipts($receipts, event).$.subscribe(set),
[] as Zap[],
) )
const goalAmount = parseInt(getTagValue("amount", event.tags) || "0") const goalAmount = parseInt(getTagValue("amount", event.tags) || "0")
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {createSearch} from "@app/welshman" import {createSearch} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {session} from "@app/welshman" import {session} from "@welshman/app"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import CheckCircle from "@assets/icons/check-circle.svg?dataurl" import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
+2 -3
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import {app} from "@app/welshman" import {deriveZapperForPubkey} from "@welshman/app"
import {Zappers} from "@welshman/app"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -13,7 +12,7 @@
const {pubkey} = $props() const {pubkey} = $props()
const zapper = app.use(Zappers).forPubkey(pubkey).$ const zapper = deriveZapperForPubkey(pubkey)
const back = () => history.back() const back = () => history.back()
</script> </script>
@@ -80,8 +80,9 @@
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="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="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: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))}
+2 -8
View File
@@ -1,14 +1,8 @@
<script lang="ts"> <script lang="ts">
import {Client} from "@pomade/core" import {Client} from "@pomade/core"
import {getPubkey} from "@welshman/util" import {getPubkey} from "@welshman/util"
import {session} from "@app/welshman" import type {SessionPomade} from "@welshman/app"
// TODO(welshman-migration): `SessionPomade` was removed from @welshman/app; the import {session} from "@welshman/app"
// new generic Session nests its descriptor under `.data` ({clientOptions, email}),
// whereas this code destructures `email`/`clientOptions` from the top level.
// Confirm whether flotilla's FlotillaSession still surfaces these at the top
// level (loginWithPomade stores them under `.data` via toSession) and adjust the
// destructuring accordingly. Local type kept loose to avoid a broken import.
type SessionPomade = {email: string; clientOptions: {secret: string; peers: any}}
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
+2 -7
View File
@@ -1,12 +1,7 @@
<script lang="ts"> <script lang="ts">
import {Client} from "@pomade/core" import {Client} from "@pomade/core"
import {session} from "@app/welshman" import type {SessionPomade} from "@welshman/app"
// TODO(welshman-migration): `SessionPomade` was removed from @welshman/app; the import {session} from "@welshman/app"
// new generic Session nests its descriptor under `.data` ({clientOptions, email}),
// whereas this code destructures from the top level. Confirm whether flotilla's
// FlotillaSession still surfaces these at the top level and adjust accordingly.
// Local type kept loose to avoid a broken import.
type SessionPomade = {email: string; clientOptions: {peers: any}}
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
+10 -4
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} from "@app/env" import {PLATFORM_TERMS, PLATFORM_PRIVACY, PLATFORM_NAME, PLATFORM_LOGO} from "@app/env"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
const logIn = () => pushModal(LogIn) const logIn = () => pushModal(LogIn)
@@ -19,9 +19,15 @@
<Modal> <Modal>
<ModalBody> <ModalBody>
<div class="py-2"> <div class="flex flex-col items-center gap-3 py-2">
<h1 class="heading">Welcome to {PLATFORM_NAME}!</h1> <img
<p class="text-center">The chat app built for self-hosted communities.</p> src={PLATFORM_LOGO}
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 FlotillaSession, makeNip07Session, makeNip55Session} from "@app/welshman" import {addSession, type Session, makeNip07Session, makeNip55Session} from "@welshman/app"
import Widget from "@assets/icons/widget-4.svg?dataurl" import Widget from "@assets/icons/widget-4.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl" import Letter from "@assets/icons/letter.svg?dataurl"
import Cpu from "@assets/icons/cpu-bolt.svg?dataurl" import Cpu from "@assets/icons/cpu-bolt.svg?dataurl"
@@ -32,8 +32,8 @@
const signUp = () => pushModal(SignUp) const signUp = () => pushModal(SignUp)
const onSuccess = async (session: FlotillaSession, pubkey: string) => { const onSuccess = async (session: Session) => {
addSession({...session, pubkey}) addSession(session)
setChecked("*") setChecked("*")
clearModals() clearModals()
} }
@@ -45,7 +45,7 @@
const pubkey = await getNip07()?.getPublicKey() const pubkey = await getNip07()?.getPublicKey()
if (pubkey) { if (pubkey) {
await onSuccess(makeNip07Session(pubkey), pubkey) await onSuccess(makeNip07Session(pubkey))
} else { } else {
pushToast({ pushToast({
theme: "error", theme: "error",
@@ -65,7 +65,7 @@
const pubkey = await signer.getPubkey() const pubkey = await signer.getPubkey()
if (pubkey) { if (pubkey) {
await onSuccess(makeNip55Session(pubkey, app.packageName), pubkey) await onSuccess(makeNip55Session(pubkey, app.packageName))
} else { } else {
pushToast({ pushToast({
theme: "error", theme: "error",
+1 -1
View File
@@ -4,7 +4,7 @@
import type {Nip46ResponseWithResult} from "@welshman/signer" import type {Nip46ResponseWithResult} from "@welshman/signer"
import {Nip46Broker} from "@welshman/signer" import {Nip46Broker} from "@welshman/signer"
import {makeSecret} from "@welshman/util" import {makeSecret} from "@welshman/util"
import {loginWithNip01, loginWithNip46} from "@app/welshman" import {loginWithNip01, loginWithNip46} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
+2 -2
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import {bytesToHex} from "@welshman/lib" import {bytesToHex} from "@welshman/lib"
import {loginWithNip01} from "@app/welshman" import {loginWithNip01} from "@welshman/app"
import {decrypt} from "nostr-tools/nip49" import {decrypt} from "nostr-tools/nip49"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import {nsecDecode} from "@lib/util" import {nsecDecode} from "@lib/util"
@@ -56,7 +56,7 @@
}) })
} }
await loginWithNip01(secret) loginWithNip01(secret)
setChecked("*") setChecked("*")
clearModals() clearModals()
} catch (e) { } catch (e) {
+1 -1
View File
@@ -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 inert pubkey={option.pubkey} /> <Profile 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 "@app/welshman" import {pubkey} from "@welshman/app"
import Server from "@assets/icons/server.svg?dataurl" import Server from "@assets/icons/server.svg?dataurl"
import GalleryMinimalistic from "@assets/icons/gallery-minimalistic.svg?dataurl" import GalleryMinimalistic from "@assets/icons/gallery-minimalistic.svg?dataurl"
import Shield from "@assets/icons/shield-minimalistic.svg?dataurl" import Shield from "@assets/icons/shield-minimalistic.svg?dataurl"
@@ -3,10 +3,8 @@
import {sum} from "@welshman/lib" import {sum} from "@welshman/lib"
import type {Zap, TrustedEvent} from "@welshman/util" import type {Zap, TrustedEvent} from "@welshman/util"
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util" import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
import {derived} from "svelte/store" import {deriveItemsByKey, deriveArray} from "@welshman/store"
import {deriveEvents} from "@welshman/store" import {repository, getValidZap} from "@welshman/app"
import {app, repository} from "@app/welshman"
import {Zappers} from "@welshman/app"
import Bolt from "@assets/icons/bolt.svg?dataurl" import Bolt from "@assets/icons/bolt.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import ContentMinimal from "@app/components/ContentMinimal.svelte" import ContentMinimal from "@app/components/ContentMinimal.svelte"
@@ -16,14 +14,13 @@
const content = getTagValue("summary", props.event.tags) const content = getTagValue("summary", props.event.tags)
const fakeEvent = {content, tags: props.event.tags} const fakeEvent = {content, tags: props.event.tags}
// Validated zaps for this goal (reactive Projection, re-derived as the set of const zaps = deriveArray(
// ZAP_RESPONSE events in the repository changes). deriveItemsByKey<Zap>({
const zapReceipts = deriveEvents({repository, filters: [{kinds: [ZAP_RESPONSE], "#e": [props.event.id]}]}) repository,
const zaps = derived( getKey: zap => zap.response.id,
zapReceipts, filters: [{kinds: [ZAP_RESPONSE], "#e": [props.event.id]}],
($receipts: TrustedEvent[], set) => eventToItem: (response: TrustedEvent) => getValidZap(response, props.event),
app.use(Zappers).validZapReceipts($receipts, props.event).$.subscribe(set), }),
[] as Zap[],
) )
const goalAmount = parseInt(getTagValue("amount", props.event.tags) || "0") const goalAmount = parseInt(getTagValue("amount", props.event.tags) || "0")
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import {Client} from "@pomade/core" import {Client} from "@pomade/core"
import type {SessionPomade} from "@welshman/app" import type {SessionPomade} from "@welshman/app"
import {session} from "@app/welshman" import {session} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
@@ -1,12 +1,7 @@
<script lang="ts"> <script lang="ts">
import {Client} from "@pomade/core" import {Client} from "@pomade/core"
import {session} from "@app/welshman" import {session} from "@welshman/app"
// TODO(welshman-migration): `SessionPomade` was removed from @welshman/app; the import type {SessionPomade} from "@welshman/app"
// new generic Session nests its descriptor under `.data` ({clientOptions, email}),
// whereas this code destructures `email` from the top level. Confirm whether
// flotilla's FlotillaSession still surfaces `email` at the top level and adjust.
// Local type kept loose to avoid a broken import.
type SessionPomade = {email: string}
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import Key from "@assets/icons/key.svg?dataurl" import Key from "@assets/icons/key.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
+3 -4
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import {insertAt, now, randomId, removeAt, removeUndefined} from "@welshman/lib" import {insertAt, now, randomId, removeAt, removeUndefined} from "@welshman/lib"
import {makeEvent, POLL} from "@welshman/util" import {makeEvent, POLL} from "@welshman/util"
import {Thunks} from "@welshman/app" import {publishThunk, waitForThunkError} from "@welshman/app"
import {app} from "@app/welshman"
import {isMobile, preventDefault} from "@lib/html" import {isMobile, preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import HamburgerMenu from "@assets/icons/hamburger-menu.svg?dataurl" import HamburgerMenu from "@assets/icons/hamburger-menu.svg?dataurl"
@@ -143,12 +142,12 @@
tags.push(PROTECTED) tags.push(PROTECTED)
} }
const pollThunk = app.use(Thunks).publish({ const pollThunk = publishThunk({
relays: [url], relays: [url],
event: makeEvent(POLL, {content: title.trim(), tags}), event: makeEvent(POLL, {content: title.trim(), tags}),
}) })
const error = await pollThunk.waitForError() const error = await waitForThunkError(pollThunk)
if (error) { if (error) {
return pushToast({theme: "error", message: error}) return pushToast({theme: "error", message: error})
+5 -7
View File
@@ -2,9 +2,7 @@
import {onDestroy} from "svelte" import {onDestroy} from "svelte"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {POLL_RESPONSE} from "@welshman/util" import {POLL_RESPONSE} from "@welshman/util"
import type {Thunk} from "@welshman/app" import {pubkey, publishThunk, abortThunk} from "@welshman/app"
import {Thunks} from "@welshman/app"
import {pubkey, app} from "@app/welshman"
import {formatTimestampRelative} from "@welshman/lib" import {formatTimestampRelative} from "@welshman/lib"
import {deriveEvents} from "@app/repository" import {deriveEvents} from "@app/repository"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
@@ -52,7 +50,7 @@
const publishSelection = (selection: string[]) => { const publishSelection = (selection: string[]) => {
if (activeThunk) { if (activeThunk) {
activeThunk.abort() abortThunk(activeThunk)
} }
if (selection.length === 0) { if (selection.length === 0) {
@@ -60,7 +58,7 @@
return return
} }
activeThunk = app.use(Thunks).publish({ activeThunk = publishThunk({
relays: [url], relays: [url],
event: makePollResponse({event, selectedIds: selection}), event: makePollResponse({event, selectedIds: selection}),
delay: publishDelay, delay: publishDelay,
@@ -94,7 +92,7 @@
} }
let selectedIds = $state<string[]>([]) let selectedIds = $state<string[]>([])
let activeThunk: Thunk | undefined let activeThunk: ReturnType<typeof publishThunk> | undefined
$effect(() => { $effect(() => {
if (ownResponse) { if (ownResponse) {
@@ -104,7 +102,7 @@
onDestroy(() => { onDestroy(() => {
if (activeThunk) { if (activeThunk) {
activeThunk.abort() abortThunk(activeThunk)
} }
}) })
</script> </script>
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {Client} from "@pomade/core" import {Client} from "@pomade/core"
import {session, isPomadeSession} from "@app/welshman" import {session, isPomadeSession} from "@welshman/app"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl" import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import {fly} from "@lib/transition" import {fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
+5 -5
View File
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {userProfile} from "@app/welshman" import {userProfile} from "@welshman/app"
import Letter from "@assets/icons/letter.svg?dataurl" import Letter from "@assets/icons/letter.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import Widget from "@assets/icons/widget-4.svg?dataurl" import Widget from "@assets/icons/widget-4.svg?dataurl"
@@ -41,8 +41,8 @@
{/if} {/if}
<div class="flex flex-col"> <div class="flex flex-col">
<PrimaryNavItem title="Settings" href="/settings/profile" prefix="/settings"> <PrimaryNavItem title="Settings" href="/settings/profile" prefix="/settings">
{#if $userProfile?.picture()} {#if $userProfile?.picture}
<ImageIcon alt="Settings" src={$userProfile?.picture()} class="rounded-full" size={10} /> <ImageIcon alt="Settings" src={$userProfile?.picture} class="rounded-full" size={10} />
{:else} {:else}
<ImageIcon alt="Settings" src={UserRounded} class="rounded-full" size={8} /> <ImageIcon alt="Settings" src={UserRounded} class="rounded-full" size={8} />
{/if} {/if}
@@ -86,8 +86,8 @@
{/if} {/if}
</div> </div>
<PrimaryNavItem title="Settings" onclick={showSettingsMenu}> <PrimaryNavItem title="Settings" onclick={showSettingsMenu}>
{#if $userProfile?.picture()} {#if $userProfile?.picture}
<ImageIcon alt="Settings" src={$userProfile?.picture()} size={10} class="rounded-full" /> <ImageIcon alt="Settings" src={$userProfile?.picture} size={10} class="rounded-full" />
{:else} {:else}
<ImageIcon alt="Settings" src={Settings} size={8} class="rounded-full" /> <ImageIcon alt="Settings" src={Settings} size={8} class="rounded-full" />
{/if} {/if}
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import {Relays} from "@welshman/app" import {deriveRelayDisplay} from "@welshman/app"
import {app} from "@app/welshman"
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte" import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
import RelayIcon from "@app/components/RelayIcon.svelte" import RelayIcon from "@app/components/RelayIcon.svelte"
import {makeSpacePath, goToSpace} from "@app/routes" import {makeSpacePath, goToSpace} from "@app/routes"
@@ -16,7 +15,7 @@
const path = makeSpacePath(url) const path = makeSpacePath(url)
const display = $derived(app.use(Relays).display(url).$) const display = $derived(deriveRelayDisplay(url))
</script> </script>
<PrimaryNavItem <PrimaryNavItem
+5 -6
View File
@@ -1,9 +1,8 @@
<script lang="ts"> <script lang="ts">
import * as nip19 from "nostr-tools/nip19" import * as nip19 from "nostr-tools/nip19"
import {removeUndefined} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {displayPubkey, displayNip05} from "@welshman/util" import {displayPubkey} from "@welshman/util"
import {Handles, Profiles} from "@welshman/app" import {deriveHandleForPubkey, displayHandle, deriveProfileDisplay} from "@welshman/app"
import {app} from "@app/welshman"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte" import ProfileCircle from "@app/components/ProfileCircle.svelte"
@@ -24,8 +23,8 @@
const {pubkey, url, showPubkey, inert, avatarSize = 10}: Props = $props() const {pubkey, url, showPubkey, inert, avatarSize = 10}: Props = $props()
const relays = removeUndefined([url]) const relays = removeUndefined([url])
const profileDisplay = app.use(Profiles).display(pubkey, relays).$ const profileDisplay = deriveProfileDisplay(pubkey, relays)
const handle = app.use(Handles).forPubkey(pubkey).$ const handle = deriveHandleForPubkey(pubkey)
const openProfile = () => { const openProfile = () => {
pushModal(ProfileDetail, {pubkey, url}) pushModal(ProfileDetail, {pubkey, url})
@@ -59,7 +58,7 @@
</div> </div>
{#if $handle} {#if $handle}
<div class="overflow-hidden text-ellipsis text-sm opacity-75"> <div class="overflow-hidden text-ellipsis text-sm opacity-75">
{displayNip05($handle?.nip05)} {displayHandle($handle)}
</div> </div>
{/if} {/if}
{#if showPubkey} {#if showPubkey}
+2 -3
View File
@@ -6,8 +6,7 @@
import {deriveEventsDesc, deriveEventsById} from "@welshman/store" import {deriveEventsDesc, deriveEventsById} from "@welshman/store"
import {formatTimestampRelative} from "@welshman/lib" import {formatTimestampRelative} from "@welshman/lib"
import {NOTE, ROOMS, COMMENT, MESSAGE} from "@welshman/util" import {NOTE, ROOMS, COMMENT, MESSAGE} from "@welshman/util"
import {RelayLists} from "@welshman/app" import {repository, loadRelayList} from "@welshman/app"
import {repository, app} from "@app/welshman"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileSpaces from "@app/components/ProfileSpaces.svelte" import ProfileSpaces from "@app/components/ProfileSpaces.svelte"
import {deriveGroupList, getSpaceUrlsFromGroupList} from "@app/groups" import {deriveGroupList, getSpaceUrlsFromGroupList} from "@app/groups"
@@ -31,7 +30,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 app.use(RelayLists).load(pubkey) await loadRelayList(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({
+33 -10
View File
@@ -1,9 +1,8 @@
<script lang="ts"> <script lang="ts">
import cx from "classnames" import cx from "classnames"
import {removeUndefined} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {Profiles} from "@welshman/app" import {deriveProfile, deriveProfileDisplay} from "@welshman/app"
import {app} from "@app/welshman" import {getColor, getBlobVariant} from "@app/theme"
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 = {
@@ -11,15 +10,39 @@
class?: string class?: string
size?: number size?: number
url?: string url?: string
shape?: "blob" | "circle"
style?: string
} }
const {pubkey, url, size = 7, ...props}: Props = $props() const {pubkey, url, size = 7, shape = "blob", style = "", ...props}: Props = $props()
const profile = app.use(Profiles).one(pubkey, removeUndefined([url])) const profile = deriveProfile(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>
<ImageIcon {#if $profile?.picture}
{size} <ImageIcon {size} alt="" {style} class={cx(props.class, shapeClass)} src={$profile.picture} />
alt="" {:else}
class={cx(props.class, "rounded-full")} <!-- Fallback: a subtle gradient derived from the pubkey + the person's initial. -->
src={$profile?.picture() || UserRounded} /> <div
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}
+13 -11
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import cx from "classnames" import cx from "classnames"
import {Profiles} from "@welshman/app" import {getProfile, loadProfile} from "@welshman/app"
import {app} from "@app/welshman"
import {isMobile} from "@lib/html" import {isMobile} from "@lib/html"
import ProfileCircle from "@app/components/ProfileCircle.svelte" import ProfileCircle from "@app/components/ProfileCircle.svelte"
@@ -15,20 +14,22 @@
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", overlap: "-mr-2", overflow: "text-[9px]"} ? {box: "h-5 w-5", circle: 4, overlap: "-mr-2", overflow: "text-[9px]"}
: size <= 6 : size <= 6
? {box: "h-6 w-6", overlap: "-mr-2.5", overflow: "text-[10px]"} ? {box: "h-6 w-6", circle: 5, overlap: "-mr-2.5", overflow: "text-[10px]"}
: {box: "h-8 w-8", overlap: "-mr-3", overflow: "text-xs"}, : {box: "h-8 w-8", circle: 7, overlap: "-mr-3", overflow: "text-xs"},
) )
for (const pubkey of pubkeys) { for (const pubkey of pubkeys) {
app.use(Profiles).load(pubkey) loadProfile(pubkey)
} }
const visiblePubkeys = $derived.by(() => { const visiblePubkeys = $derived.by(() => {
const filtered = pubkeys.filter(pubkey => app.use(Profiles).get(pubkey)?.picture()) const filtered = pubkeys.filter(pubkey => getProfile(pubkey)?.picture)
return filtered.length > 0 ? filtered : pubkeys.slice(0, 1) return filtered.length > 0 ? filtered : pubkeys.slice(0, 1)
}) })
@@ -38,20 +39,21 @@
</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 (pubkey)} {#each displayPubkeys as pubkey, i (pubkey)}
<div <div
class={cx( class={cx(
"z-feature inline-block flex items-center justify-center rounded-full bg-base-100", "z-feature inline-flex items-center justify-center rounded-full bg-base-100 transition-transform",
dimensions.box, dimensions.box,
dimensions.overlap, dimensions.overlap,
i % 2 === 0 ? "rotate-2" : "-rotate-2",
)}> )}>
<ProfileCircle class={cx(dimensions.box, "bg-base-300")} {pubkey} {size} /> <ProfileCircle class="bg-base-300" shape="circle" {pubkey} size={dimensions.circle} />
</div> </div>
{/each} {/each}
{#if overflowCount > 0} {#if overflowCount > 0}
<div <div
class={cx( class={cx(
"z-feature inline-flex items-center justify-center rounded-full bg-neutral font-medium text-neutral-content", "z-feature bg-primary text-primary-content shadow-soft font-display inline-flex rotate-2 items-center justify-center rounded-full font-bold",
dimensions.box, dimensions.box,
dimensions.overlap, dimensions.overlap,
dimensions.overflow, dimensions.overflow,
+15 -9
View File
@@ -1,9 +1,15 @@
<script lang="ts"> <script lang="ts">
import {chunk, sleep, uniq} from "@welshman/lib" import {chunk, sleep, uniq} from "@welshman/lib"
import {makeEvent, DELETE, isReplaceable, getAddress} from "@welshman/util" import {
import {ProfileBuilder} from "@welshman/domain" makeEvent,
import {Thunks, RelayLists} from "@welshman/app" createProfile,
import {pubkey, repository, app} from "@app/welshman" PROFILE,
DELETE,
isReplaceable,
getAddress,
RelayMode,
} from "@welshman/util"
import {pubkey, publishThunk, repository, derivePubkeyRelays} from "@welshman/app"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl" import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
@@ -25,7 +31,7 @@
let confirmText = $state("") let confirmText = $state("")
const CONFIRM_TEXT = "permanently delete my nostr account" const CONFIRM_TEXT = "permanently delete my nostr account"
const userWriteRelays = app.use(RelayLists).writeUrls($pubkey!).$ const userWriteRelays = derivePubkeyRelays($pubkey!, RelayMode.Write)
const confirmOk = $derived(confirmText.toLowerCase().trim() === CONFIRM_TEXT) const confirmOk = $derived(confirmText.toLowerCase().trim() === CONFIRM_TEXT)
const showProgress = $derived(progress !== undefined) const showProgress = $derived(progress !== undefined)
@@ -38,7 +44,7 @@
} }
const chunks = chunk(500, repository.query([{authors: [$pubkey!]}])) const chunks = chunk(500, repository.query([{authors: [$pubkey!]}]))
const profileEvent = await new ProfileBuilder().update({name: "[deleted]"}).toTemplate() const profileEvent = makeEvent(PROFILE, createProfile({name: "[deleted]"}))
const vanishEvent = makeEvent(62, {tags: [["relay", "ALL_RELAYS"]]}) const vanishEvent = makeEvent(62, {tags: [["relay", "ALL_RELAYS"]]})
const denominator = chunks.length + 2 const denominator = chunks.length + 2
const relays = uniq([...INDEXER_RELAYS, ...$userWriteRelays, ...$userSpaceUrls]) const relays = uniq([...INDEXER_RELAYS, ...$userWriteRelays, ...$userSpaceUrls])
@@ -52,12 +58,12 @@
} }
// First, blank out their profile in case relays don't support deletion by address // First, blank out their profile in case relays don't support deletion by address
await app.use(Thunks).publish({relays, event: profileEvent}) await publishThunk({relays, event: profileEvent})
await incrementProgress() await incrementProgress()
// Next, send a "right to vanish" event to all relays // Next, send a "right to vanish" event to all relays
await app.use(Thunks).publish({relays, event: vanishEvent}) await publishThunk({relays, event: vanishEvent})
await incrementProgress() await incrementProgress()
@@ -73,7 +79,7 @@
} }
} }
await app.use(Thunks).publish({relays, event: makeEvent(DELETE, {tags})}) await publishThunk({relays, event: makeEvent(DELETE, {tags})})
await incrementProgress() await incrementProgress()
} }
+10 -6
View File
@@ -2,8 +2,12 @@
import {onMount} from "svelte" import {onMount} from "svelte"
import {removeUndefined} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {ManagementMethod} from "@welshman/util" import {ManagementMethod} from "@welshman/util"
import {RelayManagement, Profiles, MessagingRelayLists} from "@welshman/app" import {
import {app} from "@app/welshman" manageRelay,
deriveProfile,
displayProfileByPubkey,
loadMessagingRelayList,
} from "@welshman/app"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl" import Code2 from "@assets/icons/code-2.svg?dataurl"
import Letter from "@assets/icons/letter-opened.svg?dataurl" import Letter from "@assets/icons/letter-opened.svg?dataurl"
@@ -37,7 +41,7 @@
const {pubkey, url}: Props = $props() const {pubkey, url}: Props = $props()
const profile = app.use(Profiles).one(pubkey, removeUndefined([url])) const profile = deriveProfile(pubkey, removeUndefined([url]))
const userIsAdmin = deriveUserIsSpaceAdmin(url) const userIsAdmin = deriveUserIsSpaceAdmin(url)
@@ -62,9 +66,9 @@
const banMember = () => const banMember = () =>
pushModal(Confirm, { pushModal(Confirm, {
title: "Ban User", title: "Ban User",
message: `Are you sure you want to ban @${app.use(Profiles).display(pubkey).get()} from the space?`, message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
confirm: async () => { confirm: async () => {
const {error} = await app.use(RelayManagement).post(url!, { const {error} = await manageRelay(url!, {
method: ManagementMethod.BanPubkey, method: ManagementMethod.BanPubkey,
params: [pubkey], params: [pubkey],
}) })
@@ -92,7 +96,7 @@
let showMenu = $state(false) let showMenu = $state(false)
onMount(() => { onMount(() => {
app.use(MessagingRelayLists).load(pubkey) loadMessagingRelayList(pubkey)
}) })
</script> </script>
+5 -16
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import {Profile, ProfileBuilder} from "@welshman/domain" import type {Profile} from "@welshman/util"
import {pubkey, profilesByPubkey} from "@app/welshman" import {makeProfile} from "@welshman/util"
import {pubkey, profilesByPubkey, waitForThunkError} from "@welshman/app"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import {errorMessage} from "@lib/util" import {errorMessage} from "@lib/util"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -11,28 +12,16 @@
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
import {updateProfile} from "@app/profiles" import {updateProfile} from "@app/profiles"
// The edit form binds to plain mutable fields (name/about/nip05/picture), so we const profile = $profilesByPubkey.get($pubkey!) || makeProfile()
// hand it a plain values object rather than a Profile Reader. A Reader exposes its
// raw parsed content as `.values`; a fresh profile starts from an empty builder.
const reader = $profilesByPubkey.get($pubkey!)
const profile = reader instanceof Profile ? {...reader.values} : new ProfileBuilder().update({}).values
const initialValues = {profile} const initialValues = {profile}
const back = () => history.back() const back = () => history.back()
// TODO(welshman-migration): `profile` here is a plain values object (the form binds
// mutable fields), not a Profile Reader. It is typed Profile to match ProfileEditForm's
// Values type and updateProfile's signature, both of which still say Profile; updateProfile
// routes a non-Reader through `new ProfileBuilder().update(profile)`. Confirm the intended
// values vs Reader contract once the shared types settle.
const onsubmit = async ({profile}: {profile: Profile}) => { const onsubmit = async ({profile}: {profile: Profile}) => {
loading = true loading = true
try { try {
// TODO(welshman-migration): updateProfile is async (returns Promise<Thunk>); the old const error = await waitForThunkError(updateProfile({profile}))
// waitForThunkError shim did not await its arg. Awaiting the thunk first before
// .waitForError() — confirm this matches intended behavior.
const error = await (await updateProfile({profile})).waitForError()
if (error) { if (error) {
pushToast({ pushToast({
+2 -3
View File
@@ -4,8 +4,7 @@
import {feedFromFilter, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds" import {feedFromFilter, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
import {NOTE, getReplyTags} from "@welshman/util" import {NOTE, getReplyTags} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {Feeds} from "@welshman/app" import {makeFeedController} from "@welshman/app"
import {app} from "@app/welshman"
import {createScroller} from "@lib/html" import {createScroller} from "@lib/html"
import {fly} from "@lib/transition" import {fly} from "@lib/transition"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
@@ -20,7 +19,7 @@
let {url, pubkey, events = $bindable([]), hideLoading = false}: Props = $props() let {url, pubkey, events = $bindable([]), hideLoading = false}: Props = $props()
const ctrl = app.use(Feeds).makeFeedController({ const ctrl = makeFeedController({
useWindowing: true, useWindowing: true,
feed: makeIntersectionFeed( feed: makeIntersectionFeed(
makeRelayFeed(url), makeRelayFeed(url),
+3 -4
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import {removeUndefined} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {Profiles} from "@welshman/app" import {deriveProfile} from "@welshman/app"
import {app} from "@app/welshman"
import ContentMinimal from "@app/components/ContentMinimal.svelte" import ContentMinimal from "@app/components/ContentMinimal.svelte"
export type Props = { export type Props = {
@@ -11,9 +10,9 @@
const {pubkey, url}: Props = $props() const {pubkey, url}: Props = $props()
const profile = app.use(Profiles).one(pubkey, removeUndefined([url])) const profile = deriveProfile(pubkey, removeUndefined([url]))
</script> </script>
{#if $profile} {#if $profile}
<ContentMinimal event={{content: $profile.about() || "", tags: []}} /> <ContentMinimal event={{content: $profile.about || "", tags: []}} />
{/if} {/if}
+1 -1
View File
@@ -4,7 +4,7 @@
import type {Writable} from "svelte/store" import type {Writable} from "svelte/store"
import {type Instance} from "tippy.js" import {type Instance} from "tippy.js"
import {append, remove, uniq} from "@welshman/lib" import {append, remove, uniq} from "@welshman/lib"
import {profileSearch} from "@app/welshman" import {profileSearch} from "@welshman/app"
import Suggestions from "@lib/components/Suggestions.svelte" import Suggestions from "@lib/components/Suggestions.svelte"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
+2 -3
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import {removeUndefined} from "@welshman/lib" import {removeUndefined} from "@welshman/lib"
import {Profiles} from "@welshman/app" import {deriveProfileDisplay} from "@welshman/app"
import {app} from "@app/welshman"
type Props = { type Props = {
pubkey: string pubkey: string
@@ -10,7 +9,7 @@
const {pubkey, url}: Props = $props() const {pubkey, url}: Props = $props()
const profileDisplay = app.use(Profiles).display(pubkey, removeUndefined([url])).$ const profileDisplay = deriveProfileDisplay(pubkey, removeUndefined([url]))
</script> </script>
{$profileDisplay} {$profileDisplay}
+8 -2
View File
@@ -2,8 +2,14 @@
const {current, total}: {current: number; total: number} = $props() const {current, total}: {current: number; total: number} = $props()
</script> </script>
<div class="flex w-full"> <div class="flex w-full gap-1.5">
{#each Array(total) as _, i} {#each Array(total) as _, i}
<div class="h-1 flex-1 transition-colors {i < current ? 'bg-primary' : 'bg-base-300'}"></div> <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>
+14 -15
View File
@@ -17,8 +17,7 @@
import type {TrustedEvent, EventContent, Zap} from "@welshman/util" import type {TrustedEvent, EventContent, Zap} from "@welshman/util"
import {deriveArray, deriveEventsById, deriveItemsByKey} from "@welshman/store" import {deriveArray, deriveEventsById, deriveItemsByKey} from "@welshman/store"
import {load} from "@welshman/net" import {load} from "@welshman/net"
import {Zappers, Profiles} from "@welshman/app" import {pubkey, repository, getValidZap, displayProfileByPubkey} from "@welshman/app"
import {pubkey, repository, app} from "@app/welshman"
import {isMobile, preventDefault, stopPropagation} from "@lib/html" import {isMobile, preventDefault, stopPropagation} from "@lib/html"
import Danger from "@assets/icons/danger-triangle.svg?dataurl" import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -65,15 +64,15 @@
repository, repository,
getKey: zap => zap.response.id, getKey: zap => zap.response.id,
filters: [{kinds: [ZAP_RESPONSE], "#e": eventIds}], filters: [{kinds: [ZAP_RESPONSE], "#e": eventIds}],
eventToItem: async (response: TrustedEvent) => { eventToItem: (response: TrustedEvent) => {
const zap = await app.use(Zappers).validateZapReceipt(response, event) const zap = getValidZap(response, event)
if (zap) { if (zap) {
return zap return zap
} }
if (innerEvent) { if (innerEvent) {
return await app.use(Zappers).validateZapReceipt(response, innerEvent) return getValidZap(response, innerEvent)
} }
}, },
}), }),
@@ -151,28 +150,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 => app.use(Profiles).display(pubkey).get()))} {@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
{@const tooltip = `${info} zapped`} {@const tooltip = `${info} zapped`}
<button <button
type="button" type="button"
data-tip={tooltip} data-tip={tooltip}
class={cx( class={cx(
reactionClass, reactionClass,
"flex-inline btn btn-outline btn-neutral btn-xs flex items-center gap-1 rounded-full text-xs font-normal bg-alt", "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",
{ {
tooltip: !noTooltip && !isMobile, tooltip: !noTooltip && !isMobile,
"border-neutral-content/20": !isOwn, "bg-alt border-base-content/15": !isOwn,
"btn-primary": isOwn, "border-primary/50 bg-primary/15 text-primary": isOwn,
}, },
)}> )}>
<Reaction event={zaps[0].request} /> <Reaction event={zaps[0].request} />
<span>{amount}</span> <span class="font-semibold">{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 => app.use(Profiles).display(pubkey).get()))} {@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
{@const tooltip = `${info} reacted`} {@const tooltip = `${info} reacted`}
{@const onClick = () => onReactionClick(events)} {@const onClick = () => onReactionClick(events)}
<button <button
@@ -180,17 +179,17 @@
data-tip={tooltip} data-tip={tooltip}
class={cx( class={cx(
reactionClass, reactionClass,
"flex-inline btn btn-outline btn-neutral btn-xs gap-1 rounded-full font-normal bg-alt", "flex-inline btn btn-xs gap-1 rounded-full border font-normal transition-transform motion-safe:hover:scale-110 motion-safe:active:scale-95",
{ {
tooltip: !noTooltip && !isMobile, tooltip: !noTooltip && !isMobile,
"border-neutral-content/20": !isOwn, "bg-alt border-base-content/15": !isOwn,
"btn-primary": isOwn, "border-primary/50 bg-primary/15 text-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>{events.length}</span> <span class="font-semibold">{events.length}</span>
{/if} {/if}
</button> </button>
{/each} {/each}
+8 -2
View File
@@ -10,6 +10,7 @@
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 = {
@@ -25,7 +26,9 @@
const onClick = () => goto(h ? makeRoomPath(url, h) : makeSpaceChatPath(url)) const onClick = () => goto(h ? makeRoomPath(url, h) : makeSpaceChatPath(url))
</script> </script>
<Button class="cv card2 bg-alt shadow-md" onclick={onClick}> <Button
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}
@@ -39,7 +42,10 @@
</span> </span>
</div> </div>
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">
<ProfileCircle pubkey={event.pubkey} size={10} /> <ProfileCircle
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 {relaySearch} from "@app/welshman" import {waitForThunkError, relaySearch} from "@welshman/app"
import {createScroller} from "@lib/html" import {createScroller} from "@lib/html"
import {errorMessage} from "@lib/util" import {errorMessage} from "@lib/util"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
@@ -34,7 +34,7 @@
loading.add(url) loading.add(url)
try { try {
const error = await (await addRelay(url)).waitForError() const error = await waitForThunkError(await addRelay(url))
if (error) { if (error) {
pushToast({ pushToast({
+2 -3
View File
@@ -1,10 +1,9 @@
<script lang="ts"> <script lang="ts">
import {Relays} from "@welshman/app" import {deriveRelay} from "@welshman/app"
import {app} from "@app/welshman"
const {...props} = $props() const {...props} = $props()
const relay = app.use(Relays).one(props.url) const relay = deriveRelay(props.url)
</script> </script>
{#if $relay?.description} {#if $relay?.description}
+18 -6
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import {Relays} from "@welshman/app" import cx from "classnames"
import {app} from "@app/welshman" import {deriveRelay} from "@welshman/app"
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl" import {getColor} from "@app/theme"
import ImageIcon from "@lib/components/ImageIcon.svelte" import ImageIcon from "@lib/components/ImageIcon.svelte"
type Props = { type Props = {
@@ -12,11 +12,23 @@
const {url, size = 7, ...props}: Props = $props() const {url, size = 7, ...props}: Props = $props()
const relay = app.use(Relays).one(url) const relay = deriveRelay(url)
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={props.class} /> <ImageIcon {size} alt="" src={$relay.icon} class={cx(props.class, "squircle")} />
{:else} {:else}
<ImageIcon size={size - 2} alt="" src={RemoteControllerMinimalistic} class={props.class} /> <!-- Lettered workspace tile (Slack/Discord-style) colored by the relay url. -->
<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}
+3 -4
View File
@@ -4,13 +4,12 @@
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import {displayUrl} from "@welshman/lib" import {displayUrl} from "@welshman/lib"
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import {Relays, RelayStats} from "@welshman/app" import {deriveRelay, deriveRelayStats} from "@welshman/app"
import {app} from "@app/welshman"
const {url, children} = $props() const {url, children} = $props()
const relay = app.use(Relays).one(url) const relay = deriveRelay(url)
const relayStats = app.use(RelayStats).one(url) const relayStats = deriveRelayStats(url)
const connections = $derived($relayStats?.open_count || 0) const connections = $derived($relayStats?.open_count || 0)
</script> </script>
+2 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {Readable} from "svelte/store" import type {Readable} from "svelte/store"
import {SvelteSet} from "svelte/reactivity" import {SvelteSet} from "svelte/reactivity"
import {waitForThunkError} from "@welshman/app"
import type {Thunk} from "@welshman/app" import type {Thunk} from "@welshman/app"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -36,7 +37,7 @@
loading.add(url) loading.add(url)
try { try {
const error = await (await removeRelay(url)).waitForError() const error = await waitForThunkError(await removeRelay(url))
if (error) { if (error) {
pushToast({ pushToast({
+2 -3
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import {Relays} from "@welshman/app" import {deriveRelayDisplay} from "@welshman/app"
import {app} from "@app/welshman"
type Props = { type Props = {
url: string url: string
@@ -9,7 +8,7 @@
const {url, ...props}: Props = $props() const {url, ...props}: Props = $props()
const display = $derived(app.use(Relays).display(url).$) const display = $derived(deriveRelayDisplay(url))
</script> </script>
<span class={props.class}> <span class={props.class}>
+1 -1
View File
@@ -2,7 +2,7 @@
import {REPORT} from "@welshman/util" import {REPORT} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {deriveEventsById} from "@welshman/store" import {deriveEventsById} from "@welshman/store"
import {repository} from "@app/welshman" import {repository} from "@welshman/app"
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte" import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
+5 -6
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import {getTag, ManagementMethod} from "@welshman/util" import {getTag, ManagementMethod} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {RelayManagement, Profiles} from "@welshman/app" import {pubkey, manageRelay, repository, displayProfileByPubkey} from "@welshman/app"
import {pubkey, repository, app} from "@app/welshman"
import InboxOut from "@assets/icons/inbox-out.svg?dataurl" import InboxOut from "@assets/icons/inbox-out.svg?dataurl"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl" import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl" import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
@@ -46,7 +45,7 @@
} }
const dismissReport = async () => { const dismissReport = async () => {
const {error} = await app.use(RelayManagement).post(url, { const {error} = await manageRelay(url, {
method: ManagementMethod.BanEvent, method: ManagementMethod.BanEvent,
params: [event.id, "Dismissed by admin"], params: [event.id, "Dismissed by admin"],
}) })
@@ -67,7 +66,7 @@
title: `Remove Content`, title: `Remove Content`,
message: `Are you sure you want to delete this content from the space?`, message: `Are you sure you want to delete this content from the space?`,
confirm: async () => { confirm: async () => {
const {error} = await app.use(RelayManagement).post(url, { const {error} = await manageRelay(url, {
method: ManagementMethod.BanEvent, method: ManagementMethod.BanEvent,
params: [id, reason], params: [id, reason],
}) })
@@ -90,9 +89,9 @@
pushModal(Confirm, { pushModal(Confirm, {
title: "Ban User", title: "Ban User",
message: `Are you sure you want to ban @${app.use(Profiles).display(pubkey).get()} from the space?`, message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
confirm: async () => { confirm: async () => {
const {error} = await app.use(RelayManagement).post(url, { const {error} = await manageRelay(url, {
method: ManagementMethod.BanPubkey, method: ManagementMethod.BanPubkey,
params: [pubkey, reason], params: [pubkey, reason],
}) })
+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 absolute right-4 h-10 w-10 min-w-10 rounded-full" 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"
disabled={$uploading} disabled={$uploading}
onclick={submit}> onclick={submit}>
<Icon icon={Plane} /> <Icon icon={Plane} />
+2 -3
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {Profiles} from "@welshman/app" import {displayProfileByPubkey} from "@welshman/app"
import {app} from "@app/welshman"
import {slide} from "@lib/transition" import {slide} from "@lib/transition"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -22,7 +21,7 @@
<div <div
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8" class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8"
transition:slide> transition:slide>
<p class="text-xs text-primary">{verb} @{app.use(Profiles).display(event.pubkey).get()}</p> <p class="text-xs text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
{#key event.id} {#key event.id}
<NoteContentMinimal trimParent {event} /> <NoteContentMinimal trimParent {event} />
{/key} {/key}
+9 -11
View File
@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import {goto} from "$app/navigation" import {goto} from "$app/navigation"
import {displayRelayUrl} from "@welshman/util" import type {RoomMeta} from "@welshman/util"
import {displayRelayUrl, makeRoomMeta} from "@welshman/util"
import type {Thunk} from "@welshman/app" import type {Thunk} from "@welshman/app"
import {Rooms} from "@welshman/app" import {deleteRoom, waitForThunkError, repository, joinRoom, leaveRoom} from "@welshman/app"
import {repository, app} from "@app/welshman"
import Pen from "@assets/icons/pen.svg?dataurl" import Pen from "@assets/icons/pen.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl" import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import Login3 from "@assets/icons/login-3.svg?dataurl" import Login3 from "@assets/icons/login-3.svg?dataurl"
@@ -71,13 +71,11 @@
const startEdit = () => pushModal(RoomEdit, {url, h}) const startEdit = () => pushModal(RoomEdit, {url, h})
const handleLoading = async ( const handleLoading = async (f: (url: string, room: RoomMeta) => Thunk) => {
f: (url: string, room: {h: string}) => Promise<Thunk>,
) => {
loading = true loading = true
try { try {
const message = await (await f(url, {h})).waitForError() const message = await waitForThunkError(f(url, makeRoomMeta({h})))
if (message && !message.startsWith("duplicate:")) { if (message && !message.startsWith("duplicate:")) {
pushToast({theme: "error", message}) pushToast({theme: "error", message})
@@ -87,9 +85,9 @@
} }
} }
const join = () => handleLoading((url, room) => app.use(Rooms).join(url, room)) const join = () => handleLoading(joinRoom)
const leave = () => handleLoading((url, room) => app.use(Rooms).leave(url, room)) const leave = () => handleLoading(leaveRoom)
const showMembers = () => pushModal(RoomMembers, {url, h}) const showMembers = () => pushModal(RoomMembers, {url, h})
@@ -111,8 +109,8 @@
message: message:
"This room will no longer be accessible to space members, and all messages posted to it will be deleted.", "This room will no longer be accessible to space members, and all messages posted to it will be deleted.",
confirm: async () => { confirm: async () => {
const thunk = await app.use(Rooms).delete(url, $room) const thunk = deleteRoom(url, $room)
const message = await thunk.waitForError() const message = await waitForThunkError(thunk)
if (message) { if (message) {
repository.removeEvent(thunk.event.id) repository.removeEvent(thunk.event.id)
+6 -58
View File
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import type {Snippet} from "svelte" import type {Snippet} from "svelte"
import {Rooms} from "@welshman/app" import type {RoomMeta} from "@welshman/util"
import {app} from "@app/welshman" import {makeRoomMeta} from "@welshman/util"
import {waitForThunkError, createRoom, editRoom, joinRoom} from "@welshman/app"
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl" import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
import Hashtag from "@assets/icons/hashtag.svg?dataurl" import Hashtag from "@assets/icons/hashtag.svg?dataurl"
import Volume from "@assets/icons/volume.svg?dataurl" import Volume from "@assets/icons/volume.svg?dataurl"
@@ -18,56 +19,6 @@
import {deriveHasLivekit} from "@app/relays" import {deriveHasLivekit} from "@app/relays"
import {getRoomType, RoomType} from "@app/groups" import {getRoomType, RoomType} from "@app/groups"
// Plain mutable form object (the old @welshman/util `RoomMeta` plain-object
// type, removed in the migration). The new domain `RoomMeta` is an async
// method-accessor Reader, which doesn't fit a `$state` object bound to inputs,
// so we keep a plain object here and let `app.use(Rooms).create/edit/join`
// (which accept a plain object) build the events at submit time.
type RoomMeta = {
h: string
name?: string
about?: string
picture?: string
pictureMeta?: string[]
isClosed?: boolean
isHidden?: boolean
isPrivate?: boolean
isRestricted?: boolean
livekit?: boolean
}
// TODO(welshman-migration): reimplemented inline from the removed
// @welshman/util `generateH`/`makeRoomMeta` (room-id generator). Verify the
// generated id still matches the expected `^[a-z]+[1-9]$` shape and that no
// shared generator should be used instead.
const vowels = "a,e,i,o,u,ay,ey,oy,ou,ia,ea,ough,oo,ee,argh".split(",")
const consonants =
"p,b,t,d,k,g,ch,sh,th,f,v,s,z,l,r,m,n,pl,bl,cl,gl,pr,br,tr,dr,kr,gr,fl,sl,fr,thr,str,sk,sp,st".split(
",",
)
const generateH = () => {
const n = (6 + Math.random() * 2) | 0
const s = [consonants.slice(), vowels.slice()]
if (Math.random() < 0.5) {
s.reverse()
}
return (
Array.from({length: n}, (_, i) =>
s[i % 2].splice((Math.random() * s[i % 2].length) | 0, 1),
).join("") +
(1 + Math.floor(Math.random() * 9))
)
}
const makeRoomMeta = (room: Partial<RoomMeta> = {}): RoomMeta => ({
h: room.h ?? generateH(),
...room,
})
type Props = { type Props = {
url: string url: string
header: Snippet header: Snippet
@@ -107,22 +58,19 @@
room.pictureMeta = result.tags room.pictureMeta = result.tags
} }
// TODO(welshman-migration): app.use(Rooms).create/edit/join are async const createMessage = await waitForThunkError(createRoom(url, room))
// (return Promise<Thunk>); the old code passed the un-awaited result to
// waitForThunkError. Awaiting the thunk first before .waitForError().
const createMessage = await (await app.use(Rooms).create(url, room)).waitForError()
if (createMessage && !createMessage.includes("already")) { if (createMessage && !createMessage.includes("already")) {
return pushToast({theme: "error", message: createMessage}) return pushToast({theme: "error", message: createMessage})
} }
const editMessage = await (await app.use(Rooms).edit(url, room)).waitForError() const editMessage = await waitForThunkError(editRoom(url, room))
if (editMessage) { if (editMessage) {
return pushToast({theme: "error", message: editMessage}) return pushToast({theme: "error", message: editMessage})
} }
const joinMessage = await (await app.use(Rooms).join(url, room)).waitForError() const joinMessage = await waitForThunkError(joinRoom(url, room))
if (joinMessage && !joinMessage.includes("already")) { if (joinMessage && !joinMessage.includes("already")) {
return pushToast({theme: "error", message: joinMessage}) return pushToast({theme: "error", message: joinMessage})
+13 -4
View File
@@ -3,6 +3,7 @@
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 {
@@ -16,18 +17,26 @@
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} /> <Icon size={size + 1} icon={Volume} class="text-secondary" />
{#if $room.picture} {#if $room.picture}
<span class="text-base">/</span> <span class="text-base">/</span>
<ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" /> <ImageIcon src={$room.picture} {size} alt="" class="squircle shadow-sm" />
{/if} {/if}
</div> </div>
{:else if $room.picture} {:else if $room.picture}
<ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" /> <ImageIcon src={$room.picture} {size} alt="" class="squircle shadow-sm" />
{:else} {:else}
<Icon icon={fallbackIcon} {size} /> <!-- Colored room tile with the type glyph in white. -->
<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}
+17 -21
View File
@@ -1,18 +1,16 @@
<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 { import {gte, now, displayList, formatTimestampAsTime, formatTimestampAsDate} from "@welshman/lib"
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 {Thunks, Profiles} from "@welshman/app" import {
import {thunks, pubkey, app} from "@app/welshman" thunks,
pubkey,
mergeThunks,
deriveProfileDisplay,
displayProfileByPubkey,
} from "@welshman/app"
import {isMobile} from "@lib/html" import {isMobile} from "@lib/html"
import Pen from "@assets/icons/pen.svg?dataurl" import Pen from "@assets/icons/pen.svg?dataurl"
import Reply from "@assets/icons/reply-2.svg?dataurl" import Reply from "@assets/icons/reply-2.svg?dataurl"
@@ -30,7 +28,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 {colors} from "@app/theme" import {getColor} 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"
@@ -53,9 +51,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 = app.use(Profiles).display(event.pubkey, [url]).$ const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
const thunk = app.use(Thunks).merge($thunks.filter(t => t.event.id === event.id)) const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
const [_, colorValue] = colors[hash(event.pubkey) % colors.length] const colorValue = getColor(event.pubkey)
const qTag = getTag("q", event.tags) const qTag = getTag("q", event.tags)
const isQuoteOnly = Boolean( const isQuoteOnly = Boolean(
@@ -90,10 +88,7 @@
<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 <ProfileCircle pubkey={event.pubkey} style="box-shadow: 0 0 0 2px {colorValue}" size={8} />
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>
@@ -133,7 +128,7 @@
{#if path && $innerComments.length > 0} {#if path && $innerComments.length > 0}
{@const pubkeys = $innerComments.map(e => e.pubkey)} {@const pubkeys = $innerComments.map(e => e.pubkey)}
{@const isOwn = $pubkey && pubkeys.includes($pubkey)} {@const isOwn = $pubkey && pubkeys.includes($pubkey)}
{@const info = displayList(pubkeys.map(pubkey => app.use(Profiles).display(pubkey).get()))} {@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
{@const tooltip = `${info} commented`} {@const tooltip = `${info} commented`}
<div data-tip={tooltip} class="tooltip tooltip-right flex"> <div data-tip={tooltip} class="tooltip tooltip-right flex">
<Link <Link
@@ -150,8 +145,9 @@
</div> </div>
{#if !isMobile} {#if !isMobile}
<button <button
class="join absolute right-2 top-0.5 border border-solid border-neutral text-xs opacity-0 transition-all pr-2" 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: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}
+2 -3
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {ManagementMethod} from "@welshman/util" import {ManagementMethod} from "@welshman/util"
import {RelayManagement} from "@welshman/app" import {pubkey, manageRelay, repository} from "@welshman/app"
import {pubkey, repository, app} from "@app/welshman"
import Code2 from "@assets/icons/code-2.svg?dataurl" import Code2 from "@assets/icons/code-2.svg?dataurl"
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl" import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
import Danger from "@assets/icons/danger.svg?dataurl" import Danger from "@assets/icons/danger.svg?dataurl"
@@ -46,7 +45,7 @@
title: `Delete Message`, title: `Delete Message`,
message: `Are you sure you want to delete this message from the space?`, message: `Are you sure you want to delete this message from the space?`,
confirm: async () => { confirm: async () => {
const {error} = await app.use(RelayManagement).post(url, { const {error} = await manageRelay(url, {
method: ManagementMethod.BanEvent, method: ManagementMethod.BanEvent,
params: [event.id], params: [event.id],
}) })
+1 -1
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {NativeEmoji} from "emoji-picker-element/shared" import type {NativeEmoji} from "emoji-picker-element/shared"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {pubkey} from "@app/welshman" import {pubkey} from "@welshman/app"
import Bolt from "@assets/icons/bolt.svg?dataurl" import Bolt from "@assets/icons/bolt.svg?dataurl"
import Reply from "@assets/icons/reply-2.svg?dataurl" import Reply from "@assets/icons/reply-2.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl" import Code2 from "@assets/icons/code-2.svg?dataurl"
+4 -9
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import {getTagValue, ManagementMethod} from "@welshman/util" import {getTagValue, ManagementMethod} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent, PublishedRoomMeta} from "@welshman/util"
import {RelayManagement} from "@welshman/app" import {repository, manageRelay} from "@welshman/app"
import {app, repository} from "@app/welshman"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileName from "@app/components/ProfileName.svelte" import ProfileName from "@app/components/ProfileName.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte" import ProfileDetail from "@app/components/ProfileDetail.svelte"
@@ -29,7 +28,7 @@
loading = true loading = true
try { try {
const {error} = await app.use(RelayManagement).post(url, { const {error} = await manageRelay(url, {
method: ManagementMethod.BanEvent, method: ManagementMethod.BanEvent,
params: [event.id, "Join request dismissed"], params: [event.id, "Join request dismissed"],
}) })
@@ -50,11 +49,7 @@
loading = true loading = true
try { try {
// TODO(welshman-migration): addRoomMembers now expects a RoomMeta domain const error = await addRoomMembers(url, $room as PublishedRoomMeta, [event.pubkey])
// Reader, but deriveRoom yields the plain `Room` object. Passing it through
// as `any` preserves prior runtime behavior; verify Rooms.addMember accepts
// the plain Room shape (url/h/...) or resolve a real RoomMeta reader here.
const error = await addRoomMembers(url, $room as any, [event.pubkey])
if (error) { if (error) {
pushToast({theme: "error", message: error}) pushToast({theme: "error", message: error})
+2 -4
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import {Rooms} from "@welshman/app" import {waitForThunkError, removeRoomMember} from "@welshman/app"
import {app} from "@app/welshman"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl" import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl" import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl" import AddCircle from "@assets/icons/add-circle.svg?dataurl"
@@ -52,8 +51,7 @@
title: "Remove Member", title: "Remove Member",
message: "Are you sure you want to remove this user from the room?", message: "Are you sure you want to remove this user from the room?",
confirm: async () => { confirm: async () => {
const thunk = await app.use(Rooms).removeMember(url, $room, pubkey) const error = await waitForThunkError(removeRoomMember(url, $room, pubkey))
const error = await thunk.waitForError()
if (error) { if (error) {
pushToast({theme: "error", message: error}) pushToast({theme: "error", message: error})
+4 -8
View File
@@ -2,8 +2,8 @@
import {onMount} from "svelte" import {onMount} from "svelte"
import {setKey, popKey} from "@lib/implicit" import {setKey, popKey} from "@lib/implicit"
import {sleep} from "@welshman/lib" import {sleep} from "@welshman/lib"
import {Profiles} from "@welshman/app" import {displayProfileByPubkey} from "@welshman/app"
import {app} from "@app/welshman" import type {PublishedRoomMeta} from "@welshman/util"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
@@ -42,11 +42,7 @@
// Show loading for auto submit callback // Show loading for auto submit callback
await sleep(500) await sleep(500)
// TODO(welshman-migration): addRoomMembers now expects a RoomMeta domain const error = await addRoomMembers(url, $room as PublishedRoomMeta, pubkeys)
// Reader, but deriveRoom yields the plain `Room` object. Passing it through
// as `any` preserves prior runtime behavior; verify Rooms.addMember accepts
// the plain Room shape (url/h/...) or resolve a real RoomMeta reader here.
const error = await addRoomMembers(url, $room as any, pubkeys)
if (error) { if (error) {
pushToast({theme: "error", message: error}) pushToast({theme: "error", message: error})
@@ -76,7 +72,7 @@
subtitle: "Automatically add members to space", subtitle: "Automatically add members to space",
message: message:
nonSpaceMembers.length === 1 nonSpaceMembers.length === 1
? `${app.use(Profiles).display(nonSpaceMembers[0]).get()} is not a member of this space. Add them?` ? `${displayProfileByPubkey(nonSpaceMembers[0])} is not a member of this space. Add them?`
: `${nonSpaceMembers.length} people are not members of this space. Add them?`, : `${nonSpaceMembers.length} people are not members of this space. Add them?`,
confirm: async () => { confirm: async () => {
setKey("RoomMembersAdd.confirm", true) setKey("RoomMembersAdd.confirm", true)
+13 -13
View File
@@ -1,10 +1,8 @@
<script lang="ts"> <script lang="ts">
import type {ClientOptions} from "@pomade/core" import type {ClientOptions} from "@pomade/core"
import type {Profile} from "@welshman/domain" import type {Profile} from "@welshman/util"
import {makeSecret, RELAYS, MESSAGING_RELAYS, makeEvent} from "@welshman/util" import {makeProfile, makeSecret, RELAYS, MESSAGING_RELAYS, makeEvent} from "@welshman/util"
import {ProfileBuilder} from "@welshman/domain" import {loginWithNip01, publishThunk} from "@welshman/app"
import {Thunks} from "@welshman/app"
import {app, loginWithNip01} from "@app/welshman"
import Key from "@assets/icons/key-minimalistic.svg?dataurl" import Key from "@assets/icons/key-minimalistic.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl" import Letter from "@assets/icons/letter.svg?dataurl"
import {getKey, setKey} from "@lib/implicit" import {getKey, setKey} from "@lib/implicit"
@@ -33,7 +31,7 @@
setKey("signup.email", "") setKey("signup.email", "")
setKey("signup.secret", makeSecret()) setKey("signup.secret", makeSecret())
setKey("signup.profile", new ProfileBuilder().values) setKey("signup.profile", makeProfile())
setKey("signup.clientOptions", undefined) setKey("signup.clientOptions", undefined)
const hasPomade = POMADE_SIGNERS.length >= 3 const hasPomade = POMADE_SIGNERS.length >= 3
@@ -42,13 +40,13 @@
const completeSignup = () => { const completeSignup = () => {
// Add default outbox/inbox relays // Add default outbox/inbox relays
app.use(Thunks).publish({ publishThunk({
event: makeEvent(RELAYS, {tags: DEFAULT_RELAYS.map(url => ["r", url])}), event: makeEvent(RELAYS, {tags: DEFAULT_RELAYS.map(url => ["r", url])}),
relays: [...INDEXER_RELAYS, ...DEFAULT_RELAYS], relays: [...INDEXER_RELAYS, ...DEFAULT_RELAYS],
}) })
// Add default messaging relays // Add default messaging relays
app.use(Thunks).publish({ publishThunk({
event: makeEvent(MESSAGING_RELAYS, {tags: DEFAULT_MESSAGING_RELAYS.map(url => ["r", url])}), event: makeEvent(MESSAGING_RELAYS, {tags: DEFAULT_MESSAGING_RELAYS.map(url => ["r", url])}),
relays: DEFAULT_RELAYS, relays: DEFAULT_RELAYS,
}) })
@@ -85,10 +83,10 @@
key: () => pushModal(SignUpKey, {next: flows.nostr.complete, step: 2, totalSteps: 3}), key: () => pushModal(SignUpKey, {next: flows.nostr.complete, step: 2, totalSteps: 3}),
complete: () => complete: () =>
pushModal(SignUpComplete, {next: flows.nostr.finalize, step: 3, totalSteps: 3}), pushModal(SignUpComplete, {next: flows.nostr.finalize, step: 3, totalSteps: 3}),
finalize: async () => { finalize: () => {
const secret = getKey<string>("signup.secret")! const secret = getKey<string>("signup.secret")!
await loginWithNip01(secret) loginWithNip01(secret)
completeSignup() completeSignup()
}, },
}, },
@@ -97,17 +95,19 @@
<Modal> <Modal>
<ModalBody> <ModalBody>
<h1 class="heading">Join {PLATFORM_NAME}</h1> <h1 class="heading">Join <span class="brand">{PLATFORM_NAME}</span></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"> <Button onclick={flows.email.start} class="btn btn-primary rounded-full">
<Icon icon={Letter} /> <Icon icon={Letter} />
Sign up with email Sign up with email
</Button> </Button>
{/if} {/if}
<Button onclick={flows.nostr.start} class="btn {hasPomade ? 'btn-neutral' : 'btn-primary'}"> <Button
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>
+7 -5
View File
@@ -2,12 +2,11 @@
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"
@@ -24,9 +23,12 @@
<Modal tag="form" onsubmit={preventDefault(next)}> <Modal tag="form" onsubmit={preventDefault(next)}>
<ModalBody> <ModalBody>
<ModalHeader> <div class="flex flex-col items-center gap-3">
<ModalTitle>You're all set!</ModalTitle> <div class="center bg-primary/15 text-primary size-16 rounded-full motion-safe:animate-pop">
</ModalHeader> <Icon icon={CheckCircle} size={9} />
</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 "@app/welshman" import {session, SessionMethod, signerLog} from "@welshman/app"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Danger from "@assets/icons/danger-triangle.svg?dataurl" import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl" import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
+8 -10
View File
@@ -29,22 +29,20 @@
<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="flex grow items-center justify-between gap-4"> <div class="ellipsize whitespace-nowrap flex grow items-center justify-between gap-4">
<div class="flex min-w-0 flex-col"> <div class="flex flex-col">
<div class="flex min-w-0 items-start gap-2"> <div class="flex gap-2 items-center">
<RelayIcon {url} size={5} class="shrink-0 rounded-full md:hidden" /> <RelayIcon {url} size={5} class="rounded-full md:hidden" />
<div class="hidden shrink-0 md:flex md:items-center"> <div class="hidden md:contents">
{@render leading?.()} {@render leading?.()}
</div> </div>
<div class="min-w-0"> {@render title?.()}
{@render title?.()}
</div>
</div> </div>
<div class="text-xs text-primary pl-7 md:hidden"> <div class="text-xs text-primary md:hidden">
{displayRelayUrl(url)} {displayRelayUrl(url)}
</div> </div>
</div> </div>
<div class="flex shrink-0 items-center gap-2"> <div class="flex gap-2 items-start">
{@render action?.()} {@render action?.()}
</div> </div>
</div> </div>
+2 -3
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import {displayRelayUrl} from "@welshman/util" import {displayRelayUrl} from "@welshman/util"
import {Relays} from "@welshman/app" import {deriveRelay} from "@welshman/app"
import {app} from "@app/welshman"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl" import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Pen from "@assets/icons/pen.svg?dataurl" import Pen from "@assets/icons/pen.svg?dataurl"
@@ -27,7 +26,7 @@
} }
const {url}: Props = $props() const {url}: Props = $props()
const relay = app.use(Relays).one(url) const relay = deriveRelay(url)
const owner = $derived($relay?.pubkey) const owner = $derived($relay?.pubkey)
const userIsAdmin = deriveUserIsSpaceAdmin(url) const userIsAdmin = deriveUserIsSpaceAdmin(url)
+5 -6
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import type {RelayProfile} from "@welshman/util" import type {RelayProfile} from "@welshman/util"
import {displayRelayUrl, ManagementMethod} from "@welshman/util" import {displayRelayUrl, ManagementMethod} from "@welshman/util"
import {RelayManagement, Relays} from "@welshman/app" import {manageRelay, forceLoadRelay} from "@welshman/app"
import {app} from "@app/welshman"
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl" import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
import Widget from "@assets/icons/widget-4.svg?dataurl" import Widget from "@assets/icons/widget-4.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -37,7 +36,7 @@
const submit = async () => { const submit = async () => {
if (values.name != initialValues.name) { if (values.name != initialValues.name) {
const res = await app.use(RelayManagement).post(url, { const res = await manageRelay(url, {
method: ManagementMethod.ChangeRelayName, method: ManagementMethod.ChangeRelayName,
params: [values.name || ""], params: [values.name || ""],
}) })
@@ -48,7 +47,7 @@
} }
if (values.description != initialValues.description) { if (values.description != initialValues.description) {
const res = await app.use(RelayManagement).post(url, { const res = await manageRelay(url, {
method: ManagementMethod.ChangeRelayDescription, method: ManagementMethod.ChangeRelayDescription,
params: [values.description || ""], params: [values.description || ""],
}) })
@@ -65,7 +64,7 @@
return pushToast({theme: "error", message: error}) return pushToast({theme: "error", message: error})
} }
const res = await app.use(RelayManagement).post(url, { const res = await manageRelay(url, {
method: ManagementMethod.ChangeRelayIcon, method: ManagementMethod.ChangeRelayIcon,
params: [result.url], params: [result.url],
}) })
@@ -76,7 +75,7 @@
} }
pushToast({message: "Your changes have been saved!"}) pushToast({message: "Your changes have been saved!"})
app.use(Relays).forceLoad(url) forceLoadRelay(url)
clearModals() clearModals()
} }
+3 -2
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import {debounce} from "throttle-debounce" import {debounce} from "throttle-debounce"
import {dissoc, maybe} from "@welshman/lib" import {dissoc, maybe} from "@welshman/lib"
import {goto} from "$app/navigation"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import {slideAndFade} from "@lib/transition" import {slideAndFade} from "@lib/transition"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
@@ -21,7 +22,7 @@
import RelaySummary from "@app/components/RelaySummary.svelte" import RelaySummary from "@app/components/RelaySummary.svelte"
import SpaceJoinSettings from "@app/components/SpaceJoinSettings.svelte" import SpaceJoinSettings from "@app/components/SpaceJoinSettings.svelte"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
import {goToSpace} from "@app/routes" import {makeSpacePath} from "@app/routes"
import {relaysMostlyRestricted} from "@app/policies" import {relaysMostlyRestricted} from "@app/policies"
import {notificationSettings, setSpaceNotifications} from "@app/settings" import {notificationSettings, setSpaceNotifications} from "@app/settings"
import {parseInviteLink} from "@app/invites" import {parseInviteLink} from "@app/invites"
@@ -67,7 +68,7 @@
} }
await addSpace(url) await addSpace(url)
await goToSpace(url) await goto(makeSpacePath(url), {replaceState: true})
broadcastUserData([url]) broadcastUserData([url])
relaysMostlyRestricted.update(dissoc(url)) relaysMostlyRestricted.update(dissoc(url))
+3 -2
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {goto} from "$app/navigation"
import {dissoc, maybe} from "@welshman/lib" import {dissoc, maybe} from "@welshman/lib"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -21,7 +22,7 @@
import {notificationSettings} from "@app/settings" import {notificationSettings} from "@app/settings"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {pushToast} from "@app/toast" import {pushToast} from "@app/toast"
import {goToSpace} from "@app/routes" import {makeSpacePath} from "@app/routes"
import {Push} from "@app/push" import {Push} from "@app/push"
type Props = { type Props = {
@@ -55,7 +56,7 @@
} }
await addSpace(url) await addSpace(url)
await goToSpace(url) await goto(makeSpacePath(url), {replaceState: true})
broadcastUserData([url]) broadcastUserData([url])
relaysMostlyRestricted.update(dissoc(url)) relaysMostlyRestricted.update(dissoc(url))
+5 -6
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import {ManagementMethod} from "@welshman/util" import {ManagementMethod} from "@welshman/util"
import {RelayManagement, Profiles} from "@welshman/app" import {manageRelay, displayProfileByPubkey} from "@welshman/app"
import {app} from "@app/welshman"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl" import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import UserMinus from "@assets/icons/user-minus.svg?dataurl" import UserMinus from "@assets/icons/user-minus.svg?dataurl"
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl" import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
@@ -61,9 +60,9 @@
const unallowMember = (pubkey: string) => const unallowMember = (pubkey: string) =>
pushModal(Confirm, { pushModal(Confirm, {
title: "Remove User", title: "Remove User",
message: `Are you sure you want to remove @${app.use(Profiles).display(pubkey).get()} from the space?`, message: `Are you sure you want to remove @${displayProfileByPubkey(pubkey)} from the space?`,
confirm: async () => { confirm: async () => {
const {error} = await app.use(RelayManagement).post(url, { const {error} = await manageRelay(url, {
method: ManagementMethod.UnallowPubkey, method: ManagementMethod.UnallowPubkey,
params: [pubkey], params: [pubkey],
}) })
@@ -80,9 +79,9 @@
const banMember = (pubkey: string) => const banMember = (pubkey: string) =>
pushModal(Confirm, { pushModal(Confirm, {
title: "Ban User", title: "Ban User",
message: `Are you sure you want to ban @${app.use(Profiles).display(pubkey).get()} from the space?`, message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
confirm: async () => { confirm: async () => {
const {error} = await app.use(RelayManagement).post(url, { const {error} = await manageRelay(url, {
method: ManagementMethod.BanPubkey, method: ManagementMethod.BanPubkey,
params: [pubkey], params: [pubkey],
}) })
+2 -3
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import {displayRelayUrl, ManagementMethod} from "@welshman/util" import {displayRelayUrl, ManagementMethod} from "@welshman/util"
import {RelayManagement} from "@welshman/app" import {manageRelay} from "@welshman/app"
import {app} from "@app/welshman"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl" import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import Restart from "@assets/icons/restart.svg?dataurl" import Restart from "@assets/icons/restart.svg?dataurl"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
@@ -43,7 +42,7 @@
} }
const unbanMember = async (pubkey: string) => { const unbanMember = async (pubkey: string) => {
const {error} = await app.use(RelayManagement).post(url, { const {error} = await manageRelay(url, {
method: ManagementMethod.UnbanPubkey, method: ManagementMethod.UnbanPubkey,
params: [pubkey], params: [pubkey],
}) })
+4 -5
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import {derived} from "svelte/store" import {derived} from "svelte/store"
import {displayRelayUrl, EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, POLL} from "@welshman/util" import {displayRelayUrl, EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, POLL} from "@welshman/util"
import {Relays} from "@welshman/app" import {deriveRelay, createSearch, pubkey} from "@welshman/app"
import {app, createSearch, pubkey} from "@app/welshman"
import {fly} from "@lib/transition" import {fly} from "@lib/transition"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl" import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
@@ -61,7 +60,7 @@
const {url} = $props() const {url} = $props()
const relay = app.use(Relays).one(url) const relay = deriveRelay(url)
const chatPath = makeSpacePath(url, "chat") const chatPath = makeSpacePath(url, "chat")
const goalsPath = makeSpacePath(url, "goals") const goalsPath = makeSpacePath(url, "goals")
const threadsPath = makeSpacePath(url, "threads") const threadsPath = makeSpacePath(url, "threads")
@@ -140,7 +139,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="flex items-center gap-1 relative"> <strong class="font-display relative flex items-center gap-1">
<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"
@@ -312,7 +311,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-neutral btn-sm h-10" onclick={showDetail}> <Button class="btn btn-ghost btn-sm bg-base-100 h-10 rounded-full" onclick={showDetail}>
<SocketStatusIndicator {url} /> <SocketStatusIndicator {url} />
</Button> </Button>
</div> </div>
+2 -3
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import {Relays} from "@welshman/app" import {deriveRelay} from "@welshman/app"
import {app} from "@app/welshman"
import Server from "@assets/icons/server.svg?dataurl" import Server from "@assets/icons/server.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import ProfileLink from "@app/components/ProfileLink.svelte" import ProfileLink from "@app/components/ProfileLink.svelte"
@@ -12,7 +11,7 @@
const {url}: Props = $props() const {url}: Props = $props()
const relay = app.use(Relays).one(url) const relay = deriveRelay(url)
</script> </script>
<div class="card2 bg-alt flex flex-col gap-4"> <div class="card2 bg-alt flex flex-col gap-4">
+1 -1
View File
@@ -2,7 +2,7 @@
import {tick} from "svelte" import {tick} from "svelte"
import {debounce} from "throttle-debounce" import {debounce} from "throttle-debounce"
import {request} from "@welshman/net" import {request} from "@welshman/net"
import {repository, tracker} from "@app/welshman" import {repository, tracker} from "@welshman/app"
import {formatTimestampAsDate, groupBy, uniqBy, now, MINUTE, HOUR, DAY, WEEK} from "@welshman/lib" import {formatTimestampAsDate, groupBy, uniqBy, now, MINUTE, HOUR, DAY, WEEK} from "@welshman/lib"
import type {TrustedEvent, Filter} from "@welshman/util" import type {TrustedEvent, Filter} from "@welshman/util"
import {MESSAGE, sortEventsDesc} from "@welshman/util" import {MESSAGE, sortEventsDesc} from "@welshman/util"
-40
View File
@@ -1,40 +0,0 @@
<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
@@ -1,48 +0,0 @@
<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>
+3 -4
View File
@@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import {writable} from "svelte/store" import {writable} from "svelte/store"
import {makeEvent, THREAD} from "@welshman/util" import {makeEvent, THREAD} from "@welshman/util"
import {Thunks} from "@welshman/app" import {publishThunk, waitForThunkError} from "@welshman/app"
import {app} from "@app/welshman"
import {isMobile, preventDefault} from "@lib/html" import {isMobile, preventDefault} from "@lib/html"
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl" import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@@ -80,12 +79,12 @@
tags.push(["h", h]) tags.push(["h", h])
} }
const threadThunk = app.use(Thunks).publish({ const threadThunk = publishThunk({
relays: [url], relays: [url],
event: makeEvent(THREAD, {content, tags}), event: makeEvent(THREAD, {content, tags}),
}) })
const error = await threadThunk.waitForError() const error = await waitForThunkError(threadThunk)
if (error) { if (error) {
return pushToast({theme: "error", message: error}) return pushToast({theme: "error", message: error})

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