mirror of
https://git.shipoclu.com/moon/plspace.git
synced 2026-08-14 03:02:31 +00:00
greentext
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
import { renderDisplayName } from '$lib/util/html'
|
||||
import { routeTo } from '$lib/router.svelte'
|
||||
import { quoteReferenceOf, quotesCountOf } from '$lib/util/status'
|
||||
import { accountForStatus } from '$lib/util/heleneposting'
|
||||
import { isoDate, longDate, stampDate } from '$lib/util/time'
|
||||
import { extractYouTubeVideoIds } from '$lib/util/youtube'
|
||||
import Avatar from '../common/Avatar.svelte'
|
||||
@@ -42,12 +43,12 @@
|
||||
|
||||
let { status, onupdate, ondelete, compact = false, longFormDate = false }: Props = $props()
|
||||
|
||||
const { endpoints, session } = useAppServices()
|
||||
const { endpoints, session, preferences } = useAppServices()
|
||||
/** The status actually being displayed; a boost renders its target. */
|
||||
const entry = $derived(status.reblog ?? status)
|
||||
const booster = $derived(status.reblog ? status.account : null)
|
||||
|
||||
const author = $derived(entry.account)
|
||||
const author = $derived(accountForStatus(entry, preferences.heleneposting))
|
||||
const authorName = $derived(renderDisplayName(displayNameOf(author), author.emojis))
|
||||
const handle = $derived(fullHandle(author, session.host))
|
||||
const permalink = $derived(`#/blog/${entry.id}`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { fireEvent, render, waitFor } from '@testing-library/svelte'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { APP_SERVICES } from '$lib/app-services'
|
||||
import { account, session, status, testServices } from '$test/fixtures'
|
||||
import { account, preferences, session, status, testServices } from '$test/fixtures'
|
||||
import BlogEntry from './BlogEntry.svelte'
|
||||
|
||||
function renderEntry(overrides: Parameters<typeof status>[0]) {
|
||||
@@ -67,6 +67,40 @@ describe('BlogEntry MFM rendering', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('BlogEntry Heleneposting', () => {
|
||||
it('replaces only the displayed author name and avatar when enabled', () => {
|
||||
const original = account({
|
||||
acct: 'actual-author',
|
||||
display_name: 'Actual Author',
|
||||
avatar: 'https://example.test/actual.png',
|
||||
avatar_static: 'https://example.test/actual.png',
|
||||
})
|
||||
const services = testServices({
|
||||
preferences: preferences({ heleneposting: true }),
|
||||
})
|
||||
const view = render(BlogEntry, {
|
||||
props: {
|
||||
status: status({
|
||||
account: original,
|
||||
content: '<p>Signed through <em>HTML</em><br>— Helene</p>',
|
||||
}),
|
||||
},
|
||||
context: new Map([[APP_SERVICES, services]]),
|
||||
})
|
||||
|
||||
expect(view.container.querySelector('.blog-entry-author')).toHaveAttribute(
|
||||
'href',
|
||||
'#/@actual-author',
|
||||
)
|
||||
expect(view.queryByText('Actual Author')).not.toBeInTheDocument()
|
||||
expect(view.container.querySelector('.blog-entry-avatar img')).toHaveAttribute(
|
||||
'src',
|
||||
expect.stringContaining('helene'),
|
||||
)
|
||||
expect(view.getByText('@actual-author@example.test')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('BlogEntry quote posts', () => {
|
||||
it('renders a Pleroma quote and links to quote composition and the quote list', () => {
|
||||
const quoted = status({
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { useAppServices } from '$lib/app-services'
|
||||
import { displayNameOf, fullHandle, profilePath } from '$lib/util/profile'
|
||||
import { stampDate } from '$lib/util/time'
|
||||
import { accountForStatus } from '$lib/util/heleneposting'
|
||||
import Avatar from '../common/Avatar.svelte'
|
||||
import EmojiText from '../common/EmojiText.svelte'
|
||||
import MfmContent from '../common/MfmContent.svelte'
|
||||
@@ -18,12 +19,13 @@
|
||||
}
|
||||
|
||||
let { quote }: Props = $props()
|
||||
const { endpoints, session } = useAppServices()
|
||||
const { endpoints, session, preferences } = useAppServices()
|
||||
let loadedStatus = $state<Status | null>(untrack(() => quote.status))
|
||||
let loading = $state(false)
|
||||
let loadError = $state(false)
|
||||
let loadGeneration = 0
|
||||
const status = $derived((loadedStatus ?? quote.status)?.reblog ?? loadedStatus ?? quote.status)
|
||||
const author = $derived(status ? accountForStatus(status, preferences.heleneposting) : null)
|
||||
|
||||
$effect(() => {
|
||||
const supplied = quote.status
|
||||
@@ -69,10 +71,13 @@
|
||||
<p class="quote-card-placeholder">Loading quoted entry…</p>
|
||||
{:else if status}
|
||||
<header class="quote-card-header">
|
||||
<Avatar account={status.account} plain class="quote-card-avatar" />
|
||||
<Avatar account={author ?? status.account} plain class="quote-card-avatar" />
|
||||
<span class="quote-card-byline">
|
||||
<a class="quote-card-author" href={profilePath(status.account)}>
|
||||
<EmojiText text={displayNameOf(status.account)} emojis={status.account.emojis} />
|
||||
<EmojiText
|
||||
text={displayNameOf(author ?? status.account)}
|
||||
emojis={status.account.emojis}
|
||||
/>
|
||||
</a>
|
||||
<span class="quote-card-handle">{fullHandle(status.account, session.host)}</span>
|
||||
</span>
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
import type { ApiClient } from '$lib/api/client'
|
||||
import type { Notification } from '$lib/api/types'
|
||||
import { useAppServices } from '$lib/app-services'
|
||||
import { accountForNotification } from '$lib/util/heleneposting'
|
||||
import { displayNameOf } from '$lib/util/profile'
|
||||
import {
|
||||
NOTIFICATION_DISMISS_AFTER_MS,
|
||||
NOTIFICATION_POLL_INTERVAL_MS,
|
||||
@@ -35,7 +37,7 @@
|
||||
maxToasts = 4,
|
||||
}: Props = $props()
|
||||
|
||||
const { endpoints, session } = useAppServices()
|
||||
const { endpoints, session, preferences } = useAppServices()
|
||||
const tracker = new NotificationTracker()
|
||||
const dismissTimers = new Map<string, number>()
|
||||
|
||||
@@ -141,6 +143,7 @@
|
||||
<aside class="notification-toast-stack" aria-label="New notifications" aria-live="polite">
|
||||
{#each toasts as toast (toast.notification.id)}
|
||||
{@const reaction = emojiForNotification(toast.notification)}
|
||||
{@const actor = accountForNotification(toast.notification, preferences.heleneposting)}
|
||||
<a
|
||||
class="notification-toast"
|
||||
href={toast.presentation.href}
|
||||
@@ -148,12 +151,12 @@
|
||||
data-account={toast.notification.account.acct}
|
||||
onclick={() => dismiss(toast.notification.id)}
|
||||
>
|
||||
<Avatar account={toast.notification.account} plain class="notification-toast-avatar" />
|
||||
<Avatar account={actor} plain class="notification-toast-avatar" />
|
||||
<span class="notification-toast-body">
|
||||
<span class="notification-toast-message">
|
||||
<strong>
|
||||
<EmojiText
|
||||
text={toast.presentation.actor}
|
||||
text={displayNameOf(actor)}
|
||||
emojis={toast.notification.account.emojis}
|
||||
/>
|
||||
</strong>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* it intentionally does not parse raw `$[...]` source syntax.
|
||||
*/
|
||||
import type { CustomEmoji, StatusMention, StatusTag } from '$lib/api/types'
|
||||
import { useAppServices } from '$lib/app-services'
|
||||
import { renderMfmHtml } from '$lib/util/mfm'
|
||||
|
||||
interface Props {
|
||||
@@ -33,8 +34,17 @@
|
||||
scale = false,
|
||||
}: Props = $props()
|
||||
|
||||
const { preferences } = useAppServices()
|
||||
const rendered = $derived(
|
||||
renderMfmHtml(html, { emojis, mentions, tags, inline, pause, scale }),
|
||||
renderMfmHtml(html, {
|
||||
emojis,
|
||||
mentions,
|
||||
tags,
|
||||
inline,
|
||||
pause,
|
||||
scale,
|
||||
greentext: preferences.greentexting,
|
||||
}),
|
||||
)
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { render } from '@testing-library/svelte'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { APP_SERVICES } from '$lib/app-services'
|
||||
import { preferences, testServices } from '$test/fixtures'
|
||||
import MfmContent from './MfmContent.svelte'
|
||||
|
||||
function mfm(view: { getByText(text: string): HTMLElement }, text: string): HTMLElement {
|
||||
@@ -70,4 +72,53 @@ describe('MfmContent', () => {
|
||||
'https://example.test/emoji/party.png',
|
||||
)
|
||||
})
|
||||
|
||||
it('greentexts visual lines, including prose after a leading mention', () => {
|
||||
const view = render(MfmContent, {
|
||||
props: {
|
||||
html: [
|
||||
'<p>>first line<br>',
|
||||
'<a class="mention" href="https://example.test/@alice">@alice</a> >mentioned line<br>',
|
||||
'ordinary line</p>',
|
||||
].join(''),
|
||||
},
|
||||
context: new Map([
|
||||
[
|
||||
APP_SERVICES,
|
||||
testServices({
|
||||
preferences: preferences({ greentexting: true }),
|
||||
}),
|
||||
],
|
||||
]),
|
||||
})
|
||||
|
||||
const green = view.container.querySelectorAll('.greentext')
|
||||
expect(green).toHaveLength(2)
|
||||
expect(green[0]).toHaveTextContent('>first line')
|
||||
expect(green[1]).toHaveTextContent('@alice >mentioned line')
|
||||
expect(view.getByText('ordinary line')).not.toHaveClass('greentext')
|
||||
})
|
||||
|
||||
it('does not greentext blockquotes, code, lists, or disabled content', () => {
|
||||
const enabled = render(MfmContent, {
|
||||
props: {
|
||||
html: '<blockquote>>quote</blockquote><pre>>code</pre><ul><li>>item</li></ul>',
|
||||
},
|
||||
context: new Map([
|
||||
[
|
||||
APP_SERVICES,
|
||||
testServices({
|
||||
preferences: preferences({ greentexting: true }),
|
||||
}),
|
||||
],
|
||||
]),
|
||||
})
|
||||
expect(enabled.container.querySelector('.greentext')).not.toBeInTheDocument()
|
||||
|
||||
const disabled = render(MfmContent, {
|
||||
props: { html: '<p>>plain</p>' },
|
||||
context: new Map([[APP_SERVICES, testServices()]]),
|
||||
})
|
||||
expect(disabled.container.querySelector('.greentext')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user