From c8e9f04b9b77e64f562dea52bf991a149e36bf89 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Wed, 8 Feb 2023 10:59:26 +0000 Subject: [PATCH] adjust account urls resolves #216 --- backend/src/accounts/getAccount.ts | 14 +------ backend/src/utils/adjustLocalHostDomain.ts | 12 ++++++ frontend/src/components/Status/index.tsx | 9 +++-- frontend/src/components/avatar/index.tsx | 10 +++-- .../routes/(admin)/oauth/authorize/index.tsx | 8 ++-- .../[accountId]/[statusId]/index.tsx | 3 +- frontend/src/utils/useAccountUrl.ts | 40 +++++++++++++++++++ frontend/src/utils/useDomain.ts | 4 +- ui-e2e-tests/account-page.spec.ts | 32 +++++++++------ 9 files changed, 96 insertions(+), 36 deletions(-) create mode 100644 backend/src/utils/adjustLocalHostDomain.ts create mode 100644 frontend/src/utils/useAccountUrl.ts diff --git a/backend/src/accounts/getAccount.ts b/backend/src/accounts/getAccount.ts index 3b674cb..5f3f630 100644 --- a/backend/src/accounts/getAccount.ts +++ b/backend/src/accounts/getAccount.ts @@ -6,6 +6,7 @@ import type { Handle } from 'wildebeest/backend/src/utils/parse' import { queryAcct } from 'wildebeest/backend/src/webfinger/index' import { loadExternalMastodonAccount, loadLocalMastodonAccount } from 'wildebeest/backend/src/mastodon/account' import { MastodonAccount } from '../types' +import { adjustLocalHostDomain } from '../utils/adjustLocalHostDomain' export async function getAccount(domain: string, accountId: string, db: D1Database): Promise { const handle = parseHandle(accountId) @@ -44,16 +45,3 @@ async function getLocalAccount(domain: string, db: D1Database, handle: Handle): 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') -} diff --git a/backend/src/utils/adjustLocalHostDomain.ts b/backend/src/utils/adjustLocalHostDomain.ts new file mode 100644 index 0000000..4c48db2 --- /dev/null +++ b/backend/src/utils/adjustLocalHostDomain.ts @@ -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') +} diff --git a/frontend/src/components/Status/index.tsx b/frontend/src/components/Status/index.tsx index d567e1d..2777d80 100644 --- a/frontend/src/components/Status/index.tsx +++ b/frontend/src/components/Status/index.tsx @@ -5,6 +5,7 @@ import { Avatar } from '../avatar' import type { Account, MastodonStatus } from '~/types' import styles from '../../utils/innerHtmlContent.scss?inline' import { MediaGallery } from '../MediaGallery.tsx' +import { useAccountUrl } from '~/utils/useAccountUrl' type Props = { 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 ( account && (

- + {account.display_name}  boosted @@ -79,4 +82,4 @@ export const RebloggerLink = ({ account }: { account: Account | null }) => {

) ) -} +}) diff --git a/frontend/src/components/avatar/index.tsx b/frontend/src/components/avatar/index.tsx index 9c6e15a..c93b55c 100644 --- a/frontend/src/components/avatar/index.tsx +++ b/frontend/src/components/avatar/index.tsx @@ -1,7 +1,8 @@ import { component$ } from '@builder.io/qwik' import type { Account } from '~/types' +import { useAccountUrl } from '~/utils/useAccountUrl' -type AvatarDetails = Pick +type AvatarDetails = Pick type Props = { primary: AvatarDetails @@ -9,13 +10,16 @@ type Props = { } export const Avatar = component$(({ primary, secondary }) => { + const primaryUrl = useAccountUrl(primary) + const secondaryUrl = useAccountUrl(secondary) + return (
- + {`Avatar {secondary && ( - + >(a export const userLoader = loader$< { 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 }) => { const jwt = cookie.get('CF_Authorization') if (jwt === null) { @@ -59,17 +59,18 @@ export const userLoader = loader$< const name = person.name const avatar = person.icon?.url const url = person.url + const accountId = person.id.toString() if (!name || !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$(() => { 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 (

@@ -82,6 +83,7 @@ export default component$(() => {
{ }) export const AccountCard = component$<{ status: MastodonStatus }>(({ status }) => { - const accountUrl = `/@${status.account.username}` + const accountUrl = useAccountUrl(status.account) return (
diff --git a/frontend/src/utils/useAccountUrl.ts b/frontend/src/utils/useAccountUrl.ts new file mode 100644 index 0000000..3a46241 --- /dev/null +++ b/frontend/src/utils/useAccountUrl.ts @@ -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 | 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 +} diff --git a/frontend/src/utils/useDomain.ts b/frontend/src/utils/useDomain.ts index 769c465..98bbe5e 100644 --- a/frontend/src/utils/useDomain.ts +++ b/frontend/src/utils/useDomain.ts @@ -1,8 +1,10 @@ import { useLocation } from '@builder.io/qwik-city' +import { adjustLocalHostDomain } from 'wildebeest/backend/src/utils/adjustLocalHostDomain' export const useDomain = () => { const location = useLocation() const url = new URL(location.href) const domain = url.hostname - return domain + const adjustedDomain = adjustLocalHostDomain(domain) + return adjustedDomain } diff --git a/ui-e2e-tests/account-page.spec.ts b/ui-e2e-tests/account-page.spec.ts index 7357df1..e2c9fe0 100644 --- a/ui-e2e-tests/account-page.spec.ts +++ b/ui-e2e-tests/account-page.spec.ts @@ -1,14 +1,22 @@ import { test, expect } from '@playwright/test' -test('Navigation to and view of an account', async ({ page }) => { - await page.goto('http://127.0.0.1:8788/explore') - await page.getByRole('article').getByRole('link', { name: 'Ben Rosengart', exact: true }).click() - await page.getByRole('link', { name: 'Ben Rosengart', exact: true }).click() - await page.waitForLoadState('networkidle') - await page.getByRole('link', { name: 'Ben Rosengart', exact: true }).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') -}) +const navigationsVia = ['name link', 'avatar'] as const + +navigationsVia.forEach((via) => + test(`Navigation to and view of an account (via ${via})`, async ({ page }) => { + await page.goto('http://127.0.0.1:8788/explore') + await page.getByRole('article').getByRole('link', { name: 'Ben Rosengart', exact: true }).click() + await page.getByRole('link', { name: 'Ben Rosengart', exact: true }).click() + await page.waitForLoadState('networkidle') + if (via === 'name link') { + await page.getByRole('link', { name: 'Ben Rosengart', exact: true }).click() + } 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') + }) +)