Type remaining app props, drop any and as casts, name inline handlers, use container queries in page content, and delete dead components

This commit is contained in:
Jon Staab 2026-09-26 01:12:13 -07:00
parent 66bb75797b
commit 52583f970c
108 changed files with 519 additions and 499 deletions

View file

@ -287,7 +287,7 @@ param change and lists key by id. A prop that does change while mounted needs `$
- `$bindable` is for input-like components: `value` on `TopicMultiSelect` and
`ProfileMultiSelect`, `element` on `PageContent`, `notifications` on `SpaceJoinNotifications`.
- Display flags are fine (`showRoom`, `showActivity`, `hideZap`, `class`); derived data is not.
- 24 app components and 20 lib components declare `interface Props`, against 232 using `type`.
- Props are declared as `type Props`, never an `interface` or an inline object type.
## Reading state in a component
@ -470,8 +470,10 @@ Rich text uses `makeEditor` from `src/app/editor` with `EditorContent`.
- Colors are the semantic tokens registered in `base.css`: `bg-surface`, `bg-surface-more`,
`text-content`, `text-content-muted`, `border-line`, `text-primary`, `text-error`. The clay,
flat, navy and nature themes supply the values through `data-fl-theme`.
- A few `class:` directives remain in `src/app` as leftovers; everything else builds classes with `cx`.
- Classes are built with `cx`; no `class:` directives remain.
- Container queries go on the `PageContent`, as in `@2xl:grid-cols-2` for the classifieds grid.
Beside the navs a page is about 326px narrower than the screen, so a screen `lg:` layout switch
becomes `@2xl:` inside a page, as on the home, about, goals, article and settings pages.
- Icons are `import X from "@assets/icons/<name>.svg?dataurl"` with `<Icon icon={X} size={4} />`,
where `size` counts 4px steps. List entries animate with `in:fly` from `@lib/transition`.

View file

@ -1,4 +1,5 @@
import {
DAY,
HOUR,
LOCALE,
dateToSeconds,
@ -174,8 +175,7 @@ export type CalendarBar = {
continuesAfter: boolean
}
const daysApart = (a: Date, b: Date) =>
Math.round((a.getTime() - b.getTime()) / (24 * 60 * 60 * 1000))
const daysApart = (a: Date, b: Date) => Math.round((dateToSeconds(a) - dateToSeconds(b)) / DAY)
// Multi-day events spanning `days` are laid out as bars, packed into as few lanes as possible.
export const layoutMultiDayBars = (

View file

@ -45,10 +45,9 @@
</script>
<aside
class="bg-surface flex w-full shrink-0 flex-col border-t lg:w-80 lg:border-t-0"
style="border-color: var(--line)">
<div class="flex flex-col lg:sticky lg:top-0">
<section class="flex flex-col gap-3 border-b px-4 py-4" style="border-color: var(--line)">
class="bg-surface flex w-full shrink-0 flex-col border-t @2xl:w-80 @2xl:border-t-0 border-line">
<div class="flex flex-col @2xl:sticky @2xl:top-0">
<section class="flex flex-col gap-3 border-b px-4 py-4 border-line">
<Profile pubkey={event.pubkey} {url} />
<div class="text-content-muted line-clamp-3 text-sm leading-snug">
<ProfileAbout pubkey={event.pubkey} {url} />

View file

@ -31,11 +31,11 @@
),
)
const selectAuthor = (pubkey: string) => {
const selectAuthor = (pubkey: string) => () => {
author = author === pubkey ? undefined : pubkey
}
const selectTopic = (name: string) => {
const selectTopic = (name: string) => () => {
topic = topic === name ? undefined : name
}
</script>
@ -46,7 +46,7 @@
<h2 class="text-lg">Authors</h2>
{#each authors as [pubkey, count] (pubkey)}
<Button
onclick={() => selectAuthor(pubkey)}
onclick={selectAuthor(pubkey)}
class={cx("button button-xs w-full rounded-full font-normal", {
"button-primary": pubkey === author,
})}>
@ -66,7 +66,7 @@
<div class="flex flex-wrap gap-2">
{#each topics as [name, count] (name)}
<Button
onclick={() => selectTopic(name)}
onclick={selectTopic(name)}
class={cx("button button-xs rounded-full font-normal", {
"button-primary": name === topic,
})}>

View file

@ -17,6 +17,8 @@
const {controller}: Props = $props()
const {loading, bunker} = controller
const showInfoBunker = () => pushModal(InfoBunker)
const toggleScanner = () => {
showScanner = !showScanner
}
@ -45,8 +47,7 @@
{#snippet info()}
<p>
A login link provided by a nostr signing app.
<Button class="button button-link" onclick={() => pushModal(InfoBunker)}
>What is a bunker link?</Button>
<Button class="button button-link" onclick={showInfoBunker}>What is a bunker link?</Button>
</p>
{/snippet}
</Field>

View file

@ -29,7 +29,7 @@
const {url, events, eventsByDay, date, context}: Props = $props()
// A modal's props are frozen at push time, so pass the feed's store rather than a filtered list.
const showDay = (day: Date) =>
const showDay = (day: Date) => () =>
pushModal(CalendarDay, {
url,
date: day,
@ -76,7 +76,7 @@
)}
style="grid-column: {col + 1}; grid-row: 1;"
aria-label={formatDay(day)}
onclick={() => showDay(day)}>
onclick={showDay(day)}>
{Intl.DateTimeFormat(LOCALE, {day: "numeric"}).format(day)}
</Button>
{/each}
@ -110,7 +110,7 @@
{#if dayEvents.length > 3}
<Button
class="px-1.5 text-left text-xs opacity-75 hover:opacity-100"
onclick={() => showDay(day)}>
onclick={showDay(day)}>
+{dayEvents.length - 3} more
</Button>
{/if}

View file

@ -29,7 +29,7 @@
const {url, events, eventsByDay, date, context}: Props = $props()
// A modal's props are frozen at push time, so pass the feed's store rather than a filtered list.
const showDay = (day: Date) =>
const showDay = (day: Date) => () =>
pushModal(CalendarDay, {
url,
date: day,
@ -58,7 +58,7 @@
<Button
class="flex items-baseline gap-2 self-start rounded-lg px-1 transition-colors hover:bg-surface-more"
aria-label={formatDay(day)}
onclick={() => showDay(day)}>
onclick={showDay(day)}>
<span class="text-xs uppercase opacity-75">{formatWeekday(day)}</span>
<span class={dayHeaderClass(day)}>
{Intl.DateTimeFormat(LOCALE, {day: "numeric"}).format(day)}
@ -91,7 +91,7 @@
class="mx-1 mt-1 flex items-baseline gap-2 self-start justify-self-start rounded-lg px-1 transition-colors hover:bg-surface-more"
style="grid-column: {col + 1}; grid-row: 1;"
aria-label={formatDay(day)}
onclick={() => showDay(day)}>
onclick={showDay(day)}>
<span class="text-xs uppercase opacity-75">{formatWeekday(day)}</span>
<span class={dayHeaderClass(day)}>
{Intl.DateTimeFormat(LOCALE, {day: "numeric"}).format(day)}

View file

@ -1,5 +1,4 @@
<script lang="ts">
import type {Snippet} from "svelte"
import {onDestroy, onMount} from "svelte"
import {
ago,
@ -42,14 +41,14 @@
import {makeFeedContext} from "@app/feeds"
import {navigate, pushModal} from "@app/modal"
import {prependParent} from "@app/rooms"
import type {RoomRow} from "@app/rooms"
import {pushToast} from "@app/toast"
type Props = {
pubkeys: string[]
info?: Snippet
}
const {pubkeys, info}: Props = $props()
const {pubkeys}: Props = $props()
const chatId = makeChatId(pubkeys)
const context = makeFeedContext({relays: $router.resolver.relays([userInbox()])})
@ -152,7 +151,7 @@
let composeArea: ComposeArea | undefined = $state()
const elements = $derived.by(() => {
const elements = []
const elements: RoomRow[] = []
let previousDate
let previousPubkey
@ -163,7 +162,7 @@
const date = formatTimestampAsDate(created_at)
if (date !== previousDate) {
elements.push({type: "date", value: date, id: date, showPubkey: false})
elements.push({type: "date", value: date, id: date})
}
elements.push({
@ -235,14 +234,14 @@
</div>
</div>
{/if}
{#each elements as { type, id, value, showPubkey } (id)}
{#if type === "date"}
<Divider>{value}</Divider>
{:else}
{#each elements as row (row.id)}
{#if row.type === "date"}
<Divider>{row.value}</Divider>
{:else if row.type === "note"}
<ChatMessage
event={$state.snapshot(value as TrustedEvent)}
event={$state.snapshot(row.value)}
{pubkeys}
{showPubkey}
showPubkey={row.showPubkey}
{context}
{replyTo}
canEdit={canEditEvent}
@ -258,7 +257,6 @@
End of message history
{/if}
</Spinner>
{@render info?.()}
</p>
</PageContent>

View file

@ -13,11 +13,11 @@
import {notifications} from "@app/notifications"
import {messagingRelayLists, user} from "@app/core"
interface Props {
type Props = {
id: string
pubkeys: string[]
messages: TrustedEvent[]
[key: string]: any
class?: string
}
const {...props}: Props = $props()
@ -37,11 +37,10 @@
<Button class="flex flex-col justify-start gap-1 w-full" onclick={openChat}>
<div
class={cx(
"cursor-pointer border-t border-solid px-3 py-2 transition-colors hover:bg-surface",
"cursor-pointer border-t border-solid border-line px-3 py-2 transition-colors hover:bg-surface",
props.class,
{"bg-surface": active},
)}
style="border-color: var(--line)">
)}>
<div class="flex flex-col justify-start gap-1">
<div class="flex items-center justify-between gap-2">
<div class="flex min-w-0 items-center gap-2">

View file

@ -25,7 +25,7 @@
import {colorFor} from "@app/theme"
import {pushModal} from "@app/modal"
interface Props {
type Props = {
event: TrustedEvent
replyTo: (event: TrustedEvent) => void
canEdit?: (event: TrustedEvent) => boolean
@ -74,14 +74,13 @@
<button
type="button"
aria-label="Message actions"
class="opacity-0 transition-all focus:opacity-100"
class:group-hover:opacity-100={!isMobile}
class="opacity-0 transition-all group-hover:opacity-100 focus:opacity-100"
onclick={togglePopover}>
<Icon icon={MenuDots} size={4} />
</button>
</Tippy>
{/if}
<div class="flex min-w-0 flex-col" class:items-end={isOwn}>
<div class={cx("flex min-w-0 flex-col", {"items-end": isOwn})}>
<TapTarget class={cx("chat-bubble", {"chat-bubble--user": isOwn})} onTap={showMobileMenu}>
{#if showPubkey}
<div class="flex items-center gap-2">
@ -89,8 +88,7 @@
<Button onclick={openProfile} class="flex items-center gap-1">
<ProfileCircle
pubkey={event.pubkey}
class="border border-solid"
style="border-color: var(--line)"
class="border border-solid border-line"
size={4} />
<div class="flex items-center gap-2">
<Button onclick={openProfile} class="text-sm font-bold" style="color: {colorValue}">

View file

@ -1,6 +1,9 @@
<script lang="ts">
import type {Maybe} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import type {TippyController} from "@lib/components/Tippy.svelte"
import ChatMessageEmojiButton from "@app/components/ChatMessageEmojiButton.svelte"
import EventInfo from "@app/components/EventInfo.svelte"
import {pushModal} from "@app/modal"
@ -8,13 +11,20 @@
import Reply from "@assets/icons/reply-2.svg?dataurl"
import Code2 from "@assets/icons/code-2.svg?dataurl"
const {event, pubkeys, tippy, replyTo, edit} = $props()
type Props = {
event: TrustedEvent
pubkeys: string[]
tippy: Maybe<TippyController>
replyTo?: (event: TrustedEvent) => void
edit?: () => void
}
const reply = () => replyTo(event)
const onEdit = () => edit?.()
const {event, pubkeys, tippy, replyTo, edit}: Props = $props()
const reply = () => replyTo?.(event)
const showInfo = () => {
tippy.hide()
tippy?.hide()
pushModal(EventInfo, {event})
}
</script>
@ -27,7 +37,7 @@
</Button>
{/if}
{#if edit}
<Button aria-label="Edit" class="button button-neutral button-xs join-item" onclick={onEdit}>
<Button aria-label="Edit" class="button button-neutral button-xs join-item" onclick={edit}>
<Icon size={4} icon={Pen} />
</Button>
{/if}

View file

@ -42,7 +42,9 @@
const displayArg = (arg: CommandArg) => (arg.required ? `<${arg.name}>` : `[${arg.name}]`)
const qualify = (pubkey: string) => insert(`@${new Pubkey(pubkey).toNpub()}`)
const qualify = (pubkey: string) => () => insert(`@${new Pubkey(pubkey).toNpub()}`)
const insertChoice = (choice: string) => () => insert(choice)
</script>
{#if draft}
@ -71,7 +73,7 @@
<div class="flex flex-wrap items-center gap-2">
<span class="opacity-75">Several bots answer this — pick one:</span>
{#each draft.matches as match (match.address())}
<Button class="button button-xs button-neutral" onclick={() => qualify(match.author())}>
<Button class="button button-xs button-neutral" onclick={qualify(match.author())}>
@<ProfileName pubkey={match.author()} url={target.url} />
</Button>
{/each}
@ -79,7 +81,7 @@
{:else if choices.length > 0}
<div class="flex flex-wrap items-center gap-2">
{#each choices as choice (choice)}
<Button class="button button-xs button-neutral" onclick={() => insert(choice)}>
<Button class="button button-xs button-neutral" onclick={insertChoice(choice)}>
{choice}
</Button>
{/each}

View file

@ -4,11 +4,11 @@
import Button from "@lib/components/Button.svelte"
import ComposeBar from "@app/components/ComposeBar.svelte"
const {
clear,
}: {
type Props = {
clear: () => void
} = $props()
}
const {clear}: Props = $props()
</script>
<ComposeBar class="flex h-8 items-center justify-between pr-7 text-xs">

View file

@ -201,9 +201,9 @@
<ContentToken value={parsed.value} />
{:else if isLink(parsed)}
{#if isBlock(i)}
<ContentLinkBlock value={parsed.value} {event} />
<ContentLinkBlock url={parsed.value.url.toString()} {event} />
{:else}
<ContentLinkInline value={parsed.value} {event} />
<ContentLinkInline url={parsed.value.url.toString()} {event} />
{/if}
{:else if isProfile(parsed)}
<ContentMention value={parsed.value} {url} />

View file

@ -1,9 +1,20 @@
<script lang="ts">
const {value, isBlock} = $props()
import cx from "classnames"
type Props = {
value: string
isBlock: boolean
}
const {value, isBlock}: Props = $props()
</script>
<code
class="bg-surface-more text-content w-full overflow-auto whitespace-pre rounded px-1 text-sm"
class:block={isBlock}>
class={cx(
"bg-surface-more text-content w-full overflow-auto whitespace-pre rounded px-1 text-sm",
{
block: isBlock,
},
)}>
{value.trim()}
</code>

View file

@ -17,6 +17,7 @@
<script lang="ts">
import {call, ellipsize, displayUrl} from "@welshman/lib"
import {isRelayUrl} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {Capacitor} from "@capacitor/core"
import {preventDefault, stopPropagation} from "@lib/html"
import Link from "@lib/components/Link.svelte"
@ -35,11 +36,15 @@
} from "@app/content"
import {isRoomId} from "@app/rooms"
const {value, event} = $props()
type Props = {
url: string
event: TrustedEvent
}
const {url, event}: Props = $props()
let hideImage = $state(false)
const url = value.url.toString()
const isRoomOrRelay = isRoomId(url) || isRelayUrl(url)
const [href, external] = call(() => {
if (url.startsWith(PLATFORM_URL)) {
@ -73,7 +78,7 @@
}
const expand = () =>
pushModal(ContentLinkDetail, {value, event}, {fullscreen: true, label: "Content preview"})
pushModal(ContentLinkDetail, {url, event}, {fullscreen: true, label: "Content preview"})
</script>
{#if isRoomOrRelay}
@ -94,15 +99,14 @@
{:else if isImage}
<Link {external} {href} class="my-2 block">
<button type="button" onclick={stopPropagation(preventDefault(expand))}>
<ContentLinkBlockImage {value} {event} class="m-auto max-h-96 rounded-2xl" />
<ContentLinkBlockImage {url} {event} class="m-auto max-h-96 rounded-2xl" />
</button>
</Link>
{:else}
{#await loadPreview(url)}
<Link {external} {href} class="my-2 block">
<div
class="border border-solid flex max-w-xl flex-col overflow-hidden leading-normal rounded-2xl"
style="border-color: var(--line)">
class="border border-solid flex max-w-xl flex-col overflow-hidden leading-normal rounded-2xl border-line">
<div class="flex flex-col gap-2 p-4">
<Spinner>
<span class="overflow-hidden text-ellipsis whitespace-nowrap">{displayUrl(url)}</span>
@ -113,8 +117,7 @@
{:then preview}
<Link {external} {href} class="my-2 block">
<div
class="border border-solid flex max-w-xl flex-col overflow-hidden leading-normal rounded-2xl"
style="border-color: var(--line)">
class="border border-solid flex max-w-xl flex-col overflow-hidden leading-normal rounded-2xl border-line">
{#if preview.image && !hideImage}
<img
alt=""

View file

@ -2,14 +2,20 @@
import {onMount, onDestroy} from "svelte"
import {displayUrl, once} from "@welshman/lib"
import {getBlob, makeBlossomAuthEvent, tagSpec, tagValue} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import {user} from "@app/core"
import {decryptUrl, getUrlTags} from "@app/content"
const {value, event, ...props} = $props()
type Props = {
url: string
event: TrustedEvent
class?: string
}
const {url, event, ...props}: Props = $props()
const url = value.url.toString()
const meta = getUrlTags(url, event)
// Fallback to filename if hash was omitted from the message for interoperability

View file

@ -1,12 +1,18 @@
<script lang="ts">
import type {TrustedEvent} from "@welshman/util"
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
import Button from "@lib/components/Button.svelte"
const {value, event} = $props()
type Props = {
url: string
event: TrustedEvent
}
const {url, event}: Props = $props()
const back = () => history.back()
</script>
<Button class="m-auto h-screen w-screen cursor-pointer p-4" onclick={back}>
<ContentLinkBlockImage {value} {event} class="m-auto max-h-full max-w-full rounded-2xl" />
<ContentLinkBlockImage {url} {event} class="m-auto max-h-full max-w-full rounded-2xl" />
</Button>

View file

@ -1,5 +1,6 @@
<script lang="ts">
import {displayUrl} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {preventDefault, stopPropagation} from "@lib/html"
import LinkRound from "@assets/icons/link-round.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
@ -8,13 +9,17 @@
import {pushModal} from "@app/modal"
import {getUrlContentType, IMAGE_CONTENT_TYPES} from "@app/content"
const {value, event} = $props()
type Props = {
url: string
event: TrustedEvent
}
const {url, event}: Props = $props()
const url = value.url.toString()
const fileType = getUrlContentType(url, event)
const expand = () =>
pushModal(ContentLinkDetail, {value, event}, {fullscreen: true, label: "Content preview"})
pushModal(ContentLinkDetail, {url, event}, {fullscreen: true, label: "Content preview"})
</script>
{#if url.match(/\.(jpe?g|png|gif|webp)$/) || IMAGE_CONTENT_TYPES.includes(fileType)}

View file

@ -34,7 +34,7 @@
import {entityLink} from "@app/env"
import {userSettingsValues} from "@app/settings"
interface Props {
type Props = {
event: any
trimParent?: boolean
singleLine?: boolean
@ -139,7 +139,7 @@
{:else if isCashu(parsed) || isInvoice(parsed)}
<ContentToken value={parsed.value} />
{:else if isLink(parsed)}
<ContentLinkInline value={parsed.value} {event} />
<ContentLinkInline url={parsed.value.url.toString()} {event} />
{:else if isProfile(parsed)}
<ContentMention value={parsed.value} {url} />
{:else if isQuote(parsed)}

View file

@ -1,5 +1,9 @@
<script lang="ts">
const {value} = $props()
type Props = {
value: string
}
const {value}: Props = $props()
</script>
{#each value as _, i (i)}

View file

@ -88,11 +88,7 @@
<NoteContentMinimal trimParent {url} event={$quote} />
</div>
{:else}
<NoteCard
event={$quote}
{url}
class="border border-solid rounded-2xl p-4"
style="border-color: var(--line)">
<NoteCard event={$quote} {url} class="border border-solid rounded-2xl p-4 border-line">
<NoteContentMinimal {url} event={$quote} />
</NoteCard>
{/if}

View file

@ -4,7 +4,11 @@
import Button from "@lib/components/Button.svelte"
import {clip} from "@app/toast"
const {value} = $props()
type Props = {
value: string
}
const {value}: Props = $props()
const copy = () => clip(value)
</script>

View file

@ -1,5 +1,9 @@
<script lang="ts">
const {value} = $props()
type Props = {
value: string
}
const {value}: Props = $props()
</script>
<span class="link-content">

View file

@ -15,7 +15,7 @@
import Tippy from "@lib/components/Tippy.svelte"
import type {TippyController} from "@lib/components/Tippy.svelte"
interface Props {
type Props = {
value: string
autofocus?: boolean
term?: Writable<string>

View file

@ -7,18 +7,14 @@
import Icon from "@lib/components/Icon.svelte"
import type {FeedContext} from "@app/feeds"
// hideLastActive: redundant next to a detail page's own comments below.
const {
event,
context,
size = "xs",
hideLastActive,
}: {
type Props = {
event: TrustedEvent
context: FeedContext
size?: "xs" | "sm"
hideLastActive?: boolean
} = $props()
}
const {event, context, size = "xs", hideLastActive}: Props = $props()
const buttonClass = `button button-neutral button-${size} rounded-full`

View file

@ -50,7 +50,7 @@
</script>
<section class="flex flex-col">
<div class="border-b px-5 sm:px-8" style="border-color: var(--line)">
<div class="border-b px-5 sm:px-8 border-line">
<div class={cx("flex w-full flex-wrap items-center justify-between gap-2 py-3", columnClass)}>
<h2 class="text-lg font-bold">
{nodes.length === 0

View file

@ -41,6 +41,8 @@
}
}
const fix = (healthCheck: HealthCheck) => () => review([healthCheck])
const reviewAll = () => review($pending, $needsKeyBackup ? backUpKey : undefined)
</script>
@ -61,7 +63,7 @@
<HealthCheckItem
title={healthCheck.title}
description={healthCheck.description}
onfix={() => review([healthCheck])} />
onfix={fix(healthCheck)} />
{/each}
{#if $needsKeyBackup}
<HealthCheckItem

View file

@ -12,7 +12,11 @@
import {hosting} from "@app/hosting"
import {PLATFORM_NAME} from "@app/env"
const {class: className = ""}: {class?: string} = $props()
type Props = {
class?: string
}
const {class: className = ""}: Props = $props()
const tenantRelays = $hosting.relays

View file

@ -16,7 +16,11 @@
import {followLists, relayLists, router, user} from "@app/core"
import {isFeedLoading, makeFeed, makeFeedContext, makeScrollLoader} from "@app/feeds"
const {class: className = ""}: {class?: string} = $props()
type Props = {
class?: string
}
const {class: className = ""}: Props = $props()
// The same set and limit `syncFollowNetwork` reads the follows' relay lists from.
const RELAY_LIMIT = 8

View file

@ -11,6 +11,8 @@
const {onSelect}: Props = $props()
const select = (iconUrl: string) => () => onSelect(iconUrl)
let icons: IconOption[] = $state([])
let searchTerm = $state("")
@ -45,7 +47,7 @@
type="button"
title={icon.name}
class="flex aspect-square items-center justify-center rounded-2xl transition-colors hover:bg-primary hover:text-primary-content"
onclick={() => onSelect(icon.url)}>
onclick={select(icon.url)}>
<Icon icon={icon.url} class="h-6 w-6" />
</button>
{/each}

View file

@ -1,4 +1,5 @@
<script lang="ts">
import type {Snippet} from "svelte"
import type {Maybe} from "@welshman/lib"
import {isMobile} from "@lib/html"
import Button from "@lib/components/Button.svelte"
@ -8,7 +9,13 @@
import IconPickerPopover from "@app/components/IconPickerPopover.svelte"
import {pushModal, popModal} from "@app/modal"
const {...props} = $props()
type Props = {
onSelect: (iconUrl: string) => void
class?: string
children?: Snippet
}
const {onSelect, children, ...props}: Props = $props()
const open = () => {
if (isMobile) {
@ -27,7 +34,7 @@
}
const onClick = (iconUrl: string) => {
props.onSelect(iconUrl)
onSelect(iconUrl)
close()
}
@ -41,6 +48,6 @@
props={{onSelect: onClick}}
params={{trigger: "manual", interactive: true, placement: "top-end"}}>
<Button onclick={open} class={props.class}>
{@render props.children?.()}
{@render children?.()}
</Button>
</Tippy>

View file

@ -8,7 +8,8 @@
onSelect: (image: File | string) => void
initialValue?: string
children?: Snippet
[key: string]: any
class?: string
"aria-label"?: string
}
const {onSelect, initialValue, children, ...props}: Props = $props()

View file

@ -34,8 +34,8 @@
popModal()
}
const onFileChange = (event: Event) => {
const selected = (event.target as HTMLInputElement).files?.[0]
const onFileChange = (event: Event & {currentTarget: HTMLInputElement}) => {
const selected = event.currentTarget.files?.[0]
if (selected?.type.startsWith("image/")) {
select(selected)
@ -49,9 +49,15 @@
const url = $derived(value.trim())
const isValid = $derived(isValidUrl(url))
const failed = $derived(Boolean(url) && failedUrl === url)
const submit = () => select(url)
const onError = () => {
failedUrl = url
}
</script>
<Modal tag="form" onsubmit={preventDefault(() => select(url))}>
<Modal tag="form" onsubmit={preventDefault(submit)}>
<ModalBody>
<ModalHeader>
<ModalTitle>Add an image</ModalTitle>
@ -93,7 +99,7 @@
src={url}
alt="Preview"
class="max-h-48 rounded-2xl object-contain"
onerror={() => (failedUrl = url)} />
onerror={onError} />
</div>
{/if}
</ModalBody>

View file

@ -9,7 +9,7 @@
import {compressFileForUpload, uploadFileOrFallback} from "@app/uploads"
import {pushToast} from "@app/toast"
interface Props {
type Props = {
file?: File | undefined
url?: string | undefined
}
@ -33,32 +33,31 @@
active = false
}
const onDrop = async (e: any) => {
const onDrop = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
active = false
submit(e.dataTransfer.files[0])
submit(e.dataTransfer?.files[0])
}
const onChange = async (e: any) => {
submit(e.target.files[0])
}
const onChange = (e: Event & {currentTarget: HTMLInputElement}) =>
submit(e.currentTarget.files?.[0])
const submit = async (newFile: File) => {
if (!newFile) {
const submit = async (newFile?: File) => {
if (newFile) {
loading = true
try {
file = await compressFileForUpload(newFile, compressOptions)
url = (await uploadFileOrFallback(file)).url
} catch (e) {
console.error(e)
pushToast({theme: "error", message: "Failed to process your profile picture."})
} finally {
loading = false
}
} else {
url = initialUrl
return
}
loading = true
try {
file = await compressFileForUpload(newFile, compressOptions)
url = (await uploadFileOrFallback(file)).url
} catch (e) {
console.error(e)
pushToast({theme: "error", message: "Failed to process your profile picture."})
} finally {
loading = false
}
}
@ -85,11 +84,10 @@
aria-label="Drag and drop files here."
style="background-image: url({url}); border-color: {active ? 'var(--primary)' : 'var(--line)'};"
class="bg-surface-more relative flex h-24 w-24 shrink-0 cursor-pointer items-center justify-center rounded-full border-2 border-solid bg-cover bg-center transition-all"
class:transparent={!url}
ondragenter={stopPropagation(preventDefault(onDragEnter))}
ondragover={stopPropagation(preventDefault(onDragOver))}
ondragleave={stopPropagation(preventDefault(onDragLeave))}
ondrop={stopPropagation(preventDefault(onDrop))}>
ondrop={onDrop}>
<button
onclick={url ? stopPropagation(onClear) : undefined}
class="absolute right-0 top-0 h-5 w-5 overflow-hidden rounded-full flex items-center justify-center"

View file

@ -3,6 +3,7 @@
import {onMount} from "svelte"
import {Capacitor} from "@capacitor/core"
import {getNip07, getNip55, Nip55Signer} from "@welshman/signer"
import type {Nip55AppInfo} from "@welshman/signer"
import {nip07, nip55, toSession} from "@welshman/app"
import type {Session} from "@welshman/app"
import Widget from "@assets/icons/widget-4.svg?dataurl"
@ -28,7 +29,7 @@
import {setChecked} from "@app/notifications"
import {login} from "@app/core"
let signers: any[] = $state([])
let signers: Nip55AppInfo[] = $state([])
let loading: string | undefined = $state()
const hasPomade = POMADE_SIGNERS.length >= 3
@ -37,6 +38,8 @@
const signUp = () => pushModal(SignUp)
const showInfoNostr = () => pushModal(InfoNostr)
const onSuccess = async (session: Session) => {
await login(session)
setChecked("*")
@ -65,7 +68,7 @@
}
}
const loginWithNip55 = async (signerApp: any) => {
const loginWithNip55 = (signerApp: Nip55AppInfo) => async () => {
loading = "nip55"
try {
@ -108,8 +111,8 @@
<ModalTitle>Log in with Nostr</ModalTitle>
<p class="m-auto max-w-sm text-center">
{PLATFORM_NAME} is built using the
<Button class="link" onclick={() => pushModal(InfoNostr)}>nostr protocol</Button>, which
allows you to own your social identity.
<Button class="link" onclick={showInfoNostr}>nostr protocol</Button>, which allows you to own
your social identity.
</p>
{#if getNip07()}
<Button {disabled} onclick={loginWithNip07} class="button button-primary">
@ -122,7 +125,7 @@
</Button>
{/if}
{#each signers as signerApp (signerApp.name)}
<Button {disabled} class="button button-primary" onclick={() => loginWithNip55(signerApp)}>
<Button {disabled} class="button button-primary" onclick={loginWithNip55(signerApp)}>
{#if loading === "nip55"}
<Spinner size="sm" class="mr-3" />
{:else}

View file

@ -149,7 +149,7 @@
<ModalTitle>Log In with a Signer</ModalTitle>
<ModalSubtitle>Using a remote signer app helps you keep your keys safe.</ModalSubtitle>
</ModalHeader>
<div class:hidden={mode !== "bunker"}></div>
<div class={cx({hidden: mode !== "bunker"})}></div>
{#if mode === "connect"}
<BunkerConnect {controller} />
{:else}

View file

@ -19,7 +19,7 @@
import {pushModal} from "@app/modal"
import {pushToast} from "@app/toast"
interface Props {
type Props = {
email?: string
}

View file

@ -19,7 +19,7 @@
import {clearModals} from "@app/modal"
import {pushToast} from "@app/toast"
interface Props {
type Props = {
email: string
options: AccountOption[]
clientSecret: string
@ -31,36 +31,38 @@
const back = () => history.back()
const selectAccount = async ({client, peers}: AccountOption) => {
loading = true
const selectAccount =
({client, peers}: AccountOption) =>
async () => {
loading = true
try {
const {clientOptions, ...res} = await Client.selectLogin(clientSecret, client, peers)
try {
const {clientOptions, ...res} = await Client.selectLogin(clientSecret, client, peers)
if (res.ok && clientOptions) {
await loginWithPomade(clientOptions, email)
deleteDeactivatedPomadeSessions()
setChecked("*")
clearModals()
} else {
console.error(res.messages)
if (res.ok && clientOptions) {
await loginWithPomade(clientOptions, email)
deleteDeactivatedPomadeSessions()
setChecked("*")
clearModals()
} else {
console.error(res.messages)
pushToast({
theme: "error",
message: getPomadeLoginFailureMessage(res.messages),
})
}
} catch (error) {
console.error("Login error:", error)
pushToast({
theme: "error",
message: getPomadeLoginFailureMessage(res.messages),
message: POMADE_NETWORK_ERROR_MESSAGE,
})
} finally {
loading = false
}
} catch (error) {
console.error("Login error:", error)
pushToast({
theme: "error",
message: POMADE_NETWORK_ERROR_MESSAGE,
})
} finally {
loading = false
}
}
</script>
<Modal>
@ -73,7 +75,7 @@
<div class="flex flex-col gap-2">
{#each uniqBy(o => o.pubkey, options) as option (option.pubkey)}
<Button
onclick={() => selectAccount(option)}
onclick={selectAccount(option)}
disabled={loading}
class="card flex w-full items-center p-3 text-left">
<Profile inert pubkey={option.pubkey} />

View file

@ -26,7 +26,7 @@
{/if}
<div class="grid grid-cols-3 gap-2 @md:grid-cols-5 @2xl:grid-cols-9">
{#each images as image, i (i + image)}
<ContentLinkBlock event={props.event} value={{url: image}} />
<ContentLinkBlock event={props.event} url={image} />
{/each}
</div>
</div>

View file

@ -33,7 +33,7 @@
const reference = eventToReference(event)
const selectBoard = (board: PinboardReader) =>
const selectBoard = (board: PinboardReader) => () =>
pushModal(PinSave, {url, address: board.address(), reference})
</script>
@ -54,7 +54,7 @@
{#each $boards as board (board.address())}
<Button
class="card card-sm card-interactive flex flex-row items-center justify-between gap-3"
onclick={() => selectBoard(board)}>
onclick={selectBoard(board)}>
<span class="flex min-w-0 flex-col">
<strong class="truncate min-w-0">{board.title() || "Untitled shelf"}</strong>
{#if board.description()}

View file

@ -52,11 +52,13 @@
options = [...options, {id: randomId(), value: ""}]
}
const removeOption = (id: string) => {
const removeOption = (id: string) => () => {
options = options.filter(option => option.id !== id)
}
const updateOption = (id: string, value: string) => {
const updateOption = (id: string) => (e: Event & {currentTarget: HTMLInputElement}) => {
const {value} = e.currentTarget
options = options.map(option => (option.id === id ? {...option, value} : option))
}
@ -75,7 +77,7 @@
options = insertAt(targetIndex, options[sourceIndex], removeAt(sourceIndex, options))
}
const onDragStart = (e: DragEvent, id: string) => {
const onDragStart = (id: string) => (e: DragEvent) => {
draggedOptionId = id
if (e.dataTransfer) {
@ -84,7 +86,7 @@
}
}
const onDragOver = (e: DragEvent, targetId: string) => {
const onDragOver = (targetId: string) => (e: DragEvent) => {
e.preventDefault()
reorderOptions(targetId)
}
@ -200,8 +202,8 @@
class="flex items-center gap-2"
draggable="true"
role="listitem"
ondragstart={e => onDragStart(e, option.id)}
ondragover={e => onDragOver(e, option.id)}
ondragstart={onDragStart(option.id)}
ondragover={onDragOver(option.id)}
ondrop={onDrop}
ondragend={onDragEnd}>
<div class="cursor-move opacity-70" aria-label="Drag handle">
@ -213,11 +215,9 @@
class="grow"
type="text"
placeholder={`Option ${index + 1}`}
oninput={e => updateOption(option.id, e.currentTarget.value)} />
oninput={updateOption(option.id)} />
</label>
<Button
class="button button-ghost button-sm"
onclick={() => removeOption(option.id)}>
<Button class="button button-ghost button-sm" onclick={removeOption(option.id)}>
<Icon icon={MinusCircle} size={4} />
</Button>
</div>

View file

@ -1,5 +1,6 @@
<script lang="ts">
import {onMount} from "svelte"
import {secondsToDate} from "@welshman/lib"
import MenuButton from "@lib/components/MenuButton.svelte"
import PomadeSessionMenu from "@app/components/PomadeSessionMenu.svelte"
import {pushToast} from "@app/toast"
@ -24,14 +25,12 @@
}
}
const formatDate = (timestamp: number) => {
const date = new Date(timestamp * 1000)
return date.toLocaleDateString(undefined, {
const formatDate = (timestamp: number) =>
secondsToDate(timestamp).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})
}
onMount(() => {
loadOtherPomadeSessions().then(_sessions => {
@ -41,7 +40,7 @@
</script>
{#if sessions.length > 0}
<div class="flex flex-col gap-4 border-t border-solid pt-4" style="border-color: var(--line)">
<div class="flex flex-col gap-4 border-t border-solid pt-4 border-line">
<strong>Other Sessions</strong>
{#each sessions as sessionItem (sessionItem.client)}
<div class="flex flex-col gap-2">

View file

@ -45,6 +45,8 @@
const submit = () => onsubmit($state.snapshot(values))
const showInfoHandle = () => pushModal(InfoHandle)
let file: File | undefined = $state()
</script>
@ -129,7 +131,7 @@
{/snippet}
{#snippet info()}
<p>
<Button class="button button-link" onclick={() => pushModal(InfoHandle)}
<Button class="button button-link" onclick={showInfoHandle}
>What is a nostr address?</Button>
</p>
{/snippet}

View file

@ -7,7 +7,7 @@
import {network} from "@app/core"
import {makeFeedContext} from "@app/feeds"
interface Props {
type Props = {
url: string
pubkey: string
limit?: number

View file

@ -36,12 +36,14 @@
value = uniq(append(pubkey, value))
}
const removePubkey = (pubkey: string) => {
const openProfile = (pubkey: string) => () => pushModal(ProfileDetail, {pubkey})
const removePubkey = (pubkey: string) => () => {
value = remove(pubkey, value)
}
const onInput = (e: any) => {
const pubkey = decodePubkey(e.target.value)
const onInput = (e: Event & {currentTarget: HTMLInputElement}) => {
const pubkey = decodePubkey(e.currentTarget.value)
if (pubkey) {
selectPubkey(pubkey)
@ -72,12 +74,11 @@
<div class="flex flex-col gap-1">
<div>
{#each value as pubkey (pubkey)}
{@const onClick = () => pushModal(ProfileDetail, {pubkey})}
<span class="badge badge-neutral mr-1 mb-1 gap-1">
<Button class="flex items-center" onclick={() => removePubkey(pubkey)}>
<Button class="flex items-center" onclick={removePubkey(pubkey)}>
<Icon icon={CloseCircle} size={4} />
</Button>
<Button onclick={onClick}>
<Button onclick={openProfile(pubkey)}>
<ProfileName {pubkey} />
</Button>
</span>

View file

@ -69,11 +69,10 @@
const openBannerPicker = () => bannerInput?.click()
const onBannerChange = async (e: Event) => {
const input = e.target as HTMLInputElement
const file = input.files?.[0]
const onBannerChange = async (e: Event & {currentTarget: HTMLInputElement}) => {
const file = e.currentTarget.files?.[0]
input.value = ""
e.currentTarget.value = ""
if (!file || !$profile) {
return

View file

@ -1,13 +1,21 @@
<script lang="ts">
const {current, total}: {current: number; total: number} = $props()
import cx from "classnames"
type Props = {
current: number
total: number
}
const {current, total}: Props = $props()
</script>
<div class="flex w-full">
{#each Array(total) as _, i (i)}
<div
class="h-1 flex-1 transition-colors {i < current
? 'bg-primary text-primary-content'
: 'bg-surface-more'}">
class={cx("h-1 flex-1 transition-colors", {
"bg-primary text-primary-content": i < current,
"bg-surface-more": i >= current,
})}>
</div>
{/each}
</div>

View file

@ -2,7 +2,12 @@
import Button from "@lib/components/Button.svelte"
import {clip} from "@app/toast"
const {code, ...props} = $props()
type Props = {
code: string
class?: string
}
const {code, ...props}: Props = $props()
let canvas: HTMLCanvasElement | undefined = $state()
let wrapper: Element | undefined = $state()

View file

@ -32,7 +32,7 @@
import {pushModal} from "@app/modal"
import {deriveDisplaysByPubkey} from "@app/social"
interface Props {
type Props = {
event: TrustedEvent
deleteReaction: (event: TrustedEvent) => void
createReaction: (event: EventContent) => void

View file

@ -20,7 +20,7 @@
import {pushToast} from "@app/toast"
import {app, blockedRelayLists, relayLists, user} from "@app/core"
interface Props {
type Props = {
relays: Readable<string[]>
addRelay: (url: string) => Promise<Thunk>
matchRelay?: (url: string) => boolean
@ -32,7 +32,7 @@
const customUrl = $derived(tryCatch(() => normalizeRelayUrl(term)))
const add = async (url: string) => {
const add = (url: string) => async () => {
loading.add(url)
try {
@ -117,7 +117,7 @@
<Button
class="button button-outline button-sm flex items-center"
disabled={loading.has(customUrl)}
onclick={() => add(customUrl)}>
onclick={add(customUrl)}>
{#if loading.has(customUrl)}
<Spinner size="sm" />
{:else}
@ -132,7 +132,7 @@
<Button
class="button button-outline button-sm flex items-center"
disabled={loading.has(url)}
onclick={() => add(url)}>
onclick={add(url)}>
{#if loading.has(url)}
<Spinner size="sm" />
{:else}

View file

@ -1,4 +1,5 @@
<script lang="ts">
import type {Snippet} from "svelte"
import {displayUrl} from "@welshman/lib"
import {displayRelayUrl} from "@welshman/util"
import {RelayStats} from "@welshman/app"
@ -8,7 +9,12 @@
import ContentMinimal from "@app/components/ContentMinimal.svelte"
import {app, relays} from "@app/core"
const {url, children} = $props()
type Props = {
url: string
children?: Snippet
}
const {url, children}: Props = $props()
const relay = $relays.one(url)
const relayStats = $app.use(RelayStats).one(url)

View file

@ -34,7 +34,7 @@
const add = () => pushModal(RelayAdd, {relays, addRelay, matchRelay})
const remove = async (url: string) => {
const remove = (url: string) => async () => {
loading.add(url)
try {
@ -67,7 +67,7 @@
<Button
class="button button-neutral button-sm"
disabled={loading.has(url)}
onclick={() => remove(url)}>
onclick={remove(url)}>
{#if loading.has(url)}
<Spinner size="sm" />
{:else}

View file

@ -11,10 +11,9 @@
type Props = {
url: string
hideFavorites?: boolean
}
const {url, hideFavorites}: Props = $props()
const {url}: Props = $props()
const favorited = $roomLists.pubkeysForUrl(url).$
const favoritedPubkeys = $derived(remove($user.pubkey, $favorited))
</script>
@ -29,7 +28,7 @@
<RelayIcon {url} size={10} />
</div>
</div>
{#if !hideFavorites && $userSpaceUrls.includes(url)}
{#if $userSpaceUrls.includes(url)}
<div
class="absolute -right-1 -top-1 h-5 w-5 rounded-full bg-primary text-primary-content flex justify-center items-center">
<Icon icon={CheckCircle} class="scale-120" />
@ -45,7 +44,7 @@
<RelayDescription {url} />
</div>
</div>
{#if !hideFavorites && favoritedPubkeys.length > 0}
{#if favoritedPubkeys.length > 0}
<div class="flex gap-2 card card-sm">
Favorited By:
<ProfileCircles pubkeys={favoritedPubkeys} />

View file

@ -1,4 +1,5 @@
<script lang="ts">
import type {TrustedEvent} from "@welshman/util"
import {Report} from "@welshman/domain"
import {preventDefault} from "@lib/html"
import Spinner from "@lib/components/Spinner.svelte"
@ -16,7 +17,12 @@
import {command, writer} from "@app/core"
import {pushToast} from "@app/toast"
const {url, event} = $props()
type Props = {
url: string
event: TrustedEvent
}
const {url, event}: Props = $props()
const back = () => history.back()

View file

@ -27,9 +27,8 @@
const ptag = matchTag(tagSpec("p"), event.tags)
const reason = etag?.[2] || ptag?.[2]
const onClick = (e: Event, event: TrustedEvent) => {
// @ts-ignore
if (e.target?.classList.contains("profile-name")) {
const onClick = (event: TrustedEvent) => (e: Event) => {
if (e.target instanceof Element && e.target.classList.contains("profile-name")) {
pushModal(ProfileDetail, {pubkey: event.pubkey, url})
} else {
goToEvent(event)
@ -64,7 +63,7 @@
<p>Unable to find reported note.</p>
{:else}
{@const event = reportedEvents[0]}
<Button class="flex flex-col gap-2 w-full" onclick={(e: Event) => onClick(e, event)}>
<Button class="flex flex-col gap-2 w-full" onclick={onClick(event)}>
<div class="flex items-center justify-between gap-2">
<span class="profile-name">
@<ProfileName pubkey={event.pubkey} {url} />

View file

@ -59,8 +59,8 @@
{#snippet input()}
<div class="flex items-center gap-3">
<div
class="h-8 w-8 shrink-0 rounded-full border-2"
style="background-color: {roleColor(hue)}; border-color: var(--line)">
class="h-8 w-8 shrink-0 rounded-full border-2 border-line"
style="background-color: {roleColor(hue)}">
</div>
<input
type="range"

View file

@ -2,7 +2,7 @@
import type {RelayRoleReader} from "@welshman/domain"
import RoleBadge from "@app/components/RoleBadge.svelte"
interface Props {
type Props = {
role: RelayRoleReader
}

View file

@ -1,4 +1,5 @@
<script lang="ts">
import cx from "classnames"
import {displayRelayUrl} from "@welshman/util"
import {publish} from "@welshman/app"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@ -174,7 +175,7 @@
</div>
<input type="checkbox" class="toggle" checked={$isMuted} onchange={toggleMuted} />
</div>
<div class="flex items-center justify-between" class:opacity-50={$isMuted}>
<div class={cx("flex items-center justify-between", {"opacity-50": $isMuted})}>
<div class="flex items-center gap-2">
<Icon icon={Bell} />
<span>Notifications</span>

View file

@ -5,7 +5,7 @@
import ImageIcon from "@lib/components/ImageIcon.svelte"
import {rooms} from "@app/core"
interface Props {
type Props = {
h: string
url: string
size?: number

View file

@ -100,11 +100,7 @@
<div class="flex w-full gap-3 overflow-auto">
{#if showPubkey}
<Button onclick={openProfile} class="flex items-start pt-1.5 justify-center w-8 shrink-0">
<ProfileCircle
pubkey={event.pubkey}
class="border border-solid"
style="border-color: var(--line)"
size={8} />
<ProfileCircle pubkey={event.pubkey} class="border border-solid border-line" size={8} />
</Button>
{:else}
<div class="w-8 shrink-0"></div>
@ -125,7 +121,7 @@
</span>
</div>
{/if}
<div class:mt-2={showPubkey && event.kind !== MESSAGE}>
<div class={cx({"mt-2": showPubkey && event.kind !== MESSAGE})}>
<RoomItemContent {url} event={$innerEvent ?? event} />
{#if thunk}
<ThunkFailure showToastOnRetry {thunk} class="mt-1 flex justify-end" />

View file

@ -1,9 +1,15 @@
<script lang="ts">
import type {TrustedEvent} from "@welshman/util"
import Bolt from "@assets/icons/bolt.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import ZapButton from "@app/components/ZapButton.svelte"
const {url, event} = $props()
type Props = {
url: string
event: TrustedEvent
}
const {url, event}: Props = $props()
</script>
<ZapButton aria-label="Send a zap" {url} {event} class="button button-xs button-neutral join-item">

View file

@ -17,7 +17,7 @@
import {deriveRoomMembers, deriveUserIsRoomAdmin} from "@app/rooms"
import {pushModal} from "@app/modal"
interface Props {
type Props = {
url: string
h: string
}

View file

@ -1,5 +1,5 @@
<script lang="ts">
import * as nip19 from "nostr-tools/nip19"
import cx from "classnames"
import type {TrustedEvent} from "@welshman/util"
import Pin from "@assets/icons/pin.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
@ -8,6 +8,7 @@
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ProfileName from "@app/components/ProfileName.svelte"
import RoomItemContent from "@app/components/RoomItemContent.svelte"
import RoomPinnedMessagesAll from "@app/components/RoomPinnedMessagesAll.svelte"
import {deriveRoomPinnedEvents} from "@app/roomPins"
@ -24,26 +25,13 @@
const pinnedEvents = deriveRoomPinnedEvents(url, h)
let currentIndex = $state(0)
let expanded = $state(false)
let root: HTMLElement | undefined = $state()
const total = $derived($pinnedEvents.length)
const currentEvent = $derived($pinnedEvents[currentIndex])
const counter = $derived(total > 0 ? `${currentIndex + 1}/${total}` : "")
const counter = $derived(`${currentIndex + 1}/${total}`)
const preview = (event: TrustedEvent) => event.content.replace(/\s+/g, " ").trim()
const formatNpub = (pubkey: string) => {
const npub = nip19.npubEncode(pubkey)
return `${npub.slice(0, 10)}...${npub.slice(-5)}`
}
const reset = (_url: string, _h: string) => {
currentIndex = 0
expanded = false
}
const expand = () => {
expanded = true
}
@ -52,90 +40,67 @@
expanded = false
}
const showAll = () => {
pushModal(RoomPinnedMessagesAll, {url, h})
}
const openCounter = () => {
showAll()
}
const showAll = () => pushModal(RoomPinnedMessagesAll, {url, h})
const prev = () => {
if (total === 0) {
return
}
currentIndex = (currentIndex - 1 + total) % total
}
const next = () => {
if (total === 0) {
return
}
currentIndex = (currentIndex + 1) % total
}
const jumpToMessage = () => {
if (!currentEvent) {
return
}
collapse()
goToEvent(currentEvent)
}
const onDocumentPointerDown = (event: PointerEvent) => {
if (!expanded || !root) {
return
}
if (!root.contains(event.target as Node)) {
if (event.target instanceof Node && !root?.contains(event.target)) {
collapse()
}
}
$effect(() => {
reset(url, h)
})
let expanded = $state(false)
let root: HTMLElement | undefined = $state()
$effect(() => {
if (currentIndex >= $pinnedEvents.length) {
if (currentIndex >= total) {
currentIndex = 0
}
})
$effect(() => {
if (!expanded) {
return
if (expanded) {
document.addEventListener("pointerdown", onDocumentPointerDown)
return () => document.removeEventListener("pointerdown", onDocumentPointerDown)
}
document.addEventListener("pointerdown", onDocumentPointerDown)
return () => document.removeEventListener("pointerdown", onDocumentPointerDown)
})
</script>
{#if total > 0 && currentEvent}
{#if currentEvent}
<div class="room-pins-anchor" bind:this={root} in:fly={{y: -16, duration: 250}}>
<div class="room-pins" class:room-pins--expanded={expanded}>
<div class={cx("room-pins", {"room-pins--expanded": expanded})}>
<div class="room-pins__header">
{#if expanded}
<div class="flex min-w-0 flex-1 items-start gap-3">
<ProfileCircle pubkey={currentEvent.pubkey} {url} size={6} />
<div class="min-w-0">
<p class="room-pins__label">Pinned message</p>
<p class="text-primary-content/70 truncate text-xs">
{formatNpub(currentEvent.pubkey)}
</p>
{#key currentEvent.pubkey}
{#if expanded}
<div class="flex min-w-0 flex-1 items-start gap-3">
<ProfileCircle pubkey={currentEvent.pubkey} {url} size={6} />
<div class="min-w-0">
<p class="room-pins__label">Pinned message</p>
<p class="text-primary-content/70 truncate text-xs">
<ProfileName pubkey={currentEvent.pubkey} {url} />
</p>
</div>
</div>
</div>
{:else}
<button type="button" class="room-pins__expand" onclick={expand}>
<ProfileCircle pubkey={currentEvent.pubkey} {url} size={5} />
<p class="room-pins__preview">{preview(currentEvent) || "Pinned message"}</p>
</button>
{/if}
{:else}
<button type="button" class="room-pins__expand" onclick={expand}>
<ProfileCircle pubkey={currentEvent.pubkey} {url} size={5} />
<p class="room-pins__preview">{preview(currentEvent) || "Pinned message"}</p>
</button>
{/if}
{/key}
<div class="room-pins__counter">
<Button
class="button button-ghost button-sm gap-1 text-primary-content"
@ -163,7 +128,7 @@
<Icon icon={AltArrowLeft} size={4} />
</Button>
{/if}
<button type="button" class="px-1 text-sm" onclick={openCounter}>{counter}</button>
<button type="button" class="px-1 text-sm" onclick={showAll}>{counter}</button>
{#if total > 1}
<Button
class="button button-ghost button-sm button-square text-primary-content"

View file

@ -99,7 +99,7 @@
return "older"
}
const onResultClick = (event: TrustedEvent) => {
const onResultClick = (event: TrustedEvent) => () => {
popModal()
goToEvent(event, {keepFocus: true})
}
@ -158,7 +158,7 @@
{#each events as event (event.id)}
<Button
class="card card-sm card-interactive flex flex-col gap-2"
onclick={() => onResultClick(event)}>
onclick={onResultClick(event)}>
<NoteCard minimal {event} {url}>
<NoteContentMinimal {event} />
</NoteCard>

View file

@ -33,7 +33,7 @@
const roomSearch = deriveUserRoomSearch()
const toggle = (path: string) => {
const toggle = (path: string) => () => {
selection = path === selection ? "" : path
}
@ -73,7 +73,7 @@
"flex w-full min-w-0 items-center justify-between gap-2 button",
selection === path ? "button-primary" : "button-neutral",
)}
onclick={() => toggle(path)}>
onclick={toggle(path)}>
{#if h}
<RoomNameWithImage {url} {h} />
{:else}
@ -99,7 +99,7 @@
"flex w-full min-w-0 items-center justify-start gap-2 button",
selection === path ? "button-primary" : "button-neutral",
)}
onclick={() => toggle(path)}>
onclick={toggle(path)}>
{#if others.length > 1}
<ProfileCircles pubkeys={others} size={5} />
{:else}

View file

@ -36,7 +36,7 @@
shareTo(makeRoomPath(url, selection), {type: "event", value: event})
}
const toggleRoom = (h: string) => {
const toggleRoom = (h: string) => () => {
selection = h === selection ? "" : h
}
@ -60,7 +60,7 @@
class="flex justify-center button {selection === room.h
? 'button-primary'
: 'button-neutral'}"
onclick={() => toggleRoom(room.h)}>
onclick={toggleRoom(room.h)}>
<RoomNameWithImage {url} h={room.h} />
</Button>
{/each}

View file

@ -14,7 +14,7 @@
import {REPORT} from "@welshman/util"
import {deriveSpaceActionItems} from "@app/actionItems"
interface Props {
type Props = {
url: string
}

View file

@ -33,7 +33,7 @@
const back = () => history.back()
const editMethods = (pubkey: string) => pushModal(SpaceMemberMethods, {url, pubkey})
const editMethods = (pubkey: string) => () => pushModal(SpaceMemberMethods, {url, pubkey})
let loading = $state(true)
@ -80,7 +80,7 @@
<Button
class="button button-ghost button-sm button-square"
aria-label="Edit permissions"
onclick={() => editMethods(pubkey)}>
onclick={editMethods(pubkey)}>
<Icon size={4} icon={Pen} />
</Button>
{/if}

View file

@ -11,12 +11,12 @@
import {pageLoading} from "@app/loading"
import {decodeRelay} from "@app/relays"
interface Props {
type Props = {
back?: () => unknown
leading?: Snippet
title?: Snippet
action?: Snippet
[key: string]: any
class?: string
}
const {back, leading, title, action, ...props}: Props = $props()

View file

@ -14,7 +14,11 @@
import {leaveSpace} from "@app/access"
import {pushToast} from "@app/toast"
const {url} = $props()
type Props = {
url: string
}
const {url}: Props = $props()
const back = () => history.back()

View file

@ -125,7 +125,7 @@
<RelaySummary url={inviteData.url} />
<SpaceJoinNotifications bind:notifications />
{#if error}
<SpaceJoinStatus url={inviteData.url} {error} />
<SpaceJoinStatus {error} />
{/if}
</div>
</div>

View file

@ -62,7 +62,7 @@
<RelaySummary {url} />
<SpaceJoinNotifications bind:notifications />
{#if error}
<SpaceJoinStatus {url} {error} />
<SpaceJoinStatus {error} />
{/if}
</ModalBody>
<ModalFooter>

View file

@ -2,29 +2,21 @@
import DangerTriangle from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import StatusIndicator from "@lib/components/StatusIndicator.svelte"
import SocketStatusIndicator from "@app/components/SocketStatusIndicator.svelte"
type Props = {
url: string
error?: string
error: string
}
const {url, error}: Props = $props()
const {error}: Props = $props()
</script>
<div class="card card-sm flex flex-col gap-2">
<div class="flex justify-between">
<strong>Connection Status</strong>
{#if error}
<StatusIndicator class="bg-error text-error-content">Error</StatusIndicator>
{:else}
<SocketStatusIndicator {url} />
{/if}
<StatusIndicator class="bg-error text-error-content">Error</StatusIndicator>
</div>
<div class="flex items-center gap-2">
<Icon icon={DangerTriangle} />
<p class="text-sm opacity-75">{error}</p>
</div>
{#if error}
<div class="flex items-center gap-2">
<Icon icon={DangerTriangle} />
<p class="text-sm opacity-75">{error}</p>
</div>
{/if}
</div>

View file

@ -10,7 +10,7 @@
import {pushModal} from "@app/modal"
import {profiles} from "@app/core"
interface Props {
type Props = {
url: string
pubkey: string
roles?: RelayRoleReader[]

View file

@ -15,7 +15,7 @@
import SpaceMemberBannedMenu from "@app/components/SpaceMemberBannedMenu.svelte"
import {deriveSpacePermissions, spaceManagement} from "@app/management"
interface Props {
type Props = {
url: string
}

View file

@ -1,53 +0,0 @@
<script lang="ts">
import {REPORT} from "@welshman/util"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import ReportItem from "@app/components/ReportItem.svelte"
import RelayName from "@app/components/RelayName.svelte"
import {events} from "@app/core"
interface Props {
url: string
}
const {url}: Props = $props()
const reports = $events.forUrl(url, [{kinds: [REPORT]}]).$
const back = () => history.back()
const onResolved = () => {
if ($reports.length === 0) {
back()
}
}
</script>
<Modal>
<ModalBody>
<ModalHeader>
<ModalTitle>Reports</ModalTitle>
<ModalSubtitle>on <RelayName {url} class="text-primary" /></ModalSubtitle>
</ModalHeader>
<div class="flex flex-col gap-2">
{#each $reports as event (event.id)}
<ReportItem {url} {event} {onResolved} />
{:else}
<p class="py-12 text-center">No reports found.</p>
{/each}
</div>
</ModalBody>
<ModalFooter>
<Button class="button button-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
</ModalFooter>
</Modal>

View file

@ -19,6 +19,14 @@
const toggle = () => (playing ? audio?.pause() : audio?.play())
const onPlay = () => {
playing = true
}
const onStop = () => {
playing = false
}
const display = (seconds: number) => {
const whole = Math.floor(seconds)
@ -35,9 +43,9 @@
bind:duration
src={$speech.src}
autoplay
onplay={() => (playing = true)}
onpause={() => (playing = false)}
onended={() => (playing = false)}></audio>
onplay={onPlay}
onpause={onStop}
onended={onStop}></audio>
<Button
aria-label={playing ? "Pause message" : "Play message"}
class="button button-circle button-sm button-primary shrink-0"

View file

@ -44,12 +44,12 @@
<article
id="post-{event.id}"
data-event={event.id}
class={cx("bg-surface border-b @container", {"highlight-target": $highlightedEvent === event.id})}
style="border-color: var(--line)">
class={cx("bg-surface border-b border-line @container", {
"highlight-target": $highlightedEvent === event.id,
})}>
<div class="flex flex-col @2xl:flex-row">
<aside
class="bg-surface flex shrink-0 flex-row items-center gap-3 border-b p-3 @2xl:w-40 @2xl:flex-col @2xl:items-center @2xl:border-b-0 @2xl:border-r @2xl:p-4 @2xl:text-center"
style="border-color: var(--line)">
class="bg-surface flex shrink-0 flex-row items-center gap-3 border-b p-3 @2xl:w-40 @2xl:flex-col @2xl:items-center @2xl:border-b-0 @2xl:border-r @2xl:p-4 @2xl:text-center border-line">
<Button onclick={openProfile}>
<ProfileCircle pubkey={event.pubkey} {url} size={10} class="@2xl:size-14" />
</Button>
@ -67,8 +67,7 @@
</aside>
<div class="flex min-w-0 grow flex-col">
<div
class="bg-surface flex flex-wrap items-center justify-between gap-2 border-b px-3 py-2 text-xs @lg:px-4 @lg:text-sm"
style="border-color: var(--line)">
class="bg-surface flex flex-wrap items-center justify-between gap-2 border-b px-3 py-2 text-xs @lg:px-4 @lg:text-sm border-line">
<div class="flex items-center gap-2 opacity-75">
<span>{formatTimestamp(event.created_at)}</span>
{#if replyCount !== undefined}
@ -87,8 +86,7 @@
<NoteContent showEntire {event} {url} />
</div>
<div
class="bg-surface flex shrink-0 flex-col gap-2 border-t px-3 py-3 @lg:flex-row @lg:items-center @lg:justify-between @lg:px-4"
style="border-color: var(--line)">
class="bg-surface flex shrink-0 flex-col gap-2 border-t px-3 py-3 @lg:flex-row @lg:items-center @lg:justify-between @lg:px-4 border-line">
<Button class="button button-neutral button-xs w-fit gap-1" onclick={reply}>
<Icon icon={Reply} size={4} />
Reply

View file

@ -20,8 +20,7 @@
<Button
onclick={onClick}
aria-label="Back to the top of the thread"
class="bg-surface flex w-full items-center gap-3 border-b px-4 py-2 shadow-md"
style="border-color: var(--line)">
class="bg-surface flex w-full items-center gap-3 border-b px-4 py-2 shadow-md border-line">
<ProfileCircle pubkey={event.pubkey} {url} size={8} class="shrink-0" />
<div class="flex min-w-0 flex-col">
<span class="truncate text-sm font-bold">{title || "Untitled thread"}</span>

View file

@ -24,10 +24,10 @@
<span class="opacity-50">Sending...</span>
<button
type="button"
class="underline transition-all"
class:link={isSending}
class:opacity-25={!isSending}
class:pointer-events-none={!isSending}
class={cx("underline transition-all", {
link: isSending,
"pointer-events-none opacity-25": !isSending,
})}
onclick={stopPropagation(abort)}>
Cancel
</button>

View file

@ -3,7 +3,7 @@
import ThunkFailure from "@app/components/ThunkFailure.svelte"
import ThunkPending from "@app/components/ThunkPending.svelte"
interface Props {
type Props = {
thunk: BaseThunk
showToastOnRetry?: boolean
inline?: boolean

View file

@ -8,7 +8,7 @@
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
interface Props {
type Props = {
thunk: BaseThunk
retry: (url: string) => void
}
@ -24,6 +24,8 @@
isPartial ? `Partial delivery ${successUrls.length}/${total} relays` : "Failed to send!",
)
const retryUrl = (url: string) => () => retry(url)
const relayMessage = (status: PublishStatus | undefined, detail: string | undefined) => {
if (status === PublishStatus.Timeout) {
return "request timed out."
@ -58,9 +60,7 @@
<p class="break-all">{displayRelayUrl(url)}</p>
<p class="text-xs opacity-60">{relayMessage(status, detail)}</p>
</div>
<Button
class="button button-link shrink-0 px-1"
onclick={stopPropagation(() => retry(url))}>
<Button class="button button-link shrink-0 px-1" onclick={stopPropagation(retryUrl(url))}>
Retry
</Button>
</div>

View file

@ -46,7 +46,7 @@
tippy?.hide()
}
const removeTopic = (topic: string) => {
const removeTopic = (topic: string) => () => {
value = remove(topic, value)
}
@ -83,7 +83,7 @@
<div>
{#each value as topic (topic)}
<div class="flex-inline badge badge-neutral mr-1 gap-1">
<Button class="flex items-center" onclick={() => removeTopic(topic)}>
<Button class="flex items-center" onclick={removeTopic(topic)}>
<Icon icon={CloseCircle} size={4} class="mt-px" />
</Button>
<span>#{topic}</span>

View file

@ -24,7 +24,7 @@
import {cancelJoinVoiceRoom} from "@app/callEngine"
import {profiles} from "@app/core"
interface Props {
type Props = {
url: string
h: string
notification?: boolean

View file

@ -18,7 +18,7 @@
import {WotScope} from "@welshman/app"
import {followLists, user, wot} from "@app/core"
interface Props {
type Props = {
pubkey: string
}

View file

@ -25,7 +25,7 @@
content = emoji
}
const selectAmount = (preset: number) => {
const selectAmount = (preset: number) => () => {
amount = preset
}
</script>
@ -55,7 +55,7 @@
"button-primary": preset === amount,
"button-neutral": preset !== amount,
})}
onclick={() => selectAmount(preset)}>
onclick={selectAmount(preset)}>
{preset}
</Button>
{/each}

View file

@ -36,8 +36,8 @@
const back = () => history.back()
const onFileChange = (event: Event) => {
file = (event.target as HTMLInputElement).files?.[0]
const onFileChange = (event: Event & {currentTarget: HTMLInputElement}) => {
file = event.currentTarget.files?.[0]
result = undefined
}

View file

@ -22,6 +22,10 @@
const memberLabel = (members: Plan["members"]) =>
members ? `Up to ${members} members` : "Unlimited members"
const select = (planId: string) => () => onSelect?.(planId)
const start = (planId: string) => () => onCta?.(planId)
const visiblePlans = $derived(plans.filter(plan => !plan.hidden))
const gridCols: Record<number, string> = {
@ -70,7 +74,7 @@
"button-primary": isPopular,
"button-neutral": !isPopular,
})}
onclick={() => onCta?.(plan.id)}>
onclick={start(plan.id)}>
Get started
</Button>
{/if}
@ -85,7 +89,7 @@
class={cx("card card-interactive relative flex flex-col text-left", {
"border-primary": value === plan.id,
})}
onclick={() => onSelect?.(plan.id)}>
onclick={select(plan.id)}>
{@render cardBody(plan, isPopular)}
</Button>
{:else}

View file

@ -229,7 +229,7 @@ export const makeEditor = async ({
addProseMirrorPlugins() {
return [
MentionSuggestion({
editor: (this as any).editor,
editor: this.editor,
search: (term: string) => get(searchProfiles)(term),
getRelays: (pubkey: string) => relayLists.get().writeUrls(pubkey).get(),
updateSignal: searchProfiles,
@ -244,7 +244,7 @@ export const makeEditor = async ({
TippySuggestion({
char: "~",
name: "roomref",
editor: (this as any).editor,
editor: this.editor,
search: (term: string) => get(roomReferenceSearch).searchValues(term),
updateSignal: roomReferenceSearch,
select: (id: string, props) => {
@ -265,7 +265,7 @@ export const makeEditor = async ({
...(commandTarget
? [
CommandSuggestion({
editor: (this as any).editor,
editor: this.editor,
search: (term: string) => get(commandSearch)?.searchValues(term) ?? [],
updateSignal: commandSearch,
getAttributes: getCommandAttributes,

View file

@ -28,6 +28,7 @@ import {entityLink, PLATFORM_URL, PLATFORM_RELAYS} from "@app/env"
import {decodeRelay, encodeRelay} from "@app/relays"
import {DM_KINDS} from "@app/content"
import {navigate, pushModal} from "@app/modal"
import type {NavigateOptions} from "@app/modal"
import type {NotePointer} from "@app/social"
import ChatEnable from "@app/components/ChatEnable.svelte"
import NoteDetail from "@app/components/NoteDetail.svelte"
@ -288,7 +289,7 @@ export const goToNote = (pointer: NotePointer) => {
return goToHome()
}
export const goToEvent = (event: TrustedEvent, options: Record<string, any> = {}) => {
export const goToEvent = (event: TrustedEvent, options: NavigateOptions = {}) => {
const urls = Array.from(app.get().tracker.getRelays(event.id))
if (event.kind === NOTE) {

View file

@ -210,10 +210,10 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
const result = {...task, tags, url}
return {result}
} catch (e: any) {
} catch (e) {
console.error("Error caught when uploading file:", e)
return {error: e.toString()}
return {error: String(e)}
}
}

View file

@ -42,8 +42,9 @@
import {syncKeyboard} from "@app/keyboard"
import {getPageTitle} from "@app/title"
import NewNotificationSound from "@src/app/components/NewNotificationSound.svelte"
import type {LayoutProps} from "./$types"
const {children} = $props()
const {children}: LayoutProps = $props()
applySavedTheme()

View file

@ -15,29 +15,17 @@
const {bech32} = params
const attemptToNavigate = async () => {
const profilePubkey = decodePubkey(bech32)
if (profilePubkey) {
return goto(makeProfilePath(profilePubkey), {replaceState: true})
}
const {type, data} = nip19.decode(bech32) as any
if (!["nevent", "naddr"].includes(type) && data.relays.length > 0) {
return goto("/", {replaceState: true})
}
const findEvent = (idOrAddress: string, relays: string[] = []) => {
let found = false
$network.loadComplete({
relays: [LOCAL_RELAY_URL, ...data.relays],
filters: getIdFilters([type === "nevent" ? data.id : Address.fromNaddr(bech32).toString()]),
relays: [LOCAL_RELAY_URL, ...relays],
filters: getIdFilters([idOrAddress]),
onEvent: (event: TrustedEvent) => {
found = true
if (event.kind === NOTE) {
goToNote({id: event.id, pubkey: event.pubkey, relays: data.relays})
goToNote({id: event.id, pubkey: event.pubkey, relays})
} else {
goToEvent(event, {replaceState: true})
}
@ -50,6 +38,24 @@
})
}
const attemptToNavigate = async () => {
const profilePubkey = decodePubkey(bech32)
if (profilePubkey) {
await goto(makeProfilePath(profilePubkey), {replaceState: true})
} else {
const decoded = nip19.decode(bech32)
if (decoded.type === "nevent") {
findEvent(decoded.data.id, decoded.data.relays)
} else if (decoded.type === "naddr") {
findEvent(Address.fromNaddr(bech32).toString(), decoded.data.relays)
} else {
await goto("/", {replaceState: true})
}
}
}
onMount(async () => {
try {
await attemptToNavigate()

View file

@ -11,18 +11,15 @@
<script lang="ts">
import {onMount} from "svelte"
import {append, uniq} from "@welshman/lib"
import Chat from "@app/components/Chat.svelte"
import {splitChatId} from "@app/chats"
import {pushToast} from "@app/toast"
import {Push, notificationSettings} from "@app/push"
import {user} from "@app/core"
import type {PageProps} from "./$types"
const {params}: PageProps = $props()
const {chat} = params
const pubkeys = uniq(append($user.pubkey, splitChatId(chat)))
const pubkeys = splitChatId(params.chat)
onMount(async () => {
if (!$dmNotificationsPrompted) {

View file

@ -27,7 +27,7 @@
</script>
<Page>
<PageContent bind:element class="p-0 md:p-4">
<PageContent bind:element class="@container p-0 md:p-4">
<div class="mx-auto flex w-full max-w-[1200px] min-w-0 flex-col gap-3">
<div class="flex items-center justify-between gap-4 px-4 py-3 md:px-2 md:pt-0">
<h1 class="flex items-center gap-2 text-xl font-bold">
@ -39,18 +39,18 @@
Browse spaces
</Link>
</div>
<div class="flex min-w-0 flex-col gap-3 lg:flex-row lg:items-start">
<!-- Both columns are `contents` below lg, which puts every section in one flow and lets
<div class="flex min-w-0 flex-col gap-3 @2xl:flex-row @2xl:items-start">
<!-- Both columns are `contents` on a narrow page, which puts every section in one flow and lets
the rail sit between the day's activity and the feed that would otherwise bury it. -->
<div class="contents lg:flex lg:min-w-0 lg:flex-1 lg:flex-col lg:gap-3">
<div class="contents @2xl:flex @2xl:min-w-0 @2xl:flex-1 @2xl:flex-col @2xl:gap-3">
<HomeInbox />
<HomeActivity />
<HomeNetwork class="order-last lg:order-none" />
<HomeNetwork class="order-last @2xl:order-none" />
</div>
<div class="contents lg:flex lg:w-80 lg:shrink-0 lg:flex-col lg:gap-3">
<div class="contents @2xl:flex @2xl:w-80 @2xl:shrink-0 @2xl:flex-col @2xl:gap-3">
<HomeHealthChecks />
{#if HOSTING_ENABLED}
<HomeHosting class="max-lg:hidden" />
<HomeHosting class="@max-2xl:hidden" />
{/if}
</div>
</div>

View file

@ -24,15 +24,14 @@
settings.reminder_minutes = [...settings.reminder_minutes, 60]
}
const removeReminder = (index: number) => {
const removeReminder = (index: number) => () => {
settings.reminder_minutes = removeAt(index, settings.reminder_minutes)
}
const onReminderInput = (e: Event) => {
const target = e.currentTarget as HTMLInputElement
const index = Number(target.dataset.index)
const onReminderInput = (index: number) => (e: Event & {currentTarget: HTMLInputElement}) => {
const minutes = Number(e.currentTarget.value)
settings.reminder_minutes = replaceAt(index, Number(target.value), settings.reminder_minutes)
settings.reminder_minutes = replaceAt(index, minutes, settings.reminder_minutes)
}
const onsubmit = preventDefault(async () => {
@ -157,16 +156,15 @@
<Button
class="button button-ghost button-sm"
type="button"
onclick={() => removeReminder(index)}>
onclick={removeReminder(index)}>
<Icon icon={TrashBin2} />
</Button>
<label class="input flex grow items-center gap-2">
<input
type="number"
class="w-16"
data-index={index}
value={minutes}
oninput={onReminderInput} />
oninput={onReminderInput(index)} />
<span class="text-sm opacity-75">minutes before it starts</span>
</label>
</div>

View file

@ -36,6 +36,8 @@
blossomServers = $userBlossomServerList?.urls() ?? []
}
const setTheme = (value: string) => () => theme.set(value)
const addServer = () => {
blossomServers = [...blossomServers, ""]
}
@ -98,7 +100,7 @@
<Button
type="button"
class={cx("join-item button button-xs", $theme === value ? "button-primary" : "button-neutral")}
onclick={() => theme.set(value)}>
onclick={setTheme(value)}>
<Icon {icon} size={4} />
{label}
</Button>

View file

@ -85,11 +85,15 @@
const startRecovery = () => pushModal(InfoKeys)
const toggleAdvanced = () => {
showAdvanced = !showAdvanced
}
let loading = $state(false)
let showAdvanced = $state(false)
</script>
<PageContent>
<PageContent class="@container">
<div class="card shadow-md flex flex-col gap-2">
<div class="flex justify-between gap-2">
<Link href={makeProfilePath($user.pubkey)} class="flex max-w-full gap-3">
@ -179,7 +183,7 @@
{/if}
<SignerStatus />
{#if pomadeEmail}
<div class="flex flex-col lg:flex-row gap-4 lg:gap-2 justify-end">
<div class="flex flex-col @2xl:flex-row gap-4 @2xl:gap-2 justify-end">
<Button class="button button-neutral" onclick={startPasswordReset} disabled={loading}>
<Spinner {loading}>Update your password</Spinner>
</Button>
@ -194,7 +198,7 @@
<Icon icon={Settings} />
Advanced
</strong>
<Button onclick={() => (showAdvanced = !showAdvanced)}>
<Button onclick={toggleAdvanced}>
{#if showAdvanced}
<Icon icon={AltArrowUp} />
{:else}

View file

@ -94,17 +94,14 @@
$settings.zap_amounts = [...$settings.zap_amounts, $settings.zap_amounts.at(-1) || 21]
}
const removeZapAmount = (index: number) => {
const removeZapAmount = (index: number) => () => {
if ($settings.zap_amounts.length > 1) {
$settings.zap_amounts = removeAt(index, $settings.zap_amounts)
}
}
const onZapAmountInput = (e: Event) => {
const target = e.currentTarget as HTMLInputElement
const index = Number(target.dataset.index)
$settings.zap_amounts = replaceAt(index, Number(target.value), $settings.zap_amounts)
const onZapAmountInput = (index: number) => (e: Event & {currentTarget: HTMLInputElement}) => {
$settings.zap_amounts = replaceAt(index, Number(e.currentTarget.value), $settings.zap_amounts)
}
const onZapAmountsSubmit = preventDefault(async () => {
@ -140,7 +137,7 @@
let zapAmountsLoading = $state(false)
</script>
<PageContent>
<PageContent class="@container">
<div class="card flex flex-col gap-6">
<div class="flex items-center justify-between">
<strong class="flex items-center gap-3 text-lg">
@ -171,7 +168,7 @@
{#if $wallet}
{#if $wallet.type === "webln"}
{@const {node, version} = $wallet.info}
<div class="flex flex-col justify-between gap-2 lg:flex-row">
<div class="flex flex-col justify-between gap-2 @2xl:flex-row">
<p>
{connectionVerb} <strong>{node?.alias || version || "unknown wallet"}</strong>
via <strong>{$wallet.type}</strong>
@ -199,7 +196,7 @@
</div>
{:else if $wallet.type === "nwc"}
{@const {lud16, relayUrl} = $wallet.info}
<div class="flex flex-col justify-between gap-2 lg:flex-row">
<div class="flex flex-col justify-between gap-2 @2xl:flex-row">
<p>
{connectionVerb} <strong>{lud16}</strong> via
<strong>{displayRelayUrl(relayUrl)}</strong>
@ -224,21 +221,21 @@
</p>
</div>
{/if}
<div class="flex flex-col gap-4 lg:flex-row lg:justify-between">
<div class="flex flex-col gap-4 @2xl:flex-row @2xl:justify-between">
<Button class="button button-neutral button-sm" onclick={disconnect}>
<Icon icon={CloseCircle} />
Disconnect Wallet
</Button>
<div class="flex w-full gap-4 lg:w-auto">
<div class="flex w-full gap-4 @2xl:w-auto">
<Button
class="button button-primary button-sm flex-1 justify-center lg:flex-none"
class="button button-primary button-sm flex-1 justify-center @2xl:flex-none"
onclick={pay}
disabled={!isWalletAvailable}>
<Icon icon={UploadMinimalistic} />
Send
</Button>
<Button
class="button button-secondary button-sm flex-1 justify-center lg:flex-none"
class="button button-secondary button-sm flex-1 justify-center @2xl:flex-none"
onclick={receive}
disabled={!isWalletAvailable}>
<Icon icon={DownloadMinimalistic} />
@ -280,18 +277,13 @@
<Button
class="button button-ghost button-sm"
type="button"
onclick={() => removeZapAmount(index)}
onclick={removeZapAmount(index)}
disabled={$settings.zap_amounts.length === 1}>
<Icon icon={TrashBin2} />
</Button>
<label class="input flex grow items-center gap-2">
<Icon icon={Bolt} />
<input
type="number"
class="grow"
data-index={index}
value={amount}
oninput={onZapAmountInput} />
<input type="number" class="grow" value={amount} oninput={onZapAmountInput(index)} />
</label>
</div>
{/each}

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