forked from coracle/caravel
Show pubkey copy things
This commit is contained in:
@@ -2,9 +2,9 @@ import { A } from "@solidjs/router"
|
|||||||
import { Show } from "solid-js"
|
import { Show } from "solid-js"
|
||||||
import { getProfilePicture } from "applesauce-core/helpers/profile"
|
import { getProfilePicture } from "applesauce-core/helpers/profile"
|
||||||
import { invoiceStatus, type Invoice } from "@/lib/api"
|
import { invoiceStatus, type Invoice } from "@/lib/api"
|
||||||
|
import CopyNpub from "@/components/CopyNpub"
|
||||||
import { useProfileMetadata } from "@/lib/hooks"
|
import { useProfileMetadata } from "@/lib/hooks"
|
||||||
import { formatPeriod, formatUsd } from "@/lib/format"
|
import { formatPeriod, formatUsd } from "@/lib/format"
|
||||||
import { shortenPubkey } from "@/lib/pubkey"
|
|
||||||
|
|
||||||
const invoiceStatusStyles: Record<string, string> = {
|
const invoiceStatusStyles: Record<string, string> = {
|
||||||
open: "bg-yellow-50 text-yellow-700 border-yellow-200",
|
open: "bg-yellow-50 text-yellow-700 border-yellow-200",
|
||||||
@@ -45,7 +45,12 @@ export default function AdminInvoiceListItem(props: AdminInvoiceListItemProps) {
|
|||||||
>
|
>
|
||||||
<img src={getProfilePicture(metadata())} alt="" class="h-5 w-5 flex-shrink-0 rounded-full object-cover" />
|
<img src={getProfilePicture(metadata())} alt="" class="h-5 w-5 flex-shrink-0 rounded-full object-cover" />
|
||||||
</Show>
|
</Show>
|
||||||
<span class="text-xs text-gray-500 truncate">{(metadata()?.name || metadata()?.display_name) || shortenPubkey(props.invoice.tenant_pubkey)}</span>
|
<Show
|
||||||
|
when={metadata()?.name || metadata()?.display_name}
|
||||||
|
fallback={<CopyNpub pubkey={props.invoice.tenant_pubkey} class="text-xs text-gray-500" />}
|
||||||
|
>
|
||||||
|
<span class="text-xs text-gray-500 truncate">{metadata()?.name || metadata()?.display_name}</span>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,11 +7,8 @@ import { RELAY_DOMAIN } from "@/lib/subdomain"
|
|||||||
import serverIcon from "@/assets/server.svg"
|
import serverIcon from "@/assets/server.svg"
|
||||||
import Modal from "@/components/Modal"
|
import Modal from "@/components/Modal"
|
||||||
import BillingPrompts from "@/components/BillingPrompts"
|
import BillingPrompts from "@/components/BillingPrompts"
|
||||||
|
import CopyNpub from "@/components/CopyNpub"
|
||||||
function shortenPubkey(pubkey?: string) {
|
import { shortenNpub } from "@/lib/pubkey"
|
||||||
if (!pubkey) return ""
|
|
||||||
return `${pubkey.slice(0, 8)}…${pubkey.slice(-6)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function SearchIcon() {
|
function SearchIcon() {
|
||||||
return (
|
return (
|
||||||
@@ -43,7 +40,12 @@ export default function AppShell(props: { children?: any }) {
|
|||||||
const [searchQuery, setSearchQuery] = createSignal("")
|
const [searchQuery, setSearchQuery] = createSignal("")
|
||||||
const [adminOpen, setAdminOpen] = createSignal(false)
|
const [adminOpen, setAdminOpen] = createSignal(false)
|
||||||
|
|
||||||
const username = createMemo(() => metadata()?.name || metadata()?.display_name || shortenPubkey(account()?.pubkey))
|
const displayName = createMemo(() => metadata()?.name || metadata()?.display_name)
|
||||||
|
const username = createMemo(() => {
|
||||||
|
const pubkey = account()?.pubkey
|
||||||
|
return displayName() || (pubkey ? shortenNpub(pubkey) : "")
|
||||||
|
})
|
||||||
|
const initial = createMemo(() => (displayName() || account()?.pubkey || "?").slice(0, 1).toUpperCase())
|
||||||
const nip05 = createMemo(() => metadata()?.nip05)
|
const nip05 = createMemo(() => metadata()?.nip05)
|
||||||
const searchedRelays = createMemo<Relay[]>(() => {
|
const searchedRelays = createMemo<Relay[]>(() => {
|
||||||
const list = tenantRelays() ?? []
|
const list = tenantRelays() ?? []
|
||||||
@@ -110,14 +112,19 @@ export default function AppShell(props: { children?: any }) {
|
|||||||
when={picture()}
|
when={picture()}
|
||||||
fallback={
|
fallback={
|
||||||
<div class="h-10 w-10 rounded-full bg-white/15 text-sm font-medium text-white flex items-center justify-center">
|
<div class="h-10 w-10 rounded-full bg-white/15 text-sm font-medium text-white flex items-center justify-center">
|
||||||
{username().slice(0, 1).toUpperCase()}
|
{initial()}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<img src={picture()} alt="Profile" class="h-10 w-10 rounded-full object-cover" />
|
<img src={picture()} alt="Profile" class="h-10 w-10 rounded-full object-cover" />
|
||||||
</Show>
|
</Show>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<p class="truncate text-sm font-medium text-white">{username()}</p>
|
<Show
|
||||||
|
when={displayName()}
|
||||||
|
fallback={<CopyNpub pubkey={account()?.pubkey ?? ""} class="text-sm font-medium text-white" />}
|
||||||
|
>
|
||||||
|
<p class="truncate text-sm font-medium text-white">{username()}</p>
|
||||||
|
</Show>
|
||||||
<p class="truncate text-xs text-white/60">{nip05()}</p>
|
<p class="truncate text-xs text-white/60">{nip05()}</p>
|
||||||
</div>
|
</div>
|
||||||
</A>
|
</A>
|
||||||
@@ -169,7 +176,7 @@ export default function AppShell(props: { children?: any }) {
|
|||||||
<A href="/account" aria-label="Account settings" class="rounded-full">
|
<A href="/account" aria-label="Account settings" class="rounded-full">
|
||||||
<Show
|
<Show
|
||||||
when={picture()}
|
when={picture()}
|
||||||
fallback={<div class="h-9 w-9 rounded-full bg-gray-200 text-gray-600 flex items-center justify-center text-sm font-medium">{username().slice(0, 1).toUpperCase()}</div>}
|
fallback={<div class="h-9 w-9 rounded-full bg-gray-200 text-gray-600 flex items-center justify-center text-sm font-medium">{initial()}</div>}
|
||||||
>
|
>
|
||||||
<img src={picture()} alt="Profile" class="h-9 w-9 rounded-full object-cover" />
|
<img src={picture()} alt="Profile" class="h-9 w-9 rounded-full object-cover" />
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { copyToClipboard } from "@/lib/clipboard"
|
||||||
|
import { shortenNpub, toNpub } from "@/lib/pubkey"
|
||||||
|
|
||||||
|
// Renders a pubkey as a shortened npub followed by a small copy button. The button
|
||||||
|
// copies the full npub and toasts on success. The icon inherits the surrounding
|
||||||
|
// text color (currentColor + opacity) so it reads on both light and dark
|
||||||
|
// backgrounds, and the click stops propagation so it can sit inside links/cards
|
||||||
|
// without triggering them. Pass `class` to style the npub text.
|
||||||
|
export default function CopyNpub(props: { pubkey: string; class?: string }) {
|
||||||
|
const copy = (event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
void copyToClipboard(toNpub(props.pubkey), { successMessage: "npub copied to clipboard" })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span class="inline-flex items-center gap-1 min-w-0">
|
||||||
|
<span class={`truncate ${props.class ?? ""}`}>{shortenNpub(props.pubkey)}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={copy}
|
||||||
|
title="Copy npub"
|
||||||
|
aria-label="Copy npub"
|
||||||
|
class="shrink-0 opacity-60 transition-opacity hover:opacity-100"
|
||||||
|
>
|
||||||
|
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<rect x="9" y="9" width="13" height="13" rx="2" />
|
||||||
|
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,9 +3,9 @@ import { Show } from "solid-js"
|
|||||||
import { getProfilePicture } from "applesauce-core/helpers/profile"
|
import { getProfilePicture } from "applesauce-core/helpers/profile"
|
||||||
import { invoiceStatus, type Invoice } from "@/lib/api"
|
import { invoiceStatus, type Invoice } from "@/lib/api"
|
||||||
import Field from "@/components/Field"
|
import Field from "@/components/Field"
|
||||||
|
import CopyNpub from "@/components/CopyNpub"
|
||||||
import { useProfileMetadata } from "@/lib/hooks"
|
import { useProfileMetadata } from "@/lib/hooks"
|
||||||
import { formatPeriod, formatUsd } from "@/lib/format"
|
import { formatPeriod, formatUsd } from "@/lib/format"
|
||||||
import { shortenPubkey } from "@/lib/pubkey"
|
|
||||||
|
|
||||||
const invoiceStatusStyles: Record<string, string> = {
|
const invoiceStatusStyles: Record<string, string> = {
|
||||||
open: "bg-yellow-50 text-yellow-700 border-yellow-200",
|
open: "bg-yellow-50 text-yellow-700 border-yellow-200",
|
||||||
@@ -59,7 +59,12 @@ export default function InvoiceDetailCard(props: InvoiceDetailCardProps) {
|
|||||||
>
|
>
|
||||||
<img src={getProfilePicture(metadata())} alt="" class="h-5 w-5 flex-shrink-0 rounded-full object-cover" />
|
<img src={getProfilePicture(metadata())} alt="" class="h-5 w-5 flex-shrink-0 rounded-full object-cover" />
|
||||||
</Show>
|
</Show>
|
||||||
<span class="truncate text-blue-600 group-hover:underline">{(metadata()?.name || metadata()?.display_name) || shortenPubkey(props.invoice.tenant_pubkey)}</span>
|
<Show
|
||||||
|
when={metadata()?.name || metadata()?.display_name}
|
||||||
|
fallback={<CopyNpub pubkey={props.invoice.tenant_pubkey} class="text-blue-600 group-hover:underline" />}
|
||||||
|
>
|
||||||
|
<span class="truncate text-blue-600 group-hover:underline">{metadata()?.name || metadata()?.display_name}</span>
|
||||||
|
</Show>
|
||||||
</A>
|
</A>
|
||||||
</Field>
|
</Field>
|
||||||
</dl>
|
</dl>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { Show } from "solid-js"
|
|||||||
import { getProfilePicture } from "applesauce-core/helpers/profile"
|
import { getProfilePicture } from "applesauce-core/helpers/profile"
|
||||||
import type { Relay } from "@/lib/api"
|
import type { Relay } from "@/lib/api"
|
||||||
import { PlanBadge, StatusBadge } from "@/components/relay/RelayCardHeader"
|
import { PlanBadge, StatusBadge } from "@/components/relay/RelayCardHeader"
|
||||||
|
import CopyNpub from "@/components/CopyNpub"
|
||||||
import { useProfileMetadata } from "@/lib/hooks"
|
import { useProfileMetadata } from "@/lib/hooks"
|
||||||
import { shortenPubkey } from "@/lib/pubkey"
|
|
||||||
import { RELAY_DOMAIN } from "@/lib/subdomain"
|
import { RELAY_DOMAIN } from "@/lib/subdomain"
|
||||||
|
|
||||||
type RelayListItemProps = {
|
type RelayListItemProps = {
|
||||||
@@ -38,7 +38,12 @@ export default function RelayListItem(props: RelayListItemProps) {
|
|||||||
>
|
>
|
||||||
<img src={getProfilePicture(metadata())} alt="" class="h-5 w-5 flex-shrink-0 rounded-full object-cover" />
|
<img src={getProfilePicture(metadata())} alt="" class="h-5 w-5 flex-shrink-0 rounded-full object-cover" />
|
||||||
</Show>
|
</Show>
|
||||||
<span class="text-xs text-gray-500 truncate">{(metadata()?.name || metadata()?.display_name) || shortenPubkey(props.relay.tenant_pubkey)}</span>
|
<Show
|
||||||
|
when={metadata()?.name || metadata()?.display_name}
|
||||||
|
fallback={<CopyNpub pubkey={props.relay.tenant_pubkey} class="text-xs text-gray-500" />}
|
||||||
|
>
|
||||||
|
<span class="text-xs text-gray-500 truncate">{metadata()?.name || metadata()?.display_name}</span>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Show, createEffect, createSignal, onCleanup } from "solid-js"
|
|||||||
import { getProfilePicture, type ProfileContent } from "applesauce-core/helpers/profile"
|
import { getProfilePicture, type ProfileContent } from "applesauce-core/helpers/profile"
|
||||||
import type { Relay } from "@/lib/api"
|
import type { Relay } from "@/lib/api"
|
||||||
import menuDotsIcon from "@/assets/menu-dots-2.svg"
|
import menuDotsIcon from "@/assets/menu-dots-2.svg"
|
||||||
import { shortenPubkey } from "@/lib/pubkey"
|
import CopyNpub from "@/components/CopyNpub"
|
||||||
import { RELAY_DOMAIN } from "@/lib/subdomain"
|
import { RELAY_DOMAIN } from "@/lib/subdomain"
|
||||||
|
|
||||||
const STATUS_STYLES: Record<string, string> = {
|
const STATUS_STYLES: Record<string, string> = {
|
||||||
@@ -113,7 +113,12 @@ export default function RelayCardHeader(props: RelayCardHeaderProps) {
|
|||||||
>
|
>
|
||||||
<img src={getProfilePicture(metadata())} alt="" class="h-6 w-6 flex-shrink-0 rounded-full object-cover" />
|
<img src={getProfilePicture(metadata())} alt="" class="h-6 w-6 flex-shrink-0 rounded-full object-cover" />
|
||||||
</Show>
|
</Show>
|
||||||
<span class="text-sm text-blue-600 group-hover:underline truncate">{(metadata()?.name || metadata()?.display_name) || shortenPubkey(r().tenant_pubkey)}</span>
|
<Show
|
||||||
|
when={metadata()?.name || metadata()?.display_name}
|
||||||
|
fallback={<CopyNpub pubkey={r().tenant_pubkey} class="text-sm text-blue-600 group-hover:underline" />}
|
||||||
|
>
|
||||||
|
<span class="text-sm text-blue-600 group-hover:underline truncate">{metadata()?.name || metadata()?.display_name}</span>
|
||||||
|
</Show>
|
||||||
</A>
|
</A>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={r().info_description.trim()}>
|
<Show when={r().info_description.trim()}>
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
import { npubEncode } from "applesauce-core/helpers/pointers"
|
||||||
|
|
||||||
export function shortenPubkey(pubkey: string) {
|
export function shortenPubkey(pubkey: string) {
|
||||||
return pubkey.length <= 16 ? pubkey : `${pubkey.slice(0, 8)}...${pubkey.slice(-8)}`
|
return pubkey.length <= 16 ? pubkey : `${pubkey.slice(0, 8)}...${pubkey.slice(-8)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Encode a hex pubkey as an npub for display. Falls back to the raw input when it
|
||||||
|
// isn't a valid pubkey, since npubEncode throws on malformed input.
|
||||||
|
export function toNpub(pubkey: string) {
|
||||||
|
try {
|
||||||
|
return npubEncode(pubkey)
|
||||||
|
} catch {
|
||||||
|
return pubkey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shortenNpub(pubkey: string) {
|
||||||
|
return shortenPubkey(toNpub(pubkey))
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import AdminInvoiceListItem from "@/components/AdminInvoiceListItem"
|
|||||||
import ResourceState from "@/components/ResourceState"
|
import ResourceState from "@/components/ResourceState"
|
||||||
import useMinLoading from "@/lib/useMinLoading"
|
import useMinLoading from "@/lib/useMinLoading"
|
||||||
import { useAdminTenant, useAdminTenantInvoices, useAdminTenantRelays, useProfileMetadata } from "@/lib/hooks"
|
import { useAdminTenant, useAdminTenantInvoices, useAdminTenantRelays, useProfileMetadata } from "@/lib/hooks"
|
||||||
import { shortenPubkey } from "@/lib/pubkey"
|
import CopyNpub from "@/components/CopyNpub"
|
||||||
|
import { shortenNpub } from "@/lib/pubkey"
|
||||||
|
|
||||||
export default function AdminTenantDetail() {
|
export default function AdminTenantDetail() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
@@ -40,8 +41,10 @@ export default function AdminTenantDetail() {
|
|||||||
<img src={getProfilePicture(metadata())} alt="Profile" class="h-14 w-14 flex-shrink-0 rounded-full object-cover" />
|
<img src={getProfilePicture(metadata())} alt="Profile" class="h-14 w-14 flex-shrink-0 rounded-full object-cover" />
|
||||||
</Show>
|
</Show>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<h1 class="text-2xl font-bold text-gray-900 truncate">{(metadata()?.name || metadata()?.display_name) || shortenPubkey(tenantId())}</h1>
|
<h1 class="text-2xl font-bold text-gray-900 truncate">{(metadata()?.name || metadata()?.display_name) || shortenNpub(tenantId())}</h1>
|
||||||
<p class="text-xs text-gray-500 break-all">{tenantId()}</p>
|
<p class="mt-0.5">
|
||||||
|
<CopyNpub pubkey={tenantId()} class="text-xs text-gray-500" />
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ResourceState loading={loading()} error={tenant.error || relays.error || invoices.error} loadingText="Loading tenant..." errorText="Failed to load tenant." class="mb-4" />
|
<ResourceState loading={loading()} error={tenant.error || relays.error || invoices.error} loadingText="Loading tenant..." errorText="Failed to load tenant." class="mb-4" />
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import ResourceState from "@/components/ResourceState"
|
|||||||
import SearchInput from "@/components/SearchInput"
|
import SearchInput from "@/components/SearchInput"
|
||||||
import useMinLoading from "@/lib/useMinLoading"
|
import useMinLoading from "@/lib/useMinLoading"
|
||||||
import { useAdminTenants, useProfileMetadataMap } from "@/lib/hooks"
|
import { useAdminTenants, useProfileMetadataMap } from "@/lib/hooks"
|
||||||
import { shortenPubkey } from "@/lib/pubkey"
|
import CopyNpub from "@/components/CopyNpub"
|
||||||
|
import { shortenNpub } from "@/lib/pubkey"
|
||||||
import { fuzzySearch } from "@/lib/search"
|
import { fuzzySearch } from "@/lib/search"
|
||||||
|
|
||||||
export default function AdminTenantList() {
|
export default function AdminTenantList() {
|
||||||
@@ -56,9 +57,11 @@ export default function AdminTenantList() {
|
|||||||
<img src={getProfilePicture(profile())} alt="Profile" class="h-10 w-10 rounded-full object-cover" />
|
<img src={getProfilePicture(profile())} alt="Profile" class="h-10 w-10 rounded-full object-cover" />
|
||||||
</Show>
|
</Show>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<p class="font-medium text-gray-900 truncate">{profileName() || shortenPubkey(tenant.pubkey)}</p>
|
<p class="font-medium text-gray-900 truncate">{profileName() || shortenNpub(tenant.pubkey)}</p>
|
||||||
|
<p class="-mt-1">
|
||||||
|
<CopyNpub pubkey={tenant.pubkey} class="text-xs text-gray-500" />
|
||||||
|
</p>
|
||||||
<p class="mt-1 text-sm text-gray-600 line-clamp-2">{profile()?.about || "No profile bio"}</p>
|
<p class="mt-1 text-sm text-gray-600 line-clamp-2">{profile()?.about || "No profile bio"}</p>
|
||||||
<p class="mt-1 text-xs text-gray-500 break-all">{tenant.pubkey}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class={`inline-flex flex-shrink-0 items-center rounded-full border px-2.5 py-0.5 text-xs font-medium capitalize ${tenant.churned_at ? "bg-red-50 text-red-700 border-red-200" : "bg-green-50 text-green-700 border-green-200"}`}>
|
<span class={`inline-flex flex-shrink-0 items-center rounded-full border px-2.5 py-0.5 text-xs font-medium capitalize ${tenant.churned_at ? "bg-red-50 text-red-700 border-red-200" : "bg-green-50 text-green-700 border-green-200"}`}>
|
||||||
|
|||||||
Reference in New Issue
Block a user