diff --git a/backend/src/activitypub/objects/note.ts b/backend/src/activitypub/objects/note.ts index f1f12ba..1986bbc 100644 --- a/backend/src/activitypub/objects/note.ts +++ b/backend/src/activitypub/objects/note.ts @@ -19,6 +19,7 @@ export interface Note extends objects.APObject { attachment: Array cc: Array tag: Array + spoiler_text?: string } export async function createPublicNote( diff --git a/backend/src/mastodon/status.ts b/backend/src/mastodon/status.ts index 3afadcd..06bf07f 100644 --- a/backend/src/mastodon/status.ts +++ b/backend/src/mastodon/status.ts @@ -78,10 +78,10 @@ export async function toMastodonStatusFromObject( emojis: [], tags: [], mentions: [], + spoiler_text: obj.spoiler_text ?? '', // TODO: stub values visibility: 'public', - spoiler_text: '', media_attachments: mediaAttachments, content: obj.content || '', @@ -143,10 +143,10 @@ export async function toMastodonStatusFromRow( tags: [], mentions: [], account, + spoiler_text: properties.spoiler_text ?? '', // TODO: stub values visibility: 'public', - spoiler_text: '', content: properties.content, favourites_count: row.favourites_count, diff --git a/frontend/mock-db/init.ts b/frontend/mock-db/init.ts index 7d8a151..1219e64 100644 --- a/frontend/mock-db/init.ts +++ b/frontend/mock-db/init.ts @@ -19,7 +19,8 @@ export async function init(domain: string, db: D1Database) { db, actor, status.content, - status.media_attachments as unknown as APObject[] + status.media_attachments as unknown as APObject[], + { spoiler_text: status.spoiler_text } ) loadedStatuses.push({ status, note }) } diff --git a/frontend/src/components/Status/index.tsx b/frontend/src/components/Status/index.tsx index 383b425..6e84662 100644 --- a/frontend/src/components/Status/index.tsx +++ b/frontend/src/components/Status/index.tsx @@ -1,4 +1,4 @@ -import { component$, $ } from '@builder.io/qwik' +import { component$, $, useSignal } from '@builder.io/qwik' import { Link, useNavigate } from '@builder.io/qwik-city' import { formatTimeAgo } from '~/utils/dateTime' import type { Account, MastodonStatus } from '~/types' @@ -7,9 +7,13 @@ import { useAccountUrl } from '~/utils/useAccountUrl' import { getDisplayNameElement } from '~/utils/getDisplayNameElement' import { StatusAccountCard } from '../StatusAccountCard/StatusAccountCard' import { HtmlContent } from '../HtmlContent/HtmlContent' +import { StatusInfoTray } from '../StatusInfoTray/StatusInfoTray' type Props = { status: MastodonStatus + accountSubText: 'username' | 'acct' + showInfoTray: boolean + contentClickable: boolean } export default component$((props: Props) => { @@ -21,13 +25,15 @@ export default component$((props: Props) => { const accountUrl = useAccountUrl(status.account) const statusUrl = `${accountUrl}/${status.id}` - const handleContentClick = $(() => nav(statusUrl)) + const showContent = useSignal(!status.spoiler_text) + + const handleContentClick = $(() => props.contentClickable && nav(statusUrl)) return (
- +
@@ -35,23 +41,44 @@ export default component$((props: Props) => {
-
- -
- - - {status.card && status.media_attachments.length === 0 && ( - -
- -
-
{status.card.title}
-
{status.card.provider_name}
-
-
-
+ {status.spoiler_text && ( +
+ + {status.spoiler_text} + + +
)} + + {showContent.value && ( + <> +
+ +
+ + + + {status.card && status.media_attachments.length === 0 && ( + +
+ +
+
{status.card.title}
+
{status.card.provider_name}
+
+
+
+ )} + + )} + + {props.showInfoTray && }
) }) diff --git a/frontend/src/components/StatusInfoTray/StatusInfoTray.tsx b/frontend/src/components/StatusInfoTray/StatusInfoTray.tsx new file mode 100644 index 0000000..608c1c2 --- /dev/null +++ b/frontend/src/components/StatusInfoTray/StatusInfoTray.tsx @@ -0,0 +1,45 @@ +import { component$, Slot } from '@builder.io/qwik' +import type { MastodonStatus } from '~/types' +import { formatDateTime } from '~/utils/dateTime' +import { formatRoundedNumber } from '~/utils/numbers' + +export const StatusInfoTray = component$<{ status: MastodonStatus }>(({ status }) => { + return ( +
+ + {formatDateTime(status.created_at)} + + · + + + Web + + · + + + {formatRoundedNumber(status.reblogs_count)} + + · + + + {formatRoundedNumber(status.favourites_count)} + +
+ ) +}) + +export const Info = component$<{ href: string | null }>(({ href }) => { + return ( + <> + {!href ? ( + + + + ) : ( + + + + )} + + ) +}) diff --git a/frontend/src/components/StatusesPanel/StatusesPanel.tsx b/frontend/src/components/StatusesPanel/StatusesPanel.tsx index 05ce5f9..d59d973 100644 --- a/frontend/src/components/StatusesPanel/StatusesPanel.tsx +++ b/frontend/src/components/StatusesPanel/StatusesPanel.tsx @@ -48,7 +48,7 @@ export const StatusesPanel = component$(({ initialStatuses, fetchMoreStatuses: f const divProps = isLastStatus ? { ref: lastStatusRef } : {} return (
- +
) }) diff --git a/frontend/src/dummyData/generateDummyStatus.ts b/frontend/src/dummyData/generateDummyStatus.ts index e8891de..7459d56 100644 --- a/frontend/src/dummyData/generateDummyStatus.ts +++ b/frontend/src/dummyData/generateDummyStatus.ts @@ -1,20 +1,31 @@ import { Account, MastodonStatus, MediaAttachment } from '~/types' +import { george } from './accounts' import { getRandomDateInThePastYear } from './getRandomDateInThePastYear' -export function generateDummyStatus( - content: string, - account: Account, - mediaAttachments: MediaAttachment[] = [], - inReplyTo: string | null = null, - reblog: MastodonStatus | null = null -): MastodonStatus { +type dummyStatusConfig = { + content?: string + account?: Account + mediaAttachments?: MediaAttachment[] + inReplyTo?: string | null + reblog?: MastodonStatus | null + spoiler_text?: string +} + +export function generateDummyStatus({ + content = '', + account = george, + mediaAttachments = [], + inReplyTo = null, + reblog = null, + spoiler_text = '', +}: dummyStatusConfig): MastodonStatus { return { id: `${Math.random() * 9999999}`.padStart(3, '7'), created_at: getRandomDateInThePastYear().toISOString(), in_reply_to_id: inReplyTo, in_reply_to_account_id: null, sensitive: false, - spoiler_text: '', + spoiler_text, visibility: 'public', language: 'en', uri: '', diff --git a/frontend/src/dummyData/statuses.ts b/frontend/src/dummyData/statuses.ts index 5cbf2f7..7cc633d 100644 --- a/frontend/src/dummyData/statuses.ts +++ b/frontend/src/dummyData/statuses.ts @@ -4,8 +4,8 @@ import { ben, george, penny, rafael, zak } from './accounts' // Raw statuses which follow the precise structure found mastodon does const mastodonRawStatuses: MastodonStatus[] = [ - generateDummyStatus( - ` + generateDummyStatus({ + content: `

Fine. I'll use Wildebeest!

It does look interesting:

`, - george - ), - generateDummyStatus('We did it!', george, [ - generateDummyMediaImage(`https:/loremflickr.com/640/480/victory?lock=${Math.round(Math.random() * 999999)}`), - ]), - generateDummyStatus('A very simple update: all good!', ben), - generateDummyStatus('

Hi! My name is Rafael! 👋

', rafael), - generateDummyStatus( - "

I'm Rafael and I am a web designer!

💪💪

", - rafael, - new Array(4) + account: george, + }), + generateDummyStatus({ + content: 'We did it!', + account: george, + mediaAttachments: [ + generateDummyMediaImage(`https:/loremflickr.com/640/480/victory?lock=${Math.round(Math.random() * 999999)}`), + ], + }), + generateDummyStatus({ + content: 'A very simple update: all good!', + account: ben, + }), + generateDummyStatus({ content: '

Hi! My name is Rafael! 👋

', account: rafael, spoiler_text: 'who am I?' }), + generateDummyStatus({ + content: "

I'm Rafael and I am a web designer!

💪💪

", + account: rafael, + mediaAttachments: new Array(4) .fill(null) - .map((_, idx) => generateDummyMediaImage(`https:/loremflickr.com/640/480/abstract?lock=${100 + idx}`)) - ), + .map((_, idx) => generateDummyMediaImage(`https:/loremflickr.com/640/480/abstract?lock=${100 + idx}`)), + }), ] export const statuses: MastodonStatus[] = mastodonRawStatuses.map((rawStatus) => ({ @@ -41,11 +48,11 @@ export const statuses: MastodonStatus[] = mastodonRawStatuses.map((rawStatus) => })) export const replies: MastodonStatus[] = [ - generateDummyStatus('

Yes we did! 🎉

', zak, [], statuses[1].id), - generateDummyStatus('

Yes you guys did it!

', penny, [], statuses[1].id), + generateDummyStatus({ content: '

Yes we did! 🎉

', account: zak, inReplyTo: statuses[1].id }), + generateDummyStatus({ content: '

Yes you guys did it!

', account: penny, inReplyTo: statuses[1].id }), ] -export const reblogs: MastodonStatus[] = [generateDummyStatus('', george, [], null, statuses[2])] +export const reblogs: MastodonStatus[] = [generateDummyStatus({ account: george, reblog: statuses[2] })] function getStandardMediaType(mediaAttachmentMastodonType: string): string { switch (mediaAttachmentMastodonType) { diff --git a/frontend/src/routes/(frontend)/[accountId]/[statusId]/index.tsx b/frontend/src/routes/(frontend)/[accountId]/[statusId]/index.tsx index 1dea244..0976cdf 100644 --- a/frontend/src/routes/(frontend)/[accountId]/[statusId]/index.tsx +++ b/frontend/src/routes/(frontend)/[accountId]/[statusId]/index.tsx @@ -1,18 +1,13 @@ -import { component$, Slot } from '@builder.io/qwik' +import { component$ } from '@builder.io/qwik' import { MastodonStatus, StatusContext } from '~/types' import Status from '~/components/Status' -import { formatDateTime } from '~/utils/dateTime' -import { formatRoundedNumber } from '~/utils/numbers' import * as statusAPI from 'wildebeest/functions/api/v1/statuses/[id]' import * as contextAPI from 'wildebeest/functions/api/v1/statuses/[id]/context' import { DocumentHead, loader$ } from '@builder.io/qwik-city' -import { MediaGallery } from '~/components/MediaGallery.tsx' import { getNotFoundHtml } from '~/utils/getNotFoundHtml/getNotFoundHtml' import { getErrorHtml } from '~/utils/getErrorHtml/getErrorHtml' import { getTextContent } from 'wildebeest/backend/src/activitypub/objects' import { getDocumentHead } from '~/utils/getDocumentHead' -import { StatusAccountCard } from '~/components/StatusAccountCard/StatusAccountCard' -import { HtmlContent } from '~/components/HtmlContent/HtmlContent' export const statusLoader = loader$< Promise<{ status: MastodonStatus; statusTextContent: string; context: StatusContext }>, @@ -50,67 +45,16 @@ export default component$(() => { return ( <> -
-
- -
- - - - - - -
+
{loaderData.context.descendants.map((status) => { - return + return })}
) }) -export const InfoTray = component$<{ status: MastodonStatus }>(({ status }) => { - return ( -
- - {formatDateTime(status.created_at)} - - · - - - Web - - · - - - {formatRoundedNumber(status.reblogs_count)} - - · - - - {formatRoundedNumber(status.favourites_count)} - -
- ) -}) - -export const Info = component$<{ href: string | null }>(({ href }) => { - return ( - <> - {!href ? ( - - - - ) : ( - - - - )} - - ) -}) - export const head: DocumentHead = ({ resolveValue }) => { const { status, statusTextContent } = resolveValue(statusLoader) diff --git a/functions/api/v1/notifications/[id].ts b/functions/api/v1/notifications/[id].ts index 9299548..38ef1dd 100644 --- a/functions/api/v1/notifications/[id].ts +++ b/functions/api/v1/notifications/[id].ts @@ -68,6 +68,7 @@ export async function handleRequest( media_attachments: [], tags: [], mentions: [], + spoiler_text: properties.spoiler_text ?? '', // TODO: a shortcut has been taked. We assume that the actor // generating the notification also created the object. In practice @@ -76,7 +77,6 @@ export async function handleRequest( // TODO: stub values visibility: 'public', - spoiler_text: '', } } diff --git a/ui-e2e-tests/account-page.spec.ts b/ui-e2e-tests/account-page.spec.ts index ffc3f5a..50baf0a 100644 --- a/ui-e2e-tests/account-page.spec.ts +++ b/ui-e2e-tests/account-page.spec.ts @@ -31,6 +31,11 @@ test('Navigation to and view of an account (with 2 posts)', async ({ page }) => await expect(post1Locator.getByRole('img', { name: 'Avatar of Raffa123$' })).toBeVisible() await expect(post1Locator).toContainText("I'm Rafael and I am a web designer") await expect(post2Locator.getByRole('img', { name: 'Avatar of Raffa123$' })).toBeVisible() + await expect(post2Locator.getByText('who am I?')).toBeVisible() + await expect(post2Locator.getByRole('paragraph').getByText('Hi! My name is Rafael! 👋')).not.toBeVisible() + await post2Locator.getByRole('button', { name: 'show more' }).click() + await expect(post2Locator.getByRole('paragraph').getByText('Hi! My name is Rafael! 👋')).toBeVisible() + await expect(post2Locator).toContainText('Hi! My name is Rafael!') }) diff --git a/ui-e2e-tests/explore-page.spec.ts b/ui-e2e-tests/explore-page.spec.ts index ef4080a..7b25bf3 100644 --- a/ui-e2e-tests/explore-page.spec.ts +++ b/ui-e2e-tests/explore-page.spec.ts @@ -4,8 +4,8 @@ test('Display the list of toots in the explore page', async ({ page }) => { await page.goto('http://127.0.0.1:8788/explore') const tootsTextsToCheck = [ - 'Hi! My name is Rafael!', 'We did it!', + "I'm Rafael and I am a web designer!", "Fine. I'll use Wildebeest", 'A very simple update: all good!', ] @@ -21,3 +21,14 @@ test('Correctly displays toots with truncated urls', async ({ page }) => { const articleLocator = page.locator('article').filter({ hasText: "Fine. I'll use Wildebeest" }) await expect(articleLocator.getByRole('link', { name: 'blog.cloudflare.com/welcome-to…' })).toBeVisible() }) + +test('Correctly displays toots with spoiler text', async ({ page }) => { + await page.goto('http://127.0.0.1:8788/explore') + + const articleLocator = page.getByRole('article').filter({ hasText: 'who am I?' }) + await expect(articleLocator.getByRole('paragraph').getByText('Hi! My name is Rafael! 👋')).not.toBeVisible() + await articleLocator.getByRole('button', { name: 'show more' }).click() + await expect(articleLocator.getByRole('paragraph').getByText('Hi! My name is Rafael! 👋')).toBeVisible() + await articleLocator.getByRole('button', { name: 'show less' }).click() + await expect(articleLocator.getByRole('paragraph').getByText('Hi! My name is Rafael! 👋')).not.toBeVisible() +}) diff --git a/ui-e2e-tests/invidivual-toot.spec.ts b/ui-e2e-tests/invidivual-toot.spec.ts index 46e64e2..32079b2 100644 --- a/ui-e2e-tests/invidivual-toot.spec.ts +++ b/ui-e2e-tests/invidivual-toot.spec.ts @@ -83,3 +83,18 @@ test("Navigation to and view of a toot's replies", async ({ page }) => { await expect(page.getByTestId('account-display-name').filter({ hasText: 'Penny' })).toBeVisible() await expect(page.getByText('Yes you guys did it!')).toBeVisible() }) + +test("Correctly hides and displays the toot's content based on the spoiler text", async ({ page }) => { + await page.goto('http://127.0.0.1:8788/explore') + + await page.getByText('who am I?').click() + + await expect(page.getByRole('button', { name: 'Back' })).toBeVisible() + + const articleLocator = page.getByRole('article').filter({ hasText: 'who am I?' }) + await expect(articleLocator.getByRole('paragraph').getByText('Hi! My name is Rafael! 👋')).not.toBeVisible() + await articleLocator.getByRole('button', { name: 'show more' }).click() + await expect(articleLocator.getByRole('paragraph').getByText('Hi! My name is Rafael! 👋')).toBeVisible() + await articleLocator.getByRole('button', { name: 'show less' }).click() + await expect(articleLocator.getByRole('paragraph').getByText('Hi! My name is Rafael! 👋')).not.toBeVisible() +})