Browse Source

more features

master
Silberengel 1 month ago
parent
commit
c6f42fb20b
  1. 2
      src/lib/modules/comments/CommentForm.svelte
  2. 2
      src/lib/modules/feed/CreateKind1Form.svelte
  3. 4
      src/lib/modules/feed/Kind1FeedPage.svelte
  4. 2
      src/lib/modules/feed/ReplyToKind1Form.svelte
  5. 2
      src/lib/modules/threads/CreateThreadForm.svelte
  6. 166
      src/lib/modules/threads/ThreadCard.svelte
  7. 4
      src/lib/modules/threads/ThreadList.svelte
  8. 2
      src/lib/modules/zaps/ZapInvoiceModal.svelte
  9. 32
      src/lib/modules/zaps/ZapReceipt.svelte
  10. 2
      src/routes/+page.svelte
  11. 2
      src/routes/login/+page.svelte

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

@ -106,7 +106,7 @@
{/if} {/if}
<button <button
onclick={publish} onclick={publish}
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" class="px-4 py-2 text-sm bg-fog-accent dark:bg-fog-dark-accent text-white rounded hover:opacity-90 disabled:opacity-50"
disabled={publishing || !content.trim()} disabled={publishing || !content.trim()}
> >
{publishing ? 'Publishing...' : parentEvent ? 'Reply' : 'Comment'} {publishing ? 'Publishing...' : parentEvent ? 'Reply' : 'Comment'}

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

@ -122,7 +122,7 @@
{/if} {/if}
<button <button
onclick={publish} onclick={publish}
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" class="px-4 py-2 text-sm bg-fog-accent dark:bg-fog-dark-accent text-white rounded hover:opacity-90 disabled:opacity-50"
disabled={publishing || !content.trim()} disabled={publishing || !content.trim()}
> >
{publishing ? 'Publishing...' : parentEvent ? 'Reply' : 'Post'} {publishing ? 'Publishing...' : parentEvent ? 'Reply' : 'Post'}

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

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

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

@ -115,7 +115,7 @@
{/if} {/if}
<button <button
onclick={publish} onclick={publish}
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" class="px-4 py-2 text-sm bg-fog-accent dark:bg-fog-dark-accent text-white rounded hover:opacity-90 disabled:opacity-50"
disabled={publishing || !content.trim()} disabled={publishing || !content.trim()}
> >
{publishing ? 'Publishing...' : 'Reply'} {publishing ? 'Publishing...' : 'Reply'}

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

@ -168,7 +168,7 @@
</div> </div>
</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-fog-text dark:text-fog-dark-text 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 text-white hover:opacity-90 disabled:opacity-50 transition-colors rounded">
{publishing ? 'Publishing...' : 'Create Thread'} {publishing ? 'Publishing...' : 'Create Thread'}
</button> </button>
</form> </form>

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

@ -16,6 +16,7 @@
let commentCount = $state(0); let commentCount = $state(0);
let zapTotal = $state(0); let zapTotal = $state(0);
let zapCount = $state(0); let zapCount = $state(0);
let latestResponseTime = $state<number | null>(null);
let loadingStats = $state(true); let loadingStats = $state(true);
onMount(async () => { onMount(async () => {
@ -24,60 +25,96 @@
async function loadStats() { async function loadStats() {
loadingStats = true; loadingStats = true;
const timeout = 30000; // 30 seconds
try { try {
const config = nostrClient.getConfig(); const config = nostrClient.getConfig();
// Load reactions (kind 7) // Create a timeout promise
const reactionRelays = relayManager.getThreadReadRelays(); const timeoutPromise = new Promise<never>((_, reject) => {
const reactionEvents = await nostrClient.fetchEvents( setTimeout(() => reject(new Error('Stats loading timeout')), timeout);
[{ kinds: [7], '#e': [thread.id] }], });
reactionRelays,
{ useCache: true }
);
// Count upvotes (+) and downvotes (-)
for (const reaction of reactionEvents) {
const content = reaction.content.trim();
if (content === '+' || content === '⬆' || content === '↑') {
upvotes++;
} else if (content === '-' || content === '⬇' || content === '↓') {
downvotes++;
}
}
// Load comments (kind 1111)
const commentRelays = relayManager.getCommentReadRelays();
const commentEvents = await nostrClient.fetchEvents(
[{ kinds: [1111], '#E': [thread.id], '#K': ['11'] }],
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 // Race between loading and timeout
const threshold = config.zapThreshold; await Promise.race([
zapCount = 0; (async () => {
zapTotal = 0; // Load reactions (kind 7)
for (const receipt of zapReceipts) { const reactionRelays = relayManager.getThreadReadRelays();
const amountTag = receipt.tags.find((t) => t[0] === 'amount'); const reactionEvents = await nostrClient.fetchEvents(
if (amountTag && amountTag[1]) { [{ kinds: [7], '#e': [thread.id] }],
const amount = parseInt(amountTag[1], 10); reactionRelays,
if (!isNaN(amount) && amount >= threshold) { { useCache: true }
zapTotal += amount; );
zapCount++;
// Count upvotes (+) and downvotes (-)
for (const reaction of reactionEvents) {
const content = reaction.content.trim();
if (content === '+' || content === '⬆' || content === '↑') {
upvotes++;
} else if (content === '-' || content === '⬇' || content === '↓') {
downvotes++;
}
}
// Load comments (kind 1111)
const commentRelays = relayManager.getCommentReadRelays();
const commentEvents = await nostrClient.fetchEvents(
[{ kinds: [1111], '#E': [thread.id], '#K': ['11'] }],
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++;
}
}
}
// Find latest response time (most recent comment, reaction, or zap)
let latestTime = thread.created_at;
if (commentEvents.length > 0) {
const latestComment = commentEvents.sort((a, b) => b.created_at - a.created_at)[0];
latestTime = Math.max(latestTime, latestComment.created_at);
} }
} if (reactionEvents.length > 0) {
} const latestReaction = reactionEvents.sort((a, b) => b.created_at - a.created_at)[0];
latestTime = Math.max(latestTime, latestReaction.created_at);
}
if (zapReceipts.length > 0) {
const latestZap = zapReceipts.sort((a, b) => b.created_at - a.created_at)[0];
latestTime = Math.max(latestTime, latestZap.created_at);
}
latestResponseTime = latestTime > thread.created_at ? latestTime : null;
})(),
timeoutPromise
]);
} catch (error) { } catch (error) {
console.error('Error loading thread stats:', error); console.error('Error loading thread stats:', error);
// On timeout or error, show zero stats instead of loading forever
upvotes = 0;
downvotes = 0;
commentCount = 0;
zapTotal = 0;
zapCount = 0;
latestResponseTime = null;
} finally { } finally {
loadingStats = false; loadingStats = false;
} }
@ -109,6 +146,20 @@
return 'just now'; return 'just now';
} }
function getLatestResponseTime(): string {
if (!latestResponseTime) return '';
const now = Math.floor(Date.now() / 1000);
const diff = now - latestResponseTime;
const hours = Math.floor(diff / 3600);
const days = Math.floor(diff / 86400);
const minutes = Math.floor(diff / 60);
if (days > 0) return `${days}d ago`;
if (hours > 0) return `${hours}h ago`;
if (minutes > 0) return `${minutes}m ago`;
return 'just now';
}
function getClientName(): string | null { function getClientName(): string | null {
const clientTag = thread.tags.find((t) => t[0] === 'client'); const clientTag = thread.tags.find((t) => t[0] === 'client');
return clientTag?.[1] || null; return clientTag?.[1] || null;
@ -140,18 +191,23 @@
</div> </div>
{/if} {/if}
<div class="flex items-center justify-between text-xs text-fog-text-light dark:text-fog-dark-text-light"> <div class="flex items-center justify-between text-xs text-fog-text dark:text-fog-dark-text">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4 flex-wrap">
{#if !loadingStats} {#if loadingStats}
<span>{upvotes}</span> <span class="text-fog-text-light dark:text-fog-dark-text-light">Loading stats...</span>
<span>{downvotes}</span> {:else}
<span>{commentCount} {commentCount === 1 ? 'comment' : 'comments'}</span> <span class="font-medium">{upvotes}</span>
<span class="font-medium">{downvotes}</span>
<span class="font-medium">{commentCount} {commentCount === 1 ? 'comment' : 'comments'}</span>
{#if latestResponseTime}
<span class="text-fog-text-light dark:text-fog-dark-text-light">Last: {getLatestResponseTime()}</span>
{/if}
{#if zapCount > 0} {#if zapCount > 0}
<span>{zapTotal.toLocaleString()} sats ({zapCount})</span> <span class="font-medium">{zapTotal.toLocaleString()} sats ({zapCount})</span>
{/if} {/if}
{/if} {/if}
</div> </div>
<a href="/thread/{thread.id}">View thread →</a> <a href="/thread/{thread.id}" class="ml-2 text-fog-accent dark:text-fog-dark-accent hover:underline">View thread →</a>
</div> </div>
</article> </article>

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

@ -251,7 +251,7 @@
<button <button
onclick={() => (selectedTopic = null)} onclick={() => (selectedTopic = null)}
class="px-3 py-1 rounded border transition-colors {selectedTopic === null class="px-3 py-1 rounded border transition-colors {selectedTopic === null
? '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-accent dark:bg-fog-dark-accent text-white 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'}" : '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}) All ({filterByAge(threads).length})
@ -260,7 +260,7 @@
<button <button
onclick={() => (selectedTopic = topic === null ? undefined : topic)} onclick={() => (selectedTopic = topic === null ? undefined : topic)}
class="px-3 py-1 rounded border transition-colors {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-fog-text dark:text-fog-dark-text border-fog-accent dark:border-fog-dark-accent' ? 'bg-fog-accent dark:bg-fog-dark-accent text-white 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'}" : '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}) {topic === null ? 'General' : topic} ({count})

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

@ -58,7 +58,7 @@
></textarea> ></textarea>
<button <button
onclick={copyInvoice} onclick={copyInvoice}
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" class="mt-2 px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-white rounded hover:opacity-90"
> >
Copy Invoice Copy Invoice
</button> </button>

32
src/lib/modules/zaps/ZapReceipt.svelte

@ -21,12 +21,19 @@
async function loadZapReceipts() { async function loadZapReceipts() {
loading = true; loading = true;
const timeout = 30000; // 30 seconds
try { try {
const config = nostrClient.getConfig(); const config = nostrClient.getConfig();
const threshold = config.zapThreshold; const threshold = config.zapThreshold;
// Create a timeout promise
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('Zap loading timeout')), timeout);
});
// Fetch zap receipts (kind 9735) for this event // Fetch zap receipts (kind 9735) for this event
const filters = [ const filters: any[] = [
{ {
kinds: [9735], kinds: [9735],
'#e': [eventId] '#e': [eventId]
@ -37,17 +44,24 @@
filters[0]['#p'] = [pubkey]; filters[0]['#p'] = [pubkey];
} }
const receipts = await nostrClient.fetchEvents( // Race between loading and timeout
filters, const receipts = await Promise.race([
[...config.defaultRelays], nostrClient.fetchEvents(
{ useCache: true, cacheResults: true, onUpdate: (updated) => { filters,
processReceipts(updated); [...config.defaultRelays],
}} { useCache: true, cacheResults: true, onUpdate: (updated) => {
); processReceipts(updated);
}}
),
timeoutPromise
]);
processReceipts(receipts); processReceipts(receipts);
} catch (error) { } catch (error) {
console.error('Error loading zap receipts:', error); console.error('Error loading zap receipts:', error);
// On timeout or error, show empty state
zapReceipts = [];
totalAmount = 0;
} finally { } finally {
loading = false; loading = false;
} }
@ -101,6 +115,8 @@
({zapReceipts.length} {zapReceipts.length === 1 ? 'zap' : 'zaps'}) ({zapReceipts.length} {zapReceipts.length === 1 ? 'zap' : 'zaps'})
</span> </span>
</div> </div>
{:else}
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light">No zaps</span>
{/if} {/if}
</div> </div>

2
src/routes/+page.svelte

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

2
src/routes/login/+page.svelte

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

Loading…
Cancel
Save