From a84a77ea762a5f5ab5b06e615ce82951a3194a51 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Wed, 11 Feb 2026 10:42:27 +0100 Subject: [PATCH] render all raw json with highlight.js change the syntax highlighting for terminal theme make terminal theme more persistent --- src/app.css | 792 ++++++++++++++++++ .../components/content/FileExplorer.svelte | 265 +++++- .../components/layout/UnifiedSearch.svelte | 4 +- .../components/write/CreateEventForm.svelte | 65 +- src/lib/components/write/FindEventForm.svelte | 66 +- .../discussions/DiscussionVoteButtons.svelte | 98 ++- src/lib/modules/feed/HighlightCard.svelte | 21 +- .../reactions/FeedReactionButtons.svelte | 24 +- src/lib/services/content/git-repo-fetcher.ts | 119 ++- src/lib/services/nostr/relay-manager.ts | 53 +- src/routes/feed/relay/[relay]/+page.svelte | 35 +- src/routes/find/+page.svelte | 53 +- src/routes/highlights/+page.svelte | 21 +- src/routes/relay/+page.svelte | 80 +- src/routes/repos/+page.svelte | 396 ++++++++- src/routes/repos/[naddr]/+page.svelte | 4 +- src/routes/rss/+page.svelte | 13 +- 17 files changed, 1930 insertions(+), 179 deletions(-) diff --git a/src/app.css b/src/app.css index 1b0a9d5..d64dddc 100644 --- a/src/app.css +++ b/src/app.css @@ -1143,6 +1143,632 @@ body::before { text-transform: uppercase !important; } +/* Terminal Theme - Toolbar and Editor */ +[data-design-theme="terminal"] .editor-toolbar, +[data-design-theme="terminal"] .toolbar-group { + background: #000000 !important; + border-color: #00ff00 !important; +} + +/* Terminal Theme - Textarea Buttons Toolbar (GIF/Emoji) - Transparent */ +[data-design-theme="terminal"] .textarea-buttons { + background: transparent !important; + border: none !important; +} + +/* Terminal Theme - Advanced Search Container */ +[data-design-theme="terminal"] .advanced-filters-grid { + background: #000000 !important; + border-color: #00ff00 !important; +} + +/* Terminal Theme - Event Relay Badge (in search results) */ +[data-design-theme="terminal"] .event-relay-badge { + background: #000000 !important; + border-color: #00ff00 !important; +} + +/* Terminal Theme - Reply Context and Quoted Context */ +[data-design-theme="terminal"] .reply-context, +[data-design-theme="terminal"] .quoted-context { + background: #000000 !important; + background-color: #000000 !important; + border-left-color: #00ff00 !important; + color: #00ff00 !important; + border: 1px solid #00ff00 !important; + box-shadow: 0 0 5px rgba(0, 255, 0, 0.3) !important; +} + +/* Override Tailwind background classes - need to target both light and dark variants */ +[data-design-theme="terminal"] .reply-context.bg-fog-highlight, +[data-design-theme="terminal"] .dark .reply-context.bg-fog-dark-highlight, +[data-design-theme="terminal"] .reply-context.dark\:bg-fog-dark-highlight, +[data-design-theme="terminal"] .quoted-context.bg-fog-highlight, +[data-design-theme="terminal"] .dark .quoted-context.bg-fog-dark-highlight, +[data-design-theme="terminal"] .quoted-context.dark\:bg-fog-dark-highlight { + background: #000000 !important; + background-color: #000000 !important; +} + +[data-design-theme="terminal"] .reply-context-content, +[data-design-theme="terminal"] .quoted-context-content, +[data-design-theme="terminal"] .reply-preview, +[data-design-theme="terminal"] .quoted-preview, +[data-design-theme="terminal"] .reply-kind-info, +[data-design-theme="terminal"] .quoted-kind-info { + color: #00ff00 !important; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important; +} + +/* Override Tailwind text color classes - need to target both light and dark variants */ +[data-design-theme="terminal"] .reply-context.text-fog-text-light, +[data-design-theme="terminal"] .dark .reply-context.text-fog-dark-text-light, +[data-design-theme="terminal"] .reply-context.dark\:text-fog-dark-text-light, +[data-design-theme="terminal"] .quoted-context.text-fog-text-light, +[data-design-theme="terminal"] .dark .quoted-context.text-fog-dark-text-light, +[data-design-theme="terminal"] .quoted-context.dark\:text-fog-dark-text-light { + color: #00ff00 !important; +} + +/* Terminal Theme - Referenced Event Preview */ +[data-design-theme="terminal"] .referenced-event-preview { + background: #000000 !important; + background-color: #000000 !important; + border-color: #00ff00 !important; + border-left-color: #00ff00 !important; + color: #00ff00 !important; + box-shadow: 0 0 5px rgba(0, 255, 0, 0.3) !important; +} + +[data-design-theme="terminal"] .referenced-event-label, +[data-design-theme="terminal"] .referenced-event-title, +[data-design-theme="terminal"] .referenced-event-preview-text, +[data-design-theme="terminal"] .loading-text, +[data-design-theme="terminal"] .error-text { + color: #00ff00 !important; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important; +} + +[data-design-theme="terminal"] .view-event-button, +[data-design-theme="terminal"] .view-website-button { + background: #000000 !important; + border-color: #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .view-event-button:hover, +[data-design-theme="terminal"] .view-website-button:hover { + background: rgba(0, 255, 0, 0.1) !important; + box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important; +} + +[data-design-theme="terminal"] .website-link { + color: #00ff00 !important; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important; +} + +/* Terminal Theme - JSON Preview - Sharp Text Rendering */ +[data-design-theme="terminal"] .json-preview code, +[data-design-theme="terminal"] .json-preview code.hljs, +[data-design-theme="terminal"] .json-content code, +[data-design-theme="terminal"] .json-content code.hljs, +[data-design-theme="terminal"] .example-modal .json-preview code, +[data-design-theme="terminal"] .example-modal .json-preview code.hljs, +[data-design-theme="terminal"] .event-json .json-preview code, +[data-design-theme="terminal"] .event-json .json-preview code.hljs { + text-shadow: none !important; + -webkit-font-smoothing: antialiased !important; + -moz-osx-font-smoothing: grayscale !important; + font-smoothing: antialiased !important; + text-rendering: optimizeLegibility !important; +} + +/* Remove text-shadow from highlight.js spans inside JSON */ +[data-design-theme="terminal"] .json-preview code.hljs span, +[data-design-theme="terminal"] .json-content code.hljs span, +[data-design-theme="terminal"] .example-modal .json-preview code.hljs span, +[data-design-theme="terminal"] .event-json .json-preview code.hljs span, +[data-design-theme="terminal"] .json-preview code.hljs *, +[data-design-theme="terminal"] .json-content code.hljs *, +[data-design-theme="terminal"] .example-modal .json-preview code.hljs *, +[data-design-theme="terminal"] .event-json .json-preview code.hljs * { + text-shadow: none !important; +} + +/* Terminal Theme - RSS Page */ +[data-design-theme="terminal"] .rss-setup, +[data-design-theme="terminal"] .rss-info, +[data-design-theme="terminal"] .rss-items-section, +[data-design-theme="terminal"] .rss-comment-form { + background: #000000 !important; + border-color: #00ff00 !important; +} + +[data-design-theme="terminal"] .rss-feed-item, +[data-design-theme="terminal"] .rss-item { + background: #000000 !important; + border-color: #00ff00 !important; +} + +[data-design-theme="terminal"] .rss-feed-link, +[data-design-theme="terminal"] .rss-item-link { + color: #00ff00 !important; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important; +} + +[data-design-theme="terminal"] .rss-feed-link:hover, +[data-design-theme="terminal"] .rss-item-link:hover { + color: #00ff00 !important; + text-shadow: 0 0 5px rgba(0, 255, 0, 0.8) !important; +} + +[data-design-theme="terminal"] .rss-feed-badge { + background: #000000 !important; + border: 1px solid #00ff00 !important; + color: #00ff00 !important; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important; +} + +[data-design-theme="terminal"] .rss-item-time, +[data-design-theme="terminal"] .rss-item-description, +[data-design-theme="terminal"] .feed-error { + color: #00ff00 !important; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important; +} + +[data-design-theme="terminal"] .create-rss-button, +[data-design-theme="terminal"] .edit-rss-button, +[data-design-theme="terminal"] .pagination-button { + background: #000000 !important; + border-color: #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .create-rss-button:hover:not(:disabled), +[data-design-theme="terminal"] .edit-rss-button:hover:not(:disabled), +[data-design-theme="terminal"] .pagination-button:hover:not(:disabled) { + background: rgba(0, 255, 0, 0.1) !important; + box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important; +} + +[data-design-theme="terminal"] .pagination-button:disabled { + opacity: 0.5 !important; + border-color: rgba(0, 255, 0, 0.3) !important; +} + +[data-design-theme="terminal"] .btn-action, +[data-design-theme="terminal"] .btn-primary, +[data-design-theme="terminal"] .btn-secondary { + background: #000000 !important; + border-color: #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .btn-action:hover:not(:disabled), +[data-design-theme="terminal"] .btn-primary:hover:not(:disabled), +[data-design-theme="terminal"] .btn-secondary:hover:not(:disabled) { + background: rgba(0, 255, 0, 0.1) !important; + box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important; +} + +[data-design-theme="terminal"] .btn-action:disabled, +[data-design-theme="terminal"] .btn-primary:disabled, +[data-design-theme="terminal"] .btn-secondary:disabled { + opacity: 0.5 !important; + border-color: rgba(0, 255, 0, 0.3) !important; +} + +/* Terminal Theme - Relay Page */ +[data-design-theme="terminal"] .relay-category, +[data-design-theme="terminal"] .custom-relay-section { + background: #000000 !important; + border-color: #00ff00 !important; +} + +[data-design-theme="terminal"] .category-title { + color: #00ff00 !important; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important; +} + +[data-design-theme="terminal"] .relay-item { + background: #000000 !important; + border-color: #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .relay-item:hover { + background: rgba(0, 255, 0, 0.1) !important; + box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .relay-url { + color: #00ff00 !important; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important; +} + +[data-design-theme="terminal"] .relay-item:hover .relay-url { + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .relay-status.connected { + color: #00ff00 !important; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important; +} + +[data-design-theme="terminal"] .relay-status.disconnected { + color: #ff0000 !important; + text-shadow: 0 0 3px rgba(255, 0, 0, 0.6) !important; +} + +[data-design-theme="terminal"] .relay-item:hover .relay-status { + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .relay-category-badge { + background: #000000 !important; + border: 1px solid #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .relay-item:hover .relay-category-badge { + background: rgba(0, 255, 0, 0.1) !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .relay-arrow { + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .custom-relay-text-input { + background: #000000 !important; + border-color: #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .custom-relay-text-input:focus { + box-shadow: 0 0 10px rgba(0, 255, 0, 0.6) !important; + outline: none !important; +} + +[data-design-theme="terminal"] .custom-relay-button { + background: #000000 !important; + border-color: #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .custom-relay-button:hover:not(:disabled) { + background: rgba(0, 255, 0, 0.1) !important; + box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important; +} + +[data-design-theme="terminal"] .custom-relay-button:disabled { + opacity: 0.5 !important; + border-color: rgba(0, 255, 0, 0.3) !important; +} + +/* Terminal Theme - Relay Feed Info Bar */ +[data-design-theme="terminal"] .relay-info { + background: #000000 !important; + border: 1px solid #00ff00 !important; + border-radius: 0.25rem !important; + padding: 0.75rem 1rem !important; + margin-bottom: 1rem !important; + box-shadow: 0 0 5px rgba(0, 255, 0, 0.3) !important; +} + +[data-design-theme="terminal"] .relay-info-text { + color: #00ff00 !important; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important; + margin: 0 !important; +} + +[data-design-theme="terminal"] .relay-info-text code.relay-url { + color: #00ff00 !important; + background: transparent !important; + border: none !important; + padding: 0 !important; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important; +} + +[data-design-theme="terminal"] .relay-info-text .relay-port { + color: #00ff00 !important; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important; +} + +/* Terminal Theme - Topics Page */ +[data-design-theme="terminal"] .topic-item { + background: #000000 !important; + border-color: #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .topic-item:hover { + background: rgba(0, 255, 0, 0.1) !important; + box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .topic-item.interest { + border-left-color: #00ff00 !important; +} + +[data-design-theme="terminal"] .topic-name, +[data-design-theme="terminal"] .topic-count { + color: #00ff00 !important; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important; +} + +[data-design-theme="terminal"] .topic-item:hover .topic-name, +[data-design-theme="terminal"] .topic-item:hover .topic-count { + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .pagination-button { + background: #000000 !important; + border-color: #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .pagination-button:hover:not(:disabled) { + background: rgba(0, 255, 0, 0.1) !important; + box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important; +} + +[data-design-theme="terminal"] .pagination-button:disabled { + opacity: 0.5 !important; + border-color: rgba(0, 255, 0, 0.3) !important; +} + +[data-design-theme="terminal"] .pagination-info { + color: #00ff00 !important; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important; +} + +/* Terminal Theme - Repos Page */ +[data-design-theme="terminal"] .repo-item, +[data-design-theme="terminal"] .search-results-section, +[data-design-theme="terminal"] .profile-result-card, +[data-design-theme="terminal"] .event-result-card { + background: #000000 !important; + border-color: #00ff00 !important; +} + +[data-design-theme="terminal"] .repo-item:hover, +[data-design-theme="terminal"] .profile-result-card:hover, +[data-design-theme="terminal"] .event-result-card:hover { + background: rgba(0, 255, 0, 0.1) !important; + box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important; + border-color: #00ff00 !important; +} + +[data-design-theme="terminal"] .repo-name, +[data-design-theme="terminal"] .repo-description, +[data-design-theme="terminal"] .repo-meta, +[data-design-theme="terminal"] .repo-kind, +[data-design-theme="terminal"] .results-title, +[data-design-theme="terminal"] .results-group h3 { + color: #00ff00 !important; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important; +} + +[data-design-theme="terminal"] .repo-kind { + background: #000000 !important; + border: 1px solid #00ff00 !important; + color: #00ff00 !important; +} + +/* Terminal Theme - Repo Detail Page */ +[data-design-theme="terminal"] .repo-meta-section, +[data-design-theme="terminal"] .readme-container, +[data-design-theme="terminal"] .commit-card, +[data-design-theme="terminal"] .branch-item, +[data-design-theme="terminal"] .issue-item, +[data-design-theme="terminal"] .documentation-item { + background: #000000 !important; + border-color: #00ff00 !important; +} + +[data-design-theme="terminal"] .metadata-label, +[data-design-theme="terminal"] .metadata-value, +[data-design-theme="terminal"] .clone-url, +[data-design-theme="terminal"] .event-id, +[data-design-theme="terminal"] .naddr-code { + background: #000000 !important; + border-color: #00ff00 !important; + color: #00ff00 !important; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important; +} + +[data-design-theme="terminal"] .relay-link { + background: #000000 !important; + border: 1px solid #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .relay-link:hover { + background: rgba(0, 255, 0, 0.1) !important; + box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .metadata-link { + color: #00ff00 !important; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important; +} + +[data-design-theme="terminal"] .primary-badge { + background: #000000 !important; + border: 1px solid #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .maintainer-item.is-owner { + background: #000000 !important; + border-color: #00ff00 !important; +} + +[data-design-theme="terminal"] .branch-item.default { + border-color: #00ff00 !important; +} + +[data-design-theme="terminal"] .branch-name, +[data-design-theme="terminal"] .branch-commit, +[data-design-theme="terminal"] .branch-message, +[data-design-theme="terminal"] .commit-sha, +[data-design-theme="terminal"] .commit-message, +[data-design-theme="terminal"] .commit-meta { + color: #00ff00 !important; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important; +} + +[data-design-theme="terminal"] .branch-badge { + background: #000000 !important; + border: 1px solid #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .issues-filter { + background: #000000 !important; + border-color: #00ff00 !important; +} + +[data-design-theme="terminal"] .filter-label, +[data-design-theme="terminal"] .filter-count { + color: #00ff00 !important; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important; +} + +[data-design-theme="terminal"] .status-filter-select { + background: #000000 !important; + border-color: #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .status-filter-select:hover, +[data-design-theme="terminal"] .status-filter-select:focus { + box-shadow: 0 0 10px rgba(0, 255, 0, 0.6) !important; + outline: none !important; +} + +[data-design-theme="terminal"] .issue-header { + background: #000000 !important; + border-color: #00ff00 !important; +} + +[data-design-theme="terminal"] .status-label, +[data-design-theme="terminal"] .status-changing, +[data-design-theme="terminal"] .comments-header { + color: #00ff00 !important; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important; +} + +[data-design-theme="terminal"] .status-select { + background: #000000 !important; + border-color: #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .status-select:hover:not(:disabled) { + box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important; +} + +[data-design-theme="terminal"] .status-select.open, +[data-design-theme="terminal"] .status-select.closed, +[data-design-theme="terminal"] .status-select.resolved, +[data-design-theme="terminal"] .status-select.draft { + background: #000000 !important; + border-color: #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .issue-comments { + border-top-color: #00ff00 !important; +} + +[data-design-theme="terminal"] .comment-item { + border-left-color: #00ff00 !important; +} + +[data-design-theme="terminal"] .doc-header { + border-bottom-color: #00ff00 !important; +} + +[data-design-theme="terminal"] .doc-kind { + background: #000000 !important; + border: 1px solid #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .doc-event-link { + color: #00ff00 !important; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.6) !important; +} + +[data-design-theme="terminal"] .toolbar-button { + background: #000000 !important; + border-color: #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .toolbar-button:hover:not(:disabled) { + background: rgba(0, 255, 0, 0.1) !important; + box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important; +} + +[data-design-theme="terminal"] .toolbar-separator { + background: #00ff00 !important; +} + +/* Terminal Theme - Modals */ +[data-design-theme="terminal"] .modal-overlay { + background: rgba(0, 0, 0, 0.8) !important; +} + +[data-design-theme="terminal"] .modal-content, +[data-design-theme="terminal"] .example-modal { + background: #000000 !important; + border-color: #00ff00 !important; + box-shadow: 0 0 10px rgba(0, 255, 0, 0.5) !important; +} + +[data-design-theme="terminal"] .modal-header, +[data-design-theme="terminal"] .modal-body, +[data-design-theme="terminal"] .modal-footer { + background: #000000 !important; + border-color: #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .modal-header h2, +[data-design-theme="terminal"] .modal-header h3 { + color: #00ff00 !important; + text-shadow: 0 0 3px rgba(0, 255, 0, 0.8) !important; +} + +[data-design-theme="terminal"] .close-button { + background: transparent !important; + border-color: #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .close-button:hover { + background: rgba(0, 255, 0, 0.1) !important; + box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important; +} + +/* Terminal Theme - General Buttons */ +[data-design-theme="terminal"] button:not(.toolbar-button):not(.close-button):not(.view-button):not(.toggle-button):not(.option-button):not(.action-button):not(.back-button):not(.write-button):not(.see-new-events-btn-header):not(.see-more-events-btn-header):not(.find-button):not(.create-rss-button):not(.edit-rss-button):not(.bulk-action-button):not(.load-more-button):not(.clear-kind-button) { + background: #000000 !important; + border-color: #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] button:not(.toolbar-button):not(.close-button):not(.view-button):not(.toggle-button):not(.option-button):not(.action-button):not(.back-button):not(.write-button):not(.see-new-events-btn-header):not(.see-more-events-btn-header):not(.find-button):not(.create-rss-button):not(.edit-rss-button):not(.bulk-action-button):not(.load-more-button):not(.clear-kind-button):hover:not(:disabled) { + background: rgba(0, 255, 0, 0.1) !important; + box-shadow: 0 0 5px rgba(0, 255, 0, 0.5) !important; +} + [data-design-theme="terminal"] body::before { background: repeating-linear-gradient( @@ -1945,3 +2571,169 @@ audio { background-color: #1a8cd8 !important; /* Darker blue on hover */ opacity: 1 !important; } + +/* Terminal Theme - Cache Page */ +[data-design-theme="terminal"] .cache-page, +[data-design-theme="terminal"] .stats-section, +[data-design-theme="terminal"] .filters-section, +[data-design-theme="terminal"] .archive-section, +[data-design-theme="terminal"] .bulk-actions-section, +[data-design-theme="terminal"] .events-section { + background-color: #000000 !important; + border-color: #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .back-button, +[data-design-theme="terminal"] .bulk-action-button, +[data-design-theme="terminal"] .action-button, +[data-design-theme="terminal"] .load-more-button, +[data-design-theme="terminal"] .clear-kind-button { + background-color: rgba(0, 255, 0, 0.1) !important; + border: 1px solid #00ff00 !important; + color: #00ff00 !important; + box-shadow: 0 0 5px rgba(0, 255, 0, 0.3) !important; +} + +[data-design-theme="terminal"] .back-button:hover:not(:disabled), +[data-design-theme="terminal"] .bulk-action-button:hover:not(:disabled), +[data-design-theme="terminal"] .action-button:hover:not(:disabled), +[data-design-theme="terminal"] .load-more-button:hover:not(:disabled), +[data-design-theme="terminal"] .clear-kind-button:hover:not(:disabled) { + background-color: rgba(0, 255, 0, 0.2) !important; + box-shadow: 0 0 10px rgba(0, 255, 0, 0.6) !important; +} + +[data-design-theme="terminal"] .stat-card, +[data-design-theme="terminal"] .kind-item, +[data-design-theme="terminal"] .event-card { + background-color: rgba(0, 255, 0, 0.05) !important; + border-color: #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .kind-item:hover { + background-color: rgba(0, 255, 0, 0.15) !important; + box-shadow: 0 0 8px rgba(0, 255, 0, 0.4) !important; +} + +[data-design-theme="terminal"] .kind-item:hover .kind-name, +[data-design-theme="terminal"] .kind-item:hover .kind-count { + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .stat-label, +[data-design-theme="terminal"] .stat-value, +[data-design-theme="terminal"] .kind-name, +[data-design-theme="terminal"] .kind-count, +[data-design-theme="terminal"] .event-id, +[data-design-theme="terminal"] .event-meta, +[data-design-theme="terminal"] .event-content-preview { + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .filter-label, +[data-design-theme="terminal"] .filter-input, +[data-design-theme="terminal"] .filter-select { + background-color: rgba(0, 255, 0, 0.05) !important; + border-color: #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .filter-input:focus, +[data-design-theme="terminal"] .filter-select:focus { + outline: 2px solid #00ff00 !important; + box-shadow: 0 0 8px rgba(0, 255, 0, 0.4) !important; +} + +[data-design-theme="terminal"] .event-id-code, +[data-design-theme="terminal"] .event-json { + background-color: rgba(0, 255, 0, 0.05) !important; + border-color: #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .event-json code, +[data-design-theme="terminal"] .event-meta code { + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .action-button.delete-action { + border-color: #ff0000 !important; + color: #ff0000 !important; +} + +[data-design-theme="terminal"] .action-button.delete-action:hover { + background-color: rgba(255, 0, 0, 0.2) !important; + box-shadow: 0 0 10px rgba(255, 0, 0, 0.6) !important; +} + +[data-design-theme="terminal"] .action-button.delete-request-action { + border-color: #ffaa00 !important; + color: #ffaa00 !important; +} + +[data-design-theme="terminal"] .action-button.delete-request-action:hover { + background-color: rgba(255, 170, 0, 0.2) !important; + box-shadow: 0 0 10px rgba(255, 170, 0, 0.6) !important; +} + +[data-design-theme="terminal"] .archive-stats p, +[data-design-theme="terminal"] .archive-note, +[data-design-theme="terminal"] .recover-event-section h3 { + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .loading-state, +[data-design-theme="terminal"] .empty-state { + color: #00ff00 !important; +} + +/* Terminal Theme - About Page */ +[data-design-theme="terminal"] .about-page, +[data-design-theme="terminal"] .about-section { + background-color: #000000 !important; + border-color: #00ff00 !important; + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .back-button { + background-color: rgba(0, 255, 0, 0.1) !important; + border: 1px solid #00ff00 !important; + color: #00ff00 !important; + box-shadow: 0 0 5px rgba(0, 255, 0, 0.3) !important; +} + +[data-design-theme="terminal"] .back-button:hover { + background-color: rgba(0, 255, 0, 0.2) !important; + box-shadow: 0 0 10px rgba(0, 255, 0, 0.6) !important; +} + +[data-design-theme="terminal"] .section-title, +[data-design-theme="terminal"] .section-content, +[data-design-theme="terminal"] .section-content p { + color: #00ff00 !important; +} + +[data-design-theme="terminal"] .link { + color: #00ff00 !important; + text-decoration: underline; +} + +[data-design-theme="terminal"] .link:hover { + color: #00ff00 !important; + text-shadow: 0 0 5px rgba(0, 255, 0, 0.8) !important; +} + +[data-design-theme="terminal"] .version-badge { + background-color: rgba(0, 255, 0, 0.2) !important; + border: 1px solid #00ff00 !important; + color: #00ff00 !important; + box-shadow: 0 0 5px rgba(0, 255, 0, 0.3) !important; +} + +[data-design-theme="terminal"] .features-list li, +[data-design-theme="terminal"] .changelog-list li, +[data-design-theme="terminal"] .links-list li { + color: #00ff00 !important; +} diff --git a/src/lib/components/content/FileExplorer.svelte b/src/lib/components/content/FileExplorer.svelte index 9a9298d..dcaa3c4 100644 --- a/src/lib/components/content/FileExplorer.svelte +++ b/src/lib/components/content/FileExplorer.svelte @@ -1,5 +1,8 @@
@@ -346,13 +457,37 @@

Error: {contentError}

- {:else if selectedFile && fileContent !== null} + {:else if selectedFile && (fileContent !== null || fileUrl)}

{selectedFile.path}

{formatFileSize(selectedFile.size)}
-
{fileContent}
+ {#if isImageFile(selectedFile) && fileUrl} +
+ {selectedFile.name} +
+ {:else if isVideoFile(selectedFile) && fileUrl} +
+ +
+ {:else if isAudioFile(selectedFile) && fileUrl} +
+ +
+ {:else if fileContent !== null} + {#if isCodeFile(selectedFile)} +
{fileContent}
+ {:else} +
{fileContent}
+ {/if} + {/if}
{:else}
@@ -599,16 +734,128 @@ .file-content-code { margin: 0; - font-family: 'Courier New', Courier, monospace; - font-size: 0.875rem; - line-height: 1.5; - color: var(--fog-text, #1f2937); - white-space: pre-wrap; + background: #1e1e1e !important; /* VS Code dark background, same as JSON preview */ + border: 1px solid #3e3e3e; + border-radius: 4px; + padding: 1rem; + overflow-x: auto; + white-space: pre; word-wrap: break-word; } :global(.dark) .file-content-code { - color: var(--fog-dark-text, #f9fafb); + background: #1e1e1e !important; + border-color: #3e3e3e; + } + + .file-content-code code { + display: block; + overflow-x: auto; + padding: 0; + background: transparent !important; + color: #d4d4d4; /* VS Code text color */ + font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Droid Sans Mono', 'Source Code Pro', monospace; + font-size: 0.875rem; + line-height: 1.5; + } + + .file-content-code :global(code.hljs), + .file-content-code :global(code.hljs *), + .file-content-code :global(code.hljs span), + .file-content-code :global(code.hljs .hljs-keyword), + .file-content-code :global(code.hljs .hljs-string), + .file-content-code :global(code.hljs .hljs-comment), + .file-content-code :global(code.hljs .hljs-number), + .file-content-code :global(code.hljs .hljs-function), + .file-content-code :global(code.hljs .hljs-variable), + .file-content-code :global(code.hljs .hljs-class), + .file-content-code :global(code.hljs .hljs-title), + .file-content-code :global(code.hljs .hljs-attr), + .file-content-code :global(code.hljs .hljs-tag), + .file-content-code :global(code.hljs .hljs-name), + .file-content-code :global(code.hljs .hljs-selector-id), + .file-content-code :global(code.hljs .hljs-selector-class), + .file-content-code :global(code.hljs .hljs-attribute), + .file-content-code :global(code.hljs .hljs-built_in), + .file-content-code :global(code.hljs .hljs-literal), + .file-content-code :global(code.hljs .hljs-type), + .file-content-code :global(code.hljs .hljs-property), + .file-content-code :global(code.hljs .hljs-operator), + .file-content-code :global(code.hljs .hljs-punctuation), + .file-content-code :global(code.hljs .hljs-meta), + .file-content-code :global(code.hljs .hljs-doctag), + .file-content-code :global(code.hljs .hljs-section), + .file-content-code :global(code.hljs .hljs-addition), + .file-content-code :global(code.hljs .hljs-deletion), + .file-content-code :global(code.hljs .hljs-emphasis), + .file-content-code :global(code.hljs .hljs-strong) { + text-shadow: none !important; + -webkit-font-smoothing: antialiased !important; + -moz-osx-font-smoothing: grayscale !important; + font-smoothing: antialiased !important; + text-rendering: geometricPrecision !important; + transform: translateZ(0); + backface-visibility: hidden; + filter: none !important; + will-change: auto; + } + + .file-image-container { + display: flex; + justify-content: center; + align-items: center; + padding: 1rem; + background: var(--fog-highlight, #f3f4f6); + border-radius: 0.375rem; + border: 1px solid var(--fog-border, #e5e7eb); + } + + :global(.dark) .file-image-container { + background: var(--fog-dark-highlight, #374151); + border-color: var(--fog-dark-border, #475569); + } + + .file-image { + max-width: 100%; + max-height: 70vh; + height: auto; + border-radius: 0.25rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + } + + :global(.dark) .file-image { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + } + + .file-media-container { + display: flex; + justify-content: center; + align-items: center; + padding: 1rem; + background: var(--fog-highlight, #f3f4f6); + border-radius: 0.375rem; + border: 1px solid var(--fog-border, #e5e7eb); + } + + :global(.dark) .file-media-container { + background: var(--fog-dark-highlight, #374151); + border-color: var(--fog-dark-border, #475569); + } + + .file-video { + max-width: 100%; + max-height: 70vh; + border-radius: 0.25rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + } + + :global(.dark) .file-video { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + } + + .file-audio { + width: 100%; + max-width: 600px; } .file-content-loading, diff --git a/src/lib/components/layout/UnifiedSearch.svelte b/src/lib/components/layout/UnifiedSearch.svelte index 1ea56d1..620b7d3 100644 --- a/src/lib/components/layout/UnifiedSearch.svelte +++ b/src/lib/components/layout/UnifiedSearch.svelte @@ -43,7 +43,7 @@ let cacheProfiles: string[] = []; // Map to track which relay each event came from const eventRelayMap = new Map(); - const CACHE_SEARCH_DEBOUNCE = 500; // 500ms debounce for cache search + const CACHE_SEARCH_DEBOUNCE = 1000; // 1000ms debounce for cache search // Clear results at start of search function clearResults() { @@ -1073,7 +1073,7 @@ searching = true; searchTimeout = setTimeout(() => { performSearch(); - }, 300); + }, 800); } else { searchResults = []; showResults = false; diff --git a/src/lib/components/write/CreateEventForm.svelte b/src/lib/components/write/CreateEventForm.svelte index 9f62a68..43e215c 100644 --- a/src/lib/components/write/CreateEventForm.svelte +++ b/src/lib/components/write/CreateEventForm.svelte @@ -98,6 +98,7 @@ let publicationResults = $state<{ success: string[]; failed: Array<{ relay: string; error: string }> } | null>(null); let showJsonModal = $state(false); let showPreviewModal = $state(false); + let exampleJsonPreviewRef: HTMLElement | null = $state(null); let previewContent = $state(''); let previewEvent = $state(null); let showExampleModal = $state(false); @@ -201,6 +202,21 @@ const exampleJSON = $derived(getExampleJSON()); + // Highlight example JSON when it changes + $effect(() => { + if (exampleJsonPreviewRef && exampleJSON && exampleJsonPreviewRef instanceof HTMLElement) { + try { + const highlighted = hljs.highlight(exampleJSON, { language: 'json' }).value; + exampleJsonPreviewRef.innerHTML = highlighted; + exampleJsonPreviewRef.className = 'hljs language-json'; + } catch (err) { + // Fallback to plain text if highlighting fails + exampleJsonPreviewRef.textContent = exampleJSON; + exampleJsonPreviewRef.className = 'language-json'; + } + } + }); + const isKind30040 = $derived(selectedKind === KIND.PUBLICATION_INDEX); const isKind10895 = $derived(selectedKind === KIND.RSS_FEED); @@ -888,7 +904,7 @@
-
{JSON.stringify(foundEvent, null, 2)}
+
{JSON.stringify(foundEvent, null, 2)}
+ {#if isLoggedIn && showReplyForm} +
+ { + showReplyForm = false; + }} + onCancel={() => { + showReplyForm = false; + }} + /> +
+ {/if} +
{getKindInfo(highlight.kind).number} {getKindInfo(highlight.kind).description} diff --git a/src/lib/modules/reactions/FeedReactionButtons.svelte b/src/lib/modules/reactions/FeedReactionButtons.svelte index 19bf911..99459c6 100644 --- a/src/lib/modules/reactions/FeedReactionButtons.svelte +++ b/src/lib/modules/reactions/FeedReactionButtons.svelte @@ -411,7 +411,16 @@ content: '' }; - const relays = relayManager.getReactionPublishRelays(); + // Extract relay hints from the event being reacted to (r tags) + const reactionRelayHints = event.tags + .filter(tag => tag[0] === 'r' && tag[1]) + .map(tag => tag[1]) + .filter((url): url is string => { + // Validate it's a websocket URL + return typeof url === 'string' && (url.startsWith('ws://') || url.startsWith('wss://')); + }); + + const relays = relayManager.getReactionPublishRelays(reactionRelayHints); const results = await signAndPublish(deletionEvent, relays); publicationResults = results; publicationModalOpen = true; @@ -475,8 +484,17 @@ // Sign the event first to get the ID const signedEvent = await sessionManager.signEvent(reactionEvent); - // Publish the signed event using reaction publish relays (filters read-only relays) - const relays = relayManager.getReactionPublishRelays(); + // Extract relay hints from the event being reacted to (r tags) + const reactionRelayHints = event.tags + .filter(tag => tag[0] === 'r' && tag[1]) + .map(tag => tag[1]) + .filter((url): url is string => { + // Validate it's a websocket URL + return typeof url === 'string' && (url.startsWith('ws://') || url.startsWith('wss://')); + }); + + // Publish the signed event using reaction publish relays (includes relay hints) + const relays = relayManager.getReactionPublishRelays(reactionRelayHints); const results = await nostrClient.publish(signedEvent, { relays }); publicationResults = results; publicationModalOpen = true; diff --git a/src/lib/services/content/git-repo-fetcher.ts b/src/lib/services/content/git-repo-fetcher.ts index 94371ce..19dfe02 100644 --- a/src/lib/services/content/git-repo-fetcher.ts +++ b/src/lib/services/content/git-repo-fetcher.ts @@ -199,11 +199,12 @@ async function fetchFromGitHub(owner: string, repo: string): Promise { + const readmeData = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${readmeFile}?ref=${defaultBranch}`).then(r => { if (!r.ok) throw new Error('Not found'); return r.json(); }); @@ -220,6 +221,45 @@ async function fetchFromGitHub(owner: string, repo: string): Promise 0) { + const readmePatterns = [/^readme\.adoc$/i, /^readme\.md$/i, /^readme\.rst$/i, /^readme\.txt$/i, /^readme$/i]; + let readmePath: string | null = null; + for (const file of files) { + if (file.type === 'file') { + const fileName = file.name; + for (const pattern of readmePatterns) { + if (pattern.test(fileName)) { + readmePath = file.path; + break; + } + } + if (readmePath) break; + } + } + + // If found in tree, fetch it + if (readmePath) { + try { + const readmeData = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${readmePath}?ref=${defaultBranch}`).then(r => { + if (!r.ok) throw new Error('Not found'); + return r.json(); + }); + if (readmeData.content) { + const content = atob(readmeData.content.replace(/\s/g, '')); + const format = readmePath.toLowerCase().endsWith('.adoc') ? 'asciidoc' : 'markdown'; + readme = { + path: readmePath, + content, + format + }; + } + } catch { + // Failed to fetch from tree path + } + } + } return { name: repoData.name, @@ -283,6 +323,7 @@ async function fetchFromGitLab(owner: string, repo: string, baseUrl: string): Pr } // Try to fetch README (prioritize .adoc over .md) + // First try root directory (most common case) let readme: { path: string; content: string; format: 'markdown' | 'asciidoc' } | undefined; const readmeFiles = ['README.adoc', 'README.md', 'README.rst', 'README.txt']; for (const readmeFile of readmeFiles) { @@ -301,6 +342,42 @@ async function fetchFromGitLab(owner: string, repo: string, baseUrl: string): Pr continue; // Try next file } } + + // If not found in root, search the file tree (case-insensitive) + if (!readme && files.length > 0) { + const readmePatterns = [/^readme\.adoc$/i, /^readme\.md$/i, /^readme\.rst$/i, /^readme\.txt$/i, /^readme$/i]; + let readmePath: string | null = null; + for (const file of files) { + if (file.type === 'file') { + const fileName = file.name; + for (const pattern of readmePatterns) { + if (pattern.test(fileName)) { + readmePath = file.path; + break; + } + } + if (readmePath) break; + } + } + + // If found in tree, fetch it + if (readmePath) { + try { + const fileData = await fetch(`${baseUrl}/projects/${encodedPath}/repository/files/${encodeURIComponent(readmePath)}/raw?ref=${repoData.default_branch}`).then(r => { + if (!r.ok) throw new Error('Not found'); + return r.text(); + }); + const format = readmePath.toLowerCase().endsWith('.adoc') ? 'asciidoc' : 'markdown'; + readme = { + path: readmePath, + content: fileData, + format + }; + } catch { + // Failed to fetch from tree path + } + } + } return { name: repoData.name, @@ -361,6 +438,7 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro } // Try to fetch README (prioritize .adoc over .md) + // First try root directory (most common case) let readme: { path: string; content: string; format: 'markdown' | 'asciidoc' } | undefined; const readmeFiles = ['README.adoc', 'README.md', 'README.rst', 'README.txt']; for (const readmeFile of readmeFiles) { @@ -382,6 +460,45 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro continue; // Try next file } } + + // If not found in root, search the file tree (case-insensitive) + if (!readme && files.length > 0) { + const readmePatterns = [/^readme\.adoc$/i, /^readme\.md$/i, /^readme\.rst$/i, /^readme\.txt$/i, /^readme$/i]; + let readmePath: string | null = null; + for (const file of files) { + if (file.type === 'file') { + const fileName = file.name; + for (const pattern of readmePatterns) { + if (pattern.test(fileName)) { + readmePath = file.path; + break; + } + } + if (readmePath) break; + } + } + + // If found in tree, fetch it + if (readmePath) { + try { + const fileData = await fetch(`${baseUrl}/repos/${owner}/${repo}/contents/${readmePath}?ref=${repoData.default_branch}`).then(r => { + if (!r.ok) throw new Error('Not found'); + return r.json(); + }); + if (fileData.content) { + const content = atob(fileData.content.replace(/\s/g, '')); + const format = readmePath.toLowerCase().endsWith('.adoc') ? 'asciidoc' : 'markdown'; + readme = { + path: readmePath, + content, + format + }; + } + } catch { + // Failed to fetch from tree path + } + } + } return { name: repoData.name, diff --git a/src/lib/services/nostr/relay-manager.ts b/src/lib/services/nostr/relay-manager.ts index 7f3d4ad..705d8c2 100644 --- a/src/lib/services/nostr/relay-manager.ts +++ b/src/lib/services/nostr/relay-manager.ts @@ -326,9 +326,48 @@ class RelayManager { /** * Get relays for publishing reactions (kind 7) + * If reacting to an event, include relay hints from that event + * Includes user outbox and local relays, excludes read-only relays (aggregators) */ - getReactionPublishRelays(): string[] { - return this.getPublishRelays(config.defaultRelays); + getReactionPublishRelays(reactionRelayHints?: string[]): string[] { + // Start with default relays, excluding read-only relays + const baseRelays = config.defaultRelays.filter( + relay => !config.readOnlyRelays.includes(relay) + ); + + // Get publish relays (includes user outbox and local relays) + let relays = this.getPublishRelays(baseRelays, true); + + // If reacting to an event with relay hints, add them (but filter out read-only) + if (reactionRelayHints && reactionRelayHints.length > 0) { + const filteredHints = reactionRelayHints.filter( + relay => !config.readOnlyRelays.includes(relay) + ); + relays = [...relays, ...filteredHints]; + } + + // Normalize and filter after combining all relays + relays = this.normalizeRelays(relays); + relays = this.filterBlocked(relays); + + // Ensure we always have at least the default relays (minus aggregators) to publish to + if (relays.length === 0) { + // Fallback to default relays if we somehow ended up with an empty list + relays = baseRelays.length > 0 ? baseRelays : config.defaultRelays.filter( + relay => !config.readOnlyRelays.includes(relay) + ); + } + + // Log for debugging + console.log('[RelayManager] Reaction publish relays:', { + baseRelays, + userOutbox: this.userOutbox, + userLocalRelaysWrite: this.userLocalRelaysWrite, + reactionRelayHints, + finalRelays: relays + }); + + return relays; } /** @@ -355,6 +394,16 @@ class RelayManager { updateBlockedRelays(blocked: Set): void { this.blockedRelays = blocked; } + + /** + * Get local relays (kind 10432) - both read and write + */ + getLocalRelays(): string[] { + const allLocalRelays = new Set(); + this.userLocalRelaysRead.forEach(r => allLocalRelays.add(r)); + this.userLocalRelaysWrite.forEach(r => allLocalRelays.add(r)); + return Array.from(allLocalRelays); + } } export const relayManager = new RelayManager(); diff --git a/src/routes/feed/relay/[relay]/+page.svelte b/src/routes/feed/relay/[relay]/+page.svelte index 5891642..acde373 100644 --- a/src/routes/feed/relay/[relay]/+page.svelte +++ b/src/routes/feed/relay/[relay]/+page.svelte @@ -12,19 +12,36 @@ function decodeRelayUrl(encoded: string): string | null { try { - // The relay parameter is just the domain (e.g., "nostr.wine" or "theforest.nostr1.com") - // Construct the full wss:// URL - const domain = encoded.trim(); + // The relay parameter might be just the domain or might include protocol and port + let relayUrl = encoded.trim(); - // Validate it looks like a domain - if (!domain || domain.includes('/') || domain.includes(':')) { + // If it already has a protocol, use it as-is + if (relayUrl.startsWith('ws://') || relayUrl.startsWith('wss://')) { + return relayUrl; + } + + // Check if it's prefixed with ws- to indicate ws:// protocol + if (relayUrl.startsWith('ws-')) { + relayUrl = relayUrl.substring(3); // Remove ws- prefix + return `ws://${relayUrl}`; + } + + // Validate it looks like a domain (may include port) + if (!relayUrl || relayUrl.includes('/')) { return null; } - // Construct wss:// URL (assume wss:// unless it's localhost) - const relayUrl = domain.startsWith('localhost') || domain.startsWith('127.0.0.1') - ? `ws://${domain}` - : `wss://${domain}`; + // Check if port is included (format: hostname:port) + const hasPort = relayUrl.includes(':') && !relayUrl.startsWith('localhost') && !relayUrl.startsWith('127.0.0.1') && !relayUrl.startsWith('192.168.') && !relayUrl.startsWith('10.') && !relayUrl.startsWith('172.'); + const portMatch = relayUrl.match(/^([^:]+):(\d+)$/); + + // Construct URL (preserve ws:// for localhost/127.0.0.1/local IPs, use wss:// for others) + // Preserve port if it was in the encoded string + if (relayUrl.startsWith('localhost') || relayUrl.startsWith('127.0.0.1') || relayUrl.startsWith('192.168.') || relayUrl.startsWith('10.') || relayUrl.startsWith('172.')) { + relayUrl = `ws://${relayUrl}`; + } else { + relayUrl = `wss://${relayUrl}`; + } return relayUrl; } catch (e) { diff --git a/src/routes/find/+page.svelte b/src/routes/find/+page.svelte index c7cecdb..cb73a1a 100644 --- a/src/routes/find/+page.svelte +++ b/src/routes/find/+page.svelte @@ -147,27 +147,8 @@ {/if}
-
-
- -
- -
- -
- -
- -
- - {#if cacheResults.events.length > 0 || cacheResults.profiles.length > 0 || searchResults.events.length > 0 || searchResults.profiles.length > 0} -
+ {#if cacheResults.events.length > 0 || cacheResults.profiles.length > 0 || searchResults.events.length > 0 || searchResults.profiles.length > 0} +

Search Results

{#if cacheResults.events.length > 0 || cacheResults.profiles.length > 0} @@ -249,7 +230,26 @@
{/if} - {/if} + {/if} + +
+
+ +
+ +
+ +
+ +
+ +
@@ -325,6 +325,10 @@ gap: 1.5rem; } + .find-sections.collapsed { + display: none; + } + @media (min-width: 640px) { .find-sections { gap: 2rem; @@ -364,7 +368,8 @@ .results-section { - margin-top: 1.5rem; + margin-top: 0; + margin-bottom: 1.5rem; border: 1px solid var(--fog-border, #e5e7eb); border-radius: 0.5rem; padding: 1rem; @@ -374,7 +379,7 @@ @media (min-width: 640px) { .results-section { padding: 1.5rem; - margin-top: 2rem; + margin-bottom: 2rem; } } diff --git a/src/routes/highlights/+page.svelte b/src/routes/highlights/+page.svelte index f2bc5b4..bc6da42 100644 --- a/src/routes/highlights/+page.svelte +++ b/src/routes/highlights/+page.svelte @@ -237,7 +237,7 @@

No highlights found.

{:else} -
+
allRelays.add(r)); + // Add local relays if user is logged in + const currentPubkey = sessionManager.getCurrentPubkey(); + if (currentPubkey) { + // Ensure relay manager has loaded user preferences + await relayManager.loadUserPreferences(currentPubkey); + // Get local relays (read and write combined) + const localRelays = relayManager.getLocalRelays(); + localRelays.forEach(r => allRelays.add(r)); + } + // Get connection status from nostrClient const connectedRelays = nostrClient.getConnectedRelays(); - const relayList: RelayInfo[] = Array.from(allRelays).map(url => ({ - url, - categories: categorizeRelay(url), - connected: connectedRelays.includes(url) - })); + const relayList: RelayInfo[] = Array.from(allRelays).map(url => { + const categories = categorizeRelay(url); + // Check if it's a local relay + const isLocalRelay = currentPubkey && !config.defaultRelays.includes(url) && + !config.profileRelays.includes(url) && + !config.threadPublishRelays.includes(url) && + !config.gifRelays.includes(url); + if (isLocalRelay && !categories.includes('Local')) { + categories.push('Local'); + } + return { + url, + categories, + connected: connectedRelays.includes(url) + }; + }); // Sort by first category, then by URL relayList.sort((a, b) => { @@ -100,9 +121,13 @@ let url = tag[1].trim(); // Remove trailing slash url = url.replace(/\/$/, ''); - // If no protocol, assume wss:// + // If no protocol, use ws:// for local addresses, wss:// for others if (!url.startsWith('ws://') && !url.startsWith('wss://')) { - url = `wss://${url}`; + if (url.startsWith('localhost') || url.startsWith('127.0.0.1') || url.startsWith('192.168.') || url.startsWith('10.') || url.startsWith('172.')) { + url = `ws://${url}`; + } else { + url = `wss://${url}`; + } } favoriteRelayUrls.add(url); } @@ -187,18 +212,26 @@ } function handleRelayClick(url: string) { - // Extract hostname from relay URL (e.g., wss://thecitadel.nostr1.com -> thecitadel.nostr1.com) - // The route expects just the domain without protocol or port + // Extract hostname and port from relay URL for navigation + // The route expects domain with optional port, preserving protocol info for ws:// let relayPath = url; try { const urlObj = new URL(url); - relayPath = urlObj.hostname; + // Include port if present + relayPath = urlObj.port ? `${urlObj.hostname}:${urlObj.port}` : urlObj.hostname; + // If it's ws:// and not localhost, prefix with ws- to indicate ws:// protocol + if (urlObj.protocol === 'ws:' && !relayPath.startsWith('localhost') && !relayPath.startsWith('127.0.0.1')) { + relayPath = `ws-${relayPath}`; + } } catch { - // If URL parsing fails, try to extract hostname manually - // Remove protocol (wss:// or ws://) and trailing slash + // If URL parsing fails, try to extract manually + const protocol = url.startsWith('ws://') ? 'ws://' : 'wss://'; relayPath = url.replace(/^wss?:\/\//, '').replace(/\/$/, ''); - // Remove port if present (route doesn't support ports in the parameter) - relayPath = relayPath.split(':')[0]; + // Keep port if present (don't split on colon if it's part of hostname:port) + // If it's ws:// and not localhost, prefix with ws- + if (protocol === 'ws://' && !relayPath.startsWith('localhost') && !relayPath.startsWith('127.0.0.1')) { + relayPath = `ws-${relayPath}`; + } } // Navigate to feed page with relay filter goto(`/feed/relay/${relayPath}`); @@ -210,12 +243,17 @@ // Validate URL format try { - // Try to parse as URL, add protocol if missing - let fullUrl = url; - if (!url.startsWith('ws://') && !url.startsWith('wss://')) { - fullUrl = `wss://${url}`; - } - new URL(fullUrl); // Validate URL format + // Try to parse as URL, add protocol if missing + let fullUrl = url; + if (!url.startsWith('ws://') && !url.startsWith('wss://')) { + // Use ws:// for local addresses, wss:// for others + if (url.startsWith('localhost') || url.startsWith('127.0.0.1') || url.startsWith('192.168.') || url.startsWith('10.') || url.startsWith('172.')) { + fullUrl = `ws://${url}`; + } else { + fullUrl = `wss://${url}`; + } + } + new URL(fullUrl); // Validate URL format // Navigate to the relay handleRelayClick(fullUrl); @@ -308,7 +346,7 @@ {/if} - {#each ['Default', 'Profile', 'Thread Publish', 'GIF', 'Other'] as category} + {#each ['Local', 'Default', 'Profile', 'Thread Publish', 'GIF', 'Other'] as category} {@const categoryRelays = relays.filter(r => r.categories.includes(category))} {#if categoryRelays.length > 0}
diff --git a/src/routes/repos/+page.svelte b/src/routes/repos/+page.svelte index 7b6b400..a345b4a 100644 --- a/src/routes/repos/+page.svelte +++ b/src/routes/repos/+page.svelte @@ -11,12 +11,17 @@ import { goto } from '$app/navigation'; import { getRecentCachedEvents } from '../../lib/services/cache/event-cache.js'; import { KIND } from '../../lib/types/kind-lookup.js'; + import { sessionManager } from '../../lib/services/auth/session-manager.js'; let repos = $state([]); let loading = $state(true); let filterResult = $state<{ type: 'event' | 'pubkey' | 'text' | null; value: string | null }>({ type: null, value: null }); let searchResults = $state<{ events: NostrEvent[]; profiles: string[] }>({ events: [], profiles: [] }); let unifiedSearchComponent: { triggerSearch: () => void } | null = $state(null); + let searchQuery = $state(''); + let showMyRepos = $state(false); + const isLoggedIn = $derived(sessionManager.isLoggedIn()); + const currentPubkey = $derived(sessionManager.getCurrentPubkey()); function handleFilterChange(result: { type: 'event' | 'pubkey' | 'text' | null; value: string | null }) { filterResult = result; @@ -68,36 +73,38 @@ } async function loadRepos() { - // Only show loading spinner if we don't have cached repos + // Check cache first before making any network requests + // If we already have cached repos, we'll enhance with fresh data in the background const hasCachedRepos = repos.length > 0; + + // Only show loading spinner if we don't have cached repos if (!hasCachedRepos) { loading = true; } + try { const relays = relayManager.getProfileReadRelays(); - // Fetch repo announcement events - const allRepos: NostrEvent[] = []; - - // Stream fresh data from relays (progressive enhancement) + // Fetch repo announcement events with cache-first strategy + // This will check cache before making network requests const events = await nostrClient.fetchEvents( [{ kinds: [KIND.REPO_ANNOUNCEMENT], limit: 100 }], relays, { - useCache: 'cache-first', // Already shown cache above, now stream updates - cacheResults: true, + useCache: 'cache-first', // Check cache first, then fetch from relays if needed + cacheResults: true, // Cache any new results onUpdate: (newRepos) => { - // Merge with existing repos as they stream in + // Merge with existing repos as they stream in (progressive enhancement) const reposByKey = new Map(); - // Add existing repos first + // Add existing repos first (from cache) for (const repo of repos) { const dTag = repo.tags.find(t => t[0] === 'd')?.[1] || ''; const key = `${repo.pubkey}:${dTag}`; reposByKey.set(key, repo); } - // Add/update with new repos + // Add/update with new repos from network for (const event of newRepos) { const dTag = event.tags.find(t => t[0] === 'd')?.[1] || ''; const key = `${event.pubkey}:${dTag}`; @@ -112,7 +119,7 @@ } ); - // Final merge of any remaining events + // Final merge of any remaining events (in case onUpdate didn't catch them all) const reposByKey = new Map(); // Add existing cached repos first @@ -122,7 +129,7 @@ reposByKey.set(key, repo); } - // Add/update with new repos + // Add/update with new repos from network for (const event of events) { const dTag = event.tags.find(t => t[0] === 'd')?.[1] || ''; const key = `${event.pubkey}:${dTag}`; @@ -135,8 +142,11 @@ // Sort by created_at descending repos = Array.from(reposByKey.values()).sort((a, b) => b.created_at - a.created_at); } catch (error) { - // Failed to load repos - repos = []; + // Failed to load repos - but we might still have cached data + // Only clear repos if we don't have any cached data + if (!hasCachedRepos) { + repos = []; + } } finally { loading = false; } @@ -213,29 +223,193 @@ } } - // Filter repos based on search query and pubkey filter - let filteredRepos = $derived.by(() => { - let filtered = repos; + // Helper functions to extract repo data + function getCloneUrls(event: NostrEvent): string[] { + if (!Array.isArray(event.tags)) return []; + return event.tags + .filter(t => Array.isArray(t) && t[0] === 'clone' && t[1]) + .map(t => String(t[1])); + } + + function getWebUrls(event: NostrEvent): string[] { + if (!Array.isArray(event.tags)) return []; + return event.tags + .filter(t => Array.isArray(t) && t[0] === 'web' && t[1]) + .map(t => String(t[1])); + } + + function getMaintainers(event: NostrEvent): string[] { + if (!Array.isArray(event.tags)) return []; + const maintainerTag = event.tags.find(t => Array.isArray(t) && t[0] === 'maintainers'); + if (maintainerTag && maintainerTag.length > 1) { + return maintainerTag.slice(1).filter(m => m && typeof m === 'string') as string[]; + } + return []; + } + + function getDTagFromEvent(event: NostrEvent): string { + if (!Array.isArray(event.tags)) return ''; + const dTag = event.tags.find(t => Array.isArray(t) && t[0] === 'd'); + return dTag?.[1] || ''; + } + + + // Decode bech32 pubkey (npub or nprofile) to hex + function decodePubkeyToHex(input: string): string | null { + if (!input || input.trim() === '') return null; + + const trimmed = input.trim(); - // Filter by pubkey if provided - if (filterResult.value && filterResult.type === 'pubkey') { - const normalizedPubkey = filterResult.value.toLowerCase(); - if (/^[a-f0-9]{64}$/i.test(normalizedPubkey)) { - filtered = filtered.filter(repo => repo.pubkey.toLowerCase() === normalizedPubkey); + // If it's already hex (64 chars), return as-is + if (/^[a-f0-9]{64}$/i.test(trimmed)) { + return trimmed.toLowerCase(); + } + + // Try to decode bech32 + try { + if (trimmed.startsWith('npub')) { + const decoded = nip19.decode(trimmed); + if (decoded.type === 'npub') { + return decoded.data; + } + } else if (trimmed.startsWith('nprofile')) { + const decoded = nip19.decode(trimmed); + if (decoded.type === 'nprofile') { + return decoded.data.pubkey; + } + } + } catch (e) { + // Not a valid bech32 + } + + return null; + } + + // Decode bech32 event ID (nevent, naddr, or note) to hex + function decodeEventIdToHex(input: string): string | null { + if (!input || input.trim() === '') return null; + + const trimmed = input.trim(); + + // If it's already hex (64 chars), return as-is + if (/^[a-f0-9]{64}$/i.test(trimmed)) { + return trimmed.toLowerCase(); + } + + // Try to decode bech32 + try { + if (trimmed.startsWith('nevent')) { + const decoded = nip19.decode(trimmed); + if (decoded.type === 'nevent') { + return decoded.data.id; + } + } else if (trimmed.startsWith('naddr')) { + // naddr doesn't have an event ID directly, but we can check if it matches + const decoded = nip19.decode(trimmed); + if (decoded.type === 'naddr') { + // Return the naddr as-is for matching + return trimmed; + } + } else if (trimmed.startsWith('note')) { + const decoded = nip19.decode(trimmed); + if (decoded.type === 'note') { + return decoded.data; + } } + } catch (e) { + // Not a valid bech32 } - // Filter by text search if provided - if (filterResult.value && filterResult.type === 'text') { - const query = filterResult.value.toLowerCase(); + return null; + } + + // Filter repos based on search query and filters + let filteredRepos = $derived.by(() => { + let filtered = repos; + + // Filter by "See my repos" checkbox + if (showMyRepos && currentPubkey) { filtered = filtered.filter(repo => { - const name = getRepoName(repo).toLowerCase(); - const desc = getRepoDescription(repo).toLowerCase(); - return name.includes(query) || desc.includes(query); + // Check if repo owner matches + if (repo.pubkey.toLowerCase() === currentPubkey.toLowerCase()) { + return true; + } + // Check if user is a maintainer + const maintainers = getMaintainers(repo); + return maintainers.some(m => m.toLowerCase() === currentPubkey.toLowerCase()); + }); + } + + // If no search query, return filtered list + if (!searchQuery.trim()) { + return filtered; + } + + const query = searchQuery.trim().toLowerCase(); + + // Try to decode as pubkey (hex, npub, or nprofile) + const decodedPubkey = decodePubkeyToHex(query); + if (decodedPubkey) { + return filtered.filter(repo => { + // Match by owner pubkey + if (repo.pubkey.toLowerCase() === decodedPubkey) { + return true; + } + // Match by maintainer pubkey + const maintainers = getMaintainers(repo); + return maintainers.some(m => m.toLowerCase() === decodedPubkey); + }); + } + + // Try to decode as event ID (hex, note, nevent, or naddr) + const decodedEventId = decodeEventIdToHex(query); + if (decodedEventId) { + return filtered.filter(repo => { + // Match by event ID + if (repo.id.toLowerCase() === decodedEventId.toLowerCase()) { + return true; + } + // Match by naddr + const naddr = getNaddr(repo); + if (naddr && naddr.toLowerCase() === decodedEventId.toLowerCase()) { + return true; + } + return false; }); } - return filtered; + // Text search across all fields + return filtered.filter(repo => { + // Search in name + const name = getRepoName(repo).toLowerCase(); + if (name.includes(query)) return true; + + // Search in description + const desc = getRepoDescription(repo).toLowerCase(); + if (desc.includes(query)) return true; + + // Search in clone URLs + const cloneUrls = getCloneUrls(repo); + if (cloneUrls.some(url => url.toLowerCase().includes(query))) return true; + + // Search in web URLs + const webUrls = getWebUrls(repo); + if (webUrls.some(url => url.toLowerCase().includes(query))) return true; + + // Search in d-tag + const dTag = getDTagFromEvent(repo).toLowerCase(); + if (dTag.includes(query)) return true; + + // Search in naddr + const naddr = getNaddr(repo); + if (naddr && naddr.toLowerCase().includes(query)) return true; + + // Search in maintainer pubkeys (as hex) + const maintainers = getMaintainers(repo); + if (maintainers.some(m => m.toLowerCase().includes(query))) return true; + + return false; + }); }); @@ -244,20 +418,31 @@
-

/Repos

-

- Discover and explore repositories announced on Nostr -

+
+
+

/Repos

+

+ Discover and explore repositories announced on Nostr +

+
+ {#if isLoggedIn} + + {/if} +
-
@@ -334,6 +519,20 @@

{getRepoName(repo)}

Kind {repo.kind}
+
+ Owner: + +
+ {#if getMaintainers(repo).length > 0} +
+ Maintainers: +
+ {#each getMaintainers(repo) as maintainerPubkey} + + {/each} +
+
+ {/if} {#if getRepoDescription(repo)}

{getRepoDescription(repo)}

{/if} @@ -365,8 +564,79 @@ border-bottom-color: var(--fog-dark-border, #374151); } + .repos-header-top { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; + margin-bottom: 1rem; + } + + .my-repos-checkbox { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + padding: 0.5rem 1rem; + border: 1px solid var(--fog-border, #e5e7eb); + border-radius: 0.25rem; + background: var(--fog-post, #ffffff); + color: var(--fog-text, #1e293b); + white-space: nowrap; + user-select: none; + transition: all 0.2s; + } + + .my-repos-checkbox:hover { + background: var(--fog-highlight, #f1f5f9); + border-color: var(--fog-accent, #94a3b8); + } + + :global(.dark) .my-repos-checkbox { + background: var(--fog-dark-post, #334155); + border-color: var(--fog-dark-border, #475569); + color: var(--fog-dark-text, #f1f5f9); + } + + :global(.dark) .my-repos-checkbox:hover { + background: var(--fog-dark-highlight, #475569); + border-color: var(--fog-dark-accent, #64748b); + } + + .checkbox-input { + cursor: pointer; + } + .search-container { - max-width: 500px; + max-width: 100%; + } + + .repo-search-input { + width: 100%; + padding: 0.75rem; + border: 1px solid var(--fog-border, #e5e7eb); + border-radius: 0.375rem; + background: var(--fog-post, #ffffff); + color: var(--fog-text, #1e293b); + font-size: 1rem; + transition: all 0.2s; + } + + .repo-search-input:focus { + outline: none; + border-color: var(--fog-accent, #64748b); + box-shadow: 0 0 0 3px rgba(100, 116, 139, 0.1); + } + + :global(.dark) .repo-search-input { + background: var(--fog-dark-post, #334155); + border-color: var(--fog-dark-border, #475569); + color: var(--fog-dark-text, #f9fafb); + } + + :global(.dark) .repo-search-input:focus { + border-color: var(--fog-dark-accent, #94a3b8); + box-shadow: 0 0 0 3px rgba(148, 163, 184, 0.1); } .loading-state, @@ -449,6 +719,48 @@ background: var(--fog-dark-highlight, #374151); } + .repo-owner { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0.75rem 0; + font-size: 0.875rem; + } + + .repo-owner-label { + color: var(--fog-text-light, #52667a); + font-weight: 500; + } + + :global(.dark) .repo-owner-label { + color: var(--fog-dark-text-light, #a8b8d0); + } + + .repo-maintainers { + display: flex; + align-items: flex-start; + gap: 0.5rem; + margin: 0.75rem 0; + font-size: 0.875rem; + } + + .repo-maintainers-label { + color: var(--fog-text-light, #52667a); + font-weight: 500; + flex-shrink: 0; + } + + :global(.dark) .repo-maintainers-label { + color: var(--fog-dark-text-light, #a8b8d0); + } + + .repo-maintainers-list { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; + } + .repo-description { color: var(--fog-text-light, #52667a); margin: 0.5rem 0; diff --git a/src/routes/repos/[naddr]/+page.svelte b/src/routes/repos/[naddr]/+page.svelte index 1a73af1..5852bd7 100644 --- a/src/routes/repos/[naddr]/+page.svelte +++ b/src/routes/repos/[naddr]/+page.svelte @@ -60,9 +60,9 @@ } }); - // Load git repo when repository tab is clicked + // Load git repo when repository or about tab is clicked (about tab needs README) $effect(() => { - if (activeTab === 'repository' && repoEvent && !gitRepo && !loadingGitRepo && !gitRepoFetchAttempted) { + if ((activeTab === 'repository' || activeTab === 'about') && repoEvent && !gitRepo && !loadingGitRepo && !gitRepoFetchAttempted) { loadGitRepo(); } }); diff --git a/src/routes/rss/+page.svelte b/src/routes/rss/+page.svelte index 8fa1b4b..85699cc 100644 --- a/src/routes/rss/+page.svelte +++ b/src/routes/rss/+page.svelte @@ -415,8 +415,19 @@

{#if rssEvent} { + if (rssEvent) { + const cloneData = { + kind: rssEvent.kind, + content: rssEvent.content, + tags: rssEvent.tags, + isClone: false + }; + sessionStorage.setItem('aitherboard_cloneEvent', JSON.stringify(cloneData)); + } + }} > Edit RSS Feed Event