Add per-url aliases

This commit is contained in:
Jon Staab
2025-04-15 11:03:27 -07:00
parent 91689e5b90
commit 374ca7f265
37 changed files with 321 additions and 162 deletions
+1 -1
View File
@@ -81,7 +81,7 @@
}
const content = initialValues?.content || ""
const editor = makeEditor({submit, uploading, content})
const editor = makeEditor({url, submit, uploading, content})
let title = $state(initialValues?.title || "")
let location = $state(initialValues?.location || "")
+1 -1
View File
@@ -18,7 +18,7 @@
<CalendarEventHeader {event} />
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
<span class="whitespace-nowrap py-1 text-sm opacity-75">
Posted by <ProfileLink pubkey={event.pubkey} />
Posted by <ProfileLink pubkey={event.pubkey} {url} />
</span>
<CalendarEventActions showActivity {url} {event} />
</div>
+3 -2
View File
@@ -6,16 +6,17 @@
type Props = {
event: TrustedEvent
url: string
}
const {event}: Props = $props()
const {event, url}: Props = $props()
const meta = $derived(fromPairs(event.tags) as Record<string, string>)
</script>
<div class="flex min-w-0 flex-col gap-1 text-sm opacity-75">
<span class="flex items-center gap-1">
<Icon icon="user-circle" size={4} />
Posted by <ProfileLink pubkey={event.pubkey} />
Posted by <ProfileLink pubkey={event.pubkey} {url} />
</span>
{#if meta.location}
<span class="flex items-start gap-1">
+6 -4
View File
@@ -1,16 +1,18 @@
<script lang="ts">
import {writable} from "svelte/store"
import type {EventContent} from "@welshman/util"
import {isMobile, preventDefault} from "@lib/html"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import EditorContent from "@app/editor/EditorContent.svelte"
import {makeEditor} from "@app/editor"
interface Props {
onSubmit: any
type Props = {
url?: string
onSubmit: (event: EventContent) => void
}
const {onSubmit}: Props = $props()
const {onSubmit, url}: Props = $props()
const autofocus = !isMobile
@@ -33,7 +35,7 @@
editor.chain().clearContent().run()
}
const editor = makeEditor({autofocus, submit, uploading, aggressive: true})
const editor = makeEditor({url, autofocus, submit, uploading, aggressive: true})
</script>
<form class="relative z-feature flex gap-2 p-2" onsubmit={preventDefault(submit)}>
+14 -13
View File
@@ -4,8 +4,6 @@
import {
thunks,
pubkey,
deriveProfile,
deriveProfileDisplay,
formatTimestampAsDate,
formatTimestampAsTime,
thunkIsComplete,
@@ -22,15 +20,15 @@
import ChannelMessageEmojiButton from "@app/components/ChannelMessageEmojiButton.svelte"
import ChannelMessageMenuButton from "@app/components/ChannelMessageMenuButton.svelte"
import ChannelMessageMenuMobile from "@app/components/ChannelMessageMenuMobile.svelte"
import {colors} from "@app/state"
import {colors, deriveAlias, deriveAliasDisplay} from "@app/state"
import {publishDelete, publishReaction} from "@app/commands"
import {pushModal} from "@app/modal"
interface Props {
url: any
room: any
url: string
room: string
event: TrustedEvent
replyTo?: any
replyTo?: (event: TrustedEvent) => void
showPubkey?: boolean
inert?: boolean
}
@@ -39,16 +37,16 @@
const thunk = $thunks[event.id]
const today = formatTimestampAsDate(now())
const profile = deriveProfile(event.pubkey)
const profileDisplay = deriveProfileDisplay(event.pubkey)
const alias = deriveAlias(event.pubkey, url)
const aliasDisplay = deriveAliasDisplay(event.pubkey, url)
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
const hideMenuButton = $derived($thunk && !thunkIsComplete($thunk))
const reply = () => replyTo(event)
const reply = () => replyTo!(event)
const onTap = () => pushModal(ChannelMessageMenuMobile, {url, event, reply})
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url})
const onReactionClick = (content: string, events: TrustedEvent[]) => {
const reaction = events.find(e => e.pubkey === $pubkey)
@@ -68,7 +66,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} />
<Avatar
src={$alias?.profile?.picture}
class="border border-solid border-base-content"
size={8} />
</Button>
{:else}
<div class="w-8 min-w-8 max-w-8"></div>
@@ -77,7 +78,7 @@
{#if showPubkey}
<div class="flex items-center gap-2">
<Button onclick={openProfile} class="text-sm font-bold" style="color: {colorValue}">
{$profileDisplay}
{$aliasDisplay}
</Button>
<span class="text-xs opacity-50">
{#if formatTimestampAsDate(event.created_at) === today}
@@ -90,7 +91,7 @@
</div>
{/if}
<div class="text-sm">
<Content {event} relays={[url]} />
<Content {event} {url} />
{#if thunk}
<ThunkStatus {thunk} class="mt-2" />
{/if}
+13 -10
View File
@@ -9,6 +9,7 @@
pubkey,
Router,
tagPubkey,
loadUsingOutbox,
formatTimestampAsDate,
inboxRelaySelectionsByPubkey,
} from "@welshman/app"
@@ -27,17 +28,16 @@
import ChatMessage from "@app/components/ChatMessage.svelte"
import ChatCompose from "@app/components/ChannelCompose.svelte"
import ChatComposeParent from "@app/components/ChannelComposeParent.svelte"
import {userSettingValues, deriveChat, splitChatId, PLATFORM_NAME} from "@app/state"
import {INDEXER_RELAYS, userSettingValues, deriveChat, splitChatId, PLATFORM_NAME} from "@app/state"
import {pushModal} from "@app/modal"
import {sendWrapped, prependParent} from "@app/commands"
const {
id,
info,
}: {
type Props = {
id: string
info?: Snippet
} = $props()
}
const {id, info}: Props = $props()
const chat = deriveChat(id)
const pubkeys = splitChatId(id)
@@ -107,10 +107,13 @@
onMount(() => {
// Don't use loadInboxRelaySelection because we want to force reload
load({
relays: Router.get().FromPubkeys(others).getUrls(),
filters: [{kinds: [INBOX_RELAYS], authors: others}],
})
for (const pubkey of others) {
loadUsingOutbox({
pubkey,
kind: INBOX_RELAYS,
relays: INDEXER_RELAYS,
})
}
const observer = new ResizeObserver(() => {
if (dynamicPadding && chatCompose) {
+1 -1
View File
@@ -40,7 +40,7 @@
<div class="flex items-center justify-between gap-2">
<div class="flex min-w-0 items-center gap-2">
{#if others.length === 0}
<ProfileCircle pubkey={$pubkey} size={5} />
<ProfileCircle pubkey={$pubkey!} size={5} />
Note to self
{:else if others.length === 1}
<ProfileCircle pubkey={others[0]} size={5} />
+6 -12
View File
@@ -2,13 +2,7 @@
import {type Instance} from "tippy.js"
import {hash} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {
thunks,
deriveProfile,
deriveProfileDisplay,
formatTimestampAsTime,
pubkey,
} from "@welshman/app"
import {thunks, formatTimestampAsTime, pubkey} from "@welshman/app"
import {isMobile} from "@lib/html"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
@@ -21,7 +15,7 @@
import ProfileDetail from "@app/components/ProfileDetail.svelte"
import ChatMessageMenu from "@app/components/ChatMessageMenu.svelte"
import ChatMessageMenuMobile from "@app/components/ChatMessageMenuMobile.svelte"
import {colors} from "@app/state"
import {colors, deriveAlias, deriveAliasDisplay} from "@app/state"
import {makeDelete, makeReaction, sendWrapped} from "@app/commands"
import {pushModal} from "@app/modal"
@@ -36,8 +30,8 @@
const thunk = $thunks[event.id]
const isOwn = event.pubkey === $pubkey
const profile = deriveProfile(event.pubkey)
const profileDisplay = deriveProfileDisplay(event.pubkey)
const alias = deriveAlias(event.pubkey)
const aliasDisplay = deriveAliasDisplay(event.pubkey)
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
const reply = () => replyTo(event)
@@ -107,12 +101,12 @@
{#if !isOwn}
<Button onclick={openProfile} class="flex items-center gap-1">
<Avatar
src={$profile?.picture}
src={$alias?.profile?.picture}
class="border border-solid border-base-content"
size={4} />
<div class="flex items-center gap-2">
<Button onclick={openProfile} class="text-sm font-bold" style="color: {colorValue}">
{$profileDisplay}
{$aliasDisplay}
</Button>
</div>
</Button>
+4 -4
View File
@@ -38,8 +38,8 @@
showEntire?: boolean
hideMediaAtDepth?: number
expandMode?: string
relays?: string[]
depth?: number
url?: string
}
let {
@@ -49,8 +49,8 @@
showEntire = $bindable(false),
hideMediaAtDepth = 1,
expandMode = "block",
relays = [],
depth = 0,
url,
}: Props = $props()
const fullContent = parse(event)
@@ -146,10 +146,10 @@
<ContentLinkInline value={parsed.value} />
{/if}
{:else if isProfile(parsed)}
<ContentMention value={parsed.value} />
<ContentMention value={parsed.value} {url} />
{:else if isEvent(parsed) || isAddress(parsed)}
{#if isBlock(i)}
<ContentQuote {depth} {relays} {hideMediaAtDepth} value={parsed.value} {event} />
<ContentQuote {depth} {url} {hideMediaAtDepth} value={parsed.value} {event} />
{:else}
<Link
external
+11 -4
View File
@@ -1,17 +1,24 @@
<script lang="ts">
import type {ProfilePointer} from "@welshman/content"
import {displayProfile} from "@welshman/util"
import {deriveProfile} from "@welshman/app"
import Button from "@lib/components/Button.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte"
import {pushModal} from "@app/modal"
import {deriveAlias} from "@app/state"
const {value} = $props()
type Props = {
value: ProfilePointer
url?: string
}
const profile = deriveProfile(value.pubkey)
const {value, url}: Props = $props()
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey})
const alias = deriveAlias(value.pubkey, url)
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url})
</script>
<Button onclick={openProfile} class="link-content">
@{displayProfile($profile)}
@{displayProfile($alias?.profile)}
</Button>
+19 -5
View File
@@ -3,6 +3,7 @@
import {goto} from "$app/navigation"
import {nthEq} from "@welshman/lib"
import {Router, tracker, repository} from "@welshman/app"
import type {TrustedEvent} from "@welshman/util"
import {Address, DIRECT_MESSAGE, MESSAGE, THREAD, EVENT_TIME} from "@welshman/util"
import {scrollToEvent} from "@lib/html"
import Button from "@lib/components/Button.svelte"
@@ -12,11 +13,24 @@
import {deriveEvent, entityLink, ROOM} from "@app/state"
import {makeThreadPath, makeCalendarPath, makeRoomPath} from "@app/routes"
const {value, event, depth, hideMediaAtDepth, relays = []} = $props()
type Props = {
value: any
hideMediaAtDepth: number
event: TrustedEvent
depth: number
url?: string
}
const {id, identifier, kind, pubkey, relays: relayHints = []} = value
const {value, event, depth, hideMediaAtDepth, url}: Props = $props()
const {id, identifier, kind, pubkey, relays = []} = value
const idOrAddress = id || new Address(kind, pubkey, identifier).toString()
const mergedRelays = [...relays, ...Router.get().Quote(event, idOrAddress, relayHints).getUrls()]
const mergedRelays = Router.get().Quote(event, idOrAddress, relays).getUrls()
if (url) {
mergedRelays.push(url)
}
const quote = deriveEvent(idOrAddress, mergedRelays)
const entity = id
? nip19.neventEncode({id, relays: mergedRelays})
@@ -80,8 +94,8 @@
<Button class="my-2 block max-w-full text-left" {onclick}>
{#if $quote}
<NoteCard event={$quote} class="bg-alt rounded-box p-4">
<NoteContent {hideMediaAtDepth} {relays} event={$quote} depth={depth + 1} />
<NoteCard event={$quote} {url} class="bg-alt rounded-box p-4">
<NoteContent {hideMediaAtDepth} {url} event={$quote} depth={depth + 1} />
</NoteCard>
{:else}
<div class="rounded-box p-4">
+1 -1
View File
@@ -32,7 +32,7 @@
onSubmit(publishComment({event, content, tags, relays: [url]}))
}
const editor = makeEditor({submit, uploading, autofocus: !isMobile})
const editor = makeEditor({url, submit, uploading, autofocus: !isMobile})
let form: HTMLElement
let spacer: HTMLElement
+1 -1
View File
@@ -42,7 +42,7 @@
<div class="column gap-2">
<div class="flex justify-between">
<div>
<Profile pubkey={report.pubkey} />
<Profile pubkey={report.pubkey} {url} />
<span>Reported this event as "{reason}"</span>
</div>
{#if report.pubkey === $pubkey}
+1 -1
View File
@@ -43,7 +43,7 @@
const showMembers = () =>
pushModal(
ProfileList,
{pubkeys: members, title: `Members of`, subtitle: displayRelayUrl(url)},
{url, pubkeys: members, title: `Members of`, subtitle: displayRelayUrl(url)},
{replaceState},
)
+4 -2
View File
@@ -17,12 +17,14 @@
children,
minimal = false,
hideProfile = false,
url,
...restProps
}: {
event: TrustedEvent
children: Snippet
minimal?: boolean
hideProfile?: boolean
url?: string
class?: string
} = $props()
@@ -49,9 +51,9 @@
<div class="flex justify-between gap-2">
{#if !hideProfile}
{#if minimal}
@<ProfileName pubkey={event.pubkey} />
@<ProfileName pubkey={event.pubkey} {url} />
{:else}
<Profile pubkey={event.pubkey} />
<Profile pubkey={event.pubkey} {url} />
{/if}
{/if}
<Link
+1 -1
View File
@@ -25,7 +25,7 @@
publishReaction({event, content: emoji.unicode, relays: [url]})
</script>
<NoteCard {event} class="card2 bg-alt">
<NoteCard {event} {url} class="card2 bg-alt">
<NoteContent {event} expandMode="inline" />
<div class="flex w-full justify-between gap-2">
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right">
+8 -3
View File
@@ -10,7 +10,12 @@
import ProfileInfo from "@app/components/ProfileInfo.svelte"
import {makeChatPath} from "@app/routes"
const {pubkey} = $props()
type Props = {
pubkey: string
url?: string
}
const {pubkey, url}: Props = $props()
const filters: Filter[] = [{authors: [pubkey], limit: 1}]
const events = deriveEvents(repository, {filters})
@@ -29,13 +34,13 @@
<div class="card2 bg-alt col-2 shadow-xl">
<div class="flex justify-between">
<Profile {pubkey} />
<Profile {pubkey} {url} />
<Link class="btn btn-primary hidden sm:flex" href={makeChatPath([pubkey])}>
<Icon icon="letter" />
Start a Chat
</Link>
</div>
<ProfileInfo {pubkey} />
<ProfileInfo {pubkey} {url} />
{#if $events.length > 0}
<div class="bg-alt badge badge-neutral border-none">
Last active {formatTimestampRelative($events[0].created_at)}
+12 -8
View File
@@ -4,25 +4,29 @@
session,
userFollows,
deriveUserWotScore,
deriveProfile,
deriveHandleForPubkey,
displayHandle,
deriveProfileDisplay,
} from "@welshman/app"
import Button from "@lib/components/Button.svelte"
import Avatar from "@lib/components/Avatar.svelte"
import WotScore from "@lib/components/WotScore.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte"
import {pushModal} from "@app/modal"
import {deriveAlias, deriveAliasDisplay} from "@app/state"
const {pubkey} = $props()
type Props = {
pubkey: string
url?: string
}
const profile = deriveProfile(pubkey)
const profileDisplay = deriveProfileDisplay(pubkey)
const {pubkey, url}: Props = $props()
const alias = deriveAlias(pubkey, url)
const aliasDisplay = deriveAliasDisplay(pubkey, url)
const handle = deriveHandleForPubkey(pubkey)
const score = deriveUserWotScore(pubkey)
const openProfile = () => pushModal(ProfileDetail, {pubkey})
const openProfile = () => pushModal(ProfileDetail, {pubkey, url})
const following = $derived(
pubkey === $session!.pubkey || getPubkeyTagValues(getListTags($userFollows)).includes(pubkey),
@@ -31,12 +35,12 @@
<div class="flex max-w-full gap-3">
<Button onclick={openProfile} class="py-1">
<Avatar src={$profile?.picture} size={10} />
<Avatar src={$alias?.profile?.picture} size={10} />
</Button>
<div class="flex min-w-0 flex-col">
<div class="flex items-center gap-2">
<Button onclick={openProfile} class="text-bold overflow-hidden text-ellipsis">
{$profileDisplay}
{$aliasDisplay}
</Button>
<WotScore score={$score} active={following} />
</div>
+9 -4
View File
@@ -1,10 +1,15 @@
<script lang="ts">
import {deriveProfile} from "@welshman/app"
import Avatar from "@lib/components/Avatar.svelte"
import {deriveAlias} from "@app/state"
const {...props} = $props()
type Props = {
pubkey: string
url?: string
} & Record<string, any>
const profile = deriveProfile(props.pubkey)
const {pubkey, url, ...props}: Props = $props()
const alias = deriveAlias(pubkey, url)
</script>
<Avatar src={$profile?.picture} icon="user-circle" {...props} />
<Avatar src={$alias?.profile?.picture} icon="user-circle" {...props} />
+12 -9
View File
@@ -5,10 +5,8 @@
session,
userFollows,
deriveUserWotScore,
deriveProfile,
deriveHandleForPubkey,
displayHandle,
deriveProfileDisplay,
} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
@@ -18,14 +16,19 @@
import ModalFooter from "@lib/components/ModalFooter.svelte"
import ProfileInfo from "@app/components/ProfileInfo.svelte"
import ChatEnable from "@app/components/ChatEnable.svelte"
import {canDecrypt, pubkeyLink} from "@app/state"
import {canDecrypt, pubkeyLink, deriveAlias, deriveAliasDisplay} from "@app/state"
import {pushModal} from "@app/modal"
import {makeChatPath} from "@app/routes"
const {pubkey} = $props()
export type Props = {
pubkey: string
url?: string
}
const profile = deriveProfile(pubkey)
const profileDisplay = deriveProfileDisplay(pubkey)
const {pubkey, url}: Props = $props()
const alias = deriveAlias(pubkey, url)
const aliasDisplay = deriveAliasDisplay(pubkey, url)
const handle = deriveHandleForPubkey(pubkey)
const score = deriveUserWotScore(pubkey)
@@ -43,12 +46,12 @@
<div class="column gap-4">
<div class="flex max-w-full gap-3">
<span class="py-1">
<Avatar src={$profile?.picture} size={10} />
<Avatar src={$alias?.profile?.picture} size={10} />
</span>
<div class="flex min-w-0 flex-col">
<div class="flex items-center gap-2">
<span class="text-bold overflow-hidden text-ellipsis">
{$profileDisplay}
{$aliasDisplay}
</span>
<WotScore score={$score} active={following} />
</div>
@@ -57,7 +60,7 @@
</div>
</div>
</div>
<ProfileInfo {pubkey} />
<ProfileInfo {pubkey} {url} />
<ModalFooter>
<Button onclick={back} class="btn btn-link">
<Icon icon="alt-arrow-left" />
+4 -4
View File
@@ -23,12 +23,12 @@
const back = () => history.back()
const onsubmit = ({profile, shouldBroadcast}: {profile: Profile; shouldBroadcast: boolean}) => {
const relays = shouldBroadcast
? Router.get().FromUser().getUrls()
: getMembershipUrls($userMembership)
const template = isPublishedProfile(profile) ? editProfile(profile) : createProfile(profile)
const relays = [...getMembershipUrls($userMembership)]
if (!shouldBroadcast) {
if (shouldBroadcast) {
relays.push(...Router.get().FromUser().getUrls())
} else {
template.tags = uniqTags([...template.tags, PROTECTED])
}
+1 -1
View File
@@ -91,7 +91,7 @@
{/snippet}
{#snippet info()}
<p>
If enabled, your profile will be published to the broader nostr network, as well as to
If enabled, changes will be published to the broader nostr network in addition to
spaces you are a member of.
</p>
{/snippet}
+10 -5
View File
@@ -1,12 +1,17 @@
<script lang="ts">
import {deriveProfile} from "@welshman/app"
import Content from "@app/components/Content.svelte"
import {deriveAlias} from "@app/state"
const {pubkey} = $props()
export type Props = {
pubkey: string
url?: string
}
const profile = deriveProfile(pubkey)
const {pubkey, url}: Props = $props()
const alias = deriveAlias(pubkey, url)
</script>
{#if $profile}
<Content event={{content: $profile.about, tags: []}} />
{#if $alias?.profile}
<Content event={{content: $alias.profile.about, tags: []}} hideMediaAtDepth={0} />
{/if}
+8 -3
View File
@@ -5,11 +5,16 @@
import ProfileDetail from "@app/components/ProfileDetail.svelte"
import {pushModal} from "@app/modal"
const {pubkey}: {pubkey: string} = $props()
type Props = {
pubkey: string
url?: string
}
const openProfile = () => pushModal(ProfileDetail, {pubkey})
const {pubkey, url}: Props = $props()
const openProfile = () => pushModal(ProfileDetail, {pubkey, url})
</script>
<Button onclick={preventDefault(openProfile)} class="link-content">
@<ProfileName {pubkey} />
@<ProfileName {pubkey} {url} />
</Button>
+4 -3
View File
@@ -5,11 +5,12 @@
interface Props {
title: any
subtitle?: string
pubkeys: any
subtitle?: string
url?: string
}
const {subtitle = "", pubkeys, ...restProps}: Props = $props()
const {subtitle = "", pubkeys, url, ...restProps}: Props = $props()
</script>
<div class="column gap-4">
@@ -23,7 +24,7 @@
</ModalHeader>
{#each pubkeys as pubkey (pubkey)}
<div class="card2 bg-alt">
<Profile {pubkey} />
<Profile {pubkey} {url} />
</div>
{/each}
<Button class="btn btn-primary" onclick={() => history.back()}>Got it</Button>
+9 -4
View File
@@ -1,9 +1,14 @@
<script lang="ts">
import {deriveProfileDisplay} from "@welshman/app"
import {deriveAliasDisplay} from "@app/state"
const {pubkey} = $props()
type Props = {
pubkey: string
url?: string
}
const profileDisplay = deriveProfileDisplay(pubkey)
const {pubkey, url}: Props = $props()
const aliasDisplay = deriveAliasDisplay(pubkey, url)
</script>
{$profileDisplay}
{$aliasDisplay}
+1 -1
View File
@@ -53,7 +53,7 @@
history.back()
}
const editor = makeEditor({submit, uploading, placeholder: "What's on your mind?"})
const editor = makeEditor({url, submit, uploading, placeholder: "What's on your mind?"})
let title: string = $state("")
</script>
+6 -7
View File
@@ -8,13 +8,12 @@
import ThreadActions from "@app/components/ThreadActions.svelte"
import {makeThreadPath} from "@app/routes"
const {
url,
event,
}: {
type Props = {
url: string
event: TrustedEvent
} = $props()
}
const {url, event}: Props = $props()
const title = event.tags.find(nthEq(0, "title"))?.[1]
</script>
@@ -32,10 +31,10 @@
{formatTimestamp(event.created_at)}
</p>
{/if}
<Content {event} expandMode="inline" relays={[url]} />
<Content {event} {url} expandMode="inline" />
<div class="flex w-full flex-col items-end justify-between gap-2 sm:flex-row">
<span class="whitespace-nowrap py-1 text-sm opacity-75">
Posted by <ProfileLink pubkey={event.pubkey} />
Posted by <ProfileLink pubkey={event.pubkey} {url} />
</span>
<ThreadActions showActivity {url} {event} />
</div>
+22 -20
View File
@@ -1,26 +1,28 @@
import type {NodeViewProps} from "@tiptap/core"
import {deriveProfileDisplay} from "@welshman/app"
import {deriveAliasDisplay} from "@app/state"
export const MentionNodeView = ({node}: NodeViewProps) => {
const dom = document.createElement("span")
const display = deriveProfileDisplay(node.attrs.pubkey)
export const makeMentionNodeView =
(url?: string) =>
({node}: NodeViewProps) => {
const dom = document.createElement("span")
const display = deriveAliasDisplay(node.attrs.pubkey, url)
dom.classList.add("tiptap-object")
dom.classList.add("tiptap-object")
const unsubDisplay = display.subscribe($display => {
dom.textContent = "@" + $display
})
const unsubDisplay = display.subscribe($display => {
dom.textContent = "@" + $display
})
return {
dom,
destroy: () => {
unsubDisplay()
},
selectNode() {
dom.classList.add("tiptap-active")
},
deselectNode() {
dom.classList.remove("tiptap-active")
},
return {
dom,
destroy: () => {
unsubDisplay()
},
selectNode() {
dom.classList.add("tiptap-active")
},
deselectNode() {
dom.classList.remove("tiptap-active")
},
}
}
}
+9 -4
View File
@@ -5,15 +5,20 @@
deriveUserWotScore,
deriveHandleForPubkey,
displayHandle,
deriveProfileDisplay,
} from "@welshman/app"
import WotScore from "@lib/components/WotScore.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import {deriveAliasDisplay} from "@app/state"
const {value} = $props()
type Props = {
value: string
url?: string
}
const {value, url}: Props = $props()
const pubkey = value
const profileDisplay = deriveProfileDisplay(pubkey)
const profileDisplay = deriveAliasDisplay(pubkey)
const handle = deriveHandleForPubkey(pubkey)
const score = deriveUserWotScore(pubkey)
@@ -22,7 +27,7 @@
<div class="flex max-w-full gap-3">
<div class="py-1">
<ProfileCircle {pubkey} />
<ProfileCircle {pubkey} {url} />
</div>
<div class="flex min-w-0 flex-col">
<div class="flex items-center gap-2">
+5 -3
View File
@@ -5,7 +5,7 @@ import type {StampedEvent} from "@welshman/util"
import {Router, signer, profileSearch} from "@welshman/app"
import {Editor, MentionSuggestion, WelshmanExtension} from "@welshman/editor"
import {getSetting, userSettingValues} from "@app/state"
import {MentionNodeView} from "./MentionNodeView"
import {makeMentionNodeView} from "./MentionNodeView"
import ProfileSuggestion from "./ProfileSuggestion.svelte"
export const getUploadType = () => getSetting<"nip96" | "blossom">("upload_type")
@@ -30,6 +30,7 @@ export const makeEditor = ({
charCount,
content = "",
placeholder = "",
url,
submit,
uploading,
wordCount,
@@ -39,6 +40,7 @@ export const makeEditor = ({
charCount?: Writable<number>
content?: string
placeholder?: string
url?: string
submit: () => void
uploading?: Writable<boolean>
wordCount?: Writable<number>
@@ -76,7 +78,7 @@ export const makeEditor = ({
},
nprofile: {
extend: {
addNodeView: () => MentionNodeView,
addNodeView: () => makeMentionNodeView(url),
addProseMirrorPlugins() {
return [
MentionSuggestion({
@@ -86,7 +88,7 @@ export const makeEditor = ({
createSuggestion: (value: string) => {
const target = document.createElement("div")
mount(ProfileSuggestion, {target, props: {value}})
mount(ProfileSuggestion, {target, props: {value, url}})
return target
},
+96 -2
View File
@@ -1,5 +1,5 @@
import twColors from "tailwindcss/colors"
import {get, derived} from "svelte/store"
import {get, derived, writable} from "svelte/store"
import * as nip19 from "nostr-tools/nip19"
import {
remove,
@@ -31,6 +31,7 @@ import {
GROUPS,
THREAD,
COMMENT,
PROFILE,
getGroupTags,
getRelayTagValues,
getPubkeyTagValues,
@@ -38,10 +39,13 @@ import {
displayProfile,
readList,
getListTags,
readProfile,
asDecryptedEvent,
normalizeRelayUrl,
displayPubkey,
} from "@welshman/util"
import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util"
import {LOCAL_RELAY_URL} from "@welshman/relay"
import type {TrustedEvent, Profile, SignedEvent, PublishedList, List, Filter} from "@welshman/util"
import {Nip59, decrypt} from "@welshman/signer"
import {
pubkey,
@@ -64,6 +68,8 @@ import {
makeOutboxLoader,
routerContext,
appContext,
deriveProfile,
makeCachedLoader,
} from "@welshman/app"
import type {Thunk, Relay} from "@welshman/app"
import {deriveEvents, deriveEventsMapped, withGetter, synced} from "@welshman/store"
@@ -369,6 +375,93 @@ export const alertStatuses = deriveEventsMapped<AlertStatus>(repository, {
},
})
// Aliases
export type Alias = {
url: string
pubkey: string
profile: Profile
}
export const encodeAliasKey = (pubkey: string, url: string) => `${pubkey}:${url}`
export const decodeAliasKey = (key: string) => {
const [pubkey, url] = key.split(/:(.*)/s)
return {pubkey, url}
}
export const aliasesByKey = withGetter(writable(new Map<string, Alias>()))
export const loadAliasByKey = makeCachedLoader({
name: "aliases",
indexStore: aliasesByKey,
load: (key: string) => {
const {pubkey, url} = decodeAliasKey(key)
return load({
relays: [url],
filters: [{kinds: [PROFILE], authors: [pubkey]}],
onEvent: (event: TrustedEvent) => {
const profile = readProfile(event)
aliasesByKey.update($aliasesByKey => {
$aliasesByKey.set(key, {url, pubkey, profile})
return $aliasesByKey
})
},
})
},
})
export const deriveAlias = (pubkey: string, url?: string) => {
const membershipUrls = getMembershipUrls(userMembership.get())
// Attempt to load all relevant aliases
for (const $url of [url, ...membershipUrls]) {
if ($url) {
const key = encodeAliasKey(pubkey, $url)
loadAliasByKey(key)
}
}
return derived([aliasesByKey, deriveProfile(pubkey)], ([$aliasesByKey, $profile]) => {
// Try to find an alias for the url we were asked about
if (url) {
const alias = $aliasesByKey.get(encodeAliasKey(pubkey, url))
if (alias) {
return alias
}
}
// Fall back to global profiles
if ($profile) {
return {
pubkey,
url: LOCAL_RELAY_URL,
profile: $profile,
}
}
// Fall back to other aliases we know about
for (const $url of membershipUrls) {
const alias = $aliasesByKey.get(encodeAliasKey(pubkey, $url))
if (alias) {
return alias
}
}
})
}
export const deriveAliasDisplay = (pubkey: string, url?: string) =>
derived(deriveAlias(pubkey, url), $alias =>
displayProfile($alias?.profile, displayPubkey(pubkey)),
)
// Membership
export const hasMembershipUrl = (list: List | undefined, url: string) =>
@@ -470,6 +563,7 @@ export const {
name: "chats",
store: chats,
getKey: chat => chat.id,
load: always(Promise.resolve()),
})
export const chatSearch = derived(chats, $chats =>
+1 -1
View File
@@ -9,7 +9,7 @@
import {bytesToHex, hexToBytes} from "@noble/hashes/utils"
import {identity, memoize, sleep, defer, ago, WEEK, TaskQueue} from "@welshman/lib"
import type {TrustedEvent, StampedEvent} from "@welshman/util"
import {WRAP} from "@welshman/util"
import {WRAP, PROFILE, getTag} from "@welshman/util"
import {Nip46Broker, makeSecret} from "@welshman/signer"
import type {Socket} from "@welshman/net"
import {request, defaultSocketPolicies, makeSocketPolicyAuth} from "@welshman/net"
+8 -8
View File
@@ -2,7 +2,7 @@
import * as nip19 from "nostr-tools/nip19"
import {hexToBytes} from "@noble/hashes/utils"
import {displayPubkey, displayProfile} from "@welshman/util"
import {pubkey, session, displayNip05, deriveProfile} from "@welshman/app"
import {pubkey, session, displayNip05} from "@welshman/app"
import {slideAndFade} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte"
import FieldInline from "@lib/components/FieldInline.svelte"
@@ -13,11 +13,11 @@
import ProfileDelete from "@app/components/ProfileDelete.svelte"
import InfoKeys from "@app/components/InfoKeys.svelte"
import Alerts from "@app/components/Alerts.svelte"
import {PLATFORM_NAME} from "@app/state"
import {PLATFORM_NAME, deriveAlias} from "@app/state"
import {pushModal} from "@app/modal"
import {clip} from "@app/toast"
const profile = deriveProfile($pubkey!)
const alias = deriveAlias($pubkey!)
const pubkeyDisplay = displayPubkey($pubkey!)
@@ -39,16 +39,16 @@
<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} />
<Avatar src={$alias?.profile?.picture} size={10} />
</div>
<div class="flex min-w-0 flex-col">
<div class="flex items-center gap-2">
<div class="text-bold overflow-hidden text-ellipsis">
{displayProfile($profile, pubkeyDisplay)}
{displayProfile($alias?.profile, pubkeyDisplay)}
</div>
</div>
<div class="overflow-hidden text-ellipsis text-sm opacity-75">
{$profile?.nip05 ? displayNip05($profile.nip05) : pubkeyDisplay}
{$alias?.profile?.nip05 ? displayNip05($alias?.profile.nip05) : pubkeyDisplay}
</div>
</div>
</div>
@@ -56,8 +56,8 @@
<Icon icon="pen-new-square" />
</Button>
</div>
{#key $profile?.about}
<Content event={{content: $profile?.about || "", tags: []}} hideMediaAtDepth={0} />
{#key $alias?.profile?.about}
<Content event={{content: $alias?.profile?.about || "", tags: []}} hideMediaAtDepth={0} />
{/key}
</div>
{#if $session?.email}
@@ -311,7 +311,7 @@
<ChannelComposeParent event={share} clear={clearShare} verb="Sharing" />
{/if}
</div>
<ChannelCompose bind:this={compose} {onSubmit} />
<ChannelCompose bind:this={compose} {onSubmit} {url} />
</div>
{#if showScrollButton}
@@ -81,11 +81,11 @@
<CalendarEventDate event={$event} />
<div class="flex min-w-0 flex-grow flex-col gap-1">
<CalendarEventHeader event={$event} />
<CalendarEventMeta event={$event} />
<CalendarEventMeta event={$event} {url} />
<div class="flex py-2 opacity-50">
<div class="h-px flex-grow bg-base-content opacity-25"></div>
</div>
<Content showEntire event={$event} relays={[url]} />
<Content showEntire event={$event} {url} />
</div>
</div>
<div class="flex w-full flex-col justify-end sm:flex-row">
@@ -101,9 +101,9 @@
</div>
{/if}
{#each sortBy(e => e.created_at, $replies).slice(0, showAll ? undefined : 4) as reply (reply.id)}
<NoteCard event={reply} class="card2 bg-alt z-feature w-full">
<NoteCard event={reply} {url} class="card2 bg-alt z-feature w-full">
<div class="col-3 ml-12">
<Content showEntire event={reply} />
<Content showEntire event={reply} {url} />
<CalendarEventActions event={reply} {url} />
</div>
</NoteCard>
@@ -76,9 +76,9 @@
<PageContent class="flex flex-col p-2 pt-4">
{#if $event}
<div class="flex flex-col gap-3">
<NoteCard event={$event} class="card2 bg-alt z-feature w-full">
<NoteCard event={$event} {url} class="card2 bg-alt z-feature w-full">
<div class="col-3 ml-12">
<Content showEntire event={$event} relays={[url]} />
<Content showEntire event={$event} {url} />
<ThreadActions event={$event} {url} />
</div>
</NoteCard>
@@ -91,9 +91,9 @@
</div>
{/if}
{#each sortBy(e => -e.created_at, $replies).slice(0, showAll ? undefined : 4) as reply (reply.id)}
<NoteCard event={reply} class="card2 bg-alt z-feature w-full">
<NoteCard event={reply} {url} class="card2 bg-alt z-feature w-full">
<div class="col-3 ml-12">
<Content showEntire event={reply} />
<Content showEntire event={reply} {url} />
<ThreadActions event={reply} {url} />
</div>
</NoteCard>