21 changed files with 1600 additions and 418 deletions
@ -0,0 +1,63 @@ |
|||||||
|
<script lang="ts"> |
||||||
|
import type { NostrEvent } from '../../types/nostr.js'; |
||||||
|
|
||||||
|
interface Props { |
||||||
|
parentEvent: NostrEvent; |
||||||
|
targetId?: string; // Optional ID to scroll to (defaults to parent event ID) |
||||||
|
} |
||||||
|
|
||||||
|
let { parentEvent, targetId }: Props = $props(); |
||||||
|
|
||||||
|
function getParentPreview(): string { |
||||||
|
// Create preview from parent (first 100 chars, plaintext) |
||||||
|
const plaintext = parentEvent.content.replace(/[#*_`\[\]()]/g, '').replace(/\n/g, ' ').trim(); |
||||||
|
return plaintext.slice(0, 100) + (plaintext.length > 100 ? '...' : ''); |
||||||
|
} |
||||||
|
|
||||||
|
function scrollToParent() { |
||||||
|
const elementId = targetId || `event-${parentEvent.id}`; |
||||||
|
const element = document.getElementById(elementId) || document.querySelector(`[data-event-id="${parentEvent.id}"]`); |
||||||
|
if (element) { |
||||||
|
element.scrollIntoView({ behavior: 'smooth', block: 'center' }); |
||||||
|
element.classList.add('highlight-parent'); |
||||||
|
setTimeout(() => { |
||||||
|
element.classList.remove('highlight-parent'); |
||||||
|
}, 2000); |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<div |
||||||
|
class="reply-context mb-2 p-2 bg-fog-highlight dark:bg-fog-dark-highlight rounded text-xs text-fog-text-light dark:text-fog-dark-text-light cursor-pointer hover:opacity-80 transition-opacity" |
||||||
|
onclick={scrollToParent} |
||||||
|
role="button" |
||||||
|
tabindex="0" |
||||||
|
onkeydown={(e) => { |
||||||
|
if (e.key === 'Enter' || e.key === ' ') { |
||||||
|
e.preventDefault(); |
||||||
|
scrollToParent(); |
||||||
|
} |
||||||
|
}} |
||||||
|
> |
||||||
|
<span class="font-semibold">Replying to:</span> {getParentPreview()} |
||||||
|
</div> |
||||||
|
|
||||||
|
<style> |
||||||
|
.reply-context { |
||||||
|
border-left: 2px solid var(--fog-accent, #64748b); |
||||||
|
} |
||||||
|
|
||||||
|
:global(.dark) .reply-context { |
||||||
|
border-left-color: var(--fog-dark-accent, #64748b); |
||||||
|
} |
||||||
|
|
||||||
|
:global(.highlight-parent) { |
||||||
|
outline: 2px solid var(--fog-accent, #64748b); |
||||||
|
outline-offset: 2px; |
||||||
|
transition: outline 0.3s ease; |
||||||
|
} |
||||||
|
|
||||||
|
:global(.dark .highlight-parent) { |
||||||
|
outline-color: var(--fog-dark-accent, #64748b); |
||||||
|
} |
||||||
|
</style> |
||||||
@ -0,0 +1,234 @@ |
|||||||
|
<script lang="ts"> |
||||||
|
import ProfileBadge from '../../components/layout/ProfileBadge.svelte'; |
||||||
|
import MarkdownRenderer from '../../components/content/MarkdownRenderer.svelte'; |
||||||
|
import ReplyContext from '../../components/content/ReplyContext.svelte'; |
||||||
|
import FeedReactionButtons from '../reactions/FeedReactionButtons.svelte'; |
||||||
|
import ZapButton from '../zaps/ZapButton.svelte'; |
||||||
|
import ZapReceipt from '../zaps/ZapReceipt.svelte'; |
||||||
|
import { nostrClient } from '../../services/nostr/nostr-client.js'; |
||||||
|
import { relayManager } from '../../services/nostr/relay-manager.js'; |
||||||
|
import { onMount } from 'svelte'; |
||||||
|
import type { NostrEvent } from '../../types/nostr.js'; |
||||||
|
|
||||||
|
interface Props { |
||||||
|
post: NostrEvent; |
||||||
|
parentEvent?: NostrEvent; // Optional parent event if already loaded |
||||||
|
onReply?: (post: NostrEvent) => void; |
||||||
|
} |
||||||
|
|
||||||
|
let { post, parentEvent: providedParentEvent, onReply }: Props = $props(); |
||||||
|
|
||||||
|
let loadedParentEvent = $state<NostrEvent | null>(null); |
||||||
|
let loadingParent = $state(false); |
||||||
|
let expanded = $state(false); |
||||||
|
let contentElement: HTMLElement | null = $state(null); |
||||||
|
let needsExpansion = $state(false); |
||||||
|
|
||||||
|
// Derive the effective parent event: prefer provided, fall back to loaded |
||||||
|
let parentEvent = $derived(providedParentEvent || loadedParentEvent); |
||||||
|
|
||||||
|
// Sync provided parent event changes and load if needed |
||||||
|
$effect(() => { |
||||||
|
if (providedParentEvent) { |
||||||
|
// If provided parent event is available, use it |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// If no provided parent and this is a reply, try to load it |
||||||
|
if (!loadedParentEvent && isReply()) { |
||||||
|
loadParentEvent(); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
onMount(async () => { |
||||||
|
// If parent not provided and this is a reply, try to load it |
||||||
|
if (!providedParentEvent && !loadedParentEvent && isReply()) { |
||||||
|
await loadParentEvent(); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
function getRelativeTime(): string { |
||||||
|
const now = Math.floor(Date.now() / 1000); |
||||||
|
const diff = now - post.created_at; |
||||||
|
const hours = Math.floor(diff / 3600); |
||||||
|
const days = Math.floor(diff / 86400); |
||||||
|
const minutes = Math.floor(diff / 60); |
||||||
|
|
||||||
|
if (days > 0) return `${days}d ago`; |
||||||
|
if (hours > 0) return `${hours}h ago`; |
||||||
|
if (minutes > 0) return `${minutes}m ago`; |
||||||
|
return 'just now'; |
||||||
|
} |
||||||
|
|
||||||
|
function getClientName(): string | null { |
||||||
|
const clientTag = post.tags.find((t) => t[0] === 'client'); |
||||||
|
return clientTag?.[1] || null; |
||||||
|
} |
||||||
|
|
||||||
|
function isReply(): boolean { |
||||||
|
// Check if this is a reply (has e tag pointing to another event) |
||||||
|
return post.tags.some((t) => t[0] === 'e' && t[1] !== post.id); |
||||||
|
} |
||||||
|
|
||||||
|
function getReplyEventId(): string | null { |
||||||
|
// Find the 'e' tag that's not the root (the direct parent) |
||||||
|
const replyTag = post.tags.find((t) => t[0] === 'e' && t[3] === 'reply'); |
||||||
|
if (replyTag) return replyTag[1]; |
||||||
|
|
||||||
|
// Fallback: find any 'e' tag that's not the root |
||||||
|
const rootId = getRootEventId(); |
||||||
|
const eTag = post.tags.find((t) => t[0] === 'e' && t[1] !== rootId && t[1] !== post.id); |
||||||
|
return eTag?.[1] || null; |
||||||
|
} |
||||||
|
|
||||||
|
function getRootEventId(): string | null { |
||||||
|
const rootTag = post.tags.find((t) => t[0] === 'root'); |
||||||
|
return rootTag?.[1] || null; |
||||||
|
} |
||||||
|
|
||||||
|
async function loadParentEvent() { |
||||||
|
const replyEventId = getReplyEventId(); |
||||||
|
if (!replyEventId || loadingParent) return; |
||||||
|
|
||||||
|
loadingParent = true; |
||||||
|
try { |
||||||
|
const relays = relayManager.getFeedReadRelays(); |
||||||
|
const events = await nostrClient.fetchEvents( |
||||||
|
[{ kinds: [1], ids: [replyEventId] }], |
||||||
|
relays, |
||||||
|
{ useCache: true, cacheResults: true } |
||||||
|
); |
||||||
|
|
||||||
|
if (events.length > 0) { |
||||||
|
loadedParentEvent = events[0]; |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
console.error('Error loading parent event:', error); |
||||||
|
} finally { |
||||||
|
loadingParent = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
$effect(() => { |
||||||
|
if (contentElement) { |
||||||
|
checkContentHeight(); |
||||||
|
// Use ResizeObserver to detect when content changes (e.g., images loading) |
||||||
|
const observer = new ResizeObserver(() => { |
||||||
|
checkContentHeight(); |
||||||
|
}); |
||||||
|
observer.observe(contentElement); |
||||||
|
return () => observer.disconnect(); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
function checkContentHeight() { |
||||||
|
if (contentElement) { |
||||||
|
// Use requestAnimationFrame to ensure DOM is fully updated |
||||||
|
requestAnimationFrame(() => { |
||||||
|
if (contentElement) { |
||||||
|
needsExpansion = contentElement.scrollHeight > 500; |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function toggleExpanded() { |
||||||
|
expanded = !expanded; |
||||||
|
} |
||||||
|
|
||||||
|
</script> |
||||||
|
|
||||||
|
<article class="Feed-post" data-post-id={post.id} id="event-{post.id}" data-event-id={post.id}> |
||||||
|
<div class="card-content" class:expanded bind:this={contentElement}> |
||||||
|
{#if isReply() && parentEvent} |
||||||
|
<ReplyContext {parentEvent} targetId="event-{parentEvent.id}" /> |
||||||
|
{/if} |
||||||
|
|
||||||
|
<div class="post-header flex items-center gap-2 mb-2"> |
||||||
|
<ProfileBadge pubkey={post.pubkey} /> |
||||||
|
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light">{getRelativeTime()}</span> |
||||||
|
{#if getClientName()} |
||||||
|
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light">via {getClientName()}</span> |
||||||
|
{/if} |
||||||
|
{#if isReply()} |
||||||
|
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light">↳ Reply</span> |
||||||
|
{/if} |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="post-content mb-2"> |
||||||
|
<MarkdownRenderer content={post.content} /> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="post-actions flex items-center gap-4"> |
||||||
|
<FeedReactionButtons event={post} /> |
||||||
|
<ZapButton event={post} /> |
||||||
|
<ZapReceipt eventId={post.id} pubkey={post.pubkey} /> |
||||||
|
{#if onReply} |
||||||
|
<button |
||||||
|
onclick={() => onReply(post)} |
||||||
|
class="text-xs text-fog-accent dark:text-fog-dark-accent hover:underline" |
||||||
|
> |
||||||
|
Reply |
||||||
|
</button> |
||||||
|
{/if} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{#if needsExpansion} |
||||||
|
<button |
||||||
|
onclick={toggleExpanded} |
||||||
|
class="show-more-button text-sm text-fog-accent dark:text-fog-dark-accent hover:underline mt-2" |
||||||
|
> |
||||||
|
{expanded ? 'Show less' : 'Show more'} |
||||||
|
</button> |
||||||
|
{/if} |
||||||
|
</article> |
||||||
|
|
||||||
|
<style> |
||||||
|
.Feed-post { |
||||||
|
padding: 1rem; |
||||||
|
margin-bottom: 1rem; |
||||||
|
background: var(--fog-post, #ffffff); |
||||||
|
border: 1px solid var(--fog-border, #e5e7eb); |
||||||
|
border-radius: 0.25rem; |
||||||
|
} |
||||||
|
|
||||||
|
:global(.dark) .Feed-post { |
||||||
|
background: var(--fog-dark-post, #1f2937); |
||||||
|
border-color: var(--fog-dark-border, #374151); |
||||||
|
} |
||||||
|
|
||||||
|
.post-content { |
||||||
|
line-height: 1.6; |
||||||
|
} |
||||||
|
|
||||||
|
.post-actions { |
||||||
|
padding-top: 0.5rem; |
||||||
|
border-top: 1px solid var(--fog-border, #e5e7eb); |
||||||
|
margin-top: 0.5rem; |
||||||
|
} |
||||||
|
|
||||||
|
:global(.dark) .post-actions { |
||||||
|
border-top-color: var(--fog-dark-border, #374151); |
||||||
|
} |
||||||
|
|
||||||
|
.card-content { |
||||||
|
max-height: 500px; |
||||||
|
overflow: hidden; |
||||||
|
transition: max-height 0.3s ease; |
||||||
|
} |
||||||
|
|
||||||
|
.card-content.expanded { |
||||||
|
max-height: none; |
||||||
|
} |
||||||
|
|
||||||
|
.show-more-button { |
||||||
|
width: 100%; |
||||||
|
text-align: center; |
||||||
|
padding: 0.5rem; |
||||||
|
background: transparent; |
||||||
|
border: none; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
|
||||||
|
</style> |
||||||
@ -1,121 +0,0 @@ |
|||||||
<script lang="ts"> |
|
||||||
import ProfileBadge from '../../components/layout/ProfileBadge.svelte'; |
|
||||||
import MarkdownRenderer from '../../components/content/MarkdownRenderer.svelte'; |
|
||||||
import Kind1ReactionButtons from '../reactions/Kind1ReactionButtons.svelte'; |
|
||||||
import ZapButton from '../zaps/ZapButton.svelte'; |
|
||||||
import ZapReceipt from '../zaps/ZapReceipt.svelte'; |
|
||||||
import type { NostrEvent } from '../../types/nostr.js'; |
|
||||||
|
|
||||||
interface Props { |
|
||||||
reply: NostrEvent; |
|
||||||
parentEvent?: NostrEvent; // The event this is replying to |
|
||||||
onReply?: (post: NostrEvent) => void; |
|
||||||
} |
|
||||||
|
|
||||||
let { reply, parentEvent, onReply }: Props = $props(); |
|
||||||
|
|
||||||
function getRelativeTime(): string { |
|
||||||
const now = Math.floor(Date.now() / 1000); |
|
||||||
const diff = now - reply.created_at; |
|
||||||
const hours = Math.floor(diff / 3600); |
|
||||||
const days = Math.floor(diff / 86400); |
|
||||||
const minutes = Math.floor(diff / 60); |
|
||||||
|
|
||||||
if (days > 0) return `${days}d ago`; |
|
||||||
if (hours > 0) return `${hours}h ago`; |
|
||||||
if (minutes > 0) return `${minutes}m ago`; |
|
||||||
return 'just now'; |
|
||||||
} |
|
||||||
|
|
||||||
function getClientName(): string | null { |
|
||||||
const clientTag = reply.tags.find((t) => t[0] === 'client'); |
|
||||||
return clientTag?.[1] || null; |
|
||||||
} |
|
||||||
|
|
||||||
function getParentPreview(): string { |
|
||||||
if (parentEvent) { |
|
||||||
return parentEvent.content.slice(0, 100) + (parentEvent.content.length > 100 ? '...' : ''); |
|
||||||
} |
|
||||||
// Try to extract from reply tags |
|
||||||
const eTag = reply.tags.find((t) => t[0] === 'e' && t[3] === 'reply'); |
|
||||||
if (eTag) { |
|
||||||
return 'Replying to...'; |
|
||||||
} |
|
||||||
return ''; |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<article class="kind1-reply"> |
|
||||||
{#if parentEvent} |
|
||||||
<div class="reply-context mb-2 p-2 bg-fog-highlight dark:bg-fog-dark-highlight rounded text-xs text-fog-text-light dark:text-fog-dark-text-light"> |
|
||||||
<span class="font-semibold">Replying to:</span> {getParentPreview()} |
|
||||||
</div> |
|
||||||
{/if} |
|
||||||
|
|
||||||
<div class="reply-header flex items-center gap-2 mb-2"> |
|
||||||
<ProfileBadge pubkey={reply.pubkey} /> |
|
||||||
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light">{getRelativeTime()}</span> |
|
||||||
{#if getClientName()} |
|
||||||
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light">via {getClientName()}</span> |
|
||||||
{/if} |
|
||||||
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light">↳ Reply</span> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="reply-content mb-2"> |
|
||||||
<MarkdownRenderer content={reply.content} /> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="reply-actions flex items-center gap-4"> |
|
||||||
<Kind1ReactionButtons event={reply} /> |
|
||||||
<ZapButton event={reply} /> |
|
||||||
<ZapReceipt eventId={reply.id} pubkey={reply.pubkey} /> |
|
||||||
{#if onReply} |
|
||||||
<button |
|
||||||
onclick={() => onReply(reply)} |
|
||||||
class="text-xs text-fog-accent dark:text-fog-dark-accent hover:underline" |
|
||||||
> |
|
||||||
Reply |
|
||||||
</button> |
|
||||||
{/if} |
|
||||||
</div> |
|
||||||
</article> |
|
||||||
|
|
||||||
<style> |
|
||||||
.kind1-reply { |
|
||||||
padding: 1rem; |
|
||||||
margin-bottom: 1rem; |
|
||||||
background: var(--fog-post, #ffffff); |
|
||||||
border: 1px solid var(--fog-border, #e5e7eb); |
|
||||||
border-radius: 0.25rem; |
|
||||||
border-left: 3px solid var(--fog-accent, #64748b); |
|
||||||
} |
|
||||||
|
|
||||||
:global(.dark) .kind1-reply { |
|
||||||
background: var(--fog-dark-post, #1f2937); |
|
||||||
border-color: var(--fog-dark-border, #374151); |
|
||||||
border-left-color: var(--fog-dark-accent, #64748b); |
|
||||||
} |
|
||||||
|
|
||||||
.reply-content { |
|
||||||
line-height: 1.6; |
|
||||||
} |
|
||||||
|
|
||||||
.reply-actions { |
|
||||||
padding-top: 0.5rem; |
|
||||||
border-top: 1px solid var(--fog-border, #e5e7eb); |
|
||||||
margin-top: 0.5rem; |
|
||||||
} |
|
||||||
|
|
||||||
:global(.dark) .reply-actions { |
|
||||||
border-top-color: var(--fog-dark-border, #374151); |
|
||||||
} |
|
||||||
|
|
||||||
.reply-context { |
|
||||||
cursor: pointer; |
|
||||||
transition: opacity 0.2s; |
|
||||||
} |
|
||||||
|
|
||||||
.reply-context:hover { |
|
||||||
opacity: 0.8; |
|
||||||
} |
|
||||||
</style> |
|
||||||
@ -0,0 +1,181 @@ |
|||||||
|
<script lang="ts"> |
||||||
|
import ProfileBadge from '../../components/layout/ProfileBadge.svelte'; |
||||||
|
import MarkdownRenderer from '../../components/content/MarkdownRenderer.svelte'; |
||||||
|
import FeedReactionButtons from '../reactions/FeedReactionButtons.svelte'; |
||||||
|
import ZapButton from '../zaps/ZapButton.svelte'; |
||||||
|
import ZapReceipt from '../zaps/ZapReceipt.svelte'; |
||||||
|
import type { NostrEvent } from '../../types/nostr.js'; |
||||||
|
|
||||||
|
interface Props { |
||||||
|
reply: NostrEvent; |
||||||
|
parentEvent?: NostrEvent; // The event this is replying to |
||||||
|
onReply?: (post: NostrEvent) => void; |
||||||
|
} |
||||||
|
|
||||||
|
let { reply, parentEvent, onReply }: Props = $props(); |
||||||
|
let expanded = $state(false); |
||||||
|
let contentElement: HTMLElement | null = $state(null); |
||||||
|
let needsExpansion = $state(false); |
||||||
|
|
||||||
|
function getRelativeTime(): string { |
||||||
|
const now = Math.floor(Date.now() / 1000); |
||||||
|
const diff = now - reply.created_at; |
||||||
|
const hours = Math.floor(diff / 3600); |
||||||
|
const days = Math.floor(diff / 86400); |
||||||
|
const minutes = Math.floor(diff / 60); |
||||||
|
|
||||||
|
if (days > 0) return `${days}d ago`; |
||||||
|
if (hours > 0) return `${hours}h ago`; |
||||||
|
if (minutes > 0) return `${minutes}m ago`; |
||||||
|
return 'just now'; |
||||||
|
} |
||||||
|
|
||||||
|
function getClientName(): string | null { |
||||||
|
const clientTag = reply.tags.find((t) => t[0] === 'client'); |
||||||
|
return clientTag?.[1] || null; |
||||||
|
} |
||||||
|
|
||||||
|
function getParentPreview(): string { |
||||||
|
if (parentEvent) { |
||||||
|
return parentEvent.content.slice(0, 100) + (parentEvent.content.length > 100 ? '...' : ''); |
||||||
|
} |
||||||
|
// Try to extract from reply tags |
||||||
|
const eTag = reply.tags.find((t) => t[0] === 'e' && t[3] === 'reply'); |
||||||
|
if (eTag) { |
||||||
|
return 'Replying to...'; |
||||||
|
} |
||||||
|
return ''; |
||||||
|
} |
||||||
|
|
||||||
|
$effect(() => { |
||||||
|
if (contentElement) { |
||||||
|
checkContentHeight(); |
||||||
|
// Use ResizeObserver to detect when content changes (e.g., images loading) |
||||||
|
const observer = new ResizeObserver(() => { |
||||||
|
checkContentHeight(); |
||||||
|
}); |
||||||
|
observer.observe(contentElement); |
||||||
|
return () => observer.disconnect(); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
function checkContentHeight() { |
||||||
|
if (contentElement) { |
||||||
|
// Use requestAnimationFrame to ensure DOM is fully updated |
||||||
|
requestAnimationFrame(() => { |
||||||
|
if (contentElement) { |
||||||
|
needsExpansion = contentElement.scrollHeight > 500; |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function toggleExpanded() { |
||||||
|
expanded = !expanded; |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<article class="Feed-reply"> |
||||||
|
<div class="card-content" class:expanded bind:this={contentElement}> |
||||||
|
{#if parentEvent} |
||||||
|
<div class="reply-context mb-2 p-2 bg-fog-highlight dark:bg-fog-dark-highlight rounded text-xs text-fog-text-light dark:text-fog-dark-text-light"> |
||||||
|
<span class="font-semibold">Replying to:</span> {getParentPreview()} |
||||||
|
</div> |
||||||
|
{/if} |
||||||
|
|
||||||
|
<div class="reply-header flex items-center gap-2 mb-2"> |
||||||
|
<ProfileBadge pubkey={reply.pubkey} /> |
||||||
|
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light">{getRelativeTime()}</span> |
||||||
|
{#if getClientName()} |
||||||
|
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light">via {getClientName()}</span> |
||||||
|
{/if} |
||||||
|
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light">↳ Reply</span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="reply-content mb-2"> |
||||||
|
<MarkdownRenderer content={reply.content} /> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="reply-actions flex items-center gap-4"> |
||||||
|
<FeedReactionButtons event={reply} /> |
||||||
|
<ZapButton event={reply} /> |
||||||
|
<ZapReceipt eventId={reply.id} pubkey={reply.pubkey} /> |
||||||
|
{#if onReply} |
||||||
|
<button |
||||||
|
onclick={() => onReply(reply)} |
||||||
|
class="text-xs text-fog-accent dark:text-fog-dark-accent hover:underline" |
||||||
|
> |
||||||
|
Reply |
||||||
|
</button> |
||||||
|
{/if} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{#if needsExpansion} |
||||||
|
<button |
||||||
|
onclick={toggleExpanded} |
||||||
|
class="show-more-button text-sm text-fog-accent dark:text-fog-dark-accent hover:underline mt-2" |
||||||
|
> |
||||||
|
{expanded ? 'Show less' : 'Show more'} |
||||||
|
</button> |
||||||
|
{/if} |
||||||
|
</article> |
||||||
|
|
||||||
|
<style> |
||||||
|
.Feed-reply { |
||||||
|
padding: 1rem; |
||||||
|
margin-bottom: 1rem; |
||||||
|
background: var(--fog-post, #ffffff); |
||||||
|
border: 1px solid var(--fog-border, #e5e7eb); |
||||||
|
border-radius: 0.25rem; |
||||||
|
border-left: 3px solid var(--fog-accent, #64748b); |
||||||
|
} |
||||||
|
|
||||||
|
:global(.dark) .Feed-reply { |
||||||
|
background: var(--fog-dark-post, #1f2937); |
||||||
|
border-color: var(--fog-dark-border, #374151); |
||||||
|
border-left-color: var(--fog-dark-accent, #64748b); |
||||||
|
} |
||||||
|
|
||||||
|
.reply-content { |
||||||
|
line-height: 1.6; |
||||||
|
} |
||||||
|
|
||||||
|
.reply-actions { |
||||||
|
padding-top: 0.5rem; |
||||||
|
border-top: 1px solid var(--fog-border, #e5e7eb); |
||||||
|
margin-top: 0.5rem; |
||||||
|
} |
||||||
|
|
||||||
|
:global(.dark) .reply-actions { |
||||||
|
border-top-color: var(--fog-dark-border, #374151); |
||||||
|
} |
||||||
|
|
||||||
|
.reply-context { |
||||||
|
cursor: pointer; |
||||||
|
transition: opacity 0.2s; |
||||||
|
} |
||||||
|
|
||||||
|
.reply-context:hover { |
||||||
|
opacity: 0.8; |
||||||
|
} |
||||||
|
|
||||||
|
.card-content { |
||||||
|
max-height: 500px; |
||||||
|
overflow: hidden; |
||||||
|
transition: max-height 0.3s ease; |
||||||
|
} |
||||||
|
|
||||||
|
.card-content.expanded { |
||||||
|
max-height: none; |
||||||
|
} |
||||||
|
|
||||||
|
.show-more-button { |
||||||
|
width: 100%; |
||||||
|
text-align: center; |
||||||
|
padding: 0.5rem; |
||||||
|
background: transparent; |
||||||
|
border: none; |
||||||
|
cursor: pointer; |
||||||
|
} |
||||||
|
</style> |
||||||
Loading…
Reference in new issue