kopia lustrzana https://github.com/cloudflare/wildebeest
commit
51fb115b79
|
@ -6,6 +6,7 @@ import type { Handle } from 'wildebeest/backend/src/utils/parse'
|
||||||
import { queryAcct } from 'wildebeest/backend/src/webfinger/index'
|
import { queryAcct } from 'wildebeest/backend/src/webfinger/index'
|
||||||
import { loadExternalMastodonAccount, loadLocalMastodonAccount } from 'wildebeest/backend/src/mastodon/account'
|
import { loadExternalMastodonAccount, loadLocalMastodonAccount } from 'wildebeest/backend/src/mastodon/account'
|
||||||
import { MastodonAccount } from '../types'
|
import { MastodonAccount } from '../types'
|
||||||
|
import { adjustLocalHostDomain } from '../utils/adjustLocalHostDomain'
|
||||||
|
|
||||||
export async function getAccount(domain: string, accountId: string, db: D1Database): Promise<MastodonAccount | null> {
|
export async function getAccount(domain: string, accountId: string, db: D1Database): Promise<MastodonAccount | null> {
|
||||||
const handle = parseHandle(accountId)
|
const handle = parseHandle(accountId)
|
||||||
|
@ -44,16 +45,3 @@ async function getLocalAccount(domain: string, db: D1Database, handle: Handle):
|
||||||
|
|
||||||
return await loadLocalMastodonAccount(db, actor)
|
return await loadLocalMastodonAccount(db, actor)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* checks if a domain is a localhost one ('localhost' or '127.x.x.x') and
|
|
||||||
* in that case replaces it with '0.0.0.0' (which is what we use for our local data)
|
|
||||||
*
|
|
||||||
* Note: only needed for local development
|
|
||||||
*
|
|
||||||
* @param domain the potentially localhost domain
|
|
||||||
* @returns the adjusted domain if it was a localhost one, the original domain otherwise
|
|
||||||
*/
|
|
||||||
function adjustLocalHostDomain(domain: string) {
|
|
||||||
return domain.replace(/^localhost$|^127(\.(?:\d){1,3}){3}$/, '0.0.0.0')
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
/**
|
||||||
|
* checks if a domain is a localhost one ('localhost' or '127.x.x.x') and
|
||||||
|
* in that case replaces it with '0.0.0.0' (which is what we use for our local data)
|
||||||
|
*
|
||||||
|
* Note: only needed for local development
|
||||||
|
*
|
||||||
|
* @param domain the potentially localhost domain
|
||||||
|
* @returns the adjusted domain if it was a localhost one, the original domain otherwise
|
||||||
|
*/
|
||||||
|
export function adjustLocalHostDomain(domain: string) {
|
||||||
|
return domain.replace(/^localhost$|^127(\.(?:\d){1,3}){3}$/, '0.0.0.0')
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import { Avatar } from '../avatar'
|
||||||
import type { Account, MastodonStatus } from '~/types'
|
import type { Account, MastodonStatus } from '~/types'
|
||||||
import styles from '../../utils/innerHtmlContent.scss?inline'
|
import styles from '../../utils/innerHtmlContent.scss?inline'
|
||||||
import { MediaGallery } from '../MediaGallery.tsx'
|
import { MediaGallery } from '../MediaGallery.tsx'
|
||||||
|
import { useAccountUrl } from '~/utils/useAccountUrl'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
status: MastodonStatus
|
status: MastodonStatus
|
||||||
|
@ -65,13 +66,15 @@ export default component$((props: Props) => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const RebloggerLink = ({ account }: { account: Account | null }) => {
|
export const RebloggerLink = component$(({ account }: { account: Account | null }) => {
|
||||||
|
const accountUrl = useAccountUrl(account)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
account && (
|
account && (
|
||||||
<div class="flex text-wildebeest-500 py-3">
|
<div class="flex text-wildebeest-500 py-3">
|
||||||
<p>
|
<p>
|
||||||
<i class="fa fa-retweet mr-3 w-4 inline-block" />
|
<i class="fa fa-retweet mr-3 w-4 inline-block" />
|
||||||
<a class="no-underline" href={account.url}>
|
<a class="no-underline" href={accountUrl}>
|
||||||
{account.display_name}
|
{account.display_name}
|
||||||
</a>
|
</a>
|
||||||
boosted
|
boosted
|
||||||
|
@ -79,4 +82,4 @@ export const RebloggerLink = ({ account }: { account: Account | null }) => {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { component$ } from '@builder.io/qwik'
|
import { component$ } from '@builder.io/qwik'
|
||||||
import type { Account } from '~/types'
|
import type { Account } from '~/types'
|
||||||
|
import { useAccountUrl } from '~/utils/useAccountUrl'
|
||||||
|
|
||||||
type AvatarDetails = Pick<Account, 'display_name' | 'avatar' | 'url'>
|
type AvatarDetails = Pick<Account, 'id' | 'display_name' | 'avatar' | 'url'>
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
primary: AvatarDetails
|
primary: AvatarDetails
|
||||||
|
@ -9,13 +10,16 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Avatar = component$<Props>(({ primary, secondary }) => {
|
export const Avatar = component$<Props>(({ primary, secondary }) => {
|
||||||
|
const primaryUrl = useAccountUrl(primary)
|
||||||
|
const secondaryUrl = useAccountUrl(secondary)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={`relative ${secondary && 'pr-2 pb-2'}`}>
|
<div class={`relative ${secondary && 'pr-2 pb-2'}`}>
|
||||||
<a href={primary.url}>
|
<a href={primaryUrl}>
|
||||||
<img class="rounded h-12 w-12" src={primary.avatar} alt={`Avatar of ${primary.display_name}`} />
|
<img class="rounded h-12 w-12" src={primary.avatar} alt={`Avatar of ${primary.display_name}`} />
|
||||||
</a>
|
</a>
|
||||||
{secondary && (
|
{secondary && (
|
||||||
<a href={secondary.url}>
|
<a href={secondaryUrl}>
|
||||||
<img
|
<img
|
||||||
class="absolute right-0 bottom-0 rounded h-6 w-6"
|
class="absolute right-0 bottom-0 rounded h-6 w-6"
|
||||||
src={secondary.avatar}
|
src={secondary.avatar}
|
||||||
|
|
|
@ -25,7 +25,7 @@ export const clientLoader = loader$<{ DATABASE: D1Database }, Promise<Client>>(a
|
||||||
|
|
||||||
export const userLoader = loader$<
|
export const userLoader = loader$<
|
||||||
{ DATABASE: D1Database; domain: string },
|
{ DATABASE: D1Database; domain: string },
|
||||||
Promise<{ email: string; avatar: URL; name: string; url: URL }>
|
Promise<{ email: string; avatar: URL; name: string; url: URL; accountId: string }>
|
||||||
>(async ({ cookie, platform, html, request, redirect, text }) => {
|
>(async ({ cookie, platform, html, request, redirect, text }) => {
|
||||||
const jwt = cookie.get('CF_Authorization')
|
const jwt = cookie.get('CF_Authorization')
|
||||||
if (jwt === null) {
|
if (jwt === null) {
|
||||||
|
@ -59,17 +59,18 @@ export const userLoader = loader$<
|
||||||
const name = person.name
|
const name = person.name
|
||||||
const avatar = person.icon?.url
|
const avatar = person.icon?.url
|
||||||
const url = person.url
|
const url = person.url
|
||||||
|
const accountId = person.id.toString()
|
||||||
|
|
||||||
if (!name || !avatar) {
|
if (!name || !avatar) {
|
||||||
throw html(500, getErrorHtml("The person associated with the Access JWT doesn't include a name or avatar"))
|
throw html(500, getErrorHtml("The person associated with the Access JWT doesn't include a name or avatar"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return { email: payload.email, avatar, name, url }
|
return { email: payload.email, avatar, name, url, accountId }
|
||||||
})
|
})
|
||||||
|
|
||||||
export default component$(() => {
|
export default component$(() => {
|
||||||
const client = clientLoader.use().value
|
const client = clientLoader.use().value
|
||||||
const { email, avatar, name: display_name, url } = userLoader.use().value
|
const { email, avatar, name: display_name, url, accountId } = userLoader.use().value
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col p-4 items-center">
|
<div class="flex flex-col p-4 items-center">
|
||||||
<h1 class="text-center mt-3 mb-5 flex items-center">
|
<h1 class="text-center mt-3 mb-5 flex items-center">
|
||||||
|
@ -82,6 +83,7 @@ export default component$(() => {
|
||||||
<div class="row-span-2 mr-4">
|
<div class="row-span-2 mr-4">
|
||||||
<Avatar
|
<Avatar
|
||||||
primary={{
|
primary={{
|
||||||
|
id: accountId,
|
||||||
avatar: avatar.toString(),
|
avatar: avatar.toString(),
|
||||||
display_name,
|
display_name,
|
||||||
url: url.toString(),
|
url: url.toString(),
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { MediaGallery } from '~/components/MediaGallery.tsx'
|
||||||
import { getNotFoundHtml } from '~/utils/getNotFoundHtml/getNotFoundHtml'
|
import { getNotFoundHtml } from '~/utils/getNotFoundHtml/getNotFoundHtml'
|
||||||
import { getErrorHtml } from '~/utils/getErrorHtml/getErrorHtml'
|
import { getErrorHtml } from '~/utils/getErrorHtml/getErrorHtml'
|
||||||
import styles from '../../../../utils/innerHtmlContent.scss?inline'
|
import styles from '../../../../utils/innerHtmlContent.scss?inline'
|
||||||
|
import { useAccountUrl } from '~/utils/useAccountUrl'
|
||||||
|
|
||||||
export const statusLoader = loader$<
|
export const statusLoader = loader$<
|
||||||
{ DATABASE: D1Database },
|
{ DATABASE: D1Database },
|
||||||
|
@ -67,7 +68,7 @@ export default component$(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
export const AccountCard = component$<{ status: MastodonStatus }>(({ status }) => {
|
export const AccountCard = component$<{ status: MastodonStatus }>(({ status }) => {
|
||||||
const accountUrl = `/@${status.account.username}`
|
const accountUrl = useAccountUrl(status.account)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { useSignal, useTask$ } from '@builder.io/qwik'
|
||||||
|
import { parseHandle } from 'wildebeest/backend/src/utils/parse'
|
||||||
|
import { Account } from '~/types'
|
||||||
|
import { useDomain } from './useDomain'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to get a url to use for links for the provided account.
|
||||||
|
*
|
||||||
|
* Note: using account.url is not sufficient since we want to distinguish
|
||||||
|
* between local and remote accounts and change the url accordingly
|
||||||
|
*
|
||||||
|
* @param account the target account or null
|
||||||
|
* @returns url to be used for the target account (or undefined if)
|
||||||
|
*/
|
||||||
|
export function useAccountUrl(account: Pick<Account, 'id' | 'url'> | null) {
|
||||||
|
const isLocal = useAccountIsLocal(account?.id)
|
||||||
|
|
||||||
|
if (account && isLocal.value) {
|
||||||
|
const url = new URL(account.url)
|
||||||
|
return url.pathname
|
||||||
|
}
|
||||||
|
|
||||||
|
return account?.url
|
||||||
|
}
|
||||||
|
|
||||||
|
function useAccountIsLocal(accountId: string | undefined) {
|
||||||
|
const domain = useDomain()
|
||||||
|
const isLocal = useSignal(false)
|
||||||
|
|
||||||
|
useTask$(({ track }) => {
|
||||||
|
track(() => accountId)
|
||||||
|
|
||||||
|
if (accountId) {
|
||||||
|
const handle = parseHandle(accountId)
|
||||||
|
isLocal.value = handle.domain === null || (handle.domain !== null && handle.domain === domain)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return isLocal
|
||||||
|
}
|
|
@ -1,8 +1,10 @@
|
||||||
import { useLocation } from '@builder.io/qwik-city'
|
import { useLocation } from '@builder.io/qwik-city'
|
||||||
|
import { adjustLocalHostDomain } from 'wildebeest/backend/src/utils/adjustLocalHostDomain'
|
||||||
|
|
||||||
export const useDomain = () => {
|
export const useDomain = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const url = new URL(location.href)
|
const url = new URL(location.href)
|
||||||
const domain = url.hostname
|
const domain = url.hostname
|
||||||
return domain
|
const adjustedDomain = adjustLocalHostDomain(domain)
|
||||||
|
return adjustedDomain
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,22 @@
|
||||||
import { test, expect } from '@playwright/test'
|
import { test, expect } from '@playwright/test'
|
||||||
|
|
||||||
test('Navigation to and view of an account', async ({ page }) => {
|
const navigationsVia = ['name link', 'avatar'] as const
|
||||||
await page.goto('http://127.0.0.1:8788/explore')
|
|
||||||
await page.getByRole('article').getByRole('link', { name: 'Ben Rosengart', exact: true }).click()
|
navigationsVia.forEach((via) =>
|
||||||
await page.getByRole('link', { name: 'Ben Rosengart', exact: true }).click()
|
test(`Navigation to and view of an account (via ${via})`, async ({ page }) => {
|
||||||
await page.waitForLoadState('networkidle')
|
await page.goto('http://127.0.0.1:8788/explore')
|
||||||
await page.getByRole('link', { name: 'Ben Rosengart', exact: true }).click()
|
await page.getByRole('article').getByRole('link', { name: 'Ben Rosengart', exact: true }).click()
|
||||||
await expect(page.getByRole('img', { name: 'Header of Ben Rosengart' })).toBeVisible()
|
await page.getByRole('link', { name: 'Ben Rosengart', exact: true }).click()
|
||||||
await expect(page.getByRole('img', { name: 'Avatar of Ben Rosengart' })).toBeVisible()
|
await page.waitForLoadState('networkidle')
|
||||||
await expect(page.getByRole('heading', { name: 'Ben Rosengart' })).toBeVisible()
|
if (via === 'name link') {
|
||||||
await expect(page.getByText('Joined')).toBeVisible()
|
await page.getByRole('link', { name: 'Ben Rosengart', exact: true }).click()
|
||||||
await expect(page.getByTestId('stats')).toHaveText('1Posts0Following0Followers')
|
} else {
|
||||||
})
|
await page.getByRole('link', { name: 'Avatar of Ben Rosengart' }).click()
|
||||||
|
}
|
||||||
|
await expect(page.getByRole('img', { name: 'Header of Ben Rosengart' })).toBeVisible()
|
||||||
|
await expect(page.getByRole('img', { name: 'Avatar of Ben Rosengart' })).toBeVisible()
|
||||||
|
await expect(page.getByRole('heading', { name: 'Ben Rosengart' })).toBeVisible()
|
||||||
|
await expect(page.getByText('Joined')).toBeVisible()
|
||||||
|
await expect(page.getByTestId('stats')).toHaveText('1Posts0Following0Followers')
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
Ładowanie…
Reference in New Issue