Browse Source

bug-fixes

master
Silberengel 1 month ago
parent
commit
17954ef4b7
  1. 47
      .dockerignore
  2. 22
      Dockerfile
  3. 11
      docker-compose.yml
  4. 17
      docker-entrypoint.sh
  5. 1
      httpd.conf.template
  6. 6
      src/app.css
  7. 21
      src/lib/components/layout/ProfileBadge.svelte
  8. 31
      src/lib/modules/comments/Comment.svelte
  9. 7
      src/lib/modules/comments/CommentThread.svelte
  10. 35
      src/lib/modules/discussions/DiscussionCard.svelte
  11. 7
      src/lib/modules/discussions/DiscussionList.svelte
  12. 7
      src/lib/modules/discussions/DiscussionView.svelte
  13. 131
      src/lib/modules/feed/FeedPost.svelte
  14. 16
      src/lib/modules/feed/HighlightCard.svelte
  15. 1
      src/lib/services/nostr/config.ts
  16. 6
      src/routes/discussions/+page.svelte
  17. 29
      src/routes/feed/+page.svelte
  18. 24
      src/routes/topics/+page.svelte
  19. 24
      src/routes/topics/[name]/+page.svelte

47
.dockerignore

@ -0,0 +1,47 @@
# Dependencies
node_modules
npm-debug.log
# Note: package-lock.json is needed for npm ci, so don't exclude it
# Build outputs
build
.svelte-kit
package
# Development files
.env
.env.*
!.env.example
# Git
.git
.gitignore
.gitattributes
# IDE
.vscode
.idea
*.swp
*.swo
*~
# Documentation
README.md
README_SETUP.md
*.md
# CI/CD
.github
.gitlab-ci.yml
# Test files
**/*.test.ts
**/*.test.js
**/*.spec.ts
**/*.spec.js
coverage
# Misc
.DS_Store
*.log
*.tmp

22
Dockerfile

@ -4,14 +4,12 @@ WORKDIR /app
COPY package*.json ./ COPY package*.json ./
RUN npm ci RUN npm ci
COPY . . COPY . .
# Optional build args - will use defaults from config.ts if not provided
# If ARG is not provided, ENV will be empty and config.ts will use defaults
ARG VITE_DEFAULT_RELAYS ARG VITE_DEFAULT_RELAYS
ARG VITE_ZAP_THRESHOLD
ARG VITE_THREAD_TIMEOUT_DAYS ARG VITE_THREAD_TIMEOUT_DAYS
ARG VITE_PWA_ENABLED
ENV VITE_DEFAULT_RELAYS=${VITE_DEFAULT_RELAYS} ENV VITE_DEFAULT_RELAYS=${VITE_DEFAULT_RELAYS}
ENV VITE_ZAP_THRESHOLD=${VITE_ZAP_THRESHOLD}
ENV VITE_THREAD_TIMEOUT_DAYS=${VITE_THREAD_TIMEOUT_DAYS} ENV VITE_THREAD_TIMEOUT_DAYS=${VITE_THREAD_TIMEOUT_DAYS}
ENV VITE_PWA_ENABLED=${VITE_PWA_ENABLED}
RUN npm run build RUN npm run build
FROM httpd:alpine FROM httpd:alpine
@ -19,8 +17,20 @@ RUN apk add --no-cache gettext && \
mkdir -p /usr/local/apache2/logs && \ mkdir -p /usr/local/apache2/logs && \
chown -R daemon:daemon /usr/local/apache2/logs chown -R daemon:daemon /usr/local/apache2/logs
COPY --from=builder /app/build /usr/local/apache2/htdocs/ COPY --from=builder /app/build /usr/local/apache2/htdocs/
# Ensure healthz.json exists (copy from public if not in build, or create if missing) # Ensure healthz.json exists (SvelteKit copies public/healthz.json to build/)
COPY --from=builder /app/public/healthz.json /usr/local/apache2/htdocs/healthz.json # If it doesn't exist for some reason, create a default one
RUN if [ ! -f /usr/local/apache2/htdocs/healthz.json ]; then \
echo '{"status":"ok","service":"aitherboard","version":"unknown","buildTime":"'$(date -Iseconds)'","timestamp":'$(date +%s)'}' > /usr/local/apache2/htdocs/healthz.json && \
echo "Created default healthz.json"; \
else \
echo "healthz.json found in build output"; \
fi
# Verify 200.html exists (required for SPA routing)
RUN if [ ! -f /usr/local/apache2/htdocs/200.html ]; then \
echo "ERROR: 200.html not found! SPA routing will not work." && exit 1; \
else \
echo "200.html found - SPA routing configured correctly"; \
fi
COPY httpd.conf.template /usr/local/apache2/conf/httpd.conf.template COPY httpd.conf.template /usr/local/apache2/conf/httpd.conf.template
COPY docker-entrypoint.sh /usr/local/bin/ COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh RUN chmod +x /usr/local/bin/docker-entrypoint.sh

11
docker-compose.yml

@ -1,12 +1,13 @@
services: services:
aitherboard: aitherboard:
container_name: aitherboard
build: build:
context: . context: .
args: # Optional: override defaults from config.ts if needed
VITE_DEFAULT_RELAYS: "wss://theforest.nostr1.com,wss://nostr21.com,wss://nostr.land,wss://orly-relay.imwald.eu" # Uncomment and modify if you want custom values:
VITE_ZAP_THRESHOLD: "1" # args:
VITE_THREAD_TIMEOUT_DAYS: "30" # VITE_DEFAULT_RELAYS: "wss://theforest.nostr1.com,wss://nostr21.com,wss://nostr.land"
VITE_PWA_ENABLED: "true" # VITE_THREAD_TIMEOUT_DAYS: "30"
ports: ports:
- "9876:9876" - "9876:9876"
environment: environment:

17
docker-entrypoint.sh

@ -22,10 +22,21 @@ ls -la /usr/local/apache2/htdocs/ | head -20
echo "File count: $(find /usr/local/apache2/htdocs -type f | wc -l)" echo "File count: $(find /usr/local/apache2/htdocs -type f | wc -l)"
echo "Checking if port $PORT is available..." echo "Checking if port $PORT is available..."
if ! netstat -tuln 2>/dev/null | grep -q ":$PORT "; then # Use ss (socket statistics) which is available in Alpine, fallback to netstat if available
echo "Port $PORT appears to be available" if command -v ss >/dev/null 2>&1; then
if ! ss -tuln 2>/dev/null | grep -q ":$PORT "; then
echo "Port $PORT appears to be available"
else
echo "WARNING: Port $PORT might be in use"
fi
elif command -v netstat >/dev/null 2>&1; then
if ! netstat -tuln 2>/dev/null | grep -q ":$PORT "; then
echo "Port $PORT appears to be available"
else
echo "WARNING: Port $PORT might be in use"
fi
else else
echo "WARNING: Port $PORT might be in use" echo "Port check skipped (ss/netstat not available)"
fi fi
echo "Starting Apache on port $PORT..." echo "Starting Apache on port $PORT..."

1
httpd.conf.template

@ -6,6 +6,7 @@ LoadModule log_config_module modules/mod_log_config.so
LoadModule unixd_module modules/mod_unixd.so LoadModule unixd_module modules/mod_unixd.so
LoadModule dir_module modules/mod_dir.so LoadModule dir_module modules/mod_dir.so
LoadModule deflate_module modules/mod_deflate.so LoadModule deflate_module modules/mod_deflate.so
LoadModule setenvif_module modules/mod_setenvif.so
PidFile "/usr/local/apache2/logs/httpd.pid" PidFile "/usr/local/apache2/logs/httpd.pid"
ErrorLog "/proc/self/fd/2" ErrorLog "/proc/self/fd/2"

6
src/app.css

@ -13,15 +13,15 @@
/* Base text size preferences - will be overridden by media queries if not specified */ /* Base text size preferences - will be overridden by media queries if not specified */
[data-text-size='small'] { [data-text-size='small'] {
--text-size: 10px; --text-size: 6px;
} }
[data-text-size='medium'] { [data-text-size='medium'] {
--text-size: 12px; --text-size: 8px;
} }
[data-text-size='large'] { [data-text-size='large'] {
--text-size: 14px; --text-size: 10px;
} }
[data-line-spacing='tight'] { [data-line-spacing='tight'] {

21
src/lib/components/layout/ProfileBadge.svelte

@ -172,8 +172,8 @@
}} }}
onload={(e) => { onload={(e) => {
// If compressed URL fails, try original as fallback // If compressed URL fails, try original as fallback
if (imageError && compressedPictureUrl !== profile.picture) { if (imageError && profile?.picture && compressedPictureUrl !== profile.picture) {
const img = e.currentTarget; const img = e.currentTarget as HTMLImageElement;
img.src = profile.picture; img.src = profile.picture;
imageError = false; imageError = false;
} }
@ -192,12 +192,12 @@
{/if} {/if}
{#if !pictureOnly} {#if !pictureOnly}
<div class="flex flex-col min-w-0 flex-1 max-w-full"> <div class="flex flex-col min-w-0 flex-1 max-w-full">
<div class="flex items-center gap-2 flex-wrap min-w-0"> <div class="flex items-center gap-2 flex-wrap min-w-0 max-w-full">
<span class="truncate min-w-0 max-w-full"> <span class="truncate min-w-0 max-w-full">
{profile?.name || shortenedNpub} {profile?.name || shortenedNpub}
</span> </span>
{#if profile?.nip05 && profile.nip05.length > 0} {#if profile?.nip05 && profile.nip05.length > 0}
<span class="nip05-text text-fog-text-light dark:text-fog-dark-text-light min-w-0 break-all"> <span class="nip05-text text-fog-text-light dark:text-fog-dark-text-light min-w-0 max-w-full">
{profile.nip05[0]} {profile.nip05[0]}
</span> </span>
{/if} {/if}
@ -268,5 +268,18 @@
word-break: break-word; word-break: break-word;
overflow-wrap: break-word; overflow-wrap: break-word;
max-width: 100%; max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640px) {
.profile-badge {
max-width: 100%;
}
.nip05-text {
max-width: 100%;
word-break: break-all;
}
} }
</style> </style>

31
src/lib/modules/comments/Comment.svelte

@ -116,13 +116,17 @@
<ReferencedEventPreview event={comment} /> <ReferencedEventPreview event={comment} />
{/if} {/if}
<div class="comment-header flex items-center gap-2 mb-2"> <div class="comment-header flex items-center gap-2 mb-2 min-w-0">
<ProfileBadge pubkey={comment.pubkey} /> <div class="flex items-center gap-2 flex-1 min-w-0">
<span class="text-fog-text-light dark:text-fog-dark-text-light whitespace-nowrap" style="font-size: 0.75em;">{getRelativeTime()}</span> <div class="flex-shrink-0">
{#if getClientName()} <ProfileBadge pubkey={comment.pubkey} />
<span class="text-fog-text-light dark:text-fog-dark-text-light whitespace-nowrap" style="font-size: 0.75em;">via {getClientName()}</span> </div>
{/if} <span class="text-fog-text-light dark:text-fog-dark-text-light whitespace-nowrap flex-shrink-0" style="font-size: 0.75em;">{getRelativeTime()}</span>
<div class="ml-auto flex items-center gap-2 comment-header-actions"> {#if getClientName()}
<span class="text-fog-text-light dark:text-fog-dark-text-light whitespace-nowrap flex-shrink-0" style="font-size: 0.75em;">via {getClientName()}</span>
{/if}
</div>
<div class="flex items-center gap-2 comment-header-actions flex-shrink-0">
<IconButton <IconButton
icon="eye" icon="eye"
label="View" label="View"
@ -202,6 +206,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) {
.comment {
padding: 0.75rem;
}
} }
:global(.dark) .comment { :global(.dark) .comment {
@ -272,13 +283,9 @@
gap: 0.5rem; gap: 0.5rem;
} }
.comment-header .ml-auto {
margin-left: auto;
flex-shrink: 0;
}
.comment-header-actions { .comment-header-actions {
gap: 0.375rem; gap: 0.375rem;
flex-wrap: wrap;
} }
} }

7
src/lib/modules/comments/CommentThread.svelte

@ -815,6 +815,13 @@
padding: 1rem; padding: 1rem;
} }
@media (max-width: 768px) {
.comment-thread {
max-width: 100%;
padding: 0.5rem;
}
}
.comments-list { .comments-list {
margin-bottom: 2rem; margin-bottom: 2rem;
} }

35
src/lib/modules/discussions/DiscussionCard.svelte

@ -190,11 +190,11 @@
{#if !fullView} {#if !fullView}
<a href="/event/{thread.id}" class="card-link"> <a href="/event/{thread.id}" class="card-link">
<div class="card-content" class:expanded={expanded} bind:this={contentElement}> <div class="card-content" class:expanded={expanded} bind:this={contentElement}>
<div class="flex justify-between items-start mb-2"> <div class="flex justify-between items-start mb-2 gap-2">
<h3 class="font-semibold text-fog-text dark:text-fog-dark-text"> <h3 class="font-semibold text-fog-text dark:text-fog-dark-text flex-1 min-w-0 overflow-hidden">
{getTitle()} {getTitle()}
</h3> </h3>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2 flex-shrink-0">
<span class="text-fog-text-light dark:text-fog-dark-text-light whitespace-nowrap" style="font-size: 0.875em;">{getRelativeTime()}</span> <span class="text-fog-text-light dark:text-fog-dark-text-light whitespace-nowrap" style="font-size: 0.875em;">{getRelativeTime()}</span>
<IconButton <IconButton
icon="eye" icon="eye"
@ -317,16 +317,7 @@
<!-- Card footer (stats) - always visible, outside collapsible content --> <!-- Card footer (stats) - always visible, outside collapsible content -->
<div class="flex items-center justify-between text-fog-text dark:text-fog-dark-text thread-stats mt-2" style="font-size: 0.75em;"> <div class="flex items-center justify-between text-fog-text dark:text-fog-dark-text thread-stats mt-2" style="font-size: 0.75em;">
<div class="flex items-center gap-4 flex-wrap"> <div class="flex items-center gap-4 flex-wrap">
{#if fullView} <DiscussionVoteButtons event={thread} />
<DiscussionVoteButtons event={thread} />
{:else}
<VoteCount
upvotes={providedUpvotes}
downvotes={providedDownvotes}
votesCalculated={providedVotesCalculated}
size="xs"
/>
{/if}
{#if !fullView} {#if !fullView}
{#if loadingStats} {#if loadingStats}
<span class="text-fog-text-light dark:text-fog-dark-text-light">Loading stats...</span> <span class="text-fog-text-light dark:text-fog-dark-text-light">Loading stats...</span>
@ -373,6 +364,19 @@
.thread-card { .thread-card {
max-width: var(--content-width); max-width: var(--content-width);
position: relative; position: relative;
overflow: hidden;
}
@media (max-width: 768px) {
.thread-card {
max-width: 100%;
}
}
@media (max-width: 640px) {
.thread-card {
padding: 0.75rem;
}
} }
.card-link { .card-link {
@ -470,6 +474,11 @@
.thread-stats { .thread-stats {
margin-bottom: 0.25rem; /* Decreased space between count row and kind badge */ margin-bottom: 0.25rem; /* Decreased space between count row and kind badge */
} }
h3 {
word-break: break-word;
overflow-wrap: break-word;
}
} }
:global(.dark) .kind-badge { :global(.dark) .kind-badge {

7
src/lib/modules/discussions/DiscussionList.svelte

@ -640,6 +640,13 @@
padding: 1rem; padding: 1rem;
} }
@media (max-width: 768px) {
.thread-list {
max-width: 100%;
padding: 0.5rem;
}
}
.thread-wrapper { .thread-wrapper {
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s;

7
src/lib/modules/discussions/DiscussionView.svelte

@ -154,6 +154,13 @@
padding: 1rem; padding: 1rem;
} }
@media (max-width: 768px) {
.thread-view {
max-width: 100%;
padding: 0.5rem;
}
}
.op-section { .op-section {
margin-bottom: 2rem; margin-bottom: 2rem;
padding-bottom: 1rem; padding-bottom: 1rem;

131
src/lib/modules/feed/FeedPost.svelte

@ -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;

16
src/lib/modules/feed/HighlightCard.svelte

@ -2,6 +2,7 @@
import ProfileBadge from '../../components/layout/ProfileBadge.svelte'; import ProfileBadge from '../../components/layout/ProfileBadge.svelte';
import MarkdownRenderer from '../../components/content/MarkdownRenderer.svelte'; import MarkdownRenderer from '../../components/content/MarkdownRenderer.svelte';
import EventMenu from '../../components/EventMenu.svelte'; import EventMenu from '../../components/EventMenu.svelte';
import FeedReactionButtons from '../reactions/FeedReactionButtons.svelte';
import { nostrClient } from '../../services/nostr/nostr-client.js'; import { nostrClient } from '../../services/nostr/nostr-client.js';
import { relayManager } from '../../services/nostr/relay-manager.js'; import { relayManager } from '../../services/nostr/relay-manager.js';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
@ -376,6 +377,10 @@
</div> </div>
{/if} {/if}
<div class="highlight-actions">
<FeedReactionButtons event={highlight} />
</div>
<div class="kind-badge"> <div class="kind-badge">
<span class="kind-number">{getKindInfo(highlight.kind).number}</span> <span class="kind-number">{getKindInfo(highlight.kind).number}</span>
<span class="kind-description">{getKindInfo(highlight.kind).description}</span> <span class="kind-description">{getKindInfo(highlight.kind).description}</span>
@ -431,6 +436,17 @@
border-top-color: var(--fog-dark-border, #374151); border-top-color: var(--fog-dark-border, #374151);
} }
.highlight-actions {
padding-top: 0.5rem;
padding-right: 6rem; /* Reserve space for kind badge */
border-top: 1px solid var(--fog-border, #e5e7eb);
margin-top: 0.5rem;
}
:global(.dark) .highlight-actions {
border-top-color: var(--fog-dark-border, #374151);
}
.source-link { .source-link {
color: var(--fog-accent, #64748b); color: var(--fog-accent, #64748b);
text-decoration: none; text-decoration: none;

1
src/lib/services/nostr/config.ts

@ -14,7 +14,6 @@ const PROFILE_RELAYS = [
'wss://aggr.nostr.land', 'wss://aggr.nostr.land',
'wss://profiles.nostr1.com', 'wss://profiles.nostr1.com',
'wss://relay.primal.net', 'wss://relay.primal.net',
'wss://orly-relay.imwald.eu',
'wss://nostr.wine', 'wss://nostr.wine',
'wss://nostr21.com' 'wss://nostr21.com'
]; ];

6
src/routes/discussions/+page.svelte

@ -129,6 +129,12 @@
margin: 0 auto; margin: 0 auto;
} }
@media (max-width: 768px) {
.discussions-content {
max-width: 100%;
}
}
.discussions-header-sticky { .discussions-header-sticky {
padding: 0 1rem; padding: 0 1rem;
padding-top: 1rem; padding-top: 1rem;

29
src/routes/feed/+page.svelte

@ -166,8 +166,29 @@
gap: 1rem; gap: 1rem;
} }
@media (max-width: 640px) {
.feed-controls {
flex-direction: column;
align-items: stretch;
gap: 0.75rem;
}
}
.search-section { .search-section {
flex: 1; flex: 1;
min-width: 0;
}
@media (max-width: 640px) {
.search-section {
width: 100%;
flex: none;
}
.search-section :global(.unified-search-container) {
max-width: 100%;
width: 100%;
}
} }
.feed-header-buttons { .feed-header-buttons {
@ -175,6 +196,14 @@
gap: 0.5rem; gap: 0.5rem;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
flex-shrink: 0;
}
@media (max-width: 640px) {
.feed-header-buttons {
width: 100%;
justify-content: flex-start;
}
} }
.see-new-events-btn-header { .see-new-events-btn-header {

24
src/routes/topics/+page.svelte

@ -336,6 +336,14 @@
max-width: var(--content-width); max-width: var(--content-width);
margin: 0 auto; margin: 0 auto;
padding: 0 1rem; padding: 0 1rem;
overflow: hidden;
}
@media (max-width: 768px) {
.topics-page {
max-width: 100%;
padding: 0 0.5rem;
}
} }
.filter-section { .filter-section {
@ -351,6 +359,14 @@
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 0.75rem; gap: 0.75rem;
overflow: hidden;
}
@media (max-width: 640px) {
.topics-list {
grid-template-columns: 1fr;
gap: 0.5rem;
}
} }
.topics-sentinel { .topics-sentinel {
@ -376,6 +392,14 @@
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
font-family: monospace; font-family: monospace;
overflow: hidden;
min-width: 0;
}
@media (max-width: 640px) {
.topic-item {
padding: 0.5rem 0.75rem;
}
} }
:global(.dark) .topic-item { :global(.dark) .topic-item {

24
src/routes/topics/[name]/+page.svelte

@ -230,12 +230,27 @@
.topic-content { .topic-content {
max-width: var(--content-width); max-width: var(--content-width);
margin: 0 auto; margin: 0 auto;
overflow: hidden;
}
@media (max-width: 768px) {
.topic-content {
max-width: 100%;
}
} }
.topic-header { .topic-header {
padding: 0 1rem; padding: 0 1rem;
border-bottom: 1px solid var(--fog-border, #e5e7eb); border-bottom: 1px solid var(--fog-border, #e5e7eb);
padding-bottom: 1rem; padding-bottom: 1rem;
margin-bottom: 1rem;
}
@media (max-width: 640px) {
.topic-header {
padding: 0 0.5rem;
padding-bottom: 0.75rem;
}
} }
:global(.dark) .topic-header { :global(.dark) .topic-header {
@ -252,6 +267,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
overflow: hidden;
} }
.event-item { .event-item {
@ -259,6 +275,14 @@
border: 1px solid var(--fog-border, #e5e7eb); border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.5rem; border-radius: 0.5rem;
background: var(--fog-post, #ffffff); background: var(--fog-post, #ffffff);
overflow: hidden;
}
@media (max-width: 640px) {
.event-item {
padding: 0.5rem;
margin: 0 0.5rem;
}
} }
:global(.dark) .event-item { :global(.dark) .event-item {

Loading…
Cancel
Save