|
|
|
@ -183,7 +183,7 @@ |
|
|
|
|
|
|
|
|
|
|
|
// Parse NIP-21 links and create segments for rendering |
|
|
|
// Parse NIP-21 links and create segments for rendering |
|
|
|
interface ContentSegment { |
|
|
|
interface ContentSegment { |
|
|
|
type: 'text' | 'profile' | 'event' | 'url' | 'wikilink' | 'hashtag'; |
|
|
|
type: 'text' | 'profile' | 'event' | 'url' | 'wikilink' | 'hashtag' | 'greentext'; |
|
|
|
content: string; // Display text (without nostr: prefix for links) |
|
|
|
content: string; // Display text (without nostr: prefix for links) |
|
|
|
pubkey?: string; // For profile badges |
|
|
|
pubkey?: string; // For profile badges |
|
|
|
eventId?: string; // For event links (bech32 or hex) |
|
|
|
eventId?: string; // For event links (bech32 or hex) |
|
|
|
@ -192,6 +192,43 @@ |
|
|
|
hashtag?: string; // For hashtag topic name |
|
|
|
hashtag?: string; // For hashtag topic name |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Process text to detect greentext (lines starting with >) |
|
|
|
|
|
|
|
function processGreentext(text: string): ContentSegment[] { |
|
|
|
|
|
|
|
const lines = text.split('\n'); |
|
|
|
|
|
|
|
const segments: ContentSegment[] = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < lines.length; i++) { |
|
|
|
|
|
|
|
const line = lines[i]; |
|
|
|
|
|
|
|
const trimmed = line.trimStart(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Check if line starts with > (greentext) |
|
|
|
|
|
|
|
if (trimmed.startsWith('>') && trimmed.length > 1) { |
|
|
|
|
|
|
|
// Preserve leading whitespace before > |
|
|
|
|
|
|
|
const leadingWhitespace = line.substring(0, line.length - trimmed.length); |
|
|
|
|
|
|
|
segments.push({ |
|
|
|
|
|
|
|
type: 'greentext', |
|
|
|
|
|
|
|
content: leadingWhitespace + trimmed |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
// Regular text line |
|
|
|
|
|
|
|
segments.push({ |
|
|
|
|
|
|
|
type: 'text', |
|
|
|
|
|
|
|
content: line |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add newline between lines (except for last line) |
|
|
|
|
|
|
|
if (i < lines.length - 1) { |
|
|
|
|
|
|
|
segments.push({ |
|
|
|
|
|
|
|
type: 'text', |
|
|
|
|
|
|
|
content: '\n' |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return segments; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function parseContentWithNIP21Links(): ContentSegment[] { |
|
|
|
function parseContentWithNIP21Links(): ContentSegment[] { |
|
|
|
const plaintext = getPlaintextContent(); |
|
|
|
const plaintext = getPlaintextContent(); |
|
|
|
const links = findNIP21Links(plaintext); |
|
|
|
const links = findNIP21Links(plaintext); |
|
|
|
@ -442,6 +479,20 @@ |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Process greentext on final text segments (only in feed view) |
|
|
|
|
|
|
|
if (!fullView && finalSegments.length > 0) { |
|
|
|
|
|
|
|
const processedSegments: ContentSegment[] = []; |
|
|
|
|
|
|
|
for (const segment of finalSegments) { |
|
|
|
|
|
|
|
if (segment.type === 'text') { |
|
|
|
|
|
|
|
const greentextSegments = processGreentext(segment.content); |
|
|
|
|
|
|
|
processedSegments.push(...greentextSegments); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
processedSegments.push(segment); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return processedSegments.length > 0 ? processedSegments : finalSegments; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return finalSegments.length > 0 ? finalSegments : segments; |
|
|
|
return finalSegments.length > 0 ? finalSegments : segments; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -782,13 +833,13 @@ |
|
|
|
|
|
|
|
|
|
|
|
{@const title = getTitle()} |
|
|
|
{@const title = getTitle()} |
|
|
|
{#if !hideTitle && title && title !== 'Untitled'} |
|
|
|
{#if !hideTitle && title && title !== 'Untitled'} |
|
|
|
<h2 class="post-title font-bold mb-4 text-fog-text dark:text-fog-dark-text" style="font-size: 1.5em;"> |
|
|
|
<h2 class="post-title font-bold mb-4 text-fog-text dark:text-fog-dark-text overflow-hidden" style="font-size: 1.5em;"> |
|
|
|
{title} |
|
|
|
{title} |
|
|
|
</h2> |
|
|
|
</h2> |
|
|
|
{/if} |
|
|
|
{/if} |
|
|
|
|
|
|
|
|
|
|
|
<div class="post-header flex items-center justify-between gap-2 mb-2"> |
|
|
|
<div class="post-header flex items-center justify-between gap-2 mb-2"> |
|
|
|
<div class="flex items-center gap-2 flex-nowrap flex-1 min-w-0"> |
|
|
|
<div class="flex items-center gap-2 flex-1 min-w-0 post-header-left"> |
|
|
|
<div class="flex-shrink-0"> |
|
|
|
<div class="flex-shrink-0"> |
|
|
|
<ProfileBadge pubkey={post.pubkey} /> |
|
|
|
<ProfileBadge pubkey={post.pubkey} /> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -848,7 +899,7 @@ |
|
|
|
{:else} |
|
|
|
{:else} |
|
|
|
<!-- Feed view: plaintext only, no profile pics, media as URLs --> |
|
|
|
<!-- Feed view: plaintext only, no profile pics, media as URLs --> |
|
|
|
<div class="post-header flex items-center justify-between gap-2 mb-2"> |
|
|
|
<div class="post-header flex items-center justify-between gap-2 mb-2"> |
|
|
|
<div class="flex items-center gap-2 flex-nowrap flex-1 min-w-0"> |
|
|
|
<div class="flex items-center gap-2 flex-1 min-w-0 post-header-left"> |
|
|
|
<div class="flex-shrink-0"> |
|
|
|
<div class="flex-shrink-0"> |
|
|
|
<ProfileBadge pubkey={post.pubkey} inline={true} /> |
|
|
|
<ProfileBadge pubkey={post.pubkey} inline={true} /> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -872,7 +923,7 @@ |
|
|
|
|
|
|
|
|
|
|
|
{@const title = getTitle()} |
|
|
|
{@const title = getTitle()} |
|
|
|
{#if title && title !== 'Untitled'} |
|
|
|
{#if title && title !== 'Untitled'} |
|
|
|
<h2 class="post-title font-bold mb-2 text-fog-text dark:text-fog-dark-text" style="font-size: 1.5em;"> |
|
|
|
<h2 class="post-title font-bold mb-2 text-fog-text dark:text-fog-dark-text overflow-hidden" style="font-size: 1.5em;"> |
|
|
|
{title} |
|
|
|
{title} |
|
|
|
</h2> |
|
|
|
</h2> |
|
|
|
{/if} |
|
|
|
{/if} |
|
|
|
@ -904,6 +955,8 @@ |
|
|
|
{:else} |
|
|
|
{:else} |
|
|
|
{segment.content} |
|
|
|
{segment.content} |
|
|
|
{/if} |
|
|
|
{/if} |
|
|
|
|
|
|
|
{:else if segment.type === 'greentext'} |
|
|
|
|
|
|
|
<span class="greentext">{segment.content}</span> |
|
|
|
{:else if segment.type === 'profile' && segment.pubkey} |
|
|
|
{:else if segment.type === 'profile' && segment.pubkey} |
|
|
|
<ProfileBadge pubkey={segment.pubkey} inline={true} /> |
|
|
|
<ProfileBadge pubkey={segment.pubkey} inline={true} /> |
|
|
|
{:else if segment.type === 'event' && segment.eventId} |
|
|
|
{:else if segment.type === 'event' && segment.eventId} |
|
|
|
@ -986,6 +1039,11 @@ |
|
|
|
{/if} |
|
|
|
{/if} |
|
|
|
|
|
|
|
|
|
|
|
{#if !fullView} |
|
|
|
{#if !fullView} |
|
|
|
|
|
|
|
<div class="feed-card-actions-section"> |
|
|
|
|
|
|
|
<div class="feed-card-reactions"> |
|
|
|
|
|
|
|
<FeedReactionButtons event={post} preloadedReactions={preloadedReactions} /> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
</div> |
|
|
|
<div class="feed-card-footer flex items-center justify-between"> |
|
|
|
<div class="feed-card-footer flex items-center justify-between"> |
|
|
|
<div class="feed-card-actions flex items-center gap-2"> |
|
|
|
<div class="feed-card-actions flex items-center gap-2"> |
|
|
|
{#if isLoggedIn && bookmarked} |
|
|
|
{#if isLoggedIn && bookmarked} |
|
|
|
@ -1051,6 +1109,13 @@ |
|
|
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
|
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
|
|
border-radius: 0.25rem; |
|
|
|
border-radius: 0.25rem; |
|
|
|
position: relative; |
|
|
|
position: relative; |
|
|
|
|
|
|
|
overflow: hidden; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 640px) { |
|
|
|
|
|
|
|
.Feed-post { |
|
|
|
|
|
|
|
padding: 0.75rem; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.Feed-post.collapsed { |
|
|
|
.Feed-post.collapsed { |
|
|
|
@ -1150,6 +1215,20 @@ |
|
|
|
border-top-color: var(--fog-dark-border, #374151); |
|
|
|
border-top-color: var(--fog-dark-border, #374151); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.feed-card-actions-section { |
|
|
|
|
|
|
|
margin-top: 0.5rem; |
|
|
|
|
|
|
|
padding-top: 0.5rem; |
|
|
|
|
|
|
|
border-top: 1px solid var(--fog-border, #e5e7eb); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:global(.dark) .feed-card-actions-section { |
|
|
|
|
|
|
|
border-top-color: var(--fog-dark-border, #374151); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.feed-card-reactions { |
|
|
|
|
|
|
|
margin-bottom: 0.5rem; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.feed-card-footer { |
|
|
|
.feed-card-footer { |
|
|
|
margin-top: 0.5rem; |
|
|
|
margin-top: 0.5rem; |
|
|
|
padding-top: 0.5rem; |
|
|
|
padding-top: 0.5rem; |
|
|
|
@ -1207,6 +1286,34 @@ |
|
|
|
align-items: center; |
|
|
|
align-items: center; |
|
|
|
line-height: 1.5; |
|
|
|
line-height: 1.5; |
|
|
|
position: relative; |
|
|
|
position: relative; |
|
|
|
|
|
|
|
gap: 0.5rem; |
|
|
|
|
|
|
|
min-width: 0; |
|
|
|
|
|
|
|
flex-wrap: wrap; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.post-header-left { |
|
|
|
|
|
|
|
min-width: 0; |
|
|
|
|
|
|
|
overflow: hidden; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 640px) { |
|
|
|
|
|
|
|
.post-header { |
|
|
|
|
|
|
|
flex-direction: column; |
|
|
|
|
|
|
|
align-items: flex-start; |
|
|
|
|
|
|
|
gap: 0.5rem; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.post-header-left { |
|
|
|
|
|
|
|
width: 100%; |
|
|
|
|
|
|
|
flex-wrap: wrap; |
|
|
|
|
|
|
|
gap: 0.5rem; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.post-header-actions { |
|
|
|
|
|
|
|
width: 100%; |
|
|
|
|
|
|
|
justify-content: flex-start; |
|
|
|
|
|
|
|
flex-wrap: wrap; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.post-header-divider { |
|
|
|
.post-header-divider { |
|
|
|
@ -1243,9 +1350,9 @@ |
|
|
|
flex-wrap: wrap; |
|
|
|
flex-wrap: wrap; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.post-header { |
|
|
|
.post-title { |
|
|
|
flex-wrap: wrap; |
|
|
|
word-break: break-word; |
|
|
|
gap: 0.5rem; |
|
|
|
overflow-wrap: break-word; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -1292,6 +1399,14 @@ |
|
|
|
background-color: rgba(255, 255, 0, 0.2); |
|
|
|
background-color: rgba(255, 255, 0, 0.2); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.greentext { |
|
|
|
|
|
|
|
color: #789922; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:global(.dark) .greentext { |
|
|
|
|
|
|
|
color: #8ab378; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/* Focusable wrapper for keyboard navigation */ |
|
|
|
/* Focusable wrapper for keyboard navigation */ |
|
|
|
div[role="button"] { |
|
|
|
div[role="button"] { |
|
|
|
outline: none; |
|
|
|
outline: none; |
|
|
|
|