mirror of
https://github.com/coracle-social/flotilla.git
synced 2025-12-10 02:47:06 +00:00
182 lines
5.6 KiB
Svelte
182 lines
5.6 KiB
Svelte
<script lang="ts">
|
|
import cx from "classnames"
|
|
import {onMount} from "svelte"
|
|
import type {Snippet} from "svelte"
|
|
import {groupBy, map, sum, uniq, uniqBy, batch, displayList} from "@welshman/lib"
|
|
import {
|
|
REPORT,
|
|
REACTION,
|
|
ZAP_RESPONSE,
|
|
getReplyFilters,
|
|
getEmojiTags,
|
|
getEmojiTag,
|
|
fromMsats,
|
|
getTag,
|
|
DELETE,
|
|
} from "@welshman/util"
|
|
import type {TrustedEvent, EventContent, Zap} from "@welshman/util"
|
|
import {deriveArray, deriveEventsById, deriveItemsByKey} from "@welshman/store"
|
|
import {load} from "@welshman/net"
|
|
import {pubkey, repository, getValidZap, displayProfileByPubkey} from "@welshman/app"
|
|
import {isMobile, preventDefault, stopPropagation} from "@lib/html"
|
|
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
|
import Icon from "@lib/components/Icon.svelte"
|
|
import Reaction from "@app/components/Reaction.svelte"
|
|
import ReportDetails from "@app/components/ReportDetails.svelte"
|
|
import {REACTION_KINDS} from "@app/core/state"
|
|
import {pushModal} from "@app/util/modal"
|
|
|
|
interface Props {
|
|
event: TrustedEvent
|
|
deleteReaction: (event: TrustedEvent) => void
|
|
createReaction: (event: EventContent) => void
|
|
url?: string
|
|
reactionClass?: string
|
|
noTooltip?: boolean
|
|
children?: Snippet
|
|
}
|
|
|
|
const {
|
|
event,
|
|
deleteReaction,
|
|
createReaction,
|
|
url = "",
|
|
reactionClass = "",
|
|
noTooltip = false,
|
|
children,
|
|
}: Props = $props()
|
|
|
|
const reports = deriveArray(
|
|
deriveEventsById({repository, filters: [{kinds: [REPORT], "#e": [event.id]}]}),
|
|
)
|
|
|
|
const reactions = deriveArray(
|
|
deriveEventsById({repository, filters: [{kinds: [REACTION], "#e": [event.id]}]}),
|
|
)
|
|
|
|
const zaps = deriveArray(
|
|
deriveItemsByKey<Zap>({
|
|
repository,
|
|
getKey: zap => zap.response.id,
|
|
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
|
|
eventToItem: (response: TrustedEvent) => getValidZap(response, event),
|
|
}),
|
|
)
|
|
|
|
const onReactionClick = (events: TrustedEvent[]) => {
|
|
const reaction = events.find(e => e.pubkey === $pubkey)
|
|
|
|
if (reaction) {
|
|
deleteReaction(reaction)
|
|
} else {
|
|
const [event] = events
|
|
|
|
createReaction({
|
|
content: event.content,
|
|
tags: getEmojiTags(event.content.replace(/:/g, ""), event.tags),
|
|
})
|
|
}
|
|
}
|
|
|
|
const onReportClick = () => pushModal(ReportDetails, {url, event})
|
|
|
|
const reportReasons = $derived(uniq(map(e => getTag("e", e.tags)?.[2], $reports.values())))
|
|
|
|
const getReactionKey = (e: TrustedEvent) => getEmojiTag(e.content, e.tags)?.join("") || e.content
|
|
|
|
const groupedReactions = $derived(
|
|
groupBy(
|
|
getReactionKey,
|
|
uniqBy(e => `${e.pubkey}${getReactionKey(e)}`, $reactions.values()),
|
|
),
|
|
)
|
|
|
|
const groupedZaps = $derived(groupBy(e => getReactionKey(e.request), $zaps.values()))
|
|
|
|
onMount(() => {
|
|
const controller = new AbortController()
|
|
|
|
if (url) {
|
|
load({
|
|
relays: [url],
|
|
signal: controller.signal,
|
|
filters: getReplyFilters([event], {kinds: REACTION_KINDS}),
|
|
onEvent: batch(300, (events: TrustedEvent[]) => {
|
|
load({
|
|
relays: [url],
|
|
filters: getReplyFilters(events, {kinds: [DELETE]}),
|
|
})
|
|
}),
|
|
})
|
|
}
|
|
|
|
return () => {
|
|
controller.abort()
|
|
}
|
|
})
|
|
</script>
|
|
|
|
{#if $reactions.length > 0 || $zaps.length || $reports.length > 0}
|
|
<div class="flex min-w-0 flex-wrap gap-2">
|
|
{#if url && $reports.length > 0}
|
|
<button
|
|
type="button"
|
|
data-tip={`This content has been reported as "${displayList(reportReasons)}".`}
|
|
class="btn btn-error btn-xs tooltip-right flex items-center gap-1 rounded-full font-normal"
|
|
class:tooltip={!noTooltip && !isMobile}
|
|
onclick={stopPropagation(preventDefault(onReportClick))}>
|
|
<Icon icon={Danger} />
|
|
<span>{$reports.length}</span>
|
|
</button>
|
|
{/if}
|
|
{#each groupedZaps.entries() as [key, zaps]}
|
|
{@const amount = fromMsats(sum(zaps.map(zap => zap.invoiceAmount)))}
|
|
{@const pubkeys = uniq(zaps.map(zap => zap.request.pubkey))}
|
|
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
|
|
{@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
|
|
{@const tooltip = `${info} zapped`}
|
|
<button
|
|
type="button"
|
|
data-tip={tooltip}
|
|
class={cx(
|
|
reactionClass,
|
|
"flex-inline btn btn-outline btn-neutral btn-xs flex items-center gap-1 rounded-full text-xs font-normal",
|
|
{
|
|
tooltip: !noTooltip && !isMobile,
|
|
"border-neutral-content/20": !isOwn,
|
|
"btn-primary": isOwn,
|
|
},
|
|
)}>
|
|
<Reaction event={zaps[0].request} />
|
|
<span>{amount}</span>
|
|
</button>
|
|
{/each}
|
|
{#each groupedReactions.entries() as [key, events]}
|
|
{@const pubkeys = events.map(e => e.pubkey)}
|
|
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
|
|
{@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
|
|
{@const tooltip = `${info} reacted`}
|
|
{@const onClick = () => onReactionClick(events)}
|
|
<button
|
|
type="button"
|
|
data-tip={tooltip}
|
|
class={cx(
|
|
reactionClass,
|
|
"flex-inline btn btn-outline btn-neutral btn-xs gap-1 rounded-full font-normal",
|
|
{
|
|
tooltip: !noTooltip && !isMobile,
|
|
"border-neutral-content/20": !isOwn,
|
|
"btn-primary": isOwn,
|
|
},
|
|
)}
|
|
onclick={stopPropagation(preventDefault(onClick))}>
|
|
<Reaction event={events[0]} />
|
|
{#if events.length > 1}
|
|
<span>{events.length}</span>
|
|
{/if}
|
|
</button>
|
|
{/each}
|
|
{@render children?.()}
|
|
</div>
|
|
{/if}
|