From 8ce1d43d2dc472db0942c8b204c95a0c129c838c Mon Sep 17 00:00:00 2001 From: Jure <44338+hoornet@users.noreply.github.com> Date: Sun, 15 Mar 2026 21:49:52 +0100 Subject: [PATCH] =?UTF-8?q?Bump=20to=20v0.5.0=20=E2=80=94=20note=20sharing?= =?UTF-8?q?,=20reply=20counts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/release.yml | 6 +++++- PKGBUILD | 2 +- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- src/components/feed/NoteCard.tsx | 35 ++++++++++++++++++++++++++++++-- src/hooks/useReplyCount.ts | 29 ++++++++++++++++++++++++++ src/lib/nostr/client.ts | 12 +++++++++++ src/lib/nostr/index.ts | 2 +- 10 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 src/hooks/useReplyCount.ts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 01c5fcd..c107b80 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,7 +66,11 @@ jobs: > **Windows note:** The installer is not yet code-signed. Windows SmartScreen will show an "Unknown publisher" warning — click "More info → Run anyway" to install. - ### New in v0.4.1 — Media & Feed Fixes + ### New in v0.5.0 — Sharing & Thread Indicators + - **Note sharing** — share button on every note copies a `nostr:nevent1…` URI to clipboard; works when logged out too + - **Reply count** — notes now show reply count next to the reply button; updates optimistically when you reply + + ### Previous: v0.4.1 — Media & Feed Fixes - **Video player** — direct video URLs (.mp4, .webm, .mov, etc.) now render as inline players with native controls - **Audio player** — direct audio URLs (.mp3, .wav, .flac, etc.) render as inline audio players with filename display - **YouTube/Vimeo rich cards** — YouTube links show thumbnail previews, Vimeo/Spotify/Tidal show branded cards; all open in external browser diff --git a/PKGBUILD b/PKGBUILD index f16a8af..fb42d0f 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: hoornet pkgname=wrystr -pkgver=0.4.1 +pkgver=0.5.0 pkgrel=1 pkgdesc="Cross-platform Nostr desktop client with Lightning integration" arch=('x86_64') diff --git a/package.json b/package.json index 20bf28e..df37baf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "wrystr", "private": true, - "version": "0.4.1", + "version": "0.5.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a9b8bc0..99de814 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -5824,7 +5824,7 @@ dependencies = [ [[package]] name = "wrystr" -version = "0.4.1" +version = "0.5.0" dependencies = [ "keyring", "rusqlite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9fda48d..4ec25f1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wrystr" -version = "0.4.1" +version = "0.5.0" description = "Cross-platform Nostr desktop client with Lightning integration" authors = ["hoornet"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 80270bd..71a9203 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Wrystr", - "version": "0.4.1", + "version": "0.5.0", "identifier": "com.hoornet.wrystr", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/components/feed/NoteCard.tsx b/src/components/feed/NoteCard.tsx index bd3ff2d..c24f433 100644 --- a/src/components/feed/NoteCard.tsx +++ b/src/components/feed/NoteCard.tsx @@ -2,12 +2,14 @@ import { useState, useRef, useEffect } from "react"; import { NDKEvent } from "@nostr-dev-kit/ndk"; import { useProfile } from "../../hooks/useProfile"; import { useReactionCount } from "../../hooks/useReactionCount"; +import { useReplyCount } from "../../hooks/useReplyCount"; import { useZapCount } from "../../hooks/useZapCount"; import { useUserStore } from "../../stores/user"; import { useMuteStore } from "../../stores/mute"; import { useBookmarkStore } from "../../stores/bookmark"; import { useUIStore } from "../../stores/ui"; import { timeAgo, shortenPubkey } from "../../lib/utils"; +import { nip19 } from "@nostr-dev-kit/ndk"; import { publishReaction, publishReply, publishRepost, getNDK, fetchNoteById } from "../../lib/nostr"; import { NoteContent } from "./NoteContent"; import { ZapModal } from "../zap/ZapModal"; @@ -61,6 +63,8 @@ export function NoteCard({ event, focused }: NoteCardProps) { const [liked, setLiked] = useState(() => getLiked().has(event.id)); const [liking, setLiking] = useState(false); const [reactionCount, adjustReactionCount] = useReactionCount(event.id); + const [replyCount, adjustReplyCount] = useReplyCount(event.id); + const [copied, setCopied] = useState(false); const zapData = useZapCount(event.id); const [showReply, setShowReply] = useState(false); const [replyText, setReplyText] = useState(""); @@ -102,6 +106,7 @@ export function NoteCard({ event, focused }: NoteCardProps) { await publishReply(replyText.trim(), { id: event.id, pubkey: event.pubkey }); setReplyText(""); setReplySent(true); + adjustReplyCount(1); setTimeout(() => { setShowReply(false); setReplySent(false); }, 1500); } catch (err) { setReplyError(`Failed: ${err}`); @@ -115,6 +120,13 @@ export function NoteCard({ event, focused }: NoteCardProps) { if (e.key === "Escape") setShowReply(false); }; + const handleShare = async () => { + const nevent = nip19.neventEncode({ id: event.id!, author: event.pubkey }); + await navigator.clipboard.writeText("nostr:" + nevent); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + const handleRepost = async () => { if (reposting || reposted) return; setReposting(true); @@ -235,7 +247,7 @@ export function NoteCard({ event, focused }: NoteCardProps) { showReply ? "text-accent" : "text-text-dim hover:text-text" }`} > - reply + reply{replyCount !== null && replyCount > 0 ? ` ${replyCount}` : ""} + )} {/* Stats visible when logged out */} - {!loggedIn && (reactionCount !== null && reactionCount > 0 || zapData !== null && zapData.totalSats > 0) && ( + {!loggedIn && (
+ {replyCount !== null && replyCount > 0 && ( + ↩ {replyCount} + )} {reactionCount !== null && reactionCount > 0 && ( ♥ {reactionCount} )} {zapData !== null && zapData.totalSats > 0 && ( ⚡ {zapData.totalSats.toLocaleString()} sats )} +
)} diff --git a/src/hooks/useReplyCount.ts b/src/hooks/useReplyCount.ts new file mode 100644 index 0000000..ab333df --- /dev/null +++ b/src/hooks/useReplyCount.ts @@ -0,0 +1,29 @@ +import { useEffect, useState } from "react"; +import { fetchReplyCount } from "../lib/nostr"; + +const cache = new Map(); + +export function useReplyCount(eventId: string): [number | null, (delta: number) => void] { + const [count, setCount] = useState(() => cache.get(eventId) ?? null); + + useEffect(() => { + if (cache.has(eventId)) { + setCount(cache.get(eventId)!); + return; + } + fetchReplyCount(eventId).then((n) => { + cache.set(eventId, n); + setCount(n); + }); + }, [eventId]); + + const adjust = (delta: number) => { + setCount((prev) => { + const next = (prev ?? 0) + delta; + cache.set(eventId, next); + return next; + }); + }; + + return [count, adjust]; +} diff --git a/src/lib/nostr/client.ts b/src/lib/nostr/client.ts index dc49f34..a693ad6 100644 --- a/src/lib/nostr/client.ts +++ b/src/lib/nostr/client.ts @@ -313,6 +313,18 @@ export async function fetchZapCount(eventId: string): Promise<{ count: number; t return { count: events.size, totalSats }; } +export async function fetchReplyCount(eventId: string): Promise { + const instance = getNDK(); + const filter: NDKFilter = { + kinds: [NDKKind.Text], + "#e": [eventId], + }; + const events = await instance.fetchEvents(filter, { + cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY, + }); + return events.size; +} + export async function fetchReactionCount(eventId: string): Promise { const instance = getNDK(); const filter: NDKFilter = { diff --git a/src/lib/nostr/index.ts b/src/lib/nostr/index.ts index 96f6909..0fd54ff 100644 --- a/src/lib/nostr/index.ts +++ b/src/lib/nostr/index.ts @@ -1,2 +1,2 @@ -export { getNDK, connectToRelays, fetchGlobalFeed, fetchFollowFeed, fetchReplies, publishNote, publishArticle, publishProfile, publishReaction, publishRepost, publishQuote, publishReply, publishContactList, fetchReactionCount, fetchZapCount, fetchNoteById, fetchUserNotes, fetchProfile, fetchArticle, fetchAuthorArticles, fetchZapsReceived, fetchZapsSent, fetchDMConversations, fetchDMThread, sendDM, decryptDM, fetchBookmarkList, publishBookmarkList, fetchMuteList, publishMuteList, getStoredRelayUrls, addRelay, removeRelay, searchNotes, searchUsers, fetchUserRelayList, publishRelayList, fetchUserNotesNIP65, fetchFollowSuggestions, fetchMentions } from "./client"; +export { getNDK, connectToRelays, fetchGlobalFeed, fetchFollowFeed, fetchReplies, publishNote, publishArticle, publishProfile, publishReaction, publishRepost, publishQuote, publishReply, publishContactList, fetchReactionCount, fetchReplyCount, fetchZapCount, fetchNoteById, fetchUserNotes, fetchProfile, fetchArticle, fetchAuthorArticles, fetchZapsReceived, fetchZapsSent, fetchDMConversations, fetchDMThread, sendDM, decryptDM, fetchBookmarkList, publishBookmarkList, fetchMuteList, publishMuteList, getStoredRelayUrls, addRelay, removeRelay, searchNotes, searchUsers, fetchUserRelayList, publishRelayList, fetchUserNotesNIP65, fetchFollowSuggestions, fetchMentions } from "./client"; export type { UserRelayList } from "./client";