Browse Source

bug-fixes

master
Silberengel 4 weeks ago
parent
commit
7e0c729b1a
  1. 63
      src/lib/modules/profiles/ProfilePage.svelte
  2. 16
      src/lib/services/content/git-repo-fetcher.ts
  3. 77
      src/routes/api/gitea-proxy/[...path]/+server.ts
  4. 56
      src/routes/login/+page.svelte
  5. 39
      src/routes/repos/[naddr]/+page.svelte

63
src/lib/modules/profiles/ProfilePage.svelte

@ -12,6 +12,7 @@
import { sessionManager } from '../../services/auth/session-manager.js'; import { sessionManager } from '../../services/auth/session-manager.js';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import type { NostrEvent } from '../../types/nostr.js'; import type { NostrEvent } from '../../types/nostr.js';
import { KIND } from '../../types/kind-lookup.js'; import { KIND } from '../../types/kind-lookup.js';
@ -33,6 +34,48 @@
// Compute pubkey from route params // Compute pubkey from route params
let profilePubkey = $derived.by(() => decodePubkey($page.params.pubkey)); let profilePubkey = $derived.by(() => decodePubkey($page.params.pubkey));
// Initialize activeTab from URL parameter
function getTabFromUrl(): 'pins' | 'notifications' | 'interactions' | 'wall' {
const tabParam = $page.url.searchParams.get('tab');
const validTabs: Array<'pins' | 'notifications' | 'interactions' | 'wall'> = ['pins', 'notifications', 'interactions', 'wall'];
if (tabParam && validTabs.includes(tabParam as any)) {
return tabParam as 'pins' | 'notifications' | 'interactions' | 'wall';
}
return 'pins'; // Default
}
// Update activeTab when URL changes
$effect(() => {
const urlTab = getTabFromUrl();
if (urlTab !== activeTab) {
activeTab = urlTab;
// Load data for the tab if needed
if (activeTab === 'wall' && profileEvent && wallComments.length === 0 && !loadingWall) {
loadWallComments(profileEvent.id);
}
}
});
// Function to change tab and update URL
async function setActiveTab(tab: 'pins' | 'notifications' | 'interactions' | 'wall') {
activeTab = tab;
const url = new URL($page.url);
url.searchParams.set('tab', tab);
goto(url.pathname + url.search, { replaceState: true, noScroll: true });
// Handle tab-specific logic
if (tab === 'wall') {
// Load profile event if not loaded yet
if (!profileEvent && profilePubkey) {
await loadProfileEvent(profilePubkey);
}
// Load wall comments if profile event is available and not already loaded
if (profileEvent && wallComments.length === 0 && !loadingWall) {
await loadWallComments(profileEvent.id);
}
}
}
// Cache for NIP-05 validation results (nip05+pubkey -> result) // Cache for NIP-05 validation results (nip05+pubkey -> result)
// This prevents re-validating the same NIP-05 address repeatedly // This prevents re-validating the same NIP-05 address repeatedly
const nip05ValidationCache = new Map<string, boolean>(); const nip05ValidationCache = new Map<string, boolean>();
@ -402,6 +445,8 @@
onMount(async () => { onMount(async () => {
await nostrClient.initialize(); await nostrClient.initialize();
// Initialize tab from URL
activeTab = getTabFromUrl();
// Load profile after initialization // Load profile after initialization
if ($page.params.pubkey) { if ($page.params.pubkey) {
loadProfile(); loadProfile();
@ -775,37 +820,27 @@
<div class="profile-posts"> <div class="profile-posts">
<div class="tabs mb-4 flex gap-2 sm:gap-4 border-b border-fog-border dark:border-fog-dark-border overflow-x-auto scrollbar-hide"> <div class="tabs mb-4 flex gap-2 sm:gap-4 border-b border-fog-border dark:border-fog-dark-border overflow-x-auto scrollbar-hide">
<button <button
onclick={() => activeTab = 'pins'} onclick={() => setActiveTab('pins')}
class="px-2 sm:px-4 py-2 font-semibold whitespace-nowrap flex-shrink-0 {activeTab === 'pins' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}" class="px-2 sm:px-4 py-2 font-semibold whitespace-nowrap flex-shrink-0 {activeTab === 'pins' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}"
> >
Pins ({pins.length}) Pins ({pins.length})
</button> </button>
<button <button
onclick={async () => { onclick={() => setActiveTab('wall')}
activeTab = 'wall';
// Load profile event if not loaded yet
if (!profileEvent && profilePubkey) {
await loadProfileEvent(profilePubkey);
}
// Load wall comments if profile event is available and not already loaded
if (profileEvent && wallComments.length === 0 && !loadingWall) {
await loadWallComments(profileEvent.id);
}
}}
class="px-2 sm:px-4 py-2 font-semibold whitespace-nowrap flex-shrink-0 {activeTab === 'wall' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}" class="px-2 sm:px-4 py-2 font-semibold whitespace-nowrap flex-shrink-0 {activeTab === 'wall' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}"
> >
Wall ({wallComments.length}) Wall ({wallComments.length})
</button> </button>
{#if isOwnProfile} {#if isOwnProfile}
<button <button
onclick={() => activeTab = 'notifications'} onclick={() => setActiveTab('notifications')}
class="px-2 sm:px-4 py-2 font-semibold whitespace-nowrap flex-shrink-0 {activeTab === 'notifications' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}" class="px-2 sm:px-4 py-2 font-semibold whitespace-nowrap flex-shrink-0 {activeTab === 'notifications' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}"
> >
Notifications ({notifications.length}) Notifications ({notifications.length})
</button> </button>
{:else if currentUserPubkey && currentUserPubkey !== profilePubkey} {:else if currentUserPubkey && currentUserPubkey !== profilePubkey}
<button <button
onclick={() => activeTab = 'interactions'} onclick={() => setActiveTab('interactions')}
class="px-2 sm:px-4 py-2 font-semibold whitespace-nowrap flex-shrink-0 {activeTab === 'interactions' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}" class="px-2 sm:px-4 py-2 font-semibold whitespace-nowrap flex-shrink-0 {activeTab === 'interactions' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent' : ''}"
> >
Interactions ({interactionsWithMe.length}) Interactions ({interactionsWithMe.length})

16
src/lib/services/content/git-repo-fetcher.ts

@ -435,7 +435,9 @@ async function fetchFromGitLab(owner: string, repo: string, baseUrl: string): Pr
*/ */
async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Promise<GitRepoInfo | null> { async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Promise<GitRepoInfo | null> {
try { try {
const repoResponse = await fetch(`${baseUrl}/repos/${owner}/${repo}`); // Use proxy endpoint to avoid CORS issues
const proxyBaseUrl = encodeURIComponent(baseUrl);
const repoResponse = await fetch(`/api/gitea-proxy/${proxyBaseUrl}/repos/${owner}/${repo}`);
if (!repoResponse.ok) { if (!repoResponse.ok) {
console.warn(`Gitea API error for repo ${owner}/${repo}: ${repoResponse.status} ${repoResponse.statusText}`); console.warn(`Gitea API error for repo ${owner}/${repo}: ${repoResponse.status} ${repoResponse.statusText}`);
return null; return null;
@ -445,8 +447,8 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro
const defaultBranch = repoData.default_branch || 'master'; const defaultBranch = repoData.default_branch || 'master';
const [branchesResponse, commitsResponse] = await Promise.all([ const [branchesResponse, commitsResponse] = await Promise.all([
fetch(`${baseUrl}/repos/${owner}/${repo}/branches`).catch(() => null), fetch(`/api/gitea-proxy/${proxyBaseUrl}/repos/${owner}/${repo}/branches`).catch(() => null),
fetch(`${baseUrl}/repos/${owner}/${repo}/commits?limit=10`).catch(() => null) fetch(`/api/gitea-proxy/${proxyBaseUrl}/repos/${owner}/${repo}/commits?limit=10`).catch(() => null)
]); ]);
let branchesData: any[] = []; let branchesData: any[] = [];
@ -499,7 +501,7 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro
let files: GitFile[] = []; let files: GitFile[] = [];
try { try {
// Try the git/trees endpoint first (more complete) // Try the git/trees endpoint first (more complete)
const treeResponse = await fetch(`${baseUrl}/repos/${owner}/${repo}/git/trees/${defaultBranch}?recursive=1`).catch(() => null); const treeResponse = await fetch(`/api/gitea-proxy/${proxyBaseUrl}/repos/${owner}/${repo}/git/trees/${defaultBranch}?recursive=1`).catch(() => null);
if (treeResponse && treeResponse.ok) { if (treeResponse && treeResponse.ok) {
const treeData = await treeResponse.json(); const treeData = await treeResponse.json();
if (treeData.tree && Array.isArray(treeData.tree)) { if (treeData.tree && Array.isArray(treeData.tree)) {
@ -514,7 +516,7 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro
} }
} else { } else {
// Fallback to contents endpoint (only root directory) // Fallback to contents endpoint (only root directory)
const contentsResponse = await fetch(`${baseUrl}/repos/${owner}/${repo}/contents?ref=${defaultBranch}`).catch(() => null); const contentsResponse = await fetch(`/api/gitea-proxy/${proxyBaseUrl}/repos/${owner}/${repo}/contents?ref=${defaultBranch}`).catch(() => null);
if (contentsResponse && contentsResponse.ok) { if (contentsResponse && contentsResponse.ok) {
const contentsData = await contentsResponse.json(); const contentsData = await contentsResponse.json();
if (Array.isArray(contentsData)) { if (Array.isArray(contentsData)) {
@ -537,7 +539,7 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro
const readmeFiles = ['README.adoc', 'README.md', 'README.rst', 'README.txt']; const readmeFiles = ['README.adoc', 'README.md', 'README.rst', 'README.txt'];
for (const readmeFile of readmeFiles) { for (const readmeFile of readmeFiles) {
try { try {
const fileResponse = await fetch(`${baseUrl}/repos/${owner}/${repo}/contents/${readmeFile}?ref=${defaultBranch}`); const fileResponse = await fetch(`/api/gitea-proxy/${proxyBaseUrl}/repos/${owner}/${repo}/contents/${readmeFile}?ref=${defaultBranch}`);
if (!fileResponse.ok) throw new Error('Not found'); if (!fileResponse.ok) throw new Error('Not found');
const fileData = await fileResponse.json(); const fileData = await fileResponse.json();
if (fileData.content) { if (fileData.content) {
@ -576,7 +578,7 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro
// If found in tree, fetch it // If found in tree, fetch it
if (readmePath) { if (readmePath) {
try { try {
const fileResponse = await fetch(`${baseUrl}/repos/${owner}/${repo}/contents/${readmePath}?ref=${defaultBranch}`); const fileResponse = await fetch(`/api/gitea-proxy/${proxyBaseUrl}/repos/${owner}/${repo}/contents/${readmePath}?ref=${defaultBranch}`);
if (!fileResponse.ok) throw new Error('Not found'); if (!fileResponse.ok) throw new Error('Not found');
const fileData = await fileResponse.json(); const fileData = await fileResponse.json();
if (fileData.content) { if (fileData.content) {

77
src/routes/api/gitea-proxy/[...path]/+server.ts

@ -0,0 +1,77 @@
import type { RequestHandler } from '@sveltejs/kit';
/**
* Proxy endpoint for Gitea API requests to avoid CORS issues
* Usage: /api/gitea-proxy/{baseUrl}/repos/{owner}/{repo}/...
* Example: /api/gitea-proxy/https%3A%2F%2Fgit.imwald.eu/api/v1/repos/silberengel/aitherboard
*/
export const GET: RequestHandler = async ({ params, url }: { params: { path?: string }; url: URL }) => {
try {
// Reconstruct the full path from params
const pathParts = params.path?.split('/') || [];
// Extract base URL (first part should be the encoded base URL)
if (pathParts.length < 1) {
return new Response(JSON.stringify({ error: 'Missing base URL' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
// Decode the base URL (it's URL-encoded)
const baseUrl = decodeURIComponent(pathParts[0]);
// Reconstruct the API path (everything after the base URL)
const apiPath = pathParts.slice(1).join('/');
// Add query parameters from the original request
const queryString = url.search;
const fullUrl = `${baseUrl}/${apiPath}${queryString}`;
// Fetch from Gitea API
const response = await fetch(fullUrl, {
method: 'GET',
headers: {
'Accept': 'application/json',
'User-Agent': 'aitherboard/1.0'
}
});
// Get response body
const contentType = response.headers.get('content-type') || 'application/json';
const body = await response.text();
// Return response with CORS headers
return new Response(body, {
status: response.status,
statusText: response.statusText,
headers: {
'Content-Type': contentType,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
}
});
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Gitea proxy error:', message);
return new Response(JSON.stringify({ error: message }), {
status: 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
};
export const OPTIONS: RequestHandler = async (): Promise<Response> => {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
}
});
};

56
src/routes/login/+page.svelte

@ -11,6 +11,8 @@
onMount(async () => { onMount(async () => {
await nostrClient.initialize(); await nostrClient.initialize();
// Initialize tab from URL
activeTab = getTabFromUrl();
// Check NIP-07 availability dynamically // Check NIP-07 availability dynamically
const { isNIP07Available, waitForNIP07 } = await import('../../lib/services/auth/nip07-signer.js'); const { isNIP07Available, waitForNIP07 } = await import('../../lib/services/auth/nip07-signer.js');
@ -35,6 +37,42 @@
let nip07Available = $state(false); let nip07Available = $state(false);
let isPWA = $state(false); let isPWA = $state(false);
// Initialize activeTab from URL parameter
function getTabFromUrl(): 'nip07' | 'nsec' | 'anonymous' {
const tabParam = $page.url.searchParams.get('tab');
const validTabs: Array<'nip07' | 'nsec' | 'anonymous'> = ['nip07', 'nsec', 'anonymous'];
if (tabParam && validTabs.includes(tabParam as any)) {
return tabParam as 'nip07' | 'nsec' | 'anonymous';
}
return 'nip07'; // Default
}
// Update activeTab when URL changes
$effect(() => {
const urlTab = getTabFromUrl();
if (urlTab !== activeTab) {
activeTab = urlTab;
}
});
// Function to change tab and update URL
function setActiveTab(tab: 'nip07' | 'nsec' | 'anonymous') {
activeTab = tab;
error = null;
const url = new URL($page.url);
url.searchParams.set('tab', tab);
goto(url.pathname + url.search, { replaceState: true, noScroll: true });
// Handle tab-specific logic
if (tab === 'nsec') {
showNewNsecForm = storedNsecKeys.length === 0;
selectedNsecKey = null;
} else if (tab === 'anonymous') {
showNewAnonymousForm = storedAnonymousKeys.length === 0;
selectedAnonymousKey = null;
}
}
$effect(() => { $effect(() => {
isPWA = isPWAInstalled(); isPWA = isPWAInstalled();
// Re-check NIP-07 availability when PWA state changes // Re-check NIP-07 availability when PWA state changes
@ -347,31 +385,19 @@
<!-- Tab Navigation --> <!-- Tab Navigation -->
<div class="flex gap-2 mb-4 border-b border-fog-border dark:border-fog-dark-border"> <div class="flex gap-2 mb-4 border-b border-fog-border dark:border-fog-dark-border">
<button <button
onclick={() => { activeTab = 'nip07'; error = null; }} onclick={() => setActiveTab('nip07')}
class="px-4 py-2 {activeTab === 'nip07' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent text-fog-accent dark:text-fog-dark-accent' : 'text-fog-text-light dark:text-fog-dark-text-light'} transition-colors" class="px-4 py-2 {activeTab === 'nip07' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent text-fog-accent dark:text-fog-dark-accent' : 'text-fog-text-light dark:text-fog-dark-text-light'} transition-colors"
> >
NIP-07 NIP-07
</button> </button>
<button <button
onclick={() => { onclick={() => setActiveTab('nsec')}
activeTab = 'nsec';
error = null;
// If there are stored keys, show them by default
showNewNsecForm = storedNsecKeys.length === 0;
selectedNsecKey = null;
}}
class="px-4 py-2 {activeTab === 'nsec' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent text-fog-accent dark:text-fog-dark-accent' : 'text-fog-text-light dark:text-fog-dark-text-light'} transition-colors" class="px-4 py-2 {activeTab === 'nsec' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent text-fog-accent dark:text-fog-dark-accent' : 'text-fog-text-light dark:text-fog-dark-text-light'} transition-colors"
> >
Nsec {#if storedNsecKeys.length > 0}({storedNsecKeys.length}){/if} Nsec {#if storedNsecKeys.length > 0}({storedNsecKeys.length}){/if}
</button> </button>
<button <button
onclick={() => { onclick={() => setActiveTab('anonymous')}
activeTab = 'anonymous';
error = null;
// If there are stored keys, show them by default
showNewAnonymousForm = storedAnonymousKeys.length === 0;
selectedAnonymousKey = null;
}}
class="px-4 py-2 {activeTab === 'anonymous' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent text-fog-accent dark:text-fog-dark-accent' : 'text-fog-text-light dark:text-fog-dark-text-light'} transition-colors" class="px-4 py-2 {activeTab === 'anonymous' ? 'border-b-2 border-fog-accent dark:border-fog-dark-accent text-fog-accent dark:text-fog-dark-accent' : 'text-fog-text-light dark:text-fog-dark-text-light'} transition-colors"
> >
Anonymous {#if storedAnonymousKeys.length > 0}({storedAnonymousKeys.length}){/if} Anonymous {#if storedAnonymousKeys.length > 0}({storedAnonymousKeys.length}){/if}

39
src/routes/repos/[naddr]/+page.svelte

@ -4,6 +4,7 @@
import { relayManager } from '../../../lib/services/nostr/relay-manager.js'; import { relayManager } from '../../../lib/services/nostr/relay-manager.js';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { goto } from '$app/navigation';
import type { NostrEvent } from '../../../lib/types/nostr.js'; import type { NostrEvent } from '../../../lib/types/nostr.js';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { fetchGitRepo, extractGitUrls, type GitRepoInfo, type GitFile } from '../../../lib/services/content/git-repo-fetcher.js'; import { fetchGitRepo, extractGitUrls, type GitRepoInfo, type GitFile } from '../../../lib/services/content/git-repo-fetcher.js';
@ -42,8 +43,36 @@
let loadingRepo = $state(false); // Guard to prevent concurrent loads let loadingRepo = $state(false); // Guard to prevent concurrent loads
// Initialize activeTab from URL parameter
function getTabFromUrl(): 'metadata' | 'about' | 'repository' | 'issues' | 'documentation' {
const tabParam = $page.url.searchParams.get('tab');
const validTabs: Array<'metadata' | 'about' | 'repository' | 'issues' | 'documentation'> = ['metadata', 'about', 'repository', 'issues', 'documentation'];
if (tabParam && validTabs.includes(tabParam as any)) {
return tabParam as 'metadata' | 'about' | 'repository' | 'issues' | 'documentation';
}
return 'metadata'; // Default
}
// Update activeTab when URL changes
$effect(() => {
const urlTab = getTabFromUrl();
if (urlTab !== activeTab) {
activeTab = urlTab;
}
});
// Function to change tab and update URL
function setActiveTab(tab: 'metadata' | 'about' | 'repository' | 'issues' | 'documentation') {
activeTab = tab;
const url = new URL($page.url);
url.searchParams.set('tab', tab);
goto(url.pathname + url.search, { replaceState: true, noScroll: true });
}
onMount(async () => { onMount(async () => {
await nostrClient.initialize(); await nostrClient.initialize();
// Initialize tab from URL
activeTab = getTabFromUrl();
// Don't call loadRepo here - let $effect handle it // Don't call loadRepo here - let $effect handle it
}); });
@ -913,28 +942,28 @@
<button <button
class="tab-button" class="tab-button"
class:active={activeTab === 'metadata'} class:active={activeTab === 'metadata'}
onclick={() => activeTab = 'metadata'} onclick={() => setActiveTab('metadata')}
> >
Metadata Metadata
</button> </button>
<button <button
class="tab-button" class="tab-button"
class:active={activeTab === 'about'} class:active={activeTab === 'about'}
onclick={() => activeTab = 'about'} onclick={() => setActiveTab('about')}
> >
About About
</button> </button>
<button <button
class="tab-button" class="tab-button"
class:active={activeTab === 'repository'} class:active={activeTab === 'repository'}
onclick={() => activeTab = 'repository'} onclick={() => setActiveTab('repository')}
> >
Repository Repository
</button> </button>
<button <button
class="tab-button" class="tab-button"
class:active={activeTab === 'issues'} class:active={activeTab === 'issues'}
onclick={() => activeTab = 'issues'} onclick={() => setActiveTab('issues')}
> >
Issues {issues.length > 0 ? `(${issues.length})` : ''} Issues {issues.length > 0 ? `(${issues.length})` : ''}
</button> </button>
@ -942,7 +971,7 @@
<button <button
class="tab-button" class="tab-button"
class:active={activeTab === 'documentation'} class:active={activeTab === 'documentation'}
onclick={() => activeTab = 'documentation'} onclick={() => setActiveTab('documentation')}
> >
Documentation Documentation
</button> </button>

Loading…
Cancel
Save