Browse Source

more features

master
Silberengel 1 month ago
parent
commit
153b191f94
  1. 13
      src/app.css
  2. 12
      src/lib/components/content/MarkdownRenderer.svelte
  3. 2
      src/lib/components/content/MediaAttachments.svelte
  4. 27
      src/lib/components/layout/Header.svelte
  5. 194
      src/lib/components/modals/KeyboardShortcutsModal.svelte
  6. 17
      src/lib/components/modals/PublicationStatusModal.svelte
  7. 13
      src/lib/components/modals/SearchModal.svelte
  8. 71
      src/lib/components/preferences/UserPreferences.svelte
  9. 9
      src/lib/modules/comments/Comment.svelte
  10. 4
      src/lib/modules/comments/CommentForm.svelte
  11. 4
      src/lib/modules/feed/CreateKind1Form.svelte
  12. 4
      src/lib/modules/feed/Kind1FeedPage.svelte
  13. 4
      src/lib/modules/feed/Kind1Reply.svelte
  14. 4
      src/lib/modules/feed/ReplyToKind1Form.svelte
  15. 14
      src/lib/modules/reactions/Kind1ReactionButtons.svelte
  16. 14
      src/lib/modules/reactions/ReactionButtons.svelte
  17. 10
      src/lib/modules/threads/CreateThreadForm.svelte
  18. 35
      src/lib/modules/threads/ThreadCard.svelte
  19. 45
      src/lib/modules/threads/ThreadList.svelte
  20. 2
      src/lib/modules/zaps/ZapButton.svelte
  21. 24
      src/lib/modules/zaps/ZapInvoiceModal.svelte
  22. 2
      src/routes/+page.svelte
  23. 2
      src/routes/login/+page.svelte

13
src/app.css

@ -99,7 +99,7 @@ img[src*="profile" i] { @@ -99,7 +99,7 @@ img[src*="profile" i] {
filter: grayscale(100%) sepia(20%) hue-rotate(200deg) saturate(70%) brightness(0.9);
}
/* Emoji images - grayscale with blue tinge */
/* Emoji images - grayscale like profile pics */
.emoji,
[class*="emoji"],
img[alt*="emoji" i],
@ -115,6 +115,17 @@ img[src*="emoji" i] { @@ -115,6 +115,17 @@ img[src*="emoji" i] {
filter: grayscale(100%) sepia(20%) hue-rotate(200deg) saturate(70%) brightness(0.9);
}
/* Apply grayscale filter to reaction buttons containing emojis */
.reaction-btn,
.kind1-reaction-buttons button {
filter: grayscale(100%) sepia(15%) hue-rotate(200deg) saturate(60%) brightness(0.95);
}
.dark .reaction-btn,
.dark .kind1-reaction-buttons button {
filter: grayscale(100%) sepia(20%) hue-rotate(200deg) saturate(70%) brightness(0.9);
}
/* Apply to all images in markdown content */
.markdown-content img,
.anon-content img {

12
src/lib/components/content/MarkdownRenderer.svelte

@ -167,21 +167,21 @@ @@ -167,21 +167,21 @@
}
.markdown-content :global(.nostr-link) {
color: #3b82f6;
color: var(--fog-accent, #64748b);
text-decoration: underline;
cursor: pointer;
}
.dark .markdown-content :global(.nostr-link) {
color: #60a5fa;
:global(.dark) .markdown-content :global(.nostr-link) {
color: var(--fog-dark-accent, #64748b);
}
.markdown-content :global(.nostr-link:hover) {
color: #2563eb;
color: var(--fog-text, #475569);
}
.dark .markdown-content :global(.nostr-link:hover) {
color: #93c5fd;
:global(.dark) .markdown-content :global(.nostr-link:hover) {
color: var(--fog-dark-text, #cbd5e1);
}
.markdown-content :global(code) {

2
src/lib/components/content/MediaAttachments.svelte

@ -244,7 +244,7 @@ @@ -244,7 +244,7 @@
}
.file-link {
color: var(--fog-accent, #3b82f6);
color: var(--fog-accent, #64748b);
text-decoration: none;
display: inline-flex;
align-items: center;

27
src/lib/components/layout/Header.svelte

@ -3,6 +3,11 @@ @@ -3,6 +3,11 @@
import ThemeToggle from '../preferences/ThemeToggle.svelte';
import UserPreferences from '../preferences/UserPreferences.svelte';
import ProfileBadge from '../layout/ProfileBadge.svelte';
import KeyboardShortcutsModal from '../modals/KeyboardShortcutsModal.svelte';
import { keyboardShortcuts } from '../../services/keyboard-shortcuts.js';
import { onMount } from 'svelte';
let showShortcuts = $state(false);
let currentSession = $state<UserSession | null>(sessionManager.session.value);
let isLoggedIn = $derived(currentSession !== null);
@ -15,6 +20,18 @@ @@ -15,6 +20,18 @@
});
return unsubscribe;
});
onMount(() => {
// Register ? shortcut for keyboard shortcuts help
const unregister = keyboardShortcuts.register({
key: '?',
handler: () => {
showShortcuts = true;
},
description: 'Show keyboard shortcuts'
});
return unregister;
});
</script>
<header class="relative border-b border-fog-border dark:border-fog-dark-border">
@ -50,6 +67,14 @@ @@ -50,6 +67,14 @@
{:else}
<a href="/login" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors">Login</a>
{/if}
<button
onclick={() => (showShortcuts = true)}
class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors"
title="Keyboard shortcuts (?)"
aria-label="Keyboard shortcuts"
>
?
</button>
<UserPreferences />
<ThemeToggle />
</div>
@ -57,6 +82,8 @@ @@ -57,6 +82,8 @@
</nav>
</header>
<KeyboardShortcutsModal open={showShortcuts} onClose={() => (showShortcuts = false)} />
<style>
header {
max-width: 100%;

194
src/lib/components/modals/KeyboardShortcutsModal.svelte

@ -0,0 +1,194 @@ @@ -0,0 +1,194 @@
<script lang="ts">
interface Props {
open: boolean;
onClose: () => void;
}
let { open, onClose }: Props = $props();
const shortcuts = [
{ key: '/', description: 'Open search' },
{ key: '?', description: 'Show keyboard shortcuts' },
{ key: 'j', description: 'Next post/thread' },
{ key: 'k', description: 'Previous post/thread' },
{ key: 'r', description: 'Reply to selected post' },
{ key: 'z', description: 'Zap selected post' },
{ key: 'Esc', description: 'Close modal' }
];
</script>
{#if open}
<div
class="modal-overlay"
onclick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
onkeydown={(e) => {
if (e.key === 'Escape') {
e.preventDefault();
onClose();
}
}}
role="dialog"
aria-modal="true"
aria-labelledby="shortcuts-title"
tabindex="-1"
>
<div class="modal">
<div class="modal-header">
<h2 id="shortcuts-title" class="text-xl font-bold">Keyboard Shortcuts</h2>
<button
onclick={onClose}
class="close-button"
aria-label="Close"
>
×
</button>
</div>
<div class="shortcuts-list">
{#each shortcuts as shortcut}
<div class="shortcut-item">
<kbd class="shortcut-key">{shortcut.key}</kbd>
<span class="shortcut-description">{shortcut.description}</span>
</div>
{/each}
</div>
<div class="modal-footer">
<button
onclick={onClose}
class="close-footer-button"
>
Close
</button>
</div>
</div>
</div>
{/if}
<style>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 2rem;
}
.modal {
background: var(--fog-post, #ffffff);
border-radius: 0.5rem;
padding: 1.5rem;
width: 100%;
max-width: 500px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
}
:global(.dark) .modal {
background: var(--fog-dark-post, #1f2937);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.close-button {
background: none;
border: none;
font-size: 2rem;
line-height: 1;
cursor: pointer;
color: var(--fog-text, #1f2937);
padding: 0;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
}
:global(.dark) .close-button {
color: var(--fog-dark-text, #f9fafb);
}
.close-button:hover {
opacity: 0.7;
}
.shortcuts-list {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 1.5rem;
}
.shortcut-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
background: var(--fog-post, #ffffff);
}
:global(.dark) .shortcut-item {
background: var(--fog-dark-post, #1f2937);
border-color: var(--fog-dark-border, #374151);
}
.shortcut-key {
padding: 0.25rem 0.5rem;
background: var(--fog-highlight, #f3f4f6);
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
font-family: monospace;
font-size: 0.875rem;
font-weight: 600;
color: var(--fog-text, #1f2937);
}
:global(.dark) .shortcut-key {
background: var(--fog-dark-highlight, #374151);
border-color: var(--fog-dark-border, #374151);
color: var(--fog-dark-text, #f9fafb);
}
.shortcut-description {
color: var(--fog-text, #1f2937);
font-size: 0.875rem;
}
:global(.dark) .shortcut-description {
color: var(--fog-dark-text, #f9fafb);
}
.modal-footer {
display: flex;
justify-content: flex-end;
}
.close-footer-button {
padding: 0.5rem 1rem;
background: var(--fog-accent, #64748b);
color: white;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.875rem;
}
.close-footer-button:hover {
opacity: 0.9;
}
</style>

17
src/lib/components/modals/PublicationStatusModal.svelte

@ -36,8 +36,15 @@ @@ -36,8 +36,15 @@
</script>
{#if open && results}
<div class="modal-overlay" onclick={close} onkeydown={(e) => e.key === 'Escape' && close()}>
<div class="modal-content" onclick={(e) => e.stopPropagation()}>
<div
class="modal-overlay"
onclick={close}
onkeydown={(e) => e.key === 'Escape' && close()}
role="dialog"
aria-modal="true"
tabindex="-1"
>
<div class="modal-content">
<div class="modal-header">
<h2>Publication Status</h2>
<button onclick={close} class="close-button">×</button>
@ -102,7 +109,7 @@ @@ -102,7 +109,7 @@
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
.dark .modal-content {
:global(.dark) .modal-content {
background: #1e293b;
border-color: #475569;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
@ -116,7 +123,7 @@ @@ -116,7 +123,7 @@
border-bottom: 1px solid #cbd5e1;
}
.dark .modal-header {
:global(.dark) .modal-header {
border-bottom-color: #475569;
}
@ -163,7 +170,7 @@ @@ -163,7 +170,7 @@
text-align: right;
}
.dark .modal-footer {
:global(.dark) .modal-footer {
border-top-color: #475569;
}

13
src/lib/components/modals/SearchModal.svelte

@ -112,9 +112,16 @@ @@ -112,9 +112,16 @@
onclick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
onkeydown={(e) => {
if (e.key === 'Escape') {
e.preventDefault();
onClose();
}
}}
role="dialog"
aria-modal="true"
aria-labelledby="search-title"
tabindex="-1"
>
<div class="search-modal">
<div class="search-header">
@ -272,12 +279,12 @@ @@ -272,12 +279,12 @@
.search-input:focus {
outline: none;
border-color: var(--fog-accent, #3b82f6);
border-color: var(--fog-accent, #64748b);
}
.search-button {
padding: 0.75rem 1.5rem;
background: var(--fog-accent, #3b82f6);
background: var(--fog-accent, #64748b);
color: white;
border: none;
border-radius: 0.25rem;
@ -338,7 +345,7 @@ @@ -338,7 +345,7 @@
.result-type {
font-size: 0.75rem;
font-weight: 600;
color: var(--fog-accent, #3b82f6);
color: var(--fog-accent, #64748b);
text-transform: uppercase;
}

71
src/lib/components/preferences/UserPreferences.svelte

@ -82,85 +82,107 @@ @@ -82,85 +82,107 @@
</button>
{#if showPreferences}
<div class="preferences-modal" onclick={(e) => e.target === e.currentTarget && (showPreferences = false)}>
<div class="preferences-content" onclick={(e) => e.stopPropagation()}>
<div
class="preferences-modal"
onclick={(e) => e.target === e.currentTarget && (showPreferences = false)}
onkeydown={(e) => {
if (e.key === 'Escape') {
e.preventDefault();
showPreferences = false;
}
}}
role="dialog"
aria-modal="true"
aria-labelledby="preferences-title"
tabindex="-1"
>
<div class="preferences-content">
<div class="preferences-header">
<h2>User Preferences</h2>
<button onclick={() => (showPreferences = false)} class="close-button">×</button>
<h2 id="preferences-title">User Preferences</h2>
<button onclick={() => (showPreferences = false)} class="close-button" aria-label="Close preferences">×</button>
</div>
<div class="preferences-body">
<div class="preference-group">
<label class="preference-label">Text Size</label>
<div class="preference-options">
<fieldset class="preference-group">
<legend class="preference-label">Text Size</legend>
<div class="preference-options" role="group" aria-label="Text Size">
<button
onclick={() => (textSize = 'small')}
class="preference-option {textSize === 'small' ? 'active' : ''}"
aria-pressed={textSize === 'small'}
>
Small
</button>
<button
onclick={() => (textSize = 'medium')}
class="preference-option {textSize === 'medium' ? 'active' : ''}"
aria-pressed={textSize === 'medium'}
>
Medium
</button>
<button
onclick={() => (textSize = 'large')}
class="preference-option {textSize === 'large' ? 'active' : ''}"
aria-pressed={textSize === 'large'}
>
Large
</button>
</div>
</div>
</fieldset>
<div class="preference-group">
<label class="preference-label">Line Spacing</label>
<div class="preference-options">
<fieldset class="preference-group">
<legend class="preference-label">Line Spacing</legend>
<div class="preference-options" role="group" aria-label="Line Spacing">
<button
onclick={() => (lineSpacing = 'tight')}
class="preference-option {lineSpacing === 'tight' ? 'active' : ''}"
aria-pressed={lineSpacing === 'tight'}
>
Tight
</button>
<button
onclick={() => (lineSpacing = 'normal')}
class="preference-option {lineSpacing === 'normal' ? 'active' : ''}"
aria-pressed={lineSpacing === 'normal'}
>
Normal
</button>
<button
onclick={() => (lineSpacing = 'loose')}
class="preference-option {lineSpacing === 'loose' ? 'active' : ''}"
aria-pressed={lineSpacing === 'loose'}
>
Loose
</button>
</div>
</div>
</fieldset>
<div class="preference-group">
<label class="preference-label">Content Width</label>
<div class="preference-options">
<fieldset class="preference-group">
<legend class="preference-label">Content Width</legend>
<div class="preference-options" role="group" aria-label="Content Width">
<button
onclick={() => (contentWidth = 'narrow')}
class="preference-option {contentWidth === 'narrow' ? 'active' : ''}"
aria-pressed={contentWidth === 'narrow'}
>
Narrow
</button>
<button
onclick={() => (contentWidth = 'medium')}
class="preference-option {contentWidth === 'medium' ? 'active' : ''}"
aria-pressed={contentWidth === 'medium'}
>
Medium
</button>
<button
onclick={() => (contentWidth = 'wide')}
class="preference-option {contentWidth === 'wide' ? 'active' : ''}"
aria-pressed={contentWidth === 'wide'}
>
Wide
</button>
</div>
</div>
</fieldset>
</div>
</div>
</div>
@ -241,6 +263,8 @@ @@ -241,6 +263,8 @@
.preference-group {
margin-bottom: 1.5rem;
border: none;
padding: 0;
}
.preference-label {
@ -248,6 +272,7 @@ @@ -248,6 +272,7 @@
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--fog-text, #1f2937);
padding: 0;
}
:global(.dark) .preference-label {
@ -277,7 +302,7 @@ @@ -277,7 +302,7 @@
.preference-option:hover {
background: var(--fog-highlight, #f3f4f6);
border-color: var(--fog-accent, #3b82f6);
border-color: var(--fog-accent, #64748b);
}
:global(.dark) .preference-option:hover {
@ -285,8 +310,14 @@ @@ -285,8 +310,14 @@
}
.preference-option.active {
background: var(--fog-accent, #3b82f6);
color: white;
border-color: var(--fog-accent, #3b82f6);
background: var(--fog-accent, #64748b);
color: var(--fog-text, #475569);
border-color: var(--fog-accent, #64748b);
}
:global(.dark) .preference-option.active {
background: var(--fog-dark-accent, #64748b);
color: var(--fog-dark-text, #cbd5e1);
border-color: var(--fog-dark-accent, #64748b);
}
</style>

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

@ -121,7 +121,7 @@ @@ -121,7 +121,7 @@
.comment.highlighted {
background: var(--fog-highlight, #f3f4f6);
border-color: var(--fog-accent, #3b82f6);
border-color: var(--fog-accent, #64748b);
animation: highlight 2s ease-out;
}
@ -131,7 +131,7 @@ @@ -131,7 +131,7 @@
@keyframes highlight {
0% {
background: var(--fog-accent, #3b82f6);
background: var(--fog-accent, #64748b);
opacity: 0.3;
}
100% {
@ -144,7 +144,7 @@ @@ -144,7 +144,7 @@
padding: 0.5rem;
margin-bottom: 0.5rem;
background: var(--fog-highlight, #f3f4f6);
border-left: 3px solid var(--fog-accent, #3b82f6);
border-left: 3px solid var(--fog-accent, #64748b);
border-radius: 0.25rem;
cursor: pointer;
transition: background 0.2s;
@ -152,10 +152,11 @@ @@ -152,10 +152,11 @@
:global(.dark) .parent-preview {
background: var(--fog-dark-highlight, #374151);
border-left-color: var(--fog-dark-accent, #64748b);
}
.parent-preview:hover {
background: var(--fog-accent, #3b82f6);
background: var(--fog-accent, #64748b);
opacity: 0.1;
}

4
src/lib/modules/comments/CommentForm.svelte

@ -106,7 +106,7 @@ @@ -106,7 +106,7 @@
{/if}
<button
onclick={publish}
class="px-4 py-2 text-sm bg-fog-accent dark:bg-fog-dark-accent text-white rounded hover:opacity-90 disabled:opacity-50"
class="px-4 py-2 text-sm bg-fog-accent dark:bg-fog-dark-accent text-fog-text dark:text-fog-dark-text rounded hover:opacity-90 disabled:opacity-50"
disabled={publishing || !content.trim()}
>
{publishing ? 'Publishing...' : parentEvent ? 'Reply' : 'Comment'}
@ -127,6 +127,6 @@ @@ -127,6 +127,6 @@
textarea:focus {
outline: none;
border-color: var(--fog-accent, #3b82f6);
border-color: var(--fog-accent, #64748b);
}
</style>

4
src/lib/modules/feed/CreateKind1Form.svelte

@ -122,7 +122,7 @@ @@ -122,7 +122,7 @@
{/if}
<button
onclick={publish}
class="px-4 py-2 text-sm bg-fog-accent dark:bg-fog-dark-accent text-white rounded hover:opacity-90 disabled:opacity-50"
class="px-4 py-2 text-sm bg-fog-accent dark:bg-fog-dark-accent text-fog-text dark:text-fog-dark-text rounded hover:opacity-90 disabled:opacity-50"
disabled={publishing || !content.trim()}
>
{publishing ? 'Publishing...' : parentEvent ? 'Reply' : 'Post'}
@ -143,6 +143,6 @@ @@ -143,6 +143,6 @@
textarea:focus {
outline: none;
border-color: var(--fog-accent, #3b82f6);
border-color: var(--fog-accent, #64748b);
}
</style>

4
src/lib/modules/feed/Kind1FeedPage.svelte

@ -206,7 +206,7 @@ @@ -206,7 +206,7 @@
<h1 class="text-2xl font-bold mb-4">Feed</h1>
<button
onclick={() => (showNewPostForm = !showNewPostForm)}
class="px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-white rounded hover:opacity-90"
class="px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-fog-text dark:text-fog-dark-text rounded hover:opacity-90"
>
{showNewPostForm ? 'Cancel' : 'New Post'}
</button>
@ -234,7 +234,7 @@ @@ -234,7 +234,7 @@
<div class="new-posts-indicator mb-4">
<button
onclick={handleShowNewPosts}
class="px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-white rounded hover:opacity-90 text-sm"
class="px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-fog-text dark:text-fog-dark-text rounded hover:opacity-90 text-sm"
>
{newPostsCount} new {newPostsCount === 1 ? 'post' : 'posts'} - Click to view
</button>

4
src/lib/modules/feed/Kind1Reply.svelte

@ -87,13 +87,13 @@ @@ -87,13 +87,13 @@
background: var(--fog-post, #ffffff);
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.25rem;
border-left: 3px solid var(--fog-accent, #3b82f6);
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, #60a5fa);
border-left-color: var(--fog-dark-accent, #64748b);
}
.reply-content {

4
src/lib/modules/feed/ReplyToKind1Form.svelte

@ -115,7 +115,7 @@ @@ -115,7 +115,7 @@
{/if}
<button
onclick={publish}
class="px-4 py-2 text-sm bg-fog-accent dark:bg-fog-dark-accent text-white rounded hover:opacity-90 disabled:opacity-50"
class="px-4 py-2 text-sm bg-fog-accent dark:bg-fog-dark-accent text-fog-text dark:text-fog-dark-text rounded hover:opacity-90 disabled:opacity-50"
disabled={publishing || !content.trim()}
>
{publishing ? 'Publishing...' : 'Reply'}
@ -136,6 +136,6 @@ @@ -136,6 +136,6 @@
textarea:focus {
outline: none;
border-color: var(--fog-accent, #3b82f6);
border-color: var(--fog-accent, #64748b);
}
</style>

14
src/lib/modules/reactions/Kind1ReactionButtons.svelte

@ -205,7 +205,7 @@ @@ -205,7 +205,7 @@
.reaction-btn:hover {
background: var(--fog-highlight, #f3f4f6);
border-color: var(--fog-accent, #3b82f6);
border-color: var(--fog-accent, #64748b);
}
:global(.dark) .reaction-btn:hover {
@ -213,9 +213,15 @@ @@ -213,9 +213,15 @@
}
.reaction-btn.active {
background: var(--fog-accent, #3b82f6);
color: white;
border-color: var(--fog-accent, #3b82f6);
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);
}
.more-btn {

14
src/lib/modules/reactions/ReactionButtons.svelte

@ -221,7 +221,7 @@ @@ -221,7 +221,7 @@
.reaction-btn:hover {
background: var(--fog-highlight, #f3f4f6);
border-color: var(--fog-accent, #3b82f6);
border-color: var(--fog-accent, #64748b);
}
:global(.dark) .reaction-btn:hover {
@ -229,9 +229,15 @@ @@ -229,9 +229,15 @@
}
.reaction-btn.active {
background: var(--fog-accent, #3b82f6);
color: white;
border-color: var(--fog-accent, #3b82f6);
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 {

10
src/lib/modules/threads/CreateThreadForm.svelte

@ -140,8 +140,12 @@ @@ -140,8 +140,12 @@
</div>
<div class="mb-4">
<label class="block mb-2 text-fog-text dark:text-fog-dark-text">Target Relays</label>
<div class="border border-fog-border dark:border-fog-dark-border rounded p-3 bg-fog-post dark:bg-fog-dark-post max-h-48 overflow-y-auto">
<h3 class="block mb-2 text-fog-text dark:text-fog-dark-text font-semibold">Target Relays</h3>
<div
class="border border-fog-border dark:border-fog-dark-border rounded p-3 bg-fog-post dark:bg-fog-dark-post max-h-48 overflow-y-auto"
role="group"
aria-label="Target Relays"
>
{#each Array.from(selectedRelays) as relay}
<label class="flex items-center gap-2 mb-2">
<input
@ -164,7 +168,7 @@ @@ -164,7 +168,7 @@
</div>
</div>
<button type="submit" disabled={publishing || selectedRelays.size === 0} class="px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent hover:bg-fog-text dark:hover:bg-fog-dark-text text-white disabled:opacity-50 transition-colors rounded">
<button type="submit" disabled={publishing || selectedRelays.size === 0} class="px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent hover:bg-fog-text dark:hover:bg-fog-dark-text text-fog-text dark:text-fog-dark-text disabled:opacity-50 transition-colors rounded">
{publishing ? 'Publishing...' : 'Create Thread'}
</button>
</form>

35
src/lib/modules/threads/ThreadCard.svelte

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
<script lang="ts">
import ProfileBadge from '../../components/layout/ProfileBadge.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';
@ -13,6 +14,8 @@ @@ -13,6 +14,8 @@
let upvotes = $state(0);
let downvotes = $state(0);
let commentCount = $state(0);
let zapTotal = $state(0);
let zapCount = $state(0);
let loadingStats = $state(true);
onMount(async () => {
@ -25,9 +28,10 @@ @@ -25,9 +28,10 @@
const config = nostrClient.getConfig();
// Load reactions (kind 7)
const reactionRelays = relayManager.getThreadReadRelays();
const reactionEvents = await nostrClient.fetchEvents(
[{ kinds: [7], '#e': [thread.id] }],
[...config.defaultRelays],
reactionRelays,
{ useCache: true }
);
@ -42,12 +46,36 @@ @@ -42,12 +46,36 @@
}
// Load comments (kind 1111)
const commentRelays = relayManager.getCommentReadRelays();
const commentEvents = await nostrClient.fetchEvents(
[{ kinds: [1111], '#E': [thread.id], '#K': ['11'] }],
[...config.defaultRelays],
commentRelays,
{ useCache: true }
);
commentCount = commentEvents.length;
// Load zap receipts (kind 9735)
const zapRelays = relayManager.getZapReceiptReadRelays();
const zapReceipts = await nostrClient.fetchEvents(
[{ kinds: [9735], '#e': [thread.id] }],
zapRelays,
{ useCache: true }
);
// Calculate zap totals
const threshold = config.zapThreshold;
zapCount = 0;
zapTotal = 0;
for (const receipt of zapReceipts) {
const amountTag = receipt.tags.find((t) => t[0] === 'amount');
if (amountTag && amountTag[1]) {
const amount = parseInt(amountTag[1], 10);
if (!isNaN(amount) && amount >= threshold) {
zapTotal += amount;
zapCount++;
}
}
}
} catch (error) {
console.error('Error loading thread stats:', error);
} finally {
@ -118,6 +146,9 @@ @@ -118,6 +146,9 @@
<span>{upvotes}</span>
<span>{downvotes}</span>
<span>{commentCount} {commentCount === 1 ? 'comment' : 'comments'}</span>
{#if zapCount > 0}
<span>{zapTotal.toLocaleString()} sats ({zapCount})</span>
{/if}
{/if}
</div>
<a href="/thread/{thread.id}">View thread →</a>

45
src/lib/modules/threads/ThreadList.svelte

@ -63,11 +63,15 @@ @@ -63,11 +63,15 @@
case 'newest':
return sortThreadsSync(events);
case 'active':
// Sort by most recent comment activity
// Sort by most recent activity (comments, reactions, or zaps)
// Thread bumping: active threads rise to top
const activeSorted = await Promise.all(
events.map(async (event) => {
const config = nostrClient.getConfig();
const commentRelays = relayManager.getCommentReadRelays();
const reactionRelays = relayManager.getThreadReadRelays();
const zapRelays = relayManager.getZapReceiptReadRelays();
// Get most recent comment
const comments = await nostrClient.fetchEvents(
[{ kinds: [1111], '#E': [event.id], '#K': ['11'], limit: 1 }],
commentRelays,
@ -75,8 +79,37 @@ @@ -75,8 +79,37 @@
);
const lastCommentTime = comments.length > 0
? comments.sort((a, b) => b.created_at - a.created_at)[0].created_at
: event.created_at;
return { event, lastActivity: lastCommentTime };
: 0;
// Get most recent reaction
const reactions = await nostrClient.fetchEvents(
[{ kinds: [7], '#e': [event.id], limit: 1 }],
reactionRelays,
{ useCache: true }
);
const lastReactionTime = reactions.length > 0
? reactions.sort((a, b) => b.created_at - a.created_at)[0].created_at
: 0;
// Get most recent zap
const zaps = await nostrClient.fetchEvents(
[{ kinds: [9735], '#e': [event.id], limit: 1 }],
zapRelays,
{ useCache: true }
);
const lastZapTime = zaps.length > 0
? zaps.sort((a, b) => b.created_at - a.created_at)[0].created_at
: 0;
// Last activity is the most recent of all activities
const lastActivity = Math.max(
event.created_at,
lastCommentTime,
lastReactionTime,
lastZapTime
);
return { event, lastActivity };
})
);
return activeSorted
@ -218,7 +251,7 @@ @@ -218,7 +251,7 @@
<button
onclick={() => (selectedTopic = null)}
class="px-3 py-1 rounded border transition-colors {selectedTopic === null
? 'bg-fog-accent dark:bg-fog-dark-accent text-white border-fog-accent dark:border-fog-dark-accent'
? 'bg-fog-accent dark:bg-fog-dark-accent text-fog-text dark:text-fog-dark-text border-fog-accent dark:border-fog-dark-accent'
: 'bg-fog-post dark:bg-fog-dark-post text-fog-text dark:text-fog-dark-text border-fog-border dark:border-fog-dark-border hover:bg-fog-highlight dark:hover:bg-fog-dark-highlight'}"
>
All ({filterByAge(threads).length})
@ -227,7 +260,7 @@ @@ -227,7 +260,7 @@
<button
onclick={() => (selectedTopic = topic === null ? undefined : topic)}
class="px-3 py-1 rounded border transition-colors {selectedTopic === (topic === null ? undefined : topic)
? 'bg-fog-accent dark:bg-fog-dark-accent text-white border-fog-accent dark:border-fog-dark-accent'
? 'bg-fog-accent dark:bg-fog-dark-accent text-fog-text dark:text-fog-dark-text border-fog-accent dark:border-fog-dark-accent'
: 'bg-fog-post dark:bg-fog-dark-post text-fog-text dark:text-fog-dark-text border-fog-border dark:border-fog-dark-border hover:bg-fog-highlight dark:hover:bg-fog-dark-highlight'}"
>
{topic === null ? 'General' : topic} ({count})

2
src/lib/modules/zaps/ZapButton.svelte

@ -168,7 +168,7 @@ @@ -168,7 +168,7 @@
.zap-button:hover {
background: var(--fog-highlight, #f3f4f6);
border-color: var(--fog-accent, #3b82f6);
border-color: var(--fog-accent, #64748b);
}
:global(.dark) .zap-button:hover {

24
src/lib/modules/zaps/ZapInvoiceModal.svelte

@ -22,11 +22,19 @@ @@ -22,11 +22,19 @@
});
</script>
<div class="modal-overlay" onclick={onClose} onkeydown={(e) => e.key === 'Escape' && onClose()}>
<div class="modal-content" onclick={(e) => e.stopPropagation()}>
<div
class="modal-overlay"
onclick={onClose}
onkeydown={(e) => e.key === 'Escape' && onClose()}
role="dialog"
aria-modal="true"
aria-labelledby="zap-invoice-title"
tabindex="-1"
>
<div class="modal-content">
<div class="modal-header">
<h2>Pay Invoice</h2>
<button onclick={onClose} class="close-button">×</button>
<h2 id="zap-invoice-title">Pay Invoice</h2>
<button onclick={onClose} class="close-button" aria-label="Close invoice modal">×</button>
</div>
<div class="modal-body">
@ -39,16 +47,18 @@ @@ -39,16 +47,18 @@
{/if}
<div class="invoice-container mb-4">
<label class="block text-sm font-semibold mb-2">Invoice:</label>
<label for="invoice-textarea" class="block text-sm font-semibold mb-2">Invoice:</label>
<textarea
id="invoice-textarea"
readonly
value={invoice}
class="w-full p-2 border border-fog-border dark:border-fog-dark-border rounded bg-fog-post dark:bg-fog-dark-post text-fog-text dark:text-fog-dark-text text-xs font-mono"
rows="4"
aria-label="Lightning invoice"
></textarea>
<button
onclick={copyInvoice}
class="mt-2 px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-white rounded hover:opacity-90"
class="mt-2 px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-fog-text dark:text-fog-dark-text rounded hover:opacity-90"
>
Copy Invoice
</button>
@ -155,7 +165,7 @@ @@ -155,7 +165,7 @@
.modal-footer button {
padding: 0.5rem 1rem;
background: var(--fog-accent, #3b82f6);
background: var(--fog-accent, #64748b);
color: white;
border: none;
border-radius: 4px;

2
src/routes/+page.svelte

@ -34,7 +34,7 @@ @@ -34,7 +34,7 @@
{#if isLoggedIn}
<button
onclick={() => (showCreateForm = !showCreateForm)}
class="px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-white rounded hover:opacity-90"
class="px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-fog-text dark:text-fog-dark-text rounded hover:opacity-90"
>
{showCreateForm ? 'Cancel' : 'Create Thread'}
</button>

2
src/routes/login/+page.svelte

@ -45,7 +45,7 @@ @@ -45,7 +45,7 @@
<button
onclick={loginWithNIP07}
disabled={loading}
class="w-full px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent hover:bg-fog-text dark:hover:bg-fog-dark-text text-white disabled:opacity-50 transition-colors rounded"
class="w-full px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent hover:bg-fog-text dark:hover:bg-fog-dark-text text-fog-text dark:text-fog-dark-text disabled:opacity-50 transition-colors rounded"
>
{loading ? 'Connecting...' : 'Login with NIP-07'}
</button>

Loading…
Cancel
Save