diff --git a/components/account/AccountHoverWrapper.vue b/components/account/AccountHoverWrapper.vue index ee9d6287..db52b3f0 100644 --- a/components/account/AccountHoverWrapper.vue +++ b/components/account/AccountHoverWrapper.vue @@ -1,17 +1,20 @@ <script setup lang="ts"> import type { Account } from 'masto' -defineProps<{ - account: Account +const props = defineProps<{ + account?: Account + handle?: string disabled?: boolean }>() + +const account = props.account || (props.handle ? useAccountByHandle(props.handle!) : undefined) </script> <template> - <VMenu v-if="!disabled" placement="bottom-start" :delay="{ show: 500, hide: 100 }"> + <VMenu v-if="!disabled && account" placement="bottom-start" :delay="{ show: 500, hide: 100 }"> <slot /> <template #popper> - <AccountHoverCard :account="account" /> + <AccountHoverCard v-if="account" :account="account" /> </template> </VMenu> <slot v-else /> diff --git a/components/status/StatusReplyingTo.vue b/components/status/StatusReplyingTo.vue index ae5fa301..455f541b 100644 --- a/components/status/StatusReplyingTo.vue +++ b/components/status/StatusReplyingTo.vue @@ -5,7 +5,7 @@ const { status } = defineProps<{ status: Status }>() -const account = asyncComputed(() => fetchAccount(status.inReplyToAccountId!)) +const account = useAccountById(status.inReplyToAccountId!) </script> <template> diff --git a/composables/cache.ts b/composables/cache.ts index 5b7cbbb5..37d94af7 100644 --- a/composables/cache.ts +++ b/composables/cache.ts @@ -28,7 +28,7 @@ export function fetchStatus(id: string): Promise<Status> { return promise } -export function fetchAccount(id: string): Promise<Account> { +export function fetchAccountById(id: string): Promise<Account> { const key = `account:${id}` const cached = cache.get(key) if (cached) @@ -42,7 +42,7 @@ export function fetchAccount(id: string): Promise<Account> { return promise } -export async function fetchAccountByName(acct: string): Promise<Account> { +export async function fetchAccountByHandle(acct: string): Promise<Account> { const key = `account:${acct}` const cached = cache.get(key) if (cached) @@ -56,6 +56,14 @@ export async function fetchAccountByName(acct: string): Promise<Account> { return account } +export function useAccountByHandle(acct: string) { + return useAsyncState(() => fetchAccountByHandle(acct), null).state +} + +export function useAccountById(id: string) { + return useAsyncState(() => fetchAccountById(id), null).state +} + export function cacheStatus(status: Status, override?: boolean) { setCached(`status:${status.id}`, status, override) } diff --git a/composables/content.ts b/composables/content.ts index c1a32418..4e3e87c0 100644 --- a/composables/content.ts +++ b/composables/content.ts @@ -5,6 +5,7 @@ import type { VNode } from 'vue' import { Fragment, h, isVNode } from 'vue' import { RouterLink } from 'vue-router' import ContentCode from '~/components/content/ContentCode.vue' +import AccountHoverWrapper from '~/components/account/AccountHoverWrapper.vue' type Node = DefaultTreeAdapterMap['childNode'] type Element = DefaultTreeAdapterMap['element'] @@ -18,7 +19,9 @@ function handleMention(el: Element) { if (matchUser) { const [, server, username] = matchUser // Handles need to ignore server subdomains - href.value = `/@${username}@${server.replace(/(.+\.)(.+\..+)/, '$2')}` + const handle = `@${username}@${server.replace(/(.+\.)(.+\..+)/, '$2')}` + href.value = `/${handle}` + return h(AccountHoverWrapper, { handle, class: 'inline-block' }, () => nodeToVNode(el)) } const matchTag = href.value.match(TagLinkRE) if (matchTag) { @@ -108,22 +111,13 @@ export function contentToVNode( return h(Fragment, tree.childNodes.map(n => treeToVNode(n))) } -function treeToVNode( - input: Node, -): VNode | string | null { - if (input.nodeName === '#text') { +function nodeToVNode(node: Node): VNode | string | null { + if (node.nodeName === '#text') { // @ts-expect-error casing - const text = input.value as string - return text + return input.value as string } - if ('childNodes' in input) { - const node = handleNode(input) - if (node == null) - return null - if (isVNode(node)) - return node - + if ('childNodes' in node) { const attrs = Object.fromEntries(node.attrs.map(i => [i.name, i.value])) if (node.nodeName === 'a' && (attrs.href?.startsWith('/') || attrs.href?.startsWith('.'))) { attrs.to = attrs.href @@ -144,6 +138,25 @@ function treeToVNode( return null } +function treeToVNode( + input: Node, +): VNode | string | null { + if (input.nodeName === '#text') { + // @ts-expect-error casing + return input.value as string + } + + if ('childNodes' in input) { + const node = handleNode(input) + if (node == null) + return null + if (isVNode(node)) + return node + return nodeToVNode(node) + } + return null +} + export function htmlToText(html: string) { const tree = parseFragment(html) return tree.childNodes.map(n => treeToText(n)).join('').trim() diff --git a/pages/@[account]/index/followers.vue b/pages/@[account]/index/followers.vue index 5b7ef1ed..a7109fb2 100644 --- a/pages/@[account]/index/followers.vue +++ b/pages/@[account]/index/followers.vue @@ -1,8 +1,8 @@ <script setup lang="ts"> const params = useRoute().params -const accountName = $(computedEager(() => params.account as string)) +const handle = $(computedEager(() => params.account as string)) -const account = await fetchAccountByName(accountName) +const account = await fetchAccountByHandle(handle) const paginator = account ? useMasto().accounts.getFollowersIterable(account.id, {}) : null </script> diff --git a/pages/@[account]/index/following.vue b/pages/@[account]/index/following.vue index f6dff0dc..d08e6cae 100644 --- a/pages/@[account]/index/following.vue +++ b/pages/@[account]/index/following.vue @@ -1,8 +1,8 @@ <script setup lang="ts"> const params = useRoute().params -const accountName = $(computedEager(() => params.account as string)) +const handle = $(computedEager(() => params.account as string)) -const account = await fetchAccountByName(accountName) +const account = await fetchAccountByHandle(handle) const paginator = account ? useMasto().accounts.getFollowingIterable(account.id, {}) : null </script> diff --git a/pages/@[account]/index/index.vue b/pages/@[account]/index/index.vue index e2790100..0b7de973 100644 --- a/pages/@[account]/index/index.vue +++ b/pages/@[account]/index/index.vue @@ -1,8 +1,8 @@ <script setup lang="ts"> const params = useRoute().params -const accountName = $(computedEager(() => params.account as string)) +const handle = $(computedEager(() => params.account as string)) -const account = await fetchAccountByName(accountName) +const account = await fetchAccountByHandle(handle) const { t } = useI18n() const paginatorPosts = useMasto().accounts.getStatusesIterable(account.id, { excludeReplies: true }) diff --git a/tests/content-rich.test.ts b/tests/content-rich.test.ts index ae475044..7274c211 100644 --- a/tests/content-rich.test.ts +++ b/tests/content-rich.test.ts @@ -91,3 +91,13 @@ vi.mock('../components/content/ContentCode.vue', () => { }), } }) + +vi.mock('../components/account/AccountHoverWrapper.vue', () => { + return { + default: defineComponent({ + setup(_, { slots }) { + return () => slots?.default?.() + }, + }), + } +})