Browse Source

added sorting or replies and most-zapped filter

imwald
Silberengel 5 months ago
parent
commit
edac999a41
  1. 47
      src/components/NoteInteractions/ReplySort.tsx
  2. 10
      src/components/NoteInteractions/index.tsx
  3. 85
      src/components/ReplyNoteList/index.tsx
  4. 5
      src/pages/primary/DiscussionsPage/ThreadSort.tsx
  5. 30
      src/pages/primary/DiscussionsPage/index.tsx

47
src/components/NoteInteractions/ReplySort.tsx

@ -0,0 +1,47 @@ @@ -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 (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="flex items-center gap-2 h-8">
<selectedOption.icon className="w-4 h-4" />
<span className="text-sm">{selectedOption.label}</span>
<ChevronDown className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{sortOptions.map(option => (
<DropdownMenuItem
key={option.id}
onClick={() => onSortChange(option.id)}
className="flex items-center gap-2"
>
<option.icon className="w-4 h-4" />
<span>{option.label}</span>
{option.id === selectedSort && (
<span className="ml-auto text-primary"></span>
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}

10
src/components/NoteInteractions/index.tsx

@ -10,6 +10,7 @@ import ReplyNoteList from '../ReplyNoteList' @@ -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({ @@ -19,12 +20,13 @@ export default function NoteInteractions({
event: Event
}) {
const [type, setType] = useState<TTabValue>('replies')
const [replySort, setReplySort] = useState<ReplySortOption>('newest')
const isDiscussion = event.kind === ExtendedKind.DISCUSSION
let list
switch (type) {
case 'replies':
list = <ReplyNoteList index={pageIndex} event={event} />
list = <ReplyNoteList index={pageIndex} event={event} sort={replySort} />
break
case 'quotes':
if (isDiscussion) return null // Hide quotes for discussions
@ -52,6 +54,12 @@ export default function NoteInteractions({ @@ -52,6 +54,12 @@ export default function NoteInteractions({
<ScrollBar orientation="horizontal" className="opacity-0 pointer-events-none" />
</ScrollArea>
<Separator orientation="vertical" className="h-6" />
{type === 'replies' && isDiscussion && (
<>
<ReplySort selectedSort={replySort} onSortChange={setReplySort} />
<Separator orientation="vertical" className="h-6" />
</>
)}
<div className="size-10 flex items-center justify-center">
<HideUntrustedContentButton type="interactions" />
</div>

85
src/components/ReplyNoteList/index.tsx

@ -18,6 +18,7 @@ import { useNostr } from '@/providers/NostrProvider' @@ -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 = @@ -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: @@ -41,6 +42,46 @@ export default function ReplyNoteList({ index, event }: { index?: number; event:
const { relayList: userRelayList } = useNostr()
const [rootInfo, setRootInfo] = useState<TRootInfo | undefined>(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<string>()
const replyEvents: NEvent[] = []
@ -60,8 +101,46 @@ export default function ReplyNoteList({ index, event }: { index?: number; event: @@ -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<string | undefined>(undefined)
const [until, setUntil] = useState<number | undefined>(undefined)
const [loading, setLoading] = useState<boolean>(false)

5
src/pages/primary/DiscussionsPage/ThreadSort.tsx

@ -1,9 +1,9 @@ @@ -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 @@ -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]

30
src/pages/primary/DiscussionsPage/index.tsx

@ -113,6 +113,18 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -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) => { @@ -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

Loading…
Cancel
Save