You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

253 lines
7.1 KiB

<script lang="ts">
import { sessionManager } from '../../services/auth/session-manager.js';
import { signAndPublish } from '../../services/nostr/auth-handler.js';
import { nostrClient } from '../../services/nostr/nostr-client.js';
import { onMount } from 'svelte';
import type { NostrEvent } from '../../types/nostr.js';
interface Props {
event: NostrEvent; // The event to react to (kind 11 or 1111)
}
let { event }: Props = $props();
let reactions = $state<Map<string, { content: string; pubkeys: Set<string> }>>(new Map());
let userReaction = $state<string | null>(null);
let loading = $state(true);
onMount(async () => {
await nostrClient.initialize();
loadReactions();
});
async function loadReactions() {
loading = true;
try {
const config = nostrClient.getConfig();
// Fetch reactions (kind 7) for this event
const filters = [
{
kinds: [7],
'#e': [event.id]
}
];
const reactionEvents = await nostrClient.fetchEvents(
filters,
[...config.defaultRelays],
{ useCache: true, cacheResults: true, onUpdate: (updated) => {
processReactions(updated);
}}
);
processReactions(reactionEvents);
} catch (error) {
console.error('Error loading reactions:', error);
} finally {
loading = false;
}
}
function processReactions(reactionEvents: NostrEvent[]) {
const reactionMap = new Map<string, { content: string; pubkeys: Set<string> }>();
const currentUser = sessionManager.getCurrentPubkey();
for (const reactionEvent of reactionEvents) {
const content = reactionEvent.content.trim();
// Normalize reactions for kind 11/1111: only + and - allowed
// Backward compatibility: ⬆ = +, ⬇ = -
let normalizedContent = content;
if (event.kind === 11 || event.kind === 1111) {
if (content === '⬆' || content === '↑') {
normalizedContent = '+';
} else if (content === '⬇' || content === '↓') {
normalizedContent = '-';
} else if (content !== '+' && content !== '-') {
continue; // Skip invalid reactions for threads/comments
}
}
if (!reactionMap.has(normalizedContent)) {
reactionMap.set(normalizedContent, { content: normalizedContent, pubkeys: new Set() });
}
reactionMap.get(normalizedContent)!.pubkeys.add(reactionEvent.pubkey);
// Track user's reaction
if (currentUser && reactionEvent.pubkey === currentUser) {
userReaction = normalizedContent;
}
}
reactions = reactionMap;
}
async function toggleReaction(content: string) {
if (!sessionManager.isLoggedIn()) {
alert('Please log in to react');
return;
}
// For kind 11/1111, only allow + and -
if ((event.kind === 11 || event.kind === 1111) && content !== '+' && content !== '-') {
return;
}
// If clicking the same reaction, remove it
if (userReaction === content) {
// TODO: Implement reaction deletion (kind 5 or remove event)
// For now, just remove from UI
userReaction = null;
const reaction = reactions.get(content);
if (reaction) {
const currentUser = sessionManager.getCurrentPubkey();
if (currentUser) {
reaction.pubkeys.delete(currentUser);
if (reaction.pubkeys.size === 0) {
reactions.delete(content);
}
}
}
return;
}
// Publish new reaction
try {
const tags: string[][] = [
['e', event.id],
['p', event.pubkey],
['k', event.kind.toString()]
];
if (sessionManager.getCurrentPubkey() && includeClientTag) {
tags.push(['client', 'Aitherboard']);
}
const reactionEvent: Omit<NostrEvent, 'id' | 'sig'> = {
kind: 7,
pubkey: sessionManager.getCurrentPubkey()!,
created_at: Math.floor(Date.now() / 1000),
tags,
content
};
const config = nostrClient.getConfig();
await signAndPublish(reactionEvent, [...config.defaultRelays]);
// Update local state
userReaction = content;
const currentPubkey = sessionManager.getCurrentPubkey()!;
if (!reactions.has(content)) {
reactions.set(content, { content, pubkeys: new Set([currentPubkey]) });
} else {
reactions.get(content)!.pubkeys.add(currentPubkey);
}
} catch (error) {
console.error('Error publishing reaction:', error);
alert('Error publishing reaction');
}
}
function getReactionDisplay(content: string): string {
if (content === '+') {
return event.kind === 1 ? '❤' : '↑';
}
if (content === '-') {
return '↓';
}
return content;
}
function getReactionCount(content: string): number {
return reactions.get(content)?.pubkeys.size || 0;
}
let includeClientTag = $state(true);
</script>
<div class="reaction-buttons flex gap-2 items-center">
{#if event.kind === 11 || event.kind === 1111}
<!-- Thread/Comment reactions: Only + and - -->
<button
onclick={() => toggleReaction('+')}
class="reaction-btn {userReaction === '+' ? 'active' : ''}"
title="Upvote"
aria-label="Upvote"
>
{getReactionCount('+')}
</button>
<button
onclick={() => toggleReaction('-')}
class="reaction-btn {userReaction === '-' ? 'active' : ''}"
title="Downvote"
aria-label="Downvote"
>
{getReactionCount('-')}
</button>
{:else}
<!-- Kind 1 reactions: All reactions allowed, default + -->
<button
onclick={() => toggleReaction('+')}
class="reaction-btn {userReaction === '+' ? 'active' : ''}"
title="Like"
aria-label="Like"
>
{getReactionCount('+')}
</button>
<!-- Other reactions could go in a submenu -->
{/if}
</div>
<style>
.reaction-buttons {
display: flex;
align-items: center;
}
.reaction-btn {
padding: 0.25rem 0.75rem;
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1f2937);
cursor: pointer;
transition: all 0.2s;
font-size: 0.875rem;
line-height: 1.5;
display: inline-flex;
align-items: center;
}
:global(.dark) .reaction-btn {
background: var(--fog-dark-post, #1f2937);
border-color: var(--fog-dark-border, #374151);
color: var(--fog-dark-text, #f9fafb);
}
.reaction-btn:hover {
background: var(--fog-highlight, #f3f4f6);
border-color: var(--fog-accent, #64748b);
}
:global(.dark) .reaction-btn:hover {
background: var(--fog-dark-highlight, #374151);
}
.reaction-btn.active {
background: var(--fog-accent, #64748b);
color: var(--fog-text, #475569);
border-color: var(--fog-accent, #64748b);
}
:global(.dark) .reaction-btn.active {
background: var(--fog-dark-accent, #64748b);
color: var(--fog-dark-text, #cbd5e1);
border-color: var(--fog-dark-accent, #64748b);
}
.reaction-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>