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:
parent
66bb75797b
commit
52583f970c
108 changed files with 519 additions and 499 deletions
|
|
@ -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`.
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})}>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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=""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
<script lang="ts">
|
||||
const {value} = $props()
|
||||
type Props = {
|
||||
value: string
|
||||
}
|
||||
|
||||
const {value}: Props = $props()
|
||||
</script>
|
||||
|
||||
{#each value as _, i (i)}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
<script lang="ts">
|
||||
const {value} = $props()
|
||||
type Props = {
|
||||
value: string
|
||||
}
|
||||
|
||||
const {value}: Props = $props()
|
||||
</script>
|
||||
|
||||
<span class="link-content">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
import {pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
email?: string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
import {network} from "@app/core"
|
||||
import {makeFeedContext} from "@app/feeds"
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
url: string
|
||||
pubkey: string
|
||||
limit?: number
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import type {RelayRoleReader} from "@welshman/domain"
|
||||
import RoleBadge from "@app/components/RoleBadge.svelte"
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
role: RelayRoleReader
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
import {deriveRoomMembers, deriveUserIsRoomAdmin} from "@app/rooms"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
url: string
|
||||
h: string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
import {REPORT} from "@welshman/util"
|
||||
import {deriveSpaceActionItems} from "@app/actionItems"
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
url: string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@
|
|||
<RelaySummary url={inviteData.url} />
|
||||
<SpaceJoinNotifications bind:notifications />
|
||||
{#if error}
|
||||
<SpaceJoinStatus url={inviteData.url} {error} />
|
||||
<SpaceJoinStatus {error} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@
|
|||
<RelaySummary {url} />
|
||||
<SpaceJoinNotifications bind:notifications />
|
||||
{#if error}
|
||||
<SpaceJoinStatus {url} {error} />
|
||||
<SpaceJoinStatus {error} />
|
||||
{/if}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
import {pushModal} from "@app/modal"
|
||||
import {profiles} from "@app/core"
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
url: string
|
||||
pubkey: string
|
||||
roles?: RelayRoleReader[]
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
import SpaceMemberBannedMenu from "@app/components/SpaceMemberBannedMenu.svelte"
|
||||
import {deriveSpacePermissions, spaceManagement} from "@app/management"
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
url: string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
import {cancelJoinVoiceRoom} from "@app/callEngine"
|
||||
import {profiles} from "@app/core"
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
url: string
|
||||
h: string
|
||||
notification?: boolean
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
import {WotScope} from "@welshman/app"
|
||||
import {followLists, user, wot} from "@app/core"
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
pubkey: string
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue