Add profile info dialog
This commit is contained in:
parent
531717119e
commit
ac2b8ad13f
4 changed files with 224 additions and 87 deletions
|
|
@ -4,21 +4,19 @@
|
||||||
import {displayProfileByPubkey, loadMessagingRelayList} from "@welshman/app"
|
import {displayProfileByPubkey, loadMessagingRelayList} from "@welshman/app"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
|
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
|
||||||
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
||||||
import UserMinus from "@assets/icons/user-minus.svg?dataurl"
|
import UserMinus from "@assets/icons/user-minus.svg?dataurl"
|
||||||
import Restart from "@assets/icons/restart.svg?dataurl"
|
import Restart from "@assets/icons/restart.svg?dataurl"
|
||||||
import {fly} from "@lib/transition"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Confirm from "@lib/components/Confirm.svelte"
|
import Confirm from "@lib/components/Confirm.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Popover from "@lib/components/Popover.svelte"
|
|
||||||
import Modal from "@lib/components/Modal.svelte"
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import Profile from "@app/components/Profile.svelte"
|
import Profile from "@app/components/Profile.svelte"
|
||||||
import ProfileAbout from "@app/components/ProfileAbout.svelte"
|
import ProfileAbout from "@app/components/ProfileAbout.svelte"
|
||||||
import ProfileBadges from "@app/components/ProfileBadges.svelte"
|
import ProfileBadges from "@app/components/ProfileBadges.svelte"
|
||||||
|
import ProfileMenu from "@app/components/ProfileMenu.svelte"
|
||||||
import {
|
import {
|
||||||
deriveUserIsSpaceAdmin,
|
deriveUserIsSpaceAdmin,
|
||||||
deriveSpaceBannedPubkeyItems,
|
deriveSpaceBannedPubkeyItems,
|
||||||
|
|
@ -47,14 +45,6 @@
|
||||||
|
|
||||||
const viewProfile = () => goto(makeProfilePath(pubkey), {replaceState: true})
|
const viewProfile = () => goto(makeProfilePath(pubkey), {replaceState: true})
|
||||||
|
|
||||||
const toggleMenu = () => {
|
|
||||||
showMenu = !showMenu
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeMenu = () => {
|
|
||||||
showMenu = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const banMember = () =>
|
const banMember = () =>
|
||||||
pushModal(Confirm, {
|
pushModal(Confirm, {
|
||||||
title: "Ban User",
|
title: "Ban User",
|
||||||
|
|
@ -93,8 +83,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let showMenu = $state(false)
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadMessagingRelayList(pubkey)
|
loadMessagingRelayList(pubkey)
|
||||||
})
|
})
|
||||||
|
|
@ -105,42 +93,33 @@
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<Profile showPubkey avatarSize={14} {pubkey} {url} />
|
<Profile showPubkey avatarSize={14} {pubkey} {url} />
|
||||||
{#if $userIsAdmin}
|
<ProfileMenu {pubkey} {url}>
|
||||||
<div class="relative">
|
{#snippet customActions()}
|
||||||
<Button class="button button-circle button-ghost button-sm" onclick={toggleMenu}>
|
{#if $userIsAdmin}
|
||||||
<Icon icon={MenuDots} />
|
{#if isBanned}
|
||||||
</Button>
|
<li>
|
||||||
{#if showMenu}
|
<Button onclick={restoreMember}>
|
||||||
<Popover hideOnClick onClose={closeMenu}>
|
<Icon icon={Restart} />
|
||||||
<ul
|
Restore Membership
|
||||||
transition:fly
|
</Button>
|
||||||
class="menu bg-surface absolute right-0 z-popover w-48 gap-1 rounded-2xl p-2">
|
</li>
|
||||||
{#if isBanned}
|
{:else}
|
||||||
<li>
|
<li>
|
||||||
<Button onclick={restoreMember}>
|
<Button onclick={removeMember}>
|
||||||
<Icon icon={Restart} />
|
<Icon icon={UserMinus} />
|
||||||
Restore Membership
|
Remove Member
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
{:else}
|
<li>
|
||||||
<li>
|
<Button class="text-error" onclick={banMember}>
|
||||||
<Button onclick={removeMember}>
|
<Icon icon={MinusCircle} />
|
||||||
<Icon icon={UserMinus} />
|
Ban User
|
||||||
Remove Member
|
</Button>
|
||||||
</Button>
|
</li>
|
||||||
</li>
|
{/if}
|
||||||
<li>
|
|
||||||
<Button class="text-error" onclick={banMember}>
|
|
||||||
<Icon icon={MinusCircle} />
|
|
||||||
Ban User
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
{/if}
|
|
||||||
</ul>
|
|
||||||
</Popover>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
{/snippet}
|
||||||
{/if}
|
</ProfileMenu>
|
||||||
</div>
|
</div>
|
||||||
<ProfileAbout {pubkey} {url} />
|
<ProfileAbout {pubkey} {url} />
|
||||||
<ProfileBadges {pubkey} {url} />
|
<ProfileBadges {pubkey} {url} />
|
||||||
|
|
|
||||||
121
src/app/components/ProfileInfo.svelte
Normal file
121
src/app/components/ProfileInfo.svelte
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
|
import {Router} from "@welshman/router"
|
||||||
|
import {LOCALE, secondsToDate} from "@welshman/lib"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {displayRelayUrl, toNostrURI} from "@welshman/util"
|
||||||
|
import {tracker} from "@welshman/app"
|
||||||
|
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||||
|
import Copy from "@assets/icons/copy.svg?dataurl"
|
||||||
|
import UserId from "@assets/icons/user-id.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Badge from "@lib/components/Badge.svelte"
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
|
import {clip} from "@app/toast"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url?: string
|
||||||
|
event: TrustedEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, event}: Props = $props()
|
||||||
|
|
||||||
|
const relays = url ? [url] : Router.get().FromPubkeys([event.pubkey]).getUrls()
|
||||||
|
const nprofile1 = toNostrURI(nip19.nprofileEncode({pubkey: event.pubkey, relays}))
|
||||||
|
const npub1 = nip19.npubEncode(event.pubkey)
|
||||||
|
const seenOn = tracker.getRelays(event.id)
|
||||||
|
const json = (() => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(event.content), null, 2)
|
||||||
|
} catch {
|
||||||
|
return event.content
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
const copyLink = () => clip(nprofile1)
|
||||||
|
const copyPubkey = () => clip(npub1)
|
||||||
|
const copyJson = () => clip(json)
|
||||||
|
|
||||||
|
const formatter = new Intl.DateTimeFormat(LOCALE, {
|
||||||
|
dateStyle: "long",
|
||||||
|
timeStyle: "long",
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal>
|
||||||
|
<ModalBody>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Profile Details</ModalTitle>
|
||||||
|
<ModalSubtitle>The full details of this profile are shown below.</ModalSubtitle>
|
||||||
|
</ModalHeader>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Created At</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<p>{formatter.format(secondsToDate(event.created_at))}</p>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Profile Link</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input flex w-full items-center gap-2">
|
||||||
|
<Icon icon={LinkRound} />
|
||||||
|
<input type="text" class="truncate min-w-0 grow" value={nprofile1} />
|
||||||
|
<Button onclick={copyLink} class="flex items-center">
|
||||||
|
<Icon icon={Copy} />
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Public Key</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input flex w-full items-center gap-2">
|
||||||
|
<Icon icon={UserId} />
|
||||||
|
<input type="text" class="truncate min-w-0 grow" value={npub1} />
|
||||||
|
<Button onclick={copyPubkey} class="flex items-center">
|
||||||
|
<Icon icon={Copy} />
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
{#if !url && seenOn.size > 0}
|
||||||
|
<FieldInline>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Seen On</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{#each seenOn as url, i (url)}
|
||||||
|
<Badge class="bg-surface flex gap-1">
|
||||||
|
{displayRelayUrl(url)}
|
||||||
|
</Badge>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</FieldInline>
|
||||||
|
{/if}
|
||||||
|
<div class="relative">
|
||||||
|
<pre class="card card-sm overflow-auto text-xs"><code>{json}</code></pre>
|
||||||
|
<p class="absolute right-2 top-2 flex grow items-center justify-between">
|
||||||
|
<Button onclick={copyJson} class="button button-neutral button-sm flex items-center">
|
||||||
|
<Icon icon={Copy} /> Copy
|
||||||
|
</Button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="button button-primary grow" onclick={() => history.back()}>Got it</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
73
src/app/components/ProfileMenu.svelte
Normal file
73
src/app/components/ProfileMenu.svelte
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type {Snippet} from "svelte"
|
||||||
|
import {deriveProfile} from "@welshman/app"
|
||||||
|
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||||
|
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||||
|
import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
|
||||||
|
import {fly} from "@lib/transition"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Popover from "@lib/components/Popover.svelte"
|
||||||
|
import ProfileInfo from "@app/components/ProfileInfo.svelte"
|
||||||
|
import ProfileQrCode from "@app/components/ProfileQrCode.svelte"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
pubkey: string
|
||||||
|
url?: string
|
||||||
|
customActions?: Snippet
|
||||||
|
}
|
||||||
|
|
||||||
|
const {pubkey, url, customActions}: Props = $props()
|
||||||
|
|
||||||
|
const profile = deriveProfile(pubkey)
|
||||||
|
|
||||||
|
let showMenu = $state(false)
|
||||||
|
|
||||||
|
const toggleMenu = () => {
|
||||||
|
showMenu = !showMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeMenu = () => {
|
||||||
|
showMenu = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const showInfo = () => {
|
||||||
|
closeMenu()
|
||||||
|
pushModal(ProfileInfo, {event: $profile!.event, url})
|
||||||
|
}
|
||||||
|
|
||||||
|
const showShare = () => {
|
||||||
|
closeMenu()
|
||||||
|
pushModal(ProfileQrCode, {pubkey})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative">
|
||||||
|
<Button class="button button-circle button-ghost button-sm" onclick={toggleMenu}>
|
||||||
|
<Icon icon={MenuDots} />
|
||||||
|
</Button>
|
||||||
|
{#if showMenu}
|
||||||
|
<Popover hideOnClick onClose={closeMenu}>
|
||||||
|
<ul
|
||||||
|
transition:fly
|
||||||
|
class="menu bg-surface absolute right-0 z-popover w-48 gap-1 rounded-2xl p-2">
|
||||||
|
<li>
|
||||||
|
<Button onclick={showShare}>
|
||||||
|
<Icon icon={ShareCircle} />
|
||||||
|
Share
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{#if $profile}
|
||||||
|
<li>
|
||||||
|
<Button onclick={showInfo}>
|
||||||
|
<Icon icon={Code2} />
|
||||||
|
Profile Info
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{@render customActions?.()}
|
||||||
|
</ul>
|
||||||
|
</Popover>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
@ -18,21 +18,17 @@
|
||||||
import Letter from "@assets/icons/letter-opened.svg?dataurl"
|
import Letter from "@assets/icons/letter-opened.svg?dataurl"
|
||||||
import UserPlus from "@assets/icons/user-plus.svg?dataurl"
|
import UserPlus from "@assets/icons/user-plus.svg?dataurl"
|
||||||
import PenNewSquare from "@assets/icons/pen-new-square.svg?dataurl"
|
import PenNewSquare from "@assets/icons/pen-new-square.svg?dataurl"
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
|
||||||
import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
|
|
||||||
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
||||||
import {fly} from "@lib/transition"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Popover from "@lib/components/Popover.svelte"
|
|
||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
import ProfileTrust from "@app/components/ProfileTrust.svelte"
|
import ProfileTrust from "@app/components/ProfileTrust.svelte"
|
||||||
import ProfileSharedSpaces from "@app/components/ProfileSharedSpaces.svelte"
|
import ProfileSharedSpaces from "@app/components/ProfileSharedSpaces.svelte"
|
||||||
import ProfilePageNotes from "@app/components/ProfilePageNotes.svelte"
|
import ProfilePageNotes from "@app/components/ProfilePageNotes.svelte"
|
||||||
import ProfileQrCode from "@app/components/ProfileQrCode.svelte"
|
|
||||||
import ProfileEdit from "@app/components/ProfileEdit.svelte"
|
import ProfileEdit from "@app/components/ProfileEdit.svelte"
|
||||||
|
import ProfileMenu from "@app/components/ProfileMenu.svelte"
|
||||||
import WotScore from "@app/components/WotScore.svelte"
|
import WotScore from "@app/components/WotScore.svelte"
|
||||||
import Content from "@app/components/Content.svelte"
|
import Content from "@app/components/Content.svelte"
|
||||||
import {updateProfile} from "@app/profiles"
|
import {updateProfile} from "@app/profiles"
|
||||||
|
|
@ -62,27 +58,13 @@
|
||||||
: `https://${$profile?.website || ""}`,
|
: `https://${$profile?.website || ""}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
let showMenu = $state(false)
|
|
||||||
let bannerLoading = $state(false)
|
let bannerLoading = $state(false)
|
||||||
let bannerInput: HTMLInputElement | undefined = $state()
|
let bannerInput: HTMLInputElement | undefined = $state()
|
||||||
|
|
||||||
const copyNpub = () => clip(nip19.npubEncode(target))
|
const copyNpub = () => clip(nip19.npubEncode(target))
|
||||||
|
|
||||||
const showShare = () => {
|
|
||||||
closeMenu()
|
|
||||||
pushModal(ProfileQrCode, {pubkey: target})
|
|
||||||
}
|
|
||||||
|
|
||||||
const startEdit = () => pushModal(ProfileEdit)
|
const startEdit = () => pushModal(ProfileEdit)
|
||||||
|
|
||||||
const toggleMenu = () => {
|
|
||||||
showMenu = !showMenu
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeMenu = () => {
|
|
||||||
showMenu = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const openChat = () => goToChat([target])
|
const openChat = () => goToChat([target])
|
||||||
|
|
||||||
const toggleFollow = async () => {
|
const toggleFollow = async () => {
|
||||||
|
|
@ -193,26 +175,8 @@
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="relative shrink-0">
|
<div class="shrink-0">
|
||||||
<Button
|
<ProfileMenu pubkey={target} />
|
||||||
class="button button-circle button-ghost button-sm"
|
|
||||||
onclick={toggleMenu}>
|
|
||||||
<Icon icon={MenuDots} />
|
|
||||||
</Button>
|
|
||||||
{#if showMenu}
|
|
||||||
<Popover hideOnClick onClose={closeMenu}>
|
|
||||||
<ul
|
|
||||||
transition:fly
|
|
||||||
class="menu bg-surface absolute right-0 z-popover w-48 gap-1 rounded-2xl p-2">
|
|
||||||
<li>
|
|
||||||
<Button onclick={showShare}>
|
|
||||||
<Icon icon={ShareCircle} />
|
|
||||||
Share
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Popover>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue