Refactor avatar components, add space edit form

This commit is contained in:
Jon Staab
2025-11-11 13:39:32 -08:00
parent 183aebf841
commit 8e411daaef
32 changed files with 356 additions and 157 deletions
+4 -12
View File
@@ -2,21 +2,14 @@
import {type Instance} from "tippy.js"
import {hash, formatTimestampAsTime} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {
thunks,
mergeThunks,
pubkey,
deriveProfile,
deriveProfileDisplay,
sendWrapped,
} from "@welshman/app"
import {thunks, mergeThunks, pubkey, deriveProfileDisplay, sendWrapped} from "@welshman/app"
import {isMobile} from "@lib/html"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Tippy from "@lib/components/Tippy.svelte"
import TapTarget from "@lib/components/TapTarget.svelte"
import Avatar from "@lib/components/Avatar.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import Content from "@app/components/Content.svelte"
import ReactionSummary from "@app/components/ReactionSummary.svelte"
import ThunkFailure from "@app/components/ThunkFailure.svelte"
@@ -37,7 +30,6 @@
const {event, replyTo, pubkeys, showPubkey = false}: Props = $props()
const isOwn = event.pubkey === $pubkey
const profile = deriveProfile(event.pubkey)
const profileDisplay = deriveProfileDisplay(event.pubkey)
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
@@ -107,8 +99,8 @@
<div class="flex items-center gap-2">
{#if !isOwn}
<Button onclick={openProfile} class="flex items-center gap-1">
<Avatar
src={$profile?.picture}
<ProfileCircle
pubkey={event.pubkey}
class="border border-solid border-base-content"
size={4} />
<div class="flex items-center gap-2">
+2 -2
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import {removeNil} from "@welshman/lib"
import {removeUndefined} from "@welshman/lib"
import type {ProfilePointer} from "@welshman/content"
import {deriveProfileDisplay} from "@welshman/app"
import Button from "@lib/components/Button.svelte"
@@ -13,7 +13,7 @@
const {value, url}: Props = $props()
const display = deriveProfileDisplay(value.pubkey, removeNil([url]))
const display = deriveProfileDisplay(value.pubkey, removeUndefined([url]))
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url})
</script>
+2 -2
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import Link from "@lib/components/Link.svelte"
import CardButton from "@lib/components/CardButton.svelte"
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
import RelayIcon from "@app/components/RelayIcon.svelte"
import RelayName from "@app/components/RelayName.svelte"
import RelayDescription from "@app/components/RelayDescription.svelte"
import {makeSpacePath} from "@app/util/routes"
@@ -15,7 +15,7 @@
<Link replaceState href={path}>
<CardButton class="btn-neutral shadow-md">
{#snippet icon()}
<div><SpaceAvatar {url} /></div>
<RelayIcon {url} size={12} />
{/snippet}
{#snippet title()}
<div class="flex gap-1">
+23 -18
View File
@@ -4,7 +4,15 @@
import {goto} from "$app/navigation"
import {splitAt} from "@welshman/lib"
import {userProfile, shouldUnwrap} from "@welshman/app"
import Avatar from "@lib/components/Avatar.svelte"
import Widget from "@assets/icons/widget.svg?dataurl"
import Compass from "@assets/icons/compass.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import Settings from "@assets/icons/settings.svg?dataurl"
import ImageIcon from "@lib/components/ImageIcon.svelte"
import Divider from "@lib/components/Divider.svelte"
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
import ChatEnable from "@app/components/ChatEnable.svelte"
@@ -15,13 +23,6 @@
import {pushModal} from "@app/util/modal"
import {makeSpacePath} from "@app/util/routes"
import {notifications} from "@app/util/notifications"
import Widget from "@assets/icons/widget.svg?dataurl"
import Compass from "@assets/icons/compass.svg?dataurl"
import Letter from "@assets/icons/letter.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
import Settings from "@assets/icons/settings.svg?dataurl"
type Props = {
children?: Snippet
@@ -61,7 +62,7 @@
<PrimaryNavItemSpace {url} />
{:else}
<PrimaryNavItem title="Home" href="/home" class="tooltip-right">
<Avatar src={PLATFORM_LOGO} class="!h-10 !w-10" />
<ImageIcon alt="Home" src={PLATFORM_LOGO} class="rounded-full" />
</PrimaryNavItem>
<Divider />
{#each primarySpaceUrls as url (url)}
@@ -73,11 +74,11 @@
class="tooltip-right"
onclick={showOtherSpacesMenu}
notification={otherSpaceNotifications}>
<Avatar icon={Widget} class="!h-10 !w-10" />
<ImageIcon alt="Other Spaces" src={Widget} />
</PrimaryNavItem>
{/if}
<PrimaryNavItem title="Add a Space" href="/discover" class="tooltip-right">
<Avatar icon={Compass} class="!h-10 !w-10" />
<ImageIcon alt="Add a Space" src={Compass} size={7} />
</PrimaryNavItem>
{/each}
</div>
@@ -90,17 +91,17 @@
href="/settings/profile"
prefix="/settings"
class="tooltip-right">
<Avatar src={$userProfile?.picture} class="!h-10 !w-10" />
<ImageIcon alt="Settings" src={$userProfile?.picture || UserRounded} class="rounded-full" />
</PrimaryNavItem>
<PrimaryNavItem
title="Messages"
onclick={openChat}
class="tooltip-right"
notification={$notifications.has("/chat")}>
<Avatar icon={Letter} class="!h-10 !w-10" />
<ImageIcon alt="Messages" src={Letter} size={7} />
</PrimaryNavItem>
<PrimaryNavItem title="Search" href="/people" class="tooltip-right">
<Avatar icon={Magnifier} class="!h-10 !w-10" />
<ImageIcon alt="Search" src={Magnifier} size={7} />
</PrimaryNavItem>
</div>
</div>
@@ -115,22 +116,26 @@
<div class="content-padding-x content-sizing flex justify-between px-2">
<div class="flex gap-2 sm:gap-6">
<PrimaryNavItem title="Home" href="/home">
<Avatar icon={HomeSmile} class="!h-10 !w-10" />
<ImageIcon alt="Home" src={HomeSmile} size={7} />
</PrimaryNavItem>
<PrimaryNavItem
title="Messages"
onclick={openChat}
notification={$notifications.has("/chat")}>
<Avatar icon={Letter} class="!h-10 !w-10" />
<ImageIcon alt="Messages" src={Letter} size={7} />
</PrimaryNavItem>
{#if PLATFORM_RELAYS.length !== 1}
<PrimaryNavItem title="Spaces" href="/spaces" notification={anySpaceNotifications}>
<Avatar icon={SettingsMinimalistic} class="!h-10 !w-10" />
<ImageIcon alt="Spaces" src={SettingsMinimalistic} size={7} />
</PrimaryNavItem>
{/if}
</div>
<PrimaryNavItem title="Settings" onclick={showSettingsMenu}>
<Avatar icon={Settings} src={$userProfile?.picture} class="!h-10 !w-10" />
<ImageIcon
alt="Settings"
src={$userProfile?.picture || Settings}
size={7}
class="rounded-full" />
</PrimaryNavItem>
</div>
</div>
@@ -1,7 +1,7 @@
<script lang="ts">
import {displayRelayUrl} from "@welshman/util"
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
import RelayIcon from "@app/components/RelayIcon.svelte"
import {makeSpacePath, goToSpace} from "@app/util/routes"
import {notifications} from "@app/util/notifications"
@@ -15,5 +15,5 @@
title={displayRelayUrl(url)}
class="tooltip-right"
notification={$notifications.has(makeSpacePath(url))}>
<SpaceAvatar {url} />
<RelayIcon {url} size={10} class="rounded-full" />
</PrimaryNavItem>
+5 -11
View File
@@ -1,16 +1,11 @@
<script lang="ts">
import * as nip19 from "nostr-tools/nip19"
import {removeNil} from "@welshman/lib"
import {removeUndefined} from "@welshman/lib"
import {displayPubkey} from "@welshman/util"
import {
deriveHandleForPubkey,
displayHandle,
deriveProfile,
deriveProfileDisplay,
} from "@welshman/app"
import {deriveHandleForPubkey, displayHandle, deriveProfileDisplay} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Avatar from "@lib/components/Avatar.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import WotScore from "@app/components/WotScore.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte"
import {pushModal} from "@app/util/modal"
@@ -26,8 +21,7 @@
const {pubkey, url, showPubkey, avatarSize = 10}: Props = $props()
const relays = removeNil([url])
const profile = deriveProfile(pubkey, relays)
const relays = removeUndefined([url])
const profileDisplay = deriveProfileDisplay(pubkey, relays)
const handle = deriveHandleForPubkey(pubkey)
@@ -38,7 +32,7 @@
<div class="flex max-w-full items-start gap-3">
<Button onclick={openProfile} class="py-1">
<Avatar src={$profile?.picture} size={avatarSize} />
<ProfileCircle {pubkey} size={avatarSize} />
</Button>
<div class="flex min-w-0 flex-col">
<div class="flex items-center gap-2">
+14 -7
View File
@@ -1,17 +1,24 @@
<script lang="ts">
import Avatar from "@lib/components/Avatar.svelte"
import {removeNil} from "@welshman/lib"
import cx from "classnames"
import {removeUndefined} from "@welshman/lib"
import {deriveProfile} from "@welshman/app"
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import ImageIcon from "@lib/components/ImageIcon.svelte"
type Props = {
pubkey: string
class?: string
size?: number
url?: string
} & Record<string, any>
}
const {pubkey, url, ...props}: Props = $props()
const {pubkey, url, size = 7, ...props}: Props = $props()
const profile = deriveProfile(pubkey, removeNil([url]))
const profile = deriveProfile(pubkey, removeUndefined([url]))
</script>
<Avatar src={$profile?.picture} icon={UserCircle} {...props} />
<ImageIcon
{size}
class={cx(props.class, "rounded-full")}
src={$profile?.picture || UserRounded}
alt="Profile picture" />
+8 -3
View File
@@ -1,13 +1,18 @@
<script lang="ts">
import ProfileCircle from "@app/components/ProfileCircle.svelte"
const {...props} = $props()
type Props = {
pubkeys: string[]
size?: number
}
const {pubkeys, size = 7}: Props = $props()
</script>
<div class="flex pr-3">
{#each props.pubkeys.toSorted().slice(0, 15) as pubkey (pubkey)}
{#each pubkeys.toSorted().slice(0, 15) as pubkey (pubkey)}
<div class="z-feature -mr-3 inline-block">
<ProfileCircle class="h-8 w-8 bg-base-300" {pubkey} {...props} />
<ProfileCircle class="h-8 w-8 bg-base-300" {pubkey} {size} />
</div>
{/each}
</div>
+2 -2
View File
@@ -4,7 +4,7 @@
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Letter from "@assets/icons/letter-opened.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Avatar from "@lib/components/Avatar.svelte"
import ImageIcon from "@lib/components/ImageIcon.svelte"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
@@ -41,7 +41,7 @@
</Button>
<div class="flex gap-2">
<Link external href={pubkeyLink(pubkey)} class="btn btn-neutral">
<Avatar src="/coracle.png" />
<ImageIcon alt="Open in Coracle" src="/coracle.png" />
Open in Coracle
</Link>
<Button onclick={openChat} class="btn btn-primary">
+2 -2
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import {removeNil} from "@welshman/lib"
import {removeUndefined} from "@welshman/lib"
import {deriveProfile} from "@welshman/app"
import ContentMinimal from "@app/components/ContentMinimal.svelte"
@@ -10,7 +10,7 @@
const {pubkey, url}: Props = $props()
const profile = deriveProfile(pubkey, removeNil([url]))
const profile = deriveProfile(pubkey, removeUndefined([url]))
</script>
{#if $profile}
+2 -2
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import {removeNil} from "@welshman/lib"
import {removeUndefined} from "@welshman/lib"
import {deriveProfileDisplay} from "@welshman/app"
type Props = {
@@ -9,7 +9,7 @@
const {pubkey, url}: Props = $props()
const profileDisplay = deriveProfileDisplay(pubkey, removeNil([url]))
const profileDisplay = deriveProfileDisplay(pubkey, removeUndefined([url]))
</script>
{$profileDisplay}
+2 -2
View File
@@ -5,7 +5,7 @@
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import SpaceAvatar from "@app/components/SpaceAvatar.svelte"
import RelayIcon from "@app/components/RelayIcon.svelte"
import RelayName from "@app/components/RelayName.svelte"
import {makeSpacePath} from "@app/util/routes"
import {deriveGroupSelections, getSpaceUrlsFromGroupSelections} from "@app/core/state"
@@ -26,7 +26,7 @@
{#each spaceUrls as url (url)}
<div class="card2 bg-alt flex flex-row items-center gap-2">
<div class="flex-shrink-0">
<SpaceAvatar {url} />
<RelayIcon {url} size={12} />
</div>
<div class="flex flex-grow flex-col">
<RelayName {url} />
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts">
import {deriveRelay} from "@welshman/app"
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
import ImageIcon from "@lib/components/ImageIcon.svelte"
type Props = {
url: string
size?: number
class?: string
}
const {url, size = 7, ...props}: Props = $props()
const relay = deriveRelay(url)
</script>
<ImageIcon
{size}
src={$relay?.icon || RemoteControllerMinimalistic}
alt="Relay image"
class={props.class} />
+5 -1
View File
@@ -1,7 +1,11 @@
<script lang="ts">
import {deriveRelayDisplay} from "@welshman/app"
const {url} = $props()
type Props = {
url: string
}
const {url}: Props = $props()
const display = $derived(deriveRelayDisplay(url))
</script>
+5 -4
View File
@@ -7,7 +7,6 @@
thunks,
pubkey,
mergeThunks,
deriveProfile,
deriveProfileDisplay,
displayProfileByPubkey,
} from "@welshman/app"
@@ -16,12 +15,12 @@
import Reply from "@assets/icons/reply-2.svg?dataurl"
import ReplyAlt from "@assets/icons/reply.svg?dataurl"
import TapTarget from "@lib/components/TapTarget.svelte"
import Avatar from "@lib/components/Avatar.svelte"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
import ThunkFailure from "@app/components/ThunkFailure.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ReactionSummary from "@app/components/ReactionSummary.svelte"
import RoomItemZapButton from "@app/components/RoomItemZapButton.svelte"
import RoomItemEmojiButton from "@app/components/RoomItemEmojiButton.svelte"
@@ -56,7 +55,6 @@
const path = getRoomItemPath(url, event)
const shouldProtect = canEnforceNip70(url)
const today = formatTimestampAsDate(now())
const profile = deriveProfile(event.pubkey, [url])
const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
@@ -83,7 +81,10 @@
<div class="flex w-full gap-3 overflow-auto">
{#if showPubkey}
<Button onclick={openProfile} class="flex items-start">
<Avatar src={$profile?.picture} class="border border-solid border-base-content" size={8} />
<ProfileCircle
pubkey={event.pubkey}
class="border border-solid border-base-content"
size={8} />
</Button>
{:else}
<div class="w-8 min-w-8 max-w-8"></div>
-20
View File
@@ -1,20 +0,0 @@
<script lang="ts">
import {displayRelayUrl} from "@welshman/util"
import Avatar from "@lib/components/Avatar.svelte"
import {deriveRelay} from "@welshman/app"
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
interface Props {
url?: string
}
const {url = ""}: Props = $props()
const relay = deriveRelay(url)
</script>
<Avatar
icon={RemoteControllerMinimalistic}
class="!h-10 !w-10"
alt={displayRelayUrl(url)}
src={$relay?.icon} />
+23 -1
View File
@@ -2,16 +2,22 @@
import {displayRelayUrl} from "@welshman/util"
import {deriveRelay} from "@welshman/app"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import Pen from "@assets/icons/pen.svg?dataurl"
import ShieldUser from "@assets/icons/shield-user.svg?dataurl"
import BillList from "@assets/icons/bill-list.svg?dataurl"
import Ghost from "@assets/icons/ghost-smile.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import Button from "@lib/components/Button.svelte"
import RelayName from "@app/components/RelayName.svelte"
import SpaceEdit from "@app/components/SpaceEdit.svelte"
import SpaceRelayStatus from "@app/components/SpaceRelayStatus.svelte"
import RelayDescription from "@app/components/RelayDescription.svelte"
import ProfileLatest from "@app/components/ProfileLatest.svelte"
import {deriveUserIsSpaceAdmin} from "@app/core/state"
import {pushModal} from "@app/util/modal"
type Props = {
url: string
@@ -20,8 +26,11 @@
const {url}: Props = $props()
const relay = deriveRelay(url)
const owner = $derived($relay?.pubkey)
const userIsAdmin = deriveUserIsSpaceAdmin(url)
const back = () => history.back()
const startEdit = () => pushModal(SpaceEdit, {url, initialValues: $relay})
</script>
<div class="column gap-4">
@@ -78,5 +87,18 @@
</div>
{/if}
</div>
<Button class="btn btn-primary" onclick={back}>Got it</Button>
{#if $userIsAdmin}
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
<Button class="btn btn-primary" onclick={startEdit}>
<Icon icon={Pen} />
Edit Space
</Button>
</ModalFooter>
{:else}
<Button class="btn btn-primary" onclick={back}>Got it</Button>
{/if}
</div>
+202
View File
@@ -0,0 +1,202 @@
<script lang="ts">
import {uniqBy, prop, append, ifLet} from "@welshman/lib"
import type {RelayProfile} from "@welshman/util"
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
import {manageRelay, relays, fetchRelayProfileDirectly} from "@welshman/app"
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import UploadMinimalistic from "@assets/icons/upload-minimalistic.svg?dataurl"
import {preventDefault} from "@lib/html"
import FieldInline from "@lib/components/FieldInline.svelte"
import Icon from "@lib/components/Icon.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import ImageIcon from "@lib/components/ImageIcon.svelte"
import IconPickerButton from "@lib/components/IconPickerButton.svelte"
import {pushToast} from "@app/util/toast"
import {clearModals} from "@app/util/modal"
import {uploadFile} from "@app/core/commands"
type Props = {
url: string
initialValues: RelayProfile
}
const {url, initialValues}: Props = $props()
const values = $state(initialValues)
const back = () => history.back()
const submit = async () => {
if (values.name != initialValues.name) {
const res = await manageRelay(url, {
method: ManagementMethod.ChangeRelayName,
params: [values.name || ""],
})
if (res.error) {
return pushToast({theme: "error", message: res.error})
}
}
if (values.description != initialValues.description) {
const res = await manageRelay(url, {
method: ManagementMethod.ChangeRelayDescription,
params: [values.description || ""],
})
if (res.error) {
return pushToast({theme: "error", message: res.error})
}
}
if (imageFile) {
const {error, result} = await uploadFile(imageFile, {maxWidth: 128, maxHeight: 128})
console.log(imageFile, result)
if (error) {
return pushToast({theme: "error", message: error})
}
const res = await manageRelay(url, {
method: ManagementMethod.ChangeRelayIcon,
params: [result.url],
})
if (res.error) {
return pushToast({theme: "error", message: res.error})
}
}
// Force-reload the relay
ifLet(await fetchRelayProfileDirectly(url), relay => {
relays.update($relays => uniqBy(prop("url"), append(relay, $relays)))
})
pushToast({message: "Your changes have been saved!"})
clearModals()
}
const trySubmit = async () => {
loading = true
try {
await submit()
} finally {
loading = false
}
}
let loading = $state(false)
let imageFile = $state<File | undefined>()
let imagePreview = $state(initialValues.icon)
const handleImageUpload = async (event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0]
if (file && file.type.startsWith("image/")) {
const reader = new FileReader()
reader.onload = e => {
imageFile = file
imagePreview = e.target?.result as string
}
reader.readAsDataURL(file)
}
}
const handleIconSelect = (iconUrl: string) => {
imagePreview = iconUrl
const parts = iconUrl.split(",")
const imageData = atob(parts[1])
const result = new Uint8Array(imageData.length)
for (let n = 0; n < imageData.length; n++) {
result[n] = imageData.charCodeAt(n)
}
imageFile = new File([result], `icon.svg`, {type: "image/svg+xml"})
}
</script>
<form class="column gap-4" onsubmit={preventDefault(trySubmit)}>
<ModalHeader>
{#snippet title()}
<div>Edit a Space</div>
{/snippet}
{#snippet info()}
<span class="text-primary">{displayRelayUrl(url)}</span>
{/snippet}
</ModalHeader>
<FieldInline>
{#snippet label()}
<p>Icon</p>
{/snippet}
{#snippet input()}
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
{#if imagePreview}
<div class="flex items-center gap-2">
<span class="text-sm opacity-75">Selected:</span>
<ImageIcon src={imagePreview} alt="Room icon preview" />
</div>
{:else}
<span class="text-sm opacity-75">No icon selected</span>
{/if}
<div class="flex gap-2">
<IconPickerButton onSelect={handleIconSelect} class="btn btn-primary btn-sm">
<Icon icon={StickerSmileSquare} size={4} />
Select
</IconPickerButton>
<label class="btn btn-neutral btn-sm cursor-pointer">
<Icon icon={UploadMinimalistic} size={4} />
Upload
<input type="file" accept="image/*" class="hidden" onchange={handleImageUpload} />
</label>
</div>
</div>
</div>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<p>Name</p>
{/snippet}
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
{#if imagePreview}
<ImageIcon src={imagePreview} alt="Room icon preview" />
{:else}
<Icon icon={SettingsMinimalistic} />
{/if}
<input bind:value={values.name} class="grow" type="text" />
</label>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<p>Description</p>
{/snippet}
{#snippet input()}
<label class="input input-bordered flex w-full items-center gap-2">
<input bind:value={values.description} class="grow" type="text" />
</label>
{/snippet}
</FieldInline>
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
<Button type="submit" class="btn btn-primary" disabled={loading}>
<Spinner {loading}>Save Changes</Spinner>
</Button>
</ModalFooter>
</form>
+1 -1
View File
@@ -21,7 +21,7 @@
</script>
<Link
class="col-2 card2 bg-alt w-full cursor-pointer shadow-md"
class="col-2 card2 bg-alt w-full cursor-pointer shadow-xl"
href={makeThreadPath(url, event.id)}>
{#if title}
<div class="flex w-full items-center justify-between gap-2">
+4 -2
View File
@@ -660,6 +660,8 @@ export const getBlossomServer = async (options: GetBlossomServerOptions = {}) =>
export type UploadFileOptions = {
url?: string
encrypt?: boolean
maxWidth?: number
maxHeight?: number
}
export type UploadFileResult = {
@@ -671,8 +673,8 @@ export const uploadFile = async (file: File, options: UploadFileOptions = {}) =>
try {
const {name, type} = file
if (!type.match("image/(webp|gif)")) {
file = await compressFile(file)
if (!type.match("image/(webp|gif|svg)")) {
file = await compressFile(file, options)
}
const tags: string[][] = []
+2 -2
View File
@@ -8,7 +8,7 @@ import {
sortBy,
now,
on,
isNotNil,
isDefined,
filterVals,
fromPairs,
} from "@welshman/lib"
@@ -279,6 +279,6 @@ export const requestRelayClaim = async (url: string) => {
export const requestRelayClaims = async (urls: string[]) =>
filterVals(
isNotNil,
isDefined,
fromPairs(await Promise.all(urls.map(async url => [url, await requestRelayClaim(url)]))),
)
+4 -4
View File
@@ -730,7 +730,7 @@ export const deriveSpaceMembers = (url: string) =>
return getTagValues("member", membersEvent.tags)
}
const members = new Set()
const members = new Set<string>()
for (const event of sortBy(e => e.created_at, $events)) {
const pubkeys = getPubkeyTagValues(event.tags)
@@ -765,7 +765,7 @@ export const deriveRoomMembers = (url: string, h: string) =>
return getPubkeyTagValues(membersEvent.tags)
}
const members = new Set()
const members = new Set<string>()
for (const event of sortBy(e => -e.created_at, $events)) {
const pubkeys = getPubkeyTagValues(event.tags)
@@ -825,7 +825,7 @@ export const deriveUserSpaceMembershipStatus = (url: string) =>
deriveUserIsSpaceAdmin(url),
],
([$pubkey, $members, $events, $isAdmin]) => {
const isMember = $members.includes($pubkey) || $isAdmin
const isMember = $members.includes($pubkey!) || $isAdmin
for (const event of $events) {
if (event.pubkey !== $pubkey) {
@@ -860,7 +860,7 @@ export const deriveUserRoomMembershipStatus = (url: string, h: string) =>
deriveUserIsRoomAdmin(url, h),
],
([$pubkey, $members, $events, $isAdmin]) => {
const isMember = $members.includes($pubkey) || $isAdmin
const isMember = $members.includes($pubkey!) || $isAdmin
for (const event of $events) {
if (event.pubkey !== $pubkey) {
+2 -2
View File
@@ -1,12 +1,12 @@
import type {NodeViewProps} from "@tiptap/core"
import {removeNil} from "@welshman/lib"
import {removeUndefined} from "@welshman/lib"
import {deriveProfileDisplay} from "@welshman/app"
export const makeMentionNodeView =
(url?: string) =>
({node}: NodeViewProps) => {
const dom = document.createElement("span")
const display = deriveProfileDisplay(node.attrs.pubkey, removeNil([url]))
const display = deriveProfileDisplay(node.attrs.pubkey, removeUndefined([url]))
dom.classList.add("tiptap-object")
+2 -2
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import {removeNil} from "@welshman/lib"
import {removeUndefined} from "@welshman/lib"
import {displayPubkey} from "@welshman/util"
import {deriveHandleForPubkey, displayHandle, deriveProfileDisplay} from "@welshman/app"
import WotScore from "@app/components/WotScore.svelte"
@@ -13,7 +13,7 @@
const {value, url}: Props = $props()
const pubkey = value
const profileDisplay = deriveProfileDisplay(pubkey, removeNil([url]))
const profileDisplay = deriveProfileDisplay(pubkey, removeUndefined([url]))
const handle = deriveHandleForPubkey(pubkey)
</script>
+1 -8
View File
@@ -1,13 +1,7 @@
import {writable} from "svelte/store"
import type {Nip46ResponseWithResult} from "@welshman/signer"
import {Nip46Broker, makeSecret} from "@welshman/signer"
import {
NIP46_PERMS,
PLATFORM_URL,
PLATFORM_NAME,
PLATFORM_LOGO,
SIGNER_RELAYS,
} from "@app/core/state"
import {PLATFORM_URL, PLATFORM_NAME, PLATFORM_LOGO, SIGNER_RELAYS} from "@app/core/state"
import {pushToast} from "@app/util/toast"
export class Nip46Controller {
@@ -25,7 +19,6 @@ export class Nip46Controller {
async start() {
const url = await this.broker.makeNostrconnectUrl({
perms: NIP46_PERMS,
url: PLATFORM_URL,
name: PLATFORM_NAME,
image: PLATFORM_LOGO,
-30
View File
@@ -1,30 +0,0 @@
<script lang="ts">
import {onMount} from "svelte"
import Icon from "@lib/components/Icon.svelte"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
const {src = "", size = 7, icon = UserRounded, style = "", ...restProps} = $props()
let element: HTMLElement
const rem = $derived(size * 4)
onMount(() => {
if (src) {
const image = new Image()
image.addEventListener("error", () => {
element?.querySelector(".hidden")?.classList.remove("hidden")
})
image.src = src
}
})
</script>
<div
bind:this={element}
class="{restProps.class} relative !flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-cover bg-center"
style="width: {rem}px; height: {rem}px; min-width: {rem}px; background-image: url({src}); {style}">
<Icon {icon} class={src ? "hidden" : ""} size={Math.round(size * 0.8)} />
</div>
+2 -2
View File
@@ -12,8 +12,8 @@
</script>
<div class="btn flex h-[unset] w-full flex-nowrap py-4 text-left {props.class}">
<div class="flex flex-grow flex-row items-start gap-1 sm:pl-2">
<div class="flex h-14 w-12 flex-shrink-0 items-center">
<div class="flex flex-grow flex-row items-start gap-4">
<div class="flex h-14 w-12 flex-shrink-0 items-center justify-center">
{@render props.icon?.()}
</div>
<div class="flex flex-col gap-1">
+4 -3
View File
@@ -5,13 +5,14 @@
src: string
alt: string
size?: number
class?: string
}
const {src, alt, size = 5}: Props = $props()
const {src, alt, size = 5, ...props}: Props = $props()
</script>
{#if src.includes("image/svg") || src.endsWith(".svg")}
<Icon icon={src} {size} />
<Icon icon={src} {size} class={props.class} />
{:else}
<img {src} {alt} class="h-{size} w-{size} rounded-lg object-cover" />
<img {src} {alt} class="h-{size} w-{size} aspect-square object-cover {props.class}" />
{/if}
+2 -2
View File
@@ -18,7 +18,7 @@
{#if href}
<a {href} class="relative z-nav-item flex h-14 w-14 items-center justify-center">
<div
class="avatar cursor-pointer rounded-full p-1 {restProps.class} transition-colors hover:bg-base-300"
class="avatar cursor-pointer rounded-full p-2 {restProps.class} transition-colors hover:bg-base-300"
class:bg-base-300={active}
class:tooltip={title}
data-tip={title}>
@@ -31,7 +31,7 @@
{:else}
<Button {onclick} class="relative z-nav-item flex h-14 w-14 items-center justify-center">
<div
class="avatar cursor-pointer rounded-full p-1 {restProps.class} transition-colors hover:bg-base-300"
class="avatar cursor-pointer rounded-full p-2 {restProps.class} transition-colors hover:bg-base-300"
class:bg-base-300={active}
class:tooltip={title}
data-tip={title}>
+3 -3
View File
@@ -35,7 +35,7 @@
<Button onclick={addSpace}>
<CardButton class="btn-neutral">
{#snippet icon()}
<div><Icon icon={AddCircle} size={7} /></div>
<Icon icon={AddCircle} size={7} />
{/snippet}
{#snippet title()}
<div>Add a space</div>
@@ -48,7 +48,7 @@
<Link href="/discover">
<CardButton class="btn-neutral">
{#snippet icon()}
<div><Icon icon={Compass} size={7} /></div>
<Icon icon={Compass} size={7} />
{/snippet}
{#snippet title()}
<div>Browse the network</div>
@@ -61,7 +61,7 @@
<Button onclick={openChat}>
<CardButton class="btn-neutral">
{#snippet icon()}
<div><Icon icon={ChatRound} size={7} /></div>
<Icon icon={ChatRound} size={7} />
{/snippet}
{#snippet title()}
<div>Start a conversation</div>
+2 -2
View File
@@ -16,7 +16,7 @@
import Icon from "@lib/components/Icon.svelte"
import FieldInline from "@lib/components/FieldInline.svelte"
import Button from "@lib/components/Button.svelte"
import Avatar from "@lib/components/Avatar.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import ContentMinimal from "@app/components/ContentMinimal.svelte"
import ProfileEdit from "@app/components/ProfileEdit.svelte"
import ProfileDelete from "@app/components/ProfileDelete.svelte"
@@ -48,7 +48,7 @@
<div class="flex justify-between gap-2">
<div class="flex max-w-full gap-3">
<div class="py-1">
<Avatar src={$profile?.picture} size={10} />
<ProfileCircle pubkey={$pubkey!} size={10} />
</div>
<div class="flex min-w-0 flex-col">
<div class="flex items-center gap-2">
+3 -3
View File
@@ -85,10 +85,10 @@
if (message && !message.startsWith("duplicate:")) {
return pushToast({theme: "error", message})
} else {
// Restart the feed now that we're a member
start()
}
// Restart the feed now that we're a member
start()
} finally {
joining = false
}