mirror of
https://git.shipoclu.com/moon/plspace.git
synced 2026-08-14 03:02:31 +00:00
132 lines
3.6 KiB
Svelte
132 lines
3.6 KiB
Svelte
<script lang="ts">
|
|
import type { EmojiReaction, Status } from '$lib/api/types'
|
|
import { useAppServices } from '$lib/app-services'
|
|
import { safeCustomEmojiUrl } from '$lib/util/html'
|
|
|
|
interface Props {
|
|
status: Status
|
|
onupdate?: (status: Status) => void
|
|
}
|
|
|
|
let { status, onupdate }: Props = $props()
|
|
|
|
const { endpoints, session } = useAppServices()
|
|
const reactions = $derived(
|
|
(status.pleroma?.emoji_reactions ?? []).filter(
|
|
(reaction) => reaction.name.trim().length > 0 && reaction.count > 0,
|
|
),
|
|
)
|
|
|
|
let busyEmoji = $state<string | null>(null)
|
|
let error = $state<string | null>(null)
|
|
|
|
function customLabel(reaction: EmojiReaction): string {
|
|
return `:${reaction.name}:`
|
|
}
|
|
|
|
function accessibleLabel(reaction: EmojiReaction): string {
|
|
const action = reaction.me ? 'Remove' : 'Add'
|
|
const emoji = reaction.url ? customLabel(reaction) : reaction.name
|
|
const countLabel = reaction.count === 1 ? '1 reaction' : `${reaction.count} reactions`
|
|
return `${action} ${emoji} reaction, ${countLabel}`
|
|
}
|
|
|
|
function withReactions(base: Status, next: EmojiReaction[]): Status {
|
|
return {
|
|
...base,
|
|
pleroma: {
|
|
...(base.pleroma ?? {}),
|
|
emoji_reactions: next,
|
|
},
|
|
}
|
|
}
|
|
|
|
function optimisticReactions(
|
|
source: EmojiReaction[],
|
|
selected: EmojiReaction,
|
|
on: boolean,
|
|
): EmojiReaction[] {
|
|
return source
|
|
.map((reaction) => {
|
|
if (reaction.name !== selected.name) return reaction
|
|
return {
|
|
...reaction,
|
|
count: Math.max(0, reaction.count + (on ? 1 : -1)),
|
|
me: on,
|
|
}
|
|
})
|
|
.filter((reaction) => reaction.count > 0)
|
|
}
|
|
|
|
async function toggle(reaction: EmojiReaction): Promise<void> {
|
|
if (!session.signedIn || busyEmoji) return
|
|
|
|
const before = status
|
|
const on = !reaction.me
|
|
busyEmoji = reaction.name
|
|
error = null
|
|
onupdate?.(
|
|
withReactions(
|
|
before,
|
|
optimisticReactions(before.pleroma?.emoji_reactions ?? [], reaction, on),
|
|
),
|
|
)
|
|
|
|
try {
|
|
const updated = await endpoints.setEmojiReaction(
|
|
session.api,
|
|
status.id,
|
|
reaction.name,
|
|
on,
|
|
)
|
|
onupdate?.(updated)
|
|
} catch (cause) {
|
|
onupdate?.(before)
|
|
error =
|
|
cause instanceof Error
|
|
? cause.message
|
|
: 'Could not save that emoji reaction.'
|
|
} finally {
|
|
busyEmoji = null
|
|
}
|
|
}
|
|
</script>
|
|
|
|
{#if reactions.length > 0}
|
|
<div class="emoji-reaction-bar" aria-label="Emoji reactions">
|
|
{#each reactions as reaction (reaction.url ?? reaction.name)}
|
|
{@const imageUrl = reaction.url ? safeCustomEmojiUrl(reaction.url) : null}
|
|
<button
|
|
type="button"
|
|
class="emoji-reaction"
|
|
class:emoji-reaction--mine={reaction.me}
|
|
data-custom={imageUrl ? 'true' : 'false'}
|
|
aria-label={accessibleLabel(reaction)}
|
|
aria-pressed={reaction.me ? 'true' : 'false'}
|
|
title={session.signedIn
|
|
? accessibleLabel(reaction)
|
|
: 'Sign in to react with this emoji'}
|
|
disabled={!session.signedIn || busyEmoji !== null}
|
|
onclick={() => void toggle(reaction)}
|
|
>
|
|
{#if imageUrl}
|
|
<img
|
|
class="emoji-reaction-image"
|
|
src={imageUrl}
|
|
alt={customLabel(reaction)}
|
|
loading="lazy"
|
|
decoding="async"
|
|
/>
|
|
{:else}
|
|
<span class="emoji-reaction-glyph" aria-hidden="true">{reaction.name}</span>
|
|
{/if}
|
|
<span class="emoji-reaction-count">{reaction.count}</span>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
|
|
{#if error}
|
|
<p class="error-note emoji-reaction-error" role="alert">{error}</p>
|
|
{/if}
|