forked from Mirrors/elk
feat: hover card for content @
This commit is contained in:
parent
66393cd838
commit
2becb254b4
8 changed files with 61 additions and 27 deletions
|
@ -1,17 +1,20 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Account } from 'masto'
|
import type { Account } from 'masto'
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
account: Account
|
account?: Account
|
||||||
|
handle?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const account = props.account || (props.handle ? useAccountByHandle(props.handle!) : undefined)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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 />
|
<slot />
|
||||||
<template #popper>
|
<template #popper>
|
||||||
<AccountHoverCard :account="account" />
|
<AccountHoverCard v-if="account" :account="account" />
|
||||||
</template>
|
</template>
|
||||||
</VMenu>
|
</VMenu>
|
||||||
<slot v-else />
|
<slot v-else />
|
||||||
|
|
|
@ -5,7 +5,7 @@ const { status } = defineProps<{
|
||||||
status: Status
|
status: Status
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const account = asyncComputed(() => fetchAccount(status.inReplyToAccountId!))
|
const account = useAccountById(status.inReplyToAccountId!)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -28,7 +28,7 @@ export function fetchStatus(id: string): Promise<Status> {
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchAccount(id: string): Promise<Account> {
|
export function fetchAccountById(id: string): Promise<Account> {
|
||||||
const key = `account:${id}`
|
const key = `account:${id}`
|
||||||
const cached = cache.get(key)
|
const cached = cache.get(key)
|
||||||
if (cached)
|
if (cached)
|
||||||
|
@ -42,7 +42,7 @@ export function fetchAccount(id: string): Promise<Account> {
|
||||||
return promise
|
return promise
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAccountByName(acct: string): Promise<Account> {
|
export async function fetchAccountByHandle(acct: string): Promise<Account> {
|
||||||
const key = `account:${acct}`
|
const key = `account:${acct}`
|
||||||
const cached = cache.get(key)
|
const cached = cache.get(key)
|
||||||
if (cached)
|
if (cached)
|
||||||
|
@ -56,6 +56,14 @@ export async function fetchAccountByName(acct: string): Promise<Account> {
|
||||||
return 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) {
|
export function cacheStatus(status: Status, override?: boolean) {
|
||||||
setCached(`status:${status.id}`, status, override)
|
setCached(`status:${status.id}`, status, override)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import type { VNode } from 'vue'
|
||||||
import { Fragment, h, isVNode } from 'vue'
|
import { Fragment, h, isVNode } from 'vue'
|
||||||
import { RouterLink } from 'vue-router'
|
import { RouterLink } from 'vue-router'
|
||||||
import ContentCode from '~/components/content/ContentCode.vue'
|
import ContentCode from '~/components/content/ContentCode.vue'
|
||||||
|
import AccountHoverWrapper from '~/components/account/AccountHoverWrapper.vue'
|
||||||
|
|
||||||
type Node = DefaultTreeAdapterMap['childNode']
|
type Node = DefaultTreeAdapterMap['childNode']
|
||||||
type Element = DefaultTreeAdapterMap['element']
|
type Element = DefaultTreeAdapterMap['element']
|
||||||
|
@ -18,7 +19,9 @@ function handleMention(el: Element) {
|
||||||
if (matchUser) {
|
if (matchUser) {
|
||||||
const [, server, username] = matchUser
|
const [, server, username] = matchUser
|
||||||
// Handles need to ignore server subdomains
|
// 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)
|
const matchTag = href.value.match(TagLinkRE)
|
||||||
if (matchTag) {
|
if (matchTag) {
|
||||||
|
@ -108,22 +111,13 @@ export function contentToVNode(
|
||||||
return h(Fragment, tree.childNodes.map(n => treeToVNode(n)))
|
return h(Fragment, tree.childNodes.map(n => treeToVNode(n)))
|
||||||
}
|
}
|
||||||
|
|
||||||
function treeToVNode(
|
function nodeToVNode(node: Node): VNode | string | null {
|
||||||
input: Node,
|
if (node.nodeName === '#text') {
|
||||||
): VNode | string | null {
|
|
||||||
if (input.nodeName === '#text') {
|
|
||||||
// @ts-expect-error casing
|
// @ts-expect-error casing
|
||||||
const text = input.value as string
|
return input.value as string
|
||||||
return text
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('childNodes' in input) {
|
if ('childNodes' in node) {
|
||||||
const node = handleNode(input)
|
|
||||||
if (node == null)
|
|
||||||
return null
|
|
||||||
if (isVNode(node))
|
|
||||||
return node
|
|
||||||
|
|
||||||
const attrs = Object.fromEntries(node.attrs.map(i => [i.name, i.value]))
|
const attrs = Object.fromEntries(node.attrs.map(i => [i.name, i.value]))
|
||||||
if (node.nodeName === 'a' && (attrs.href?.startsWith('/') || attrs.href?.startsWith('.'))) {
|
if (node.nodeName === 'a' && (attrs.href?.startsWith('/') || attrs.href?.startsWith('.'))) {
|
||||||
attrs.to = attrs.href
|
attrs.to = attrs.href
|
||||||
|
@ -144,6 +138,25 @@ function treeToVNode(
|
||||||
return null
|
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) {
|
export function htmlToText(html: string) {
|
||||||
const tree = parseFragment(html)
|
const tree = parseFragment(html)
|
||||||
return tree.childNodes.map(n => treeToText(n)).join('').trim()
|
return tree.childNodes.map(n => treeToText(n)).join('').trim()
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const params = useRoute().params
|
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
|
const paginator = account ? useMasto().accounts.getFollowersIterable(account.id, {}) : null
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const params = useRoute().params
|
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
|
const paginator = account ? useMasto().accounts.getFollowingIterable(account.id, {}) : null
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const params = useRoute().params
|
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 { t } = useI18n()
|
||||||
|
|
||||||
const paginatorPosts = useMasto().accounts.getStatusesIterable(account.id, { excludeReplies: true })
|
const paginatorPosts = useMasto().accounts.getStatusesIterable(account.id, { excludeReplies: true })
|
||||||
|
|
|
@ -91,3 +91,13 @@ vi.mock('../components/content/ContentCode.vue', () => {
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
vi.mock('../components/account/AccountHoverWrapper.vue', () => {
|
||||||
|
return {
|
||||||
|
default: defineComponent({
|
||||||
|
setup(_, { slots }) {
|
||||||
|
return () => slots?.default?.()
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
Loading…
Reference in a new issue