diff --git a/src/components/NoteInteractions/ReplySort.tsx b/src/components/NoteInteractions/ReplySort.tsx
new file mode 100644
index 0000000..b7cee74
--- /dev/null
+++ b/src/components/NoteInteractions/ReplySort.tsx
@@ -0,0 +1,47 @@
+import { Button } from '@/components/ui/button'
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
+import { ChevronDown, Clock, TrendingUp, ArrowUpDown, Zap } from 'lucide-react'
+import { useTranslation } from 'react-i18next'
+
+export type ReplySortOption = 'newest' | 'oldest' | 'top' | 'controversial' | 'most-zapped'
+
+export default function ReplySort({ selectedSort, onSortChange }: { selectedSort: ReplySortOption; onSortChange: (sort: ReplySortOption) => void }) {
+ const { t } = useTranslation()
+
+ const sortOptions = [
+ { id: 'newest' as ReplySortOption, label: t('Newest'), icon: Clock },
+ { id: 'oldest' as ReplySortOption, label: t('Oldest'), icon: Clock },
+ { id: 'top' as ReplySortOption, label: t('Top'), icon: TrendingUp },
+ { id: 'controversial' as ReplySortOption, label: t('Controversial'), icon: ArrowUpDown },
+ { id: 'most-zapped' as ReplySortOption, label: t('Most Zapped'), icon: Zap },
+ ]
+
+ const selectedOption = sortOptions.find(option => option.id === selectedSort) || sortOptions[0]
+
+ return (
+
+
+
+
+
+ {sortOptions.map(option => (
+ onSortChange(option.id)}
+ className="flex items-center gap-2"
+ >
+
+ {option.label}
+ {option.id === selectedSort && (
+ ✓
+ )}
+
+ ))}
+
+
+ )
+}
diff --git a/src/components/NoteInteractions/index.tsx b/src/components/NoteInteractions/index.tsx
index cfe30d6..c878c07 100644
--- a/src/components/NoteInteractions/index.tsx
+++ b/src/components/NoteInteractions/index.tsx
@@ -10,6 +10,7 @@ import ReplyNoteList from '../ReplyNoteList'
import RepostList from '../RepostList'
import ZapList from '../ZapList'
import { Tabs, TTabValue } from './Tabs'
+import ReplySort, { ReplySortOption } from './ReplySort'
export default function NoteInteractions({
pageIndex,
@@ -19,12 +20,13 @@ export default function NoteInteractions({
event: Event
}) {
const [type, setType] = useState('replies')
+ const [replySort, setReplySort] = useState('newest')
const isDiscussion = event.kind === ExtendedKind.DISCUSSION
let list
switch (type) {
case 'replies':
- list =
+ list =
break
case 'quotes':
if (isDiscussion) return null // Hide quotes for discussions
@@ -52,6 +54,12 @@ export default function NoteInteractions({
+ {type === 'replies' && isDiscussion && (
+ <>
+
+
+ >
+ )}
diff --git a/src/components/ReplyNoteList/index.tsx b/src/components/ReplyNoteList/index.tsx
index 04c4b21..2e4ac8d 100644
--- a/src/components/ReplyNoteList/index.tsx
+++ b/src/components/ReplyNoteList/index.tsx
@@ -18,6 +18,7 @@ import { useNostr } from '@/providers/NostrProvider'
import { useReply } from '@/providers/ReplyProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
import client from '@/services/client.service'
+import noteStatsService from '@/services/note-stats.service'
import { Filter, Event as NEvent, kinds } from 'nostr-tools'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -32,7 +33,7 @@ type TRootInfo =
const LIMIT = 100
const SHOW_COUNT = 10
-export default function ReplyNoteList({ index, event }: { index?: number; event: NEvent }) {
+export default function ReplyNoteList({ index, event, sort = 'newest' }: { index?: number; event: NEvent; sort?: 'newest' | 'oldest' | 'top' | 'controversial' | 'most-zapped' }) {
const { t } = useTranslation()
const { push, currentIndex } = useSecondaryPage()
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
@@ -41,6 +42,46 @@ export default function ReplyNoteList({ index, event }: { index?: number; event:
const { relayList: userRelayList } = useNostr()
const [rootInfo, setRootInfo] = useState(undefined)
const { repliesMap, addReplies } = useReply()
+
+ // Helper function to get vote score for a reply
+ const getReplyVoteScore = (reply: NEvent) => {
+ const stats = noteStatsService.getNoteStats(reply.id)
+ if (!stats?.likes) {
+ return 0
+ }
+
+ const upvoteReactions = stats.likes.filter(r => r.emoji === '⬆️')
+ const downvoteReactions = stats.likes.filter(r => r.emoji === '⬇️')
+ const score = upvoteReactions.length - downvoteReactions.length
+
+ return score
+ }
+
+ // Helper function to get controversy score for a reply
+ const getReplyControversyScore = (reply: NEvent) => {
+ const stats = noteStatsService.getNoteStats(reply.id)
+ if (!stats?.likes) {
+ return 0
+ }
+
+ const upvoteReactions = stats.likes.filter(r => r.emoji === '⬆️')
+ const downvoteReactions = stats.likes.filter(r => r.emoji === '⬇️')
+
+ // Controversy = minimum of upvotes and downvotes (both need to be high)
+ const controversy = Math.min(upvoteReactions.length, downvoteReactions.length)
+ return controversy
+ }
+
+ // Helper function to get total zap amount for a reply
+ const getReplyZapAmount = (reply: NEvent) => {
+ const stats = noteStatsService.getNoteStats(reply.id)
+ if (!stats?.zaps) {
+ return 0
+ }
+
+ const totalAmount = stats.zaps.reduce((sum, zap) => sum + zap.amount, 0)
+ return totalAmount
+ }
const replies = useMemo(() => {
const replyIdSet = new Set()
const replyEvents: NEvent[] = []
@@ -60,8 +101,46 @@ export default function ReplyNoteList({ index, event }: { index?: number; event:
})
parentEventKeys = events.map((evt) => evt.id)
}
- return replyEvents.sort((a, b) => a.created_at - b.created_at)
- }, [event.id, repliesMap])
+ // Apply sorting based on the sort parameter
+ switch (sort) {
+ case 'oldest':
+ return replyEvents.sort((a, b) => a.created_at - b.created_at)
+ case 'newest':
+ return replyEvents.sort((a, b) => b.created_at - a.created_at)
+ case 'top':
+ // Sort by vote score (upvotes - downvotes), then by newest if tied
+ return replyEvents.sort((a, b) => {
+ const scoreA = getReplyVoteScore(a)
+ const scoreB = getReplyVoteScore(b)
+ if (scoreA !== scoreB) {
+ return scoreB - scoreA // Higher scores first
+ }
+ return b.created_at - a.created_at // Newest first if tied
+ })
+ case 'controversial':
+ // Sort by controversy score (min of upvotes and downvotes), then by newest if tied
+ return replyEvents.sort((a, b) => {
+ const controversyA = getReplyControversyScore(a)
+ const controversyB = getReplyControversyScore(b)
+ if (controversyA !== controversyB) {
+ return controversyB - controversyA // Higher controversy first
+ }
+ return b.created_at - a.created_at // Newest first if tied
+ })
+ case 'most-zapped':
+ // Sort by total zap amount, then by newest if tied
+ return replyEvents.sort((a, b) => {
+ const zapAmountA = getReplyZapAmount(a)
+ const zapAmountB = getReplyZapAmount(b)
+ if (zapAmountA !== zapAmountB) {
+ return zapAmountB - zapAmountA // Higher zap amounts first
+ }
+ return b.created_at - a.created_at // Newest first if tied
+ })
+ default:
+ return replyEvents.sort((a, b) => b.created_at - a.created_at)
+ }
+ }, [event.id, repliesMap, sort])
const [timelineKey, setTimelineKey] = useState(undefined)
const [until, setUntil] = useState(undefined)
const [loading, setLoading] = useState(false)
diff --git a/src/pages/primary/DiscussionsPage/ThreadSort.tsx b/src/pages/primary/DiscussionsPage/ThreadSort.tsx
index db453b1..16611c2 100644
--- a/src/pages/primary/DiscussionsPage/ThreadSort.tsx
+++ b/src/pages/primary/DiscussionsPage/ThreadSort.tsx
@@ -1,9 +1,9 @@
import { Button } from '@/components/ui/button'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
-import { ChevronDown, Clock, TrendingUp, ArrowUpDown } from 'lucide-react'
+import { ChevronDown, Clock, TrendingUp, ArrowUpDown, Zap } from 'lucide-react'
import { useTranslation } from 'react-i18next'
-export type SortOption = 'newest' | 'oldest' | 'top' | 'controversial'
+export type SortOption = 'newest' | 'oldest' | 'top' | 'controversial' | 'most-zapped'
export default function ThreadSort({ selectedSort, onSortChange }: { selectedSort: SortOption; onSortChange: (sort: SortOption) => void }) {
const { t } = useTranslation()
@@ -13,6 +13,7 @@ export default function ThreadSort({ selectedSort, onSortChange }: { selectedSor
{ id: 'oldest' as SortOption, label: t('Oldest'), icon: Clock },
{ id: 'top' as SortOption, label: t('Top'), icon: TrendingUp },
{ id: 'controversial' as SortOption, label: t('Controversial'), icon: ArrowUpDown },
+ { id: 'most-zapped' as SortOption, label: t('Most Zapped'), icon: Zap },
]
const selectedOption = sortOptions.find(option => option.id === selectedSort) || sortOptions[0]
diff --git a/src/pages/primary/DiscussionsPage/index.tsx b/src/pages/primary/DiscussionsPage/index.tsx
index d9e0ced..e257ef9 100644
--- a/src/pages/primary/DiscussionsPage/index.tsx
+++ b/src/pages/primary/DiscussionsPage/index.tsx
@@ -113,6 +113,18 @@ const DiscussionsPage = forwardRef((_, ref) => {
return controversy
}
+ // Helper function to get total zap amount for a thread
+ const getThreadZapAmount = (thread: NostrEvent) => {
+ const stats = noteStatsService.getNoteStats(thread.id)
+ if (!stats?.zaps) {
+ return 0
+ }
+
+ const totalAmount = stats.zaps.reduce((sum, zap) => sum + zap.amount, 0)
+ console.log(`Thread ${thread.id}: ${stats.zaps.length} zaps, total amount: ${totalAmount}`)
+ return totalAmount
+ }
+
useEffect(() => {
setCustomVoteStats({}) // Clear custom stats when relay changes
fetchAllThreads()
@@ -366,6 +378,24 @@ const DiscussionsPage = forwardRef((_, ref) => {
console.log('Sorted by controversial')
break
+ case 'most-zapped':
+ // Sort by total zap amount, then by newest if tied
+ const sortedMostZapped = [...threadsForTopic].sort((a, b) => {
+ const zapAmountA = getThreadZapAmount(a)
+ const zapAmountB = getThreadZapAmount(b)
+ console.log(`Comparing ${a.id.slice(0,8)} (zaps: ${zapAmountA}) vs ${b.id.slice(0,8)} (zaps: ${zapAmountB})`)
+ if (zapAmountA !== zapAmountB) {
+ return zapAmountB - zapAmountA // Higher zap amounts first
+ }
+ return b.created_at - a.created_at // Newest first if tied
+ })
+
+ // Replace the original array
+ threadsForTopic.length = 0
+ threadsForTopic.push(...sortedMostZapped)
+
+ console.log('Sorted by most zapped')
+ break
default:
const sortedDefault = [...threadsForTopic].sort((a, b) => b.created_at - a.created_at)
threadsForTopic.length = 0