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 @@ @@ -106,7 +106,7 @@
{/if}
<button
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()}
>
{publishing ? 'Publishing...' : parentEvent ? 'Reply' : 'Comment'}

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

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

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

@ -206,7 +206,7 @@ @@ -206,7 +206,7 @@
<h1 class="text-2xl font-bold mb-4">Feed</h1>
<button
onclick={() => (showNewPostForm = !showNewPostForm)}
class="px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-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'}
</button>
@ -234,7 +234,7 @@ @@ -234,7 +234,7 @@
<div class="new-posts-indicator mb-4">
<button
onclick={handleShowNewPosts}
class="px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-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
</button>

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

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

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

@ -168,7 +168,7 @@ @@ -168,7 +168,7 @@
</div>
</div>
<button type="submit" disabled={publishing || selectedRelays.size === 0} class="px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent hover:bg-fog-text dark:hover:bg-fog-dark-text text-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'}
</button>
</form>

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

@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
let commentCount = $state(0);
let zapTotal = $state(0);
let zapCount = $state(0);
let latestResponseTime = $state<number | null>(null);
let loadingStats = $state(true);
onMount(async () => {
@ -24,60 +25,96 @@ @@ -24,60 +25,96 @@
async function loadStats() {
loadingStats = true;
const timeout = 30000; // 30 seconds
try {
const config = nostrClient.getConfig();
// Load reactions (kind 7)
const reactionRelays = relayManager.getThreadReadRelays();
const reactionEvents = await nostrClient.fetchEvents(
[{ 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 }
);
// Create a timeout promise
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('Stats loading timeout')), timeout);
});
// 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++;
// Race between loading and timeout
await Promise.race([
(async () => {
// Load reactions (kind 7)
const reactionRelays = relayManager.getThreadReadRelays();
const reactionEvents = await nostrClient.fetchEvents(
[{ 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
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) {
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 {
loadingStats = false;
}
@ -109,6 +146,20 @@ @@ -109,6 +146,20 @@
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 {
const clientTag = thread.tags.find((t) => t[0] === 'client');
return clientTag?.[1] || null;
@ -140,18 +191,23 @@ @@ -140,18 +191,23 @@
</div>
{/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 gap-4">
{#if !loadingStats}
<span>{upvotes}</span>
<span>{downvotes}</span>
<span>{commentCount} {commentCount === 1 ? 'comment' : 'comments'}</span>
<div class="flex items-center justify-between text-xs text-fog-text dark:text-fog-dark-text">
<div class="flex items-center gap-4 flex-wrap">
{#if loadingStats}
<span class="text-fog-text-light dark:text-fog-dark-text-light">Loading stats...</span>
{:else}
<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}
<span>{zapTotal.toLocaleString()} sats ({zapCount})</span>
<span class="font-medium">{zapTotal.toLocaleString()} sats ({zapCount})</span>
{/if}
{/if}
</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>
</article>

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

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

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

@ -58,7 +58,7 @@ @@ -58,7 +58,7 @@
></textarea>
<button
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
</button>

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

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

2
src/routes/+page.svelte

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

2
src/routes/login/+page.svelte

@ -45,7 +45,7 @@ @@ -45,7 +45,7 @@
<button
onclick={loginWithNIP07}
disabled={loading}
class="w-full px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent hover:bg-fog-text dark:hover:bg-fog-dark-text text-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'}
</button>

Loading…
Cancel
Save