mirror of
https://git.shipoclu.com/moon/plspace.git
synced 2026-08-13 02:42:30 +00:00
refactor and fix all references to emojis
This commit is contained in:
@@ -6,14 +6,16 @@
|
|||||||
* when it's revealed, and the reveal is per-attachment because a single post
|
* when it's revealed, and the reveal is per-attachment because a single post
|
||||||
* can mix flagged and unflagged media.
|
* can mix flagged and unflagged media.
|
||||||
*/
|
*/
|
||||||
import type { MediaAttachment } from '$lib/api/types'
|
import type { CustomEmoji, MediaAttachment } from '$lib/api/types'
|
||||||
|
import EmojiText from '../common/EmojiText.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
attachments: MediaAttachment[]
|
attachments: MediaAttachment[]
|
||||||
|
emojis?: CustomEmoji[]
|
||||||
sensitive?: boolean
|
sensitive?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
let { attachments, sensitive = false }: Props = $props()
|
let { attachments, emojis, sensitive = false }: Props = $props()
|
||||||
|
|
||||||
let revealed = $state<Record<string, boolean>>({})
|
let revealed = $state<Record<string, boolean>>({})
|
||||||
|
|
||||||
@@ -81,7 +83,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if media.description}
|
{#if media.description}
|
||||||
<figcaption class="attachment-caption">{media.description}</figcaption>
|
<figcaption class="attachment-caption">
|
||||||
|
<EmojiText text={media.description} {emojis} />
|
||||||
|
</figcaption>
|
||||||
{/if}
|
{/if}
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
import { isoDate, longDate, stampDate } from '$lib/util/time'
|
import { isoDate, longDate, stampDate } from '$lib/util/time'
|
||||||
import { extractYouTubeVideoIds } from '$lib/util/youtube'
|
import { extractYouTubeVideoIds } from '$lib/util/youtube'
|
||||||
import Avatar from '../common/Avatar.svelte'
|
import Avatar from '../common/Avatar.svelte'
|
||||||
|
import EmojiText from '../common/EmojiText.svelte'
|
||||||
import MfmContent from '../common/MfmContent.svelte'
|
import MfmContent from '../common/MfmContent.svelte'
|
||||||
import Attachments from './Attachments.svelte'
|
import Attachments from './Attachments.svelte'
|
||||||
import EmojiReactions from './EmojiReactions.svelte'
|
import EmojiReactions from './EmojiReactions.svelte'
|
||||||
@@ -145,7 +146,10 @@
|
|||||||
>
|
>
|
||||||
{#if booster}
|
{#if booster}
|
||||||
<p class="blog-entry-attribution">
|
<p class="blog-entry-attribution">
|
||||||
<a href={profilePath(booster)}>{displayNameOf(booster)}</a> reposted this
|
<a href={profilePath(booster)}>
|
||||||
|
<EmojiText text={displayNameOf(booster)} emojis={booster.emojis} />
|
||||||
|
</a>
|
||||||
|
reposted this
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -181,7 +185,9 @@
|
|||||||
<div class="blog-entry-body">
|
<div class="blog-entry-body">
|
||||||
{#if entry.spoiler_text}
|
{#if entry.spoiler_text}
|
||||||
<details class="content-warning">
|
<details class="content-warning">
|
||||||
<summary class="content-warning-summary">{entry.spoiler_text}</summary>
|
<summary class="content-warning-summary">
|
||||||
|
<EmojiText text={entry.spoiler_text} emojis={entry.emojis} />
|
||||||
|
</summary>
|
||||||
<MfmContent
|
<MfmContent
|
||||||
html={entry.content}
|
html={entry.content}
|
||||||
emojis={entry.emojis}
|
emojis={entry.emojis}
|
||||||
@@ -190,7 +196,7 @@
|
|||||||
lang={entry.language}
|
lang={entry.language}
|
||||||
/>
|
/>
|
||||||
{#if entry.media_attachments.length > 0}
|
{#if entry.media_attachments.length > 0}
|
||||||
<Attachments attachments={entry.media_attachments} sensitive={entry.sensitive} />
|
<Attachments attachments={entry.media_attachments} emojis={entry.emojis} sensitive={entry.sensitive} />
|
||||||
{/if}
|
{/if}
|
||||||
<YouTubeEmbeds videoIds={youtubeVideoIds} sensitive={entry.sensitive} />
|
<YouTubeEmbeds videoIds={youtubeVideoIds} sensitive={entry.sensitive} />
|
||||||
</details>
|
</details>
|
||||||
@@ -203,7 +209,7 @@
|
|||||||
lang={entry.language}
|
lang={entry.language}
|
||||||
/>
|
/>
|
||||||
{#if entry.media_attachments.length > 0}
|
{#if entry.media_attachments.length > 0}
|
||||||
<Attachments attachments={entry.media_attachments} sensitive={entry.sensitive} />
|
<Attachments attachments={entry.media_attachments} emojis={entry.emojis} sensitive={entry.sensitive} />
|
||||||
{/if}
|
{/if}
|
||||||
<YouTubeEmbeds videoIds={youtubeVideoIds} sensitive={entry.sensitive} />
|
<YouTubeEmbeds videoIds={youtubeVideoIds} sensitive={entry.sensitive} />
|
||||||
{/if}
|
{/if}
|
||||||
@@ -211,6 +217,7 @@
|
|||||||
{#if entry.poll}
|
{#if entry.poll}
|
||||||
<PollView
|
<PollView
|
||||||
poll={entry.poll}
|
poll={entry.poll}
|
||||||
|
emojis={entry.poll.emojis?.length ? entry.poll.emojis : entry.emojis}
|
||||||
authorId={entry.account.id}
|
authorId={entry.account.id}
|
||||||
onupdate={(poll) => onupdate?.(applyLocal(status, { poll }))}
|
onupdate={(poll) => onupdate?.(applyLocal(status, { poll }))}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { EmojiReaction, Status } from '$lib/api/types'
|
import type { EmojiReaction, Status } from '$lib/api/types'
|
||||||
import { useAppServices } from '$lib/app-services'
|
import { useAppServices } from '$lib/app-services'
|
||||||
|
import { safeCustomEmojiUrl } from '$lib/util/html'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
status: Status
|
status: Status
|
||||||
@@ -94,11 +95,12 @@
|
|||||||
{#if reactions.length > 0}
|
{#if reactions.length > 0}
|
||||||
<div class="emoji-reaction-bar" aria-label="Emoji reactions">
|
<div class="emoji-reaction-bar" aria-label="Emoji reactions">
|
||||||
{#each reactions as reaction (reaction.url ?? reaction.name)}
|
{#each reactions as reaction (reaction.url ?? reaction.name)}
|
||||||
|
{@const imageUrl = reaction.url ? safeCustomEmojiUrl(reaction.url) : null}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="emoji-reaction"
|
class="emoji-reaction"
|
||||||
class:emoji-reaction--mine={reaction.me}
|
class:emoji-reaction--mine={reaction.me}
|
||||||
data-custom={reaction.url ? 'true' : 'false'}
|
data-custom={imageUrl ? 'true' : 'false'}
|
||||||
aria-label={accessibleLabel(reaction)}
|
aria-label={accessibleLabel(reaction)}
|
||||||
aria-pressed={reaction.me ? 'true' : 'false'}
|
aria-pressed={reaction.me ? 'true' : 'false'}
|
||||||
title={session.signedIn
|
title={session.signedIn
|
||||||
@@ -107,10 +109,10 @@
|
|||||||
disabled={!session.signedIn || busyEmoji !== null}
|
disabled={!session.signedIn || busyEmoji !== null}
|
||||||
onclick={() => void toggle(reaction)}
|
onclick={() => void toggle(reaction)}
|
||||||
>
|
>
|
||||||
{#if reaction.url}
|
{#if imageUrl}
|
||||||
<img
|
<img
|
||||||
class="emoji-reaction-image"
|
class="emoji-reaction-image"
|
||||||
src={reaction.url}
|
src={imageUrl}
|
||||||
alt={customLabel(reaction)}
|
alt={customLabel(reaction)}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
/** Mastodon/Pleroma poll choices, voting and results. */
|
/** Mastodon/Pleroma poll choices, voting and results. */
|
||||||
import { untrack } from 'svelte'
|
import { untrack } from 'svelte'
|
||||||
import type { Poll } from '$lib/api/types'
|
import type { CustomEmoji, Poll } from '$lib/api/types'
|
||||||
import { useAppServices } from '$lib/app-services'
|
import { useAppServices } from '$lib/app-services'
|
||||||
import { formatCount } from '$lib/util/profile'
|
import { formatCount } from '$lib/util/profile'
|
||||||
|
import EmojiText from '../common/EmojiText.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
poll: Poll
|
poll: Poll
|
||||||
|
emojis?: CustomEmoji[]
|
||||||
authorId?: string
|
authorId?: string
|
||||||
onupdate?: (poll: Poll) => void
|
onupdate?: (poll: Poll) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
let { poll, authorId, onupdate }: Props = $props()
|
let { poll, emojis = poll.emojis ?? [], authorId, onupdate }: Props = $props()
|
||||||
const { endpoints, session } = useAppServices()
|
const { endpoints, session } = useAppServices()
|
||||||
|
|
||||||
function pollSignature(value: Poll): string {
|
function pollSignature(value: Poll): string {
|
||||||
@@ -113,7 +115,7 @@
|
|||||||
{#if currentPoll.own_votes?.includes(index)}
|
{#if currentPoll.own_votes?.includes(index)}
|
||||||
<span class="poll-own-vote" aria-label="Your vote">✓</span>
|
<span class="poll-own-vote" aria-label="Your vote">✓</span>
|
||||||
{/if}
|
{/if}
|
||||||
{option.title}
|
<EmojiText text={option.title} {emojis} />
|
||||||
</span>
|
</span>
|
||||||
<span class="poll-option-share">
|
<span class="poll-option-share">
|
||||||
{option.votes_count === null ? '—' : `${share(option.votes_count)}%`}
|
{option.votes_count === null ? '—' : `${share(option.votes_count)}%`}
|
||||||
@@ -137,7 +139,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<input type="radio" name={`poll-${currentPoll.id}`} value={index} bind:group={singleChoice} />
|
<input type="radio" name={`poll-${currentPoll.id}`} value={index} bind:group={singleChoice} />
|
||||||
{/if}
|
{/if}
|
||||||
<span>{option.title}</span>
|
<EmojiText text={option.title} {emojis} />
|
||||||
</label>
|
</label>
|
||||||
{/each}
|
{/each}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -91,4 +91,33 @@ describe('PollView', () => {
|
|||||||
expect(view.getByRole('link', { name: 'Sign in to vote' })).toHaveAttribute('href', '#/login')
|
expect(view.getByRole('link', { name: 'Sign in to vote' })).toHaveAttribute('href', '#/login')
|
||||||
expect(view.queryByRole('button', { name: 'Vote' })).not.toBeInTheDocument()
|
expect(view.queryByRole('button', { name: 'Vote' })).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('renders custom emoji in poll choices', () => {
|
||||||
|
const view = render(PollView, {
|
||||||
|
props: {
|
||||||
|
poll: poll({
|
||||||
|
options: [
|
||||||
|
{ title: 'Choose :blobcat:', votes_count: null },
|
||||||
|
{ title: 'No thanks', votes_count: null },
|
||||||
|
],
|
||||||
|
emojis: [
|
||||||
|
{
|
||||||
|
shortcode: 'blobcat',
|
||||||
|
url: 'https://cdn.example/blobcat.png',
|
||||||
|
static_url: 'https://cdn.example/blobcat.png',
|
||||||
|
visible_in_picker: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
authorId: 'someone-else',
|
||||||
|
},
|
||||||
|
context: new Map([[APP_SERVICES, testServices()]]),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(view.getByAltText(':blobcat:')).toHaveAttribute(
|
||||||
|
'src',
|
||||||
|
'https://cdn.example/blobcat.png',
|
||||||
|
)
|
||||||
|
expect(view.getByRole('radio', { name: 'Choose :blobcat:' })).toBeDisabled()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,10 +11,12 @@
|
|||||||
NOTIFICATION_DISMISS_AFTER_MS,
|
NOTIFICATION_DISMISS_AFTER_MS,
|
||||||
NOTIFICATION_POLL_INTERVAL_MS,
|
NOTIFICATION_POLL_INTERVAL_MS,
|
||||||
NotificationTracker,
|
NotificationTracker,
|
||||||
|
emojiForNotification,
|
||||||
presentNotification,
|
presentNotification,
|
||||||
type NotificationPresentation,
|
type NotificationPresentation,
|
||||||
} from '$lib/notifications'
|
} from '$lib/notifications'
|
||||||
import Avatar from '../common/Avatar.svelte'
|
import Avatar from '../common/Avatar.svelte'
|
||||||
|
import EmojiText from '../common/EmojiText.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pollIntervalMs?: number
|
pollIntervalMs?: number
|
||||||
@@ -138,6 +140,7 @@
|
|||||||
|
|
||||||
<aside class="notification-toast-stack" aria-label="New notifications" aria-live="polite">
|
<aside class="notification-toast-stack" aria-label="New notifications" aria-live="polite">
|
||||||
{#each toasts as toast (toast.notification.id)}
|
{#each toasts as toast (toast.notification.id)}
|
||||||
|
{@const reaction = emojiForNotification(toast.notification)}
|
||||||
<a
|
<a
|
||||||
class="notification-toast"
|
class="notification-toast"
|
||||||
href={toast.presentation.href}
|
href={toast.presentation.href}
|
||||||
@@ -148,11 +151,23 @@
|
|||||||
<Avatar account={toast.notification.account} plain class="notification-toast-avatar" />
|
<Avatar account={toast.notification.account} plain class="notification-toast-avatar" />
|
||||||
<span class="notification-toast-body">
|
<span class="notification-toast-body">
|
||||||
<span class="notification-toast-message">
|
<span class="notification-toast-message">
|
||||||
<strong>{toast.presentation.actor}</strong>
|
<strong>
|
||||||
|
<EmojiText
|
||||||
|
text={toast.presentation.actor}
|
||||||
|
emojis={toast.notification.account.emojis}
|
||||||
|
/>
|
||||||
|
</strong>
|
||||||
{toast.presentation.message}
|
{toast.presentation.message}
|
||||||
|
{#if reaction}
|
||||||
|
with <EmojiText text={reaction.text} emojis={reaction.emojis} />
|
||||||
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{#if toast.presentation.excerpt}
|
{#if toast.presentation.excerpt}
|
||||||
<span class="notification-toast-excerpt">{toast.presentation.excerpt}</span>
|
<EmojiText
|
||||||
|
class="notification-toast-excerpt"
|
||||||
|
text={toast.presentation.excerpt}
|
||||||
|
emojis={toast.notification.status?.emojis}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -71,4 +71,37 @@ describe('NotificationToasts', () => {
|
|||||||
})
|
})
|
||||||
expect(view.queryByText('A new mention')).not.toBeInTheDocument()
|
expect(view.queryByText('A new mention')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('shows a safely sized custom emoji from a reaction notification', async () => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
const oldNotification = notification({ id: 'old-notification' })
|
||||||
|
const reaction = notification({
|
||||||
|
id: 'reaction-notification',
|
||||||
|
type: 'pleroma:emoji_reaction',
|
||||||
|
emoji: ':dinosaur:',
|
||||||
|
emoji_url: 'https://cdn.example/dinosaur.gif',
|
||||||
|
})
|
||||||
|
const fetchNotifications = vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValueOnce({ items: [oldNotification], links: {} })
|
||||||
|
.mockResolvedValueOnce({ items: [reaction, oldNotification], links: {} })
|
||||||
|
const services = testServices({
|
||||||
|
session: session({ token: 'token', me: account(), signedIn: true }),
|
||||||
|
endpoints: { fetchNotifications },
|
||||||
|
})
|
||||||
|
const view = render(NotificationToasts, {
|
||||||
|
props: { pollIntervalMs: 1_000 },
|
||||||
|
context: new Map([[APP_SERVICES, services]]),
|
||||||
|
})
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await Promise.resolve()
|
||||||
|
await vi.advanceTimersByTimeAsync(1_000)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(view.getByAltText(':dinosaur:')).toHaveAttribute(
|
||||||
|
'src',
|
||||||
|
'https://cdn.example/dinosaur.gif',
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/**
|
||||||
|
* Escaped plain text with server-declared `:shortcode:` values replaced by
|
||||||
|
* custom emoji. Use this for names, content warnings, poll choices and other
|
||||||
|
* API strings that are not HTML.
|
||||||
|
*/
|
||||||
|
import type { CustomEmoji } from '$lib/api/types'
|
||||||
|
import { renderEmojiText } from '$lib/util/html'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
text: string | null | undefined
|
||||||
|
emojis?: CustomEmoji[]
|
||||||
|
class?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
let { text, emojis, class: extraClass = '' }: Props = $props()
|
||||||
|
const rendered = $derived(renderEmojiText(text ?? '', emojis))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -- escaped in renderEmojiText -->
|
||||||
|
<span class="emoji-text {extraClass}">{@html rendered}</span>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { render } from '@testing-library/svelte'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import EmojiText from './EmojiText.svelte'
|
||||||
|
|
||||||
|
const wideEmoji = {
|
||||||
|
shortcode: 'wide',
|
||||||
|
url: 'https://cdn.example/wide.png',
|
||||||
|
static_url: 'https://cdn.example/wide.png',
|
||||||
|
visible_in_picker: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('EmojiText', () => {
|
||||||
|
it('escapes plain text and replaces declared custom emoji', () => {
|
||||||
|
const view = render(EmojiText, {
|
||||||
|
props: {
|
||||||
|
text: '<b>unsafe</b> :wide:',
|
||||||
|
emojis: [wideEmoji],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(view.getByText('<b>unsafe</b>')).toBeInTheDocument()
|
||||||
|
expect(view.queryByText('unsafe', { selector: 'b' })).not.toBeInTheDocument()
|
||||||
|
expect(view.getByAltText(':wide:')).toHaveAttribute('src', wideEmoji.url)
|
||||||
|
expect(view.getByAltText(':wide:')).toHaveClass('custom-emoji')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not inject emoji images from unsafe URL schemes', () => {
|
||||||
|
const view = render(EmojiText, {
|
||||||
|
props: {
|
||||||
|
text: ':wide:',
|
||||||
|
emojis: [{ ...wideEmoji, url: 'data:image/svg+xml,<svg></svg>' }],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(view.queryByRole('img')).not.toBeInTheDocument()
|
||||||
|
expect(view.getByText(':wide:')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -6,11 +6,15 @@
|
|||||||
* panel blue caption bar (left rail)
|
* panel blue caption bar (left rail)
|
||||||
* band peach caption bar (main column)
|
* band peach caption bar (main column)
|
||||||
* plain no chrome, just the heading
|
* plain no chrome, just the heading
|
||||||
*/
|
*/
|
||||||
import type { Snippet } from 'svelte'
|
import type { Snippet } from 'svelte'
|
||||||
|
import type { CustomEmoji } from '$lib/api/types'
|
||||||
|
import EmojiText from './EmojiText.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string
|
title?: string
|
||||||
|
/** Custom emoji available to user-derived titles. */
|
||||||
|
titleEmojis?: CustomEmoji[]
|
||||||
variant?: 'panel' | 'band' | 'plain'
|
variant?: 'panel' | 'band' | 'plain'
|
||||||
/** Right-aligned link in the caption bar, e.g. "[view all]". */
|
/** Right-aligned link in the caption bar, e.g. "[view all]". */
|
||||||
action?: Snippet
|
action?: Snippet
|
||||||
@@ -23,6 +27,7 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
title,
|
title,
|
||||||
|
titleEmojis,
|
||||||
variant = 'panel',
|
variant = 'panel',
|
||||||
action,
|
action,
|
||||||
flush = false,
|
flush = false,
|
||||||
@@ -38,7 +43,7 @@
|
|||||||
<section class="module {variantClass} {extraClass}" data-variant={variant}>
|
<section class="module {variantClass} {extraClass}" data-variant={variant}>
|
||||||
{#if title}
|
{#if title}
|
||||||
<h2 class="module-header">
|
<h2 class="module-header">
|
||||||
<span class="module-header-title">{title}</span>
|
<EmojiText class="module-header-title" text={title} emojis={titleEmojis} />
|
||||||
{#if action}
|
{#if action}
|
||||||
<span class="module-header-action">{@render action()}</span>
|
<span class="module-header-action">{@render action()}</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
import { useAppServices } from '$lib/app-services'
|
import { useAppServices } from '$lib/app-services'
|
||||||
import { displayNameOf } from '$lib/util/profile'
|
import { displayNameOf } from '$lib/util/profile'
|
||||||
import Module from '../common/Module.svelte'
|
import Module from '../common/Module.svelte'
|
||||||
|
import EmojiText from '../common/EmojiText.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
account: Account
|
account: Account
|
||||||
@@ -73,7 +74,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Module title={`Contacting ${firstName}`}>
|
<Module title={`Contacting ${firstName}`} titleEmojis={account.emojis}>
|
||||||
{#if error}
|
{#if error}
|
||||||
<p class="error-note" role="alert">{error}</p>
|
<p class="error-note" role="alert">{error}</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -137,6 +138,8 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{#if relationship?.followed_by && !isSelf}
|
{#if relationship?.followed_by && !isSelf}
|
||||||
<p class="contact-note muted">{firstName} has you on their friends list.</p>
|
<p class="contact-note muted">
|
||||||
|
<EmojiText text={firstName} emojis={account.emojis} /> has you on their friends list.
|
||||||
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</Module>
|
</Module>
|
||||||
|
|||||||
@@ -7,23 +7,28 @@
|
|||||||
* an unverified link that looks verified is a phishing surface.
|
* an unverified link that looks verified is a phishing surface.
|
||||||
*/
|
*/
|
||||||
import type { ProfileField } from '$lib/util/profile'
|
import type { ProfileField } from '$lib/util/profile'
|
||||||
|
import type { CustomEmoji } from '$lib/api/types'
|
||||||
import Module from '../common/Module.svelte'
|
import Module from '../common/Module.svelte'
|
||||||
|
import EmojiText from '../common/EmojiText.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string
|
title: string
|
||||||
fields: ProfileField[]
|
fields: ProfileField[]
|
||||||
|
emojis?: CustomEmoji[]
|
||||||
}
|
}
|
||||||
|
|
||||||
let { title, fields }: Props = $props()
|
let { title, fields, emojis }: Props = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if fields.length > 0}
|
{#if fields.length > 0}
|
||||||
<Module {title} flush>
|
<Module {title} titleEmojis={emojis} flush>
|
||||||
<table class="data-table details-table">
|
<table class="data-table details-table">
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each fields as field, index (`${field.name}:${index}`)}
|
{#each fields as field, index (`${field.name}:${index}`)}
|
||||||
<tr class="details-row" data-verified={field.verified ? 'true' : 'false'}>
|
<tr class="details-row" data-verified={field.verified ? 'true' : 'false'}>
|
||||||
<th class="data-table-label details-label" scope="row">{field.name}</th>
|
<th class="data-table-label details-label" scope="row">
|
||||||
|
<EmojiText text={field.name} {emojis} />
|
||||||
|
</th>
|
||||||
<td class="data-table-value details-value" data-verified={field.verified ? 'true' : 'false'}>
|
<td class="data-table-value details-value" data-verified={field.verified ? 'true' : 'false'}>
|
||||||
{#if field.verified}
|
{#if field.verified}
|
||||||
<span class="verified-mark" title="Ownership of this link is verified">✓</span>
|
<span class="verified-mark" title="Ownership of this link is verified">✓</span>
|
||||||
|
|||||||
@@ -6,15 +6,17 @@
|
|||||||
* accounts often return an empty list rather than an error, so the count and
|
* accounts often return an empty list rather than an error, so the count and
|
||||||
* the grid are allowed to disagree; the count is authoritative.
|
* the grid are allowed to disagree; the count is authoritative.
|
||||||
*/
|
*/
|
||||||
import type { Account } from '$lib/api/types'
|
import type { Account, CustomEmoji } from '$lib/api/types'
|
||||||
import { displayNameOf, formatCount, profilePath } from '$lib/util/profile'
|
import { displayNameOf, formatCount, profilePath } from '$lib/util/profile'
|
||||||
import Module from '../common/Module.svelte'
|
import Module from '../common/Module.svelte'
|
||||||
import Avatar from '../common/Avatar.svelte'
|
import Avatar from '../common/Avatar.svelte'
|
||||||
|
import EmojiText from '../common/EmojiText.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string
|
title: string
|
||||||
/** The subject, used in "Tom has 527 friends." */
|
/** The subject, used in "Tom has 527 friends." */
|
||||||
ownerName: string
|
ownerName: string
|
||||||
|
ownerEmojis?: CustomEmoji[]
|
||||||
friends: Account[]
|
friends: Account[]
|
||||||
total: number
|
total: number
|
||||||
viewAllHref: string
|
viewAllHref: string
|
||||||
@@ -29,6 +31,7 @@
|
|||||||
let {
|
let {
|
||||||
title,
|
title,
|
||||||
ownerName,
|
ownerName,
|
||||||
|
ownerEmojis,
|
||||||
friends,
|
friends,
|
||||||
total,
|
total,
|
||||||
viewAllHref,
|
viewAllHref,
|
||||||
@@ -39,7 +42,7 @@
|
|||||||
}: Props = $props()
|
}: Props = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Module {title} variant="band">
|
<Module {title} titleEmojis={ownerEmojis} variant="band">
|
||||||
{#snippet action()}
|
{#snippet action()}
|
||||||
<a href={viewAllHref}>[view all]</a>
|
<a href={viewAllHref}>[view all]</a>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
@@ -47,10 +50,13 @@
|
|||||||
<!-- Never render a withheld count as "0 friends" — that reports a privacy
|
<!-- Never render a withheld count as "0 friends" — that reports a privacy
|
||||||
setting as a fact about the person. -->
|
setting as a fact about the person. -->
|
||||||
{#if countHidden}
|
{#if countHidden}
|
||||||
<p class="friend-count">{ownerName} keeps their friend count private.</p>
|
<p class="friend-count">
|
||||||
|
<EmojiText text={ownerName} emojis={ownerEmojis} /> keeps their friend count private.
|
||||||
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="friend-count">
|
<p class="friend-count">
|
||||||
{ownerName} has <span class="friend-count-value">{formatCount(total)}</span>
|
<EmojiText text={ownerName} emojis={ownerEmojis} /> has
|
||||||
|
<span class="friend-count-value">{formatCount(total)}</span>
|
||||||
friend{total === 1 ? '' : 's'}.
|
friend{total === 1 ? '' : 's'}.
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -66,7 +72,11 @@
|
|||||||
{#each friends as friend (friend.id)}
|
{#each friends as friend (friend.id)}
|
||||||
<li class="friend-card" data-account={friend.acct}>
|
<li class="friend-card" data-account={friend.acct}>
|
||||||
<a class="friend-card-link" href={profilePath(friend)}>
|
<a class="friend-card-link" href={profilePath(friend)}>
|
||||||
<span class="friend-card-name">{displayNameOf(friend)}</span>
|
<EmojiText
|
||||||
|
class="friend-card-name"
|
||||||
|
text={displayNameOf(friend)}
|
||||||
|
emojis={friend.emojis}
|
||||||
|
/>
|
||||||
<Avatar account={friend} plain size="friend" class="friend-card-photo" />
|
<Avatar account={friend} plain size="friend" class="friend-card-photo" />
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -6,18 +6,20 @@
|
|||||||
* Mastodon account gets a compact box rather than six empty rows.
|
* Mastodon account gets a compact box rather than six empty rows.
|
||||||
*/
|
*/
|
||||||
import type { InterestEntry } from '$lib/util/profile'
|
import type { InterestEntry } from '$lib/util/profile'
|
||||||
|
import type { CustomEmoji } from '$lib/api/types'
|
||||||
import Module from '../common/Module.svelte'
|
import Module from '../common/Module.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string
|
title: string
|
||||||
interests: InterestEntry[]
|
interests: InterestEntry[]
|
||||||
|
emojis?: CustomEmoji[]
|
||||||
}
|
}
|
||||||
|
|
||||||
let { title, interests }: Props = $props()
|
let { title, interests, emojis }: Props = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if interests.length > 0}
|
{#if interests.length > 0}
|
||||||
<Module {title} flush>
|
<Module {title} titleEmojis={emojis} flush>
|
||||||
<table class="data-table interests-table">
|
<table class="data-table interests-table">
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each interests as entry, index (`${entry.row}:${index}`)}
|
{#each interests as entry, index (`${entry.row}:${index}`)}
|
||||||
|
|||||||
@@ -4,15 +4,17 @@
|
|||||||
* stream. Videos/audio stay in the Blog; reposted pictures are not somebody's
|
* stream. Videos/audio stay in the Blog; reposted pictures are not somebody's
|
||||||
* own Pics.
|
* own Pics.
|
||||||
*/
|
*/
|
||||||
import type { MediaAttachment, Status } from '$lib/api/types'
|
import type { CustomEmoji, MediaAttachment, Status } from '$lib/api/types'
|
||||||
import type { Feed } from '$lib/stores/feed.svelte'
|
import type { Feed } from '$lib/stores/feed.svelte'
|
||||||
import { toPlainText } from '$lib/util/html'
|
import { toPlainText } from '$lib/util/html'
|
||||||
import { stampDate } from '$lib/util/time'
|
import { stampDate } from '$lib/util/time'
|
||||||
import Pager from '../common/Pager.svelte'
|
import Pager from '../common/Pager.svelte'
|
||||||
|
import EmojiText from '../common/EmojiText.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
feed: Feed<Status>
|
feed: Feed<Status>
|
||||||
ownerName: string
|
ownerName: string
|
||||||
|
ownerEmojis?: CustomEmoji[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Picture {
|
interface Picture {
|
||||||
@@ -22,7 +24,7 @@
|
|||||||
caption: string
|
caption: string
|
||||||
}
|
}
|
||||||
|
|
||||||
let { feed, ownerName }: Props = $props()
|
let { feed, ownerName, ownerEmojis }: Props = $props()
|
||||||
let revealed = $state<Record<string, boolean>>({})
|
let revealed = $state<Record<string, boolean>>({})
|
||||||
|
|
||||||
const pictures = $derived.by<Picture[]>(() =>
|
const pictures = $derived.by<Picture[]>(() =>
|
||||||
@@ -46,7 +48,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p class="pic-stream-intro">
|
<p class="pic-stream-intro">
|
||||||
Pictures from {ownerName}'s Blog Entries. Click a picture to view the full-size original.
|
Pictures from <EmojiText text={ownerName} emojis={ownerEmojis} />'s Blog Entries. Click a
|
||||||
|
picture to view the full-size original.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{#if !feed.initialized && feed.loading}
|
{#if !feed.initialized && feed.loading}
|
||||||
@@ -97,10 +100,17 @@
|
|||||||
<figcaption class="pic-card-caption">
|
<figcaption class="pic-card-caption">
|
||||||
{#if hidden}
|
{#if hidden}
|
||||||
<span class="pic-card-description">
|
<span class="pic-card-description">
|
||||||
{picture.status.spoiler_text || 'Sensitive picture'}
|
<EmojiText
|
||||||
|
text={picture.status.spoiler_text || 'Sensitive picture'}
|
||||||
|
emojis={picture.status.emojis}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
{:else if picture.caption}
|
{:else if picture.caption}
|
||||||
<span class="pic-card-description">{picture.caption}</span>
|
<EmojiText
|
||||||
|
class="pic-card-description"
|
||||||
|
text={picture.caption}
|
||||||
|
emojis={picture.status.emojis}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<a class="pic-card-entry-link" href={`#/blog/${picture.status.id}`}>
|
<a class="pic-card-entry-link" href={`#/blog/${picture.status.id}`}>
|
||||||
Posted {stampDate(picture.status.created_at)} · view entry
|
Posted {stampDate(picture.status.created_at)} · view entry
|
||||||
@@ -115,6 +125,6 @@
|
|||||||
<Pager
|
<Pager
|
||||||
{feed}
|
{feed}
|
||||||
label="View More Pictures"
|
label="View More Pictures"
|
||||||
emptyText={`${ownerName} hasn't posted any pictures yet.`}
|
emptyText="There aren't any pictures here yet."
|
||||||
endText={pictures.length > 0 ? 'That’s the whole picture stream.' : ''}
|
endText={pictures.length > 0 ? 'That’s the whole picture stream.' : ''}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,9 +10,9 @@
|
|||||||
*/
|
*/
|
||||||
import type { ProfileView } from '$lib/util/profile'
|
import type { ProfileView } from '$lib/util/profile'
|
||||||
import { displayNameOf, fullHandle } from '$lib/util/profile'
|
import { displayNameOf, fullHandle } from '$lib/util/profile'
|
||||||
import { renderDisplayName } from '$lib/util/html'
|
|
||||||
import { relativeTime, shortDate, yearsSince } from '$lib/util/time'
|
import { relativeTime, shortDate, yearsSince } from '$lib/util/time'
|
||||||
import { useAppServices } from '$lib/app-services'
|
import { useAppServices } from '$lib/app-services'
|
||||||
|
import EmojiText from '../common/EmojiText.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
profile: ProfileView
|
profile: ProfileView
|
||||||
@@ -22,7 +22,6 @@
|
|||||||
|
|
||||||
const { session } = useAppServices()
|
const { session } = useAppServices()
|
||||||
const account = $derived(profile.account)
|
const account = $derived(profile.account)
|
||||||
const name = $derived(renderDisplayName(displayNameOf(account), account.emojis))
|
|
||||||
const handle = $derived(fullHandle(account, session.host))
|
const handle = $derived(fullHandle(account, session.host))
|
||||||
const accountAge = $derived(profile.age ?? yearsSince(account.created_at))
|
const accountAge = $derived(profile.age ?? yearsSince(account.created_at))
|
||||||
const photo = $derived(account.avatar || account.avatar_static)
|
const photo = $derived(account.avatar || account.avatar_static)
|
||||||
@@ -41,12 +40,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="profile-vitals-wrap">
|
<div class="profile-vitals-wrap">
|
||||||
<p class="profile-headline">{profile.headline}</p>
|
<p class="profile-headline">
|
||||||
|
<EmojiText text={profile.headline} emojis={account.emojis} />
|
||||||
|
</p>
|
||||||
|
|
||||||
<dl class="profile-vitals">
|
<dl class="profile-vitals">
|
||||||
{#if profile.gender}
|
{#if profile.gender}
|
||||||
<dt>Gender</dt>
|
<dt>Gender</dt>
|
||||||
<dd class="profile-vital profile-vital--gender">{profile.gender}</dd>
|
<dd class="profile-vital profile-vital--gender">
|
||||||
|
<EmojiText text={profile.gender} emojis={account.emojis} />
|
||||||
|
</dd>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if accountAge !== null}
|
{#if accountAge !== null}
|
||||||
@@ -61,7 +64,9 @@
|
|||||||
|
|
||||||
{#if profile.location}
|
{#if profile.location}
|
||||||
<dt>Location</dt>
|
<dt>Location</dt>
|
||||||
<dd class="profile-vital profile-vital--location">{profile.location}</dd>
|
<dd class="profile-vital profile-vital--location">
|
||||||
|
<EmojiText text={profile.location} emojis={account.emojis} />
|
||||||
|
</dd>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<dt>Last active</dt>
|
<dt>Last active</dt>
|
||||||
@@ -78,7 +83,8 @@
|
|||||||
|
|
||||||
{#if profile.mood}
|
{#if profile.mood}
|
||||||
<p class="profile-mood">
|
<p class="profile-mood">
|
||||||
Mood: <span class="profile-mood-value">{profile.mood}</span>
|
Mood:
|
||||||
|
<EmojiText class="profile-mood-value" text={profile.mood} emojis={account.emojis} />
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -261,6 +261,9 @@ export interface Notification {
|
|||||||
created_at: string
|
created_at: string
|
||||||
account: Account
|
account: Account
|
||||||
status?: Status | null
|
status?: Status | null
|
||||||
|
/** Pleroma/Akkoma emoji-reaction notification payload. */
|
||||||
|
emoji?: string | null
|
||||||
|
emoji_url?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Context {
|
export interface Context {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Notification } from './api/types'
|
import type { CustomEmoji, Notification } from './api/types'
|
||||||
import { toPlainText } from './util/html'
|
import { toPlainText } from './util/html'
|
||||||
import { displayNameOf, profilePath } from './util/profile'
|
import { displayNameOf, profilePath } from './util/profile'
|
||||||
|
|
||||||
@@ -53,6 +53,30 @@ export interface NotificationPresentation {
|
|||||||
href: string
|
href: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NotificationEmoji {
|
||||||
|
text: string
|
||||||
|
emojis: CustomEmoji[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Turn Pleroma's separate reaction name/URL fields into EmojiText input. */
|
||||||
|
export function emojiForNotification(notification: Notification): NotificationEmoji | null {
|
||||||
|
const text = notification.emoji?.trim()
|
||||||
|
if (!text) return null
|
||||||
|
const shortcode = text.match(/^:([^:]+):$/)?.[1]
|
||||||
|
if (!shortcode || !notification.emoji_url) return { text, emojis: [] }
|
||||||
|
return {
|
||||||
|
text,
|
||||||
|
emojis: [
|
||||||
|
{
|
||||||
|
shortcode,
|
||||||
|
url: notification.emoji_url,
|
||||||
|
static_url: notification.emoji_url,
|
||||||
|
visible_in_picker: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const MESSAGE: Record<string, string> = {
|
const MESSAGE: Record<string, string> = {
|
||||||
mention: 'mentioned you in an entry',
|
mention: 'mentioned you in an entry',
|
||||||
status: 'posted a new entry',
|
status: 'posted a new entry',
|
||||||
|
|||||||
+19
-3
@@ -119,6 +119,15 @@ export function escapeHtml(value: string): string {
|
|||||||
* Runs on the *sanitized* string and only injects `<img>` with a URL taken from
|
* Runs on the *sanitized* string and only injects `<img>` with a URL taken from
|
||||||
* the emoji list, so it cannot reintroduce markup from the original content.
|
* the emoji list, so it cannot reintroduce markup from the original content.
|
||||||
*/
|
*/
|
||||||
|
export function safeCustomEmojiUrl(value: string): string | null {
|
||||||
|
try {
|
||||||
|
const url = new URL(value, window.location.href)
|
||||||
|
return url.protocol === 'http:' || url.protocol === 'https:' ? url.href : null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function applyEmojis(html: string, emojis: CustomEmoji[] | undefined): string {
|
function applyEmojis(html: string, emojis: CustomEmoji[] | undefined): string {
|
||||||
if (!emojis?.length) return html
|
if (!emojis?.length) return html
|
||||||
const table = new Map(emojis.map((emoji) => [emoji.shortcode, emoji]))
|
const table = new Map(emojis.map((emoji) => [emoji.shortcode, emoji]))
|
||||||
@@ -128,7 +137,9 @@ function applyEmojis(html: string, emojis: CustomEmoji[] | undefined): string {
|
|||||||
const replaced = text.replace(/:([a-zA-Z0-9_+-]+):/g, (whole, shortcode: string) => {
|
const replaced = text.replace(/:([a-zA-Z0-9_+-]+):/g, (whole, shortcode: string) => {
|
||||||
const emoji = table.get(shortcode)
|
const emoji = table.get(shortcode)
|
||||||
if (!emoji) return whole
|
if (!emoji) return whole
|
||||||
return `<img class="custom-emoji" src="${escapeHtml(emoji.url)}" alt=":${escapeHtml(
|
const url = safeCustomEmojiUrl(emoji.url)
|
||||||
|
if (!url) return whole
|
||||||
|
return `<img class="custom-emoji" src="${escapeHtml(url)}" alt=":${escapeHtml(
|
||||||
shortcode,
|
shortcode,
|
||||||
)}:" title=":${escapeHtml(shortcode)}:" draggable="false" />`
|
)}:" title=":${escapeHtml(shortcode)}:" draggable="false" />`
|
||||||
})
|
})
|
||||||
@@ -213,7 +224,12 @@ export function toPlainText(source: string | null | undefined): string {
|
|||||||
return (container.textContent ?? '').replace(/\s+/g, ' ').trim()
|
return (container.textContent ?? '').replace(/\s+/g, ' ').trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Emoji-substituted display name, safe for `{@html}`. */
|
/** Emoji-substituted plain text, escaped and safe for `{@html}`. */
|
||||||
|
export function renderEmojiText(text: string, emojis: CustomEmoji[] | undefined): string {
|
||||||
|
return applyEmojis(escapeHtml(text), emojis)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Backward-compatible semantic name for existing display-name callers. */
|
||||||
export function renderDisplayName(name: string, emojis: CustomEmoji[] | undefined): string {
|
export function renderDisplayName(name: string, emojis: CustomEmoji[] | undefined): string {
|
||||||
return applyEmojis(escapeHtml(name), emojis)
|
return renderEmojiText(name, emojis)
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-8
@@ -15,12 +15,13 @@
|
|||||||
instanceThumbnail,
|
instanceThumbnail,
|
||||||
} from '$lib/api/endpoints'
|
} from '$lib/api/endpoints'
|
||||||
import { displayNameOf, fallbackMood, formatCount, profilePath } from '$lib/util/profile'
|
import { displayNameOf, fallbackMood, formatCount, profilePath } from '$lib/util/profile'
|
||||||
import { toPlainText } from '$lib/util/html'
|
import { escapeHtml, toPlainText } from '$lib/util/html'
|
||||||
import { relativeTime, shortDate, stampDate } from '$lib/util/time'
|
import { relativeTime, shortDate, stampDate } from '$lib/util/time'
|
||||||
import { useTimelineRefresh } from '$lib/timeline-refresh'
|
import { useTimelineRefresh } from '$lib/timeline-refresh'
|
||||||
import { reconcileRefreshItems } from '$lib/stores/feed.svelte'
|
import { reconcileRefreshItems } from '$lib/stores/feed.svelte'
|
||||||
import Module from '$components/common/Module.svelte'
|
import Module from '$components/common/Module.svelte'
|
||||||
import Avatar from '$components/common/Avatar.svelte'
|
import Avatar from '$components/common/Avatar.svelte'
|
||||||
|
import EmojiText from '$components/common/EmojiText.svelte'
|
||||||
import RichText from '$components/common/RichText.svelte'
|
import RichText from '$components/common/RichText.svelte'
|
||||||
import MfmContent from '$components/common/MfmContent.svelte'
|
import MfmContent from '$components/common/MfmContent.svelte'
|
||||||
import Composer from '$components/blog/Composer.svelte'
|
import Composer from '$components/blog/Composer.svelte'
|
||||||
@@ -217,7 +218,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<h1 class="page-title">
|
<h1 class="page-title">
|
||||||
{#if me}Hello, {displayNameOf(me).split(/\s+/)[0]}!{:else}{domain}{/if}
|
{#if me}
|
||||||
|
Hello, <EmojiText text={displayNameOf(me).split(/\s+/)[0]} emojis={me.emojis} />!
|
||||||
|
{:else}
|
||||||
|
{domain}
|
||||||
|
{/if}
|
||||||
</h1>
|
</h1>
|
||||||
{#if me}
|
{#if me}
|
||||||
<p class="page-subtitle">
|
<p class="page-subtitle">
|
||||||
@@ -243,7 +248,9 @@
|
|||||||
<Avatar account={me} size="friend" />
|
<Avatar account={me} size="friend" />
|
||||||
</p>
|
</p>
|
||||||
<p class="center">
|
<p class="center">
|
||||||
<a href={profilePath(me)}>{displayNameOf(me)}</a>
|
<a href={profilePath(me)}>
|
||||||
|
<EmojiText text={displayNameOf(me)} emojis={me.emojis} />
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p class="center muted">
|
<p class="center muted">
|
||||||
Profile views: {formatCount(me.statuses_count)} entries
|
Profile views: {formatCount(me.statuses_count)} entries
|
||||||
@@ -327,10 +334,10 @@
|
|||||||
<Avatar account={entry.account} />
|
<Avatar account={entry.account} />
|
||||||
<div class="status-line-body">
|
<div class="status-line-body">
|
||||||
<a class="status-line-author" href={profilePath(entry.account)}>
|
<a class="status-line-author" href={profilePath(entry.account)}>
|
||||||
{displayNameOf(entry.account)}
|
<EmojiText text={displayNameOf(entry.account)} emojis={entry.account.emojis} />
|
||||||
</a>
|
</a>
|
||||||
<MfmContent
|
<MfmContent
|
||||||
html={entry.spoiler_text ? `<p>${entry.spoiler_text}</p>` : entry.content}
|
html={entry.spoiler_text ? `<p>${escapeHtml(entry.spoiler_text)}</p>` : entry.content}
|
||||||
emojis={entry.emojis}
|
emojis={entry.emojis}
|
||||||
mentions={entry.mentions}
|
mentions={entry.mentions}
|
||||||
tags={entry.tags}
|
tags={entry.tags}
|
||||||
@@ -376,12 +383,17 @@
|
|||||||
{@const entry = status.reblog ?? status}
|
{@const entry = status.reblog ?? status}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="bulletin-from">
|
<td class="bulletin-from">
|
||||||
<a href={profilePath(entry.account)}>{displayNameOf(entry.account)}</a>
|
<a href={profilePath(entry.account)}>
|
||||||
|
<EmojiText text={displayNameOf(entry.account)} emojis={entry.account.emojis} />
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="bulletin-date">{stampDate(entry.created_at)}</td>
|
<td class="bulletin-date">{stampDate(entry.created_at)}</td>
|
||||||
<td class="bulletin-subject">
|
<td class="bulletin-subject">
|
||||||
<a href={`#/blog/${entry.id}`}>
|
<a href={`#/blog/${entry.id}`}>
|
||||||
{toPlainText(entry.spoiler_text || entry.content).slice(0, 90) || '(no text)'}
|
<EmojiText
|
||||||
|
text={toPlainText(entry.spoiler_text || entry.content).slice(0, 90) || '(no text)'}
|
||||||
|
emojis={entry.emojis}
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -407,7 +419,11 @@
|
|||||||
{#each following as friend (friend.id)}
|
{#each following as friend (friend.id)}
|
||||||
<li class="friend-card" data-account={friend.acct}>
|
<li class="friend-card" data-account={friend.acct}>
|
||||||
<a class="friend-card-link" href={profilePath(friend)}>
|
<a class="friend-card-link" href={profilePath(friend)}>
|
||||||
<span class="friend-card-name">{displayNameOf(friend)}</span>
|
<EmojiText
|
||||||
|
class="friend-card-name"
|
||||||
|
text={displayNameOf(friend)}
|
||||||
|
emojis={friend.emojis}
|
||||||
|
/>
|
||||||
<img
|
<img
|
||||||
class="friend-card-photo"
|
class="friend-card-photo"
|
||||||
src={friend.avatar_static || friend.avatar}
|
src={friend.avatar_static || friend.avatar}
|
||||||
|
|||||||
+17
-4
@@ -13,10 +13,12 @@
|
|||||||
import { Feed } from '$lib/stores/feed.svelte'
|
import { Feed } from '$lib/stores/feed.svelte'
|
||||||
import { displayNameOf, fullHandle, profilePath } from '$lib/util/profile'
|
import { displayNameOf, fullHandle, profilePath } from '$lib/util/profile'
|
||||||
import { toPlainText } from '$lib/util/html'
|
import { toPlainText } from '$lib/util/html'
|
||||||
|
import { emojiForNotification } from '$lib/notifications'
|
||||||
import { stampDate } from '$lib/util/time'
|
import { stampDate } from '$lib/util/time'
|
||||||
import Module from '$components/common/Module.svelte'
|
import Module from '$components/common/Module.svelte'
|
||||||
import Pager from '$components/common/Pager.svelte'
|
import Pager from '$components/common/Pager.svelte'
|
||||||
import Avatar from '$components/common/Avatar.svelte'
|
import Avatar from '$components/common/Avatar.svelte'
|
||||||
|
import EmojiText from '$components/common/EmojiText.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
folder?: string
|
folder?: string
|
||||||
@@ -160,7 +162,9 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<strong>
|
<strong>
|
||||||
<a href={profilePath(account)}>{displayNameOf(account)}</a>
|
<a href={profilePath(account)}>
|
||||||
|
<EmojiText text={displayNameOf(account)} emojis={account.emojis} />
|
||||||
|
</a>
|
||||||
</strong>
|
</strong>
|
||||||
wants to be your friend!
|
wants to be your friend!
|
||||||
<div class="person-row-handle">{fullHandle(account, session.host)}</div>
|
<div class="person-row-handle">{fullHandle(account, session.host)}</div>
|
||||||
@@ -205,6 +209,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each notifications.items as item (item.id)}
|
{#each notifications.items as item (item.id)}
|
||||||
|
{@const reaction = emojiForNotification(item)}
|
||||||
<tr class="mail-row" data-kind={item.type} data-account={item.account.acct}>
|
<tr class="mail-row" data-kind={item.type} data-account={item.account.acct}>
|
||||||
<td class="mail-table-date">{stampDate(item.created_at)}</td>
|
<td class="mail-table-date">{stampDate(item.created_at)}</td>
|
||||||
<td class="mail-table-from">
|
<td class="mail-table-from">
|
||||||
@@ -214,14 +219,22 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="mail-table-subject">
|
<td class="mail-table-subject">
|
||||||
<strong>
|
<strong>
|
||||||
<a href={profilePath(item.account)}>{displayNameOf(item.account)}</a>
|
<a href={profilePath(item.account)}>
|
||||||
|
<EmojiText text={displayNameOf(item.account)} emojis={item.account.emojis} />
|
||||||
|
</a>
|
||||||
</strong>
|
</strong>
|
||||||
{VERB[item.type] ?? item.type}
|
{VERB[item.type] ?? item.type}
|
||||||
|
{#if reaction}
|
||||||
|
with <EmojiText text={reaction.text} emojis={reaction.emojis} />
|
||||||
|
{/if}
|
||||||
{#if item.status}
|
{#if item.status}
|
||||||
<p class="mail-table-excerpt">
|
<p class="mail-table-excerpt">
|
||||||
<a href={`#/blog/${item.status.id}`}>
|
<a href={`#/blog/${item.status.id}`}>
|
||||||
{toPlainText(item.status.spoiler_text || item.status.content).slice(0, 140) ||
|
<EmojiText
|
||||||
'(no text)'}
|
text={toPlainText(item.status.spoiler_text || item.status.content).slice(0, 140) ||
|
||||||
|
'(no text)'}
|
||||||
|
emojis={item.status.emojis}
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
+34
-11
@@ -25,6 +25,7 @@
|
|||||||
} from '$lib/util/profile'
|
} from '$lib/util/profile'
|
||||||
import { toPlainText } from '$lib/util/html'
|
import { toPlainText } from '$lib/util/html'
|
||||||
import Module from '$components/common/Module.svelte'
|
import Module from '$components/common/Module.svelte'
|
||||||
|
import EmojiText from '$components/common/EmojiText.svelte'
|
||||||
import ProfileIdentity from '$components/profile/ProfileIdentity.svelte'
|
import ProfileIdentity from '$components/profile/ProfileIdentity.svelte'
|
||||||
import ContactBox from '$components/profile/ContactBox.svelte'
|
import ContactBox from '$components/profile/ContactBox.svelte'
|
||||||
import InterestsTable from '$components/profile/InterestsTable.svelte'
|
import InterestsTable from '$components/profile/InterestsTable.svelte'
|
||||||
@@ -204,7 +205,9 @@
|
|||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
{:else if account && profile}
|
{:else if account && profile}
|
||||||
<h1 class="page-title profile-name">{displayNameOf(account)}</h1>
|
<h1 class="page-title profile-name">
|
||||||
|
<EmojiText text={displayNameOf(account)} emojis={account.emojis} />
|
||||||
|
</h1>
|
||||||
|
|
||||||
{#if account.moved}
|
{#if account.moved}
|
||||||
<p class="profile-moved">
|
<p class="profile-moved">
|
||||||
@@ -229,10 +232,18 @@
|
|||||||
</p>
|
</p>
|
||||||
</Module>
|
</Module>
|
||||||
|
|
||||||
<InterestsTable title={`${firstName}'s Interests`} interests={profile.interests} />
|
<InterestsTable
|
||||||
<DetailsTable title={`${firstName}'s Details`} fields={profile.details} />
|
title={`${firstName}'s Interests`}
|
||||||
|
interests={profile.interests}
|
||||||
|
emojis={account.emojis}
|
||||||
|
/>
|
||||||
|
<DetailsTable
|
||||||
|
title={`${firstName}'s Details`}
|
||||||
|
fields={profile.details}
|
||||||
|
emojis={account.emojis}
|
||||||
|
/>
|
||||||
|
|
||||||
<Module title={`${firstName}'s Stats`} flush>
|
<Module title={`${firstName}'s Stats`} titleEmojis={account.emojis} flush>
|
||||||
<table class="data-table stats-table">
|
<table class="data-table stats-table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -275,6 +286,7 @@
|
|||||||
<FriendSpace
|
<FriendSpace
|
||||||
title={`${firstName}'s Friend Space`}
|
title={`${firstName}'s Friend Space`}
|
||||||
ownerName={firstName}
|
ownerName={firstName}
|
||||||
|
ownerEmojis={account.emojis}
|
||||||
{friends}
|
{friends}
|
||||||
total={account.followers_count}
|
total={account.followers_count}
|
||||||
viewAllHref={`#/@${account.acct}`}
|
viewAllHref={`#/@${account.acct}`}
|
||||||
@@ -283,7 +295,7 @@
|
|||||||
{countHidden}
|
{countHidden}
|
||||||
/>
|
/>
|
||||||
{:else if view === 'blog'}
|
{:else if view === 'blog'}
|
||||||
<Module title={`${firstName}'s Blog`} variant="band">
|
<Module title={`${firstName}'s Blog`} titleEmojis={account.emojis} variant="band">
|
||||||
{#snippet action()}
|
{#snippet action()}
|
||||||
<a href={base}>[Back to Profile]</a>
|
<a href={base}>[Back to Profile]</a>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
@@ -309,12 +321,12 @@
|
|||||||
/>
|
/>
|
||||||
</Module>
|
</Module>
|
||||||
{:else if view === 'pics'}
|
{:else if view === 'pics'}
|
||||||
<Module title={`${firstName}'s Pics`} variant="band">
|
<Module title={`${firstName}'s Pics`} titleEmojis={account.emojis} variant="band">
|
||||||
{#snippet action()}
|
{#snippet action()}
|
||||||
<a href={base}>[Back to Profile]</a>
|
<a href={base}>[Back to Profile]</a>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<PicStream feed={entries} ownerName={firstName} />
|
<PicStream feed={entries} ownerName={firstName} ownerEmojis={account.emojis} />
|
||||||
</Module>
|
</Module>
|
||||||
{:else}
|
{:else}
|
||||||
<!--
|
<!--
|
||||||
@@ -323,7 +335,11 @@
|
|||||||
posts of media push the Blurbs and Friend Space off the bottom,
|
posts of media push the Blurbs and Friend Space off the bottom,
|
||||||
which is the wrong shape for a profile.
|
which is the wrong shape for a profile.
|
||||||
-->
|
-->
|
||||||
<Module title={`${firstName}'s Latest Blog Entries`} variant="band">
|
<Module
|
||||||
|
title={`${firstName}'s Latest Blog Entries`}
|
||||||
|
titleEmojis={account.emojis}
|
||||||
|
variant="band"
|
||||||
|
>
|
||||||
{#snippet action()}
|
{#snippet action()}
|
||||||
<a href={`${base}/blog`}>[View Blog]</a>
|
<a href={`${base}/blog`}>[View Blog]</a>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
@@ -337,7 +353,11 @@
|
|||||||
{#each entries.items.slice(0, 6) as status (status.id)}
|
{#each entries.items.slice(0, 6) as status (status.id)}
|
||||||
{@const entry = status.reblog ?? status}
|
{@const entry = status.reblog ?? status}
|
||||||
<li class="entry-teaser" data-status-id={entry.id}>
|
<li class="entry-teaser" data-status-id={entry.id}>
|
||||||
<span class="entry-teaser-text">{teaserFor(entry)}</span>
|
<EmojiText
|
||||||
|
class="entry-teaser-text"
|
||||||
|
text={teaserFor(entry)}
|
||||||
|
emojis={entry.emojis}
|
||||||
|
/>
|
||||||
<a class="entry-teaser-link" href={`#/blog/${entry.id}`}>(view more)</a>
|
<a class="entry-teaser-link" href={`#/blog/${entry.id}`}>(view more)</a>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -348,7 +368,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</Module>
|
</Module>
|
||||||
|
|
||||||
<Module title={`${firstName}'s Blurbs`} variant="band">
|
<Module title={`${firstName}'s Blurbs`} titleEmojis={account.emojis} variant="band">
|
||||||
<h3 class="section-heading">About me:</h3>
|
<h3 class="section-heading">About me:</h3>
|
||||||
{#if profile.about}
|
{#if profile.about}
|
||||||
<div class="rich-text blurb-body">
|
<div class="rich-text blurb-body">
|
||||||
@@ -356,7 +376,9 @@
|
|||||||
{@html profile.about}
|
{@html profile.about}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="empty-note">{firstName} hasn't written an About me yet.</p>
|
<p class="empty-note">
|
||||||
|
<EmojiText text={firstName} emojis={account.emojis} /> hasn't written an About me yet.
|
||||||
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<h3 class="section-heading">Who I'd like to meet:</h3>
|
<h3 class="section-heading">Who I'd like to meet:</h3>
|
||||||
@@ -375,6 +397,7 @@
|
|||||||
<FriendSpace
|
<FriendSpace
|
||||||
title={`${firstName}'s Friend Space`}
|
title={`${firstName}'s Friend Space`}
|
||||||
ownerName={firstName}
|
ownerName={firstName}
|
||||||
|
ownerEmojis={account.emojis}
|
||||||
friends={friends.slice(0, FRIEND_PREVIEW)}
|
friends={friends.slice(0, FRIEND_PREVIEW)}
|
||||||
total={account.followers_count}
|
total={account.followers_count}
|
||||||
viewAllHref={`#/@${account.acct}/friends`}
|
viewAllHref={`#/@${account.acct}/friends`}
|
||||||
|
|||||||
@@ -7,6 +7,51 @@ import { account, deferred, session, status, testServices, theme } from '$test/f
|
|||||||
import Profile from './Profile.svelte'
|
import Profile from './Profile.svelte'
|
||||||
|
|
||||||
describe('Profile', () => {
|
describe('Profile', () => {
|
||||||
|
it('renders account custom emoji in the public profile heading', async () => {
|
||||||
|
const profileAccount = account({
|
||||||
|
display_name: ':smugkura: Kura :disconnecting: :kura_explode:',
|
||||||
|
emojis: [
|
||||||
|
{
|
||||||
|
shortcode: 'smugkura',
|
||||||
|
url: 'https://cdn.example/smugkura.png',
|
||||||
|
static_url: 'https://cdn.example/smugkura.png',
|
||||||
|
visible_in_picker: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shortcode: 'disconnecting',
|
||||||
|
url: 'https://cdn.example/disconnecting.png',
|
||||||
|
static_url: 'https://cdn.example/disconnecting.png',
|
||||||
|
visible_in_picker: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shortcode: 'kura_explode',
|
||||||
|
url: 'https://cdn.example/kura_explode.png',
|
||||||
|
static_url: 'https://cdn.example/kura_explode.png',
|
||||||
|
visible_in_picker: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
const services = testServices({
|
||||||
|
session: session(),
|
||||||
|
theme: theme(),
|
||||||
|
endpoints: {
|
||||||
|
lookupAccount: vi.fn().mockResolvedValue(profileAccount),
|
||||||
|
fetchAccountStatuses: vi.fn().mockResolvedValue({ items: [], links: {} }),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const view = render(Profile, {
|
||||||
|
props: { acct: 'alice' },
|
||||||
|
context: new Map([[APP_SERVICES, services]]),
|
||||||
|
})
|
||||||
|
|
||||||
|
const headingEmoji = (await view.findAllByAltText(':smugkura:')).find((node) =>
|
||||||
|
node.closest('h1.profile-name'),
|
||||||
|
)
|
||||||
|
expect(headingEmoji).toBeDefined()
|
||||||
|
expect(view.getAllByAltText(':disconnecting:').length).toBeGreaterThan(0)
|
||||||
|
expect(view.getAllByAltText(':kura_explode:').length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
it('does not apply profile CSS after it has unmounted', async () => {
|
it('does not apply profile CSS after it has unmounted', async () => {
|
||||||
const lookup = deferred<Account>()
|
const lookup = deferred<Account>()
|
||||||
const applyProfileCss = vi.fn()
|
const applyProfileCss = vi.fn()
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
import { instanceDomain } from '$lib/api/endpoints'
|
import { instanceDomain } from '$lib/api/endpoints'
|
||||||
import { displayNameOf, profilePath } from '$lib/util/profile'
|
import { displayNameOf, profilePath } from '$lib/util/profile'
|
||||||
import Module from '$components/common/Module.svelte'
|
import Module from '$components/common/Module.svelte'
|
||||||
|
import EmojiText from '$components/common/EmojiText.svelte'
|
||||||
import PublicProfileEditor from '$components/profile/PublicProfileEditor.svelte'
|
import PublicProfileEditor from '$components/profile/PublicProfileEditor.svelte'
|
||||||
import PublishedCssEditor from '$components/profile/PublishedCssEditor.svelte'
|
import PublishedCssEditor from '$components/profile/PublishedCssEditor.svelte'
|
||||||
|
|
||||||
@@ -97,7 +98,9 @@
|
|||||||
{#if session.signedIn && session.me}
|
{#if session.signedIn && session.me}
|
||||||
<p>
|
<p>
|
||||||
Signed in as
|
Signed in as
|
||||||
<a href={profilePath(session.me)}>{displayNameOf(session.me)}</a>
|
<a href={profilePath(session.me)}>
|
||||||
|
<EmojiText text={displayNameOf(session.me)} emojis={session.me.emojis} />
|
||||||
|
</a>
|
||||||
on <strong>{domain}</strong>.
|
on <strong>{domain}</strong>.
|
||||||
</p>
|
</p>
|
||||||
<p class="field-row">
|
<p class="field-row">
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
import { useTimelineRefresh } from '$lib/timeline-refresh'
|
import { useTimelineRefresh } from '$lib/timeline-refresh'
|
||||||
import Module from '$components/common/Module.svelte'
|
import Module from '$components/common/Module.svelte'
|
||||||
import Avatar from '$components/common/Avatar.svelte'
|
import Avatar from '$components/common/Avatar.svelte'
|
||||||
|
import EmojiText from '$components/common/EmojiText.svelte'
|
||||||
import MfmContent from '$components/common/MfmContent.svelte'
|
import MfmContent from '$components/common/MfmContent.svelte'
|
||||||
import BlogEntry from '$components/blog/BlogEntry.svelte'
|
import BlogEntry from '$components/blog/BlogEntry.svelte'
|
||||||
import Composer from '$components/blog/Composer.svelte'
|
import Composer from '$components/blog/Composer.svelte'
|
||||||
@@ -162,7 +163,9 @@
|
|||||||
</p>
|
</p>
|
||||||
{:else if status}
|
{:else if status}
|
||||||
<h1 class="page-title">
|
<h1 class="page-title">
|
||||||
<a href={profilePath(status.account)}>{displayNameOf(status.account)}</a>'s Blog
|
<a href={profilePath(status.account)}>
|
||||||
|
<EmojiText text={displayNameOf(status.account)} emojis={status.account.emojis} />
|
||||||
|
</a>'s Blog
|
||||||
</h1>
|
</h1>
|
||||||
<p class="page-subtitle">
|
<p class="page-subtitle">
|
||||||
<time datetime={isoDate(status.created_at)}>{stampDate(status.created_at)}</time>
|
<time datetime={isoDate(status.created_at)}>{stampDate(status.created_at)}</time>
|
||||||
@@ -219,7 +222,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="comment-body">
|
<div class="comment-body">
|
||||||
<a class="comment-author" href={profilePath(reply.status.account)}>
|
<a class="comment-author" href={profilePath(reply.status.account)}>
|
||||||
{displayNameOf(reply.status.account)}
|
<EmojiText
|
||||||
|
text={displayNameOf(reply.status.account)}
|
||||||
|
emojis={reply.status.account.emojis}
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
<a class="comment-date" href={`#/blog/${reply.status.id}`}>
|
<a class="comment-date" href={`#/blog/${reply.status.id}`}>
|
||||||
<time datetime={isoDate(reply.status.created_at)}>
|
<time datetime={isoDate(reply.status.created_at)}>
|
||||||
@@ -228,7 +234,9 @@
|
|||||||
</a>
|
</a>
|
||||||
{#if reply.status.spoiler_text}
|
{#if reply.status.spoiler_text}
|
||||||
<details class="content-warning">
|
<details class="content-warning">
|
||||||
<summary class="content-warning-summary">{reply.status.spoiler_text}</summary>
|
<summary class="content-warning-summary">
|
||||||
|
<EmojiText text={reply.status.spoiler_text} emojis={reply.status.emojis} />
|
||||||
|
</summary>
|
||||||
<MfmContent
|
<MfmContent
|
||||||
html={reply.status.content}
|
html={reply.status.content}
|
||||||
emojis={reply.status.emojis}
|
emojis={reply.status.emojis}
|
||||||
|
|||||||
+2
-1
@@ -175,8 +175,9 @@ table {
|
|||||||
|
|
||||||
/* Custom emoji injected into sanitized HTML by lib/util/html.ts. */
|
/* Custom emoji injected into sanitized HTML by lib/util/html.ts. */
|
||||||
.custom-emoji {
|
.custom-emoji {
|
||||||
width: var(--ms-emoji-size);
|
|
||||||
height: var(--ms-emoji-size);
|
height: var(--ms-emoji-size);
|
||||||
|
width: auto;
|
||||||
|
max-width: calc(var(--ms-emoji-size) * 2);
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -406,8 +406,9 @@
|
|||||||
|
|
||||||
.emoji-reaction-image {
|
.emoji-reaction-image {
|
||||||
display: block;
|
display: block;
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
width: auto;
|
||||||
|
max-width: 40px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -62,8 +62,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rich-text .mfm .custom-emoji {
|
.rich-text .mfm .custom-emoji {
|
||||||
/* Misskey's emoji width knows no bounds. */
|
/* Wide emoji are supported, but never wider than twice their height. */
|
||||||
max-width: unset !important;
|
max-width: calc(var(--emoji-size) * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rich-text:hover .mfm {
|
.rich-text:hover .mfm {
|
||||||
|
|||||||
Reference in New Issue
Block a user