diff --git a/frontend/src/components/Status/index.tsx b/frontend/src/components/Status/index.tsx
index 9ebe75c..1ef3698 100644
--- a/frontend/src/components/Status/index.tsx
+++ b/frontend/src/components/Status/index.tsx
@@ -6,6 +6,7 @@ import type { Account, MastodonStatus } from '~/types'
import styles from '../../utils/innerHtmlContent.scss?inline'
import { MediaGallery } from '../MediaGallery.tsx'
import { useAccountUrl } from '~/utils/useAccountUrl'
+import { getDisplayNameElement } from '~/utils/getDisplayNameElement'
type Props = {
status: MastodonStatus
@@ -33,7 +34,7 @@ export default component$((props: Props) => {
- {status.account.display_name}
+ {getDisplayNameElement(status.account)}
@{status.account.username}
@@ -75,7 +76,7 @@ export const RebloggerLink = component$(({ account }: { account: Account | null
- {account.display_name}
+ {getDisplayNameElement(account)}
boosted
diff --git a/frontend/src/routes/(frontend)/[accountId]/[statusId]/index.tsx b/frontend/src/routes/(frontend)/[accountId]/[statusId]/index.tsx
index 5981033..cb68843 100644
--- a/frontend/src/routes/(frontend)/[accountId]/[statusId]/index.tsx
+++ b/frontend/src/routes/(frontend)/[accountId]/[statusId]/index.tsx
@@ -15,6 +15,7 @@ import styles from '../../../../utils/innerHtmlContent.scss?inline'
import { getTextContent } from 'wildebeest/backend/src/activitypub/objects'
import { getDocumentHead } from '~/utils/getDocumentHead'
import { useAccountUrl } from '~/utils/useAccountUrl'
+import { getDisplayNameElement } from '~/utils/getDisplayNameElement'
export const statusLoader = loader$<
{ DATABASE: D1Database },
@@ -81,7 +82,7 @@ export const AccountCard = component$<{ status: MastodonStatus }>(({ status }) =
- {status.account.display_name}
+ {getDisplayNameElement(status.account)}
@{status.account.acct}
diff --git a/frontend/src/routes/(frontend)/[accountId]/index.tsx b/frontend/src/routes/(frontend)/[accountId]/index.tsx
index 43dadc2..31ada58 100644
--- a/frontend/src/routes/(frontend)/[accountId]/index.tsx
+++ b/frontend/src/routes/(frontend)/[accountId]/index.tsx
@@ -9,10 +9,11 @@ import { getAccount } from 'wildebeest/backend/src/accounts/getAccount'
import { getNotFoundHtml } from '~/utils/getNotFoundHtml/getNotFoundHtml'
import { getErrorHtml } from '~/utils/getErrorHtml/getErrorHtml'
import { getDocumentHead } from '~/utils/getDocumentHead'
-import type { MastodonStatus } from '~/types'
+import type { Account, MastodonStatus } from '~/types'
import { StatusesPanel } from '~/components/StatusesPanel/StatusesPanel'
import { parseHandle } from 'wildebeest/backend/src/utils/parse'
import { getLocalStatuses } from 'wildebeest/functions/api/v1/accounts/[id]/statuses'
+import { getDisplayNameElement } from '~/utils/getDisplayNameElement'
export const accountLoader = loader$<
{ DATABASE: D1Database },
@@ -93,7 +94,7 @@ export default component$(() => {
/>
-
{accountDetails.account.display_name}
+
{getDisplayNameElement(accountDetails.account as Account)}
{accountDetails.accountHandle}
diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss
index bd41489..9929e83 100644
--- a/frontend/src/styles.scss
+++ b/frontend/src/styles.scss
@@ -65,3 +65,14 @@ button {
.pointer {
cursor: pointer;
}
+
+.custom-emoji {
+ display: inline-block;
+ font-size: inherit;
+ vertical-align: middle;
+ -o-object-fit: contain;
+ object-fit: contain;
+ margin: -.2ex 0.15em .2ex;
+ width: 1rem;
+ height: 1rem;
+}
diff --git a/frontend/src/utils/getDisplayNameElement.tsx b/frontend/src/utils/getDisplayNameElement.tsx
new file mode 100644
index 0000000..5858d9b
--- /dev/null
+++ b/frontend/src/utils/getDisplayNameElement.tsx
@@ -0,0 +1,21 @@
+import { type JSXNode } from '@builder.io/qwik'
+import { type Account } from '~/types'
+
+export function getDisplayNameElement(account: Account): JSXNode {
+ return (
+ <>
+ {account.display_name.split(/(:[^\s:]+:)/g).map((str) => {
+ const customEmojiMatch = str.match(/^:([^\s:]+):$/)
+ if (customEmojiMatch) {
+ const shortCode = customEmojiMatch[1]
+ const customEmojiInfo = account.emojis.find((emoji) => emoji.shortcode === shortCode)
+ if (customEmojiInfo) {
+ // eslint-disable-next-line qwik/single-jsx-root
+ return
+ }
+ }
+ return <>{str}>
+ })}
+ >
+ )
+}
diff --git a/ui-e2e-tests/custom-emojis.spec.ts b/ui-e2e-tests/custom-emojis.spec.ts
new file mode 100644
index 0000000..6e89afa
--- /dev/null
+++ b/ui-e2e-tests/custom-emojis.spec.ts
@@ -0,0 +1,49 @@
+import { test, expect } from '@playwright/test'
+
+test('View of custom emojis in an toots author display name', async ({ page, browserName }) => {
+ // this page.route is a hack to mock the custom emojis since they haven't
+ // yet been implemented in the backend (this should be not needed and removed
+ // when those are implemented)
+ test.skip(
+ browserName !== 'chromium',
+ "Only chromium seem to test this well, I suspect it's because of the page.route"
+ )
+ await page.route('http://127.0.0.1:8788/@george/*/q-data.json', async (route) => {
+ const response = await route.fetch()
+ let body = await response.text()
+ body = body.replace(
+ /"emojis":\[\]/g,
+ `"emojis": ${JSON.stringify([
+ {
+ shortcode: 'verified',
+ url: 'https://files.mastodon.social/cache/custom_emojis/images/000/452/462/original/947cae7ac4dfdfa0.png',
+ static_url:
+ 'https://files.mastodon.social/cache/custom_emojis/images/000/452/462/static/947cae7ac4dfdfa0.png',
+ visible_in_picker: true,
+ },
+ ])}`
+ )
+ await route.fulfill({
+ response,
+ body,
+ })
+ })
+
+ await page.goto('http://127.0.0.1:8788/explore')
+
+ await page
+ .locator('article')
+ .filter({ hasText: 'George' })
+ .filter({
+ hasText: 'We did it!',
+ })
+ .locator('i.fa-globe + span')
+ .click()
+
+ const customEmojiLocator = page.getByRole('link', { name: 'George :verified: 👍', exact: true }).getByRole('img')
+ await expect(customEmojiLocator).toBeVisible()
+ await expect(customEmojiLocator).toHaveAttribute(
+ 'src',
+ 'https://files.mastodon.social/cache/custom_emojis/images/000/452/462/original/947cae7ac4dfdfa0.png'
+ )
+})
diff --git a/ui-e2e-tests/infinite-scrolling.spec.ts b/ui-e2e-tests/infinite-scrolling.spec.ts
index aae895b..d023db2 100644
--- a/ui-e2e-tests/infinite-scrolling.spec.ts
+++ b/ui-e2e-tests/infinite-scrolling.spec.ts
@@ -84,7 +84,7 @@ export function getMockStatusFn(): () => MastodonStatus {
const paddedNum = `${numOfGeneratedMockStatuses}`.padStart(3, '0')
const status = {
content: `Mock Fetched Status #${paddedNum}`,
- account: {} as Account,
+ account: { display_name: 'test', emojis: [] } as unknown as Account,
media_attachments: [],
} as unknown as MastodonStatus
numOfGeneratedMockStatuses++