Quoted words
', + account: account({ id: 'quoted-author', display_name: 'Quoted Author' }), + }) + const outer = status({ + id: 'outer-entry', + content: 'My commentary
', + quotes_count: 3, + pleroma: { quote: quoted, quote_id: quoted.id, quote_visible: true }, + }) + const services = testServices({ + session: session({ signedIn: true, token: 'token', me: account() }), + }) + const view = render(BlogEntry, { + props: { status: outer }, + context: new Map([[APP_SERVICES, services]]), + }) + + expect(view.getByText('Quoted words')).toBeInTheDocument() + expect(view.getByRole('link', { name: 'Quote' })).toHaveAttribute( + 'href', + '#/compose?quote=outer-entry', + ) + expect(view.getByRole('link', { name: 'Quotes (3)' })).toHaveAttribute( + 'href', + '#/blog/outer-entry/quotes', + ) + }) + + it('shows a Mastodon quote authorization placeholder without leaking content', () => { + const hidden = status({ id: 'hidden-quote', content: 'Do not display me
' }) + const view = renderEntry({ + quote: { state: 'blocked_account', quoted_status: hidden }, + }) + + expect(view.getByText('The quoted account is blocked.')).toBeInTheDocument() + expect(view.queryByText('Do not display me')).not.toBeInTheDocument() + }) + + it('does not offer quoting when Mastodon says the viewer is denied', () => { + const view = renderEntry({ + quote_approval: { + automatic: [], + manual: [], + current_user: 'denied', + }, + }) + + expect(view.queryByRole('link', { name: 'Quote' })).not.toBeInTheDocument() + }) +}) + describe('BlogEntry emoji reactions', () => { it('collapses the reaction bar when the entry has no reactions', () => { const view = renderEntry({ pleroma: { emoji_reactions: [] } }) diff --git a/src/components/blog/Composer.svelte b/src/components/blog/Composer.svelte index 3554afb..106a202 100644 --- a/src/components/blog/Composer.svelte +++ b/src/components/blog/Composer.svelte @@ -9,10 +9,13 @@ import { untrack } from 'svelte' import type { MediaAttachment, Status, StatusVisibility } from '$lib/api/types' import { useAppServices } from '$lib/app-services' + import QuoteCard from './QuoteCard.svelte' interface Props { /** Set to reply to an existing entry. */ inReplyTo?: Status | null + /** Status attached as a structured quote. */ + quote?: Status | null /** Prefilled body, e.g. the mentions of the entry being replied to. */ initialText?: string initialVisibility?: StatusVisibility @@ -23,6 +26,7 @@ let { inReplyTo = null, + quote = null, initialText = '', initialVisibility = 'public', placeholder = 'What are you up to?', @@ -33,6 +37,7 @@ const { endpoints, session } = useAppServices() // Seeded once from the prop; afterwards the textarea owns the value. let text = $state(untrack(() => initialText)) + let quotedStatus = $state