Browse Source

reply reactions-voting

removed repost and republishing from discussion replies
imwald
Silberengel 5 months ago
parent
commit
2b0207cd95
  1. 27
      src/components/NoteOptions/useMenuActions.tsx
  2. 70
      src/components/NoteStats/LikeButton.tsx
  3. 32
      src/components/NoteStats/index.tsx
  4. 23
      src/components/SuggestedEmojis/DiscussionEmojis.tsx

27
src/components/NoteOptions/useMenuActions.tsx

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
import { ExtendedKind } from '@/constants'
import { getNoteBech32Id, isProtectedEvent } from '@/lib/event'
import { getNoteBech32Id, isProtectedEvent, getRootEventHexId } from '@/lib/event'
import { toNjump } from '@/lib/link'
import { pubkeyToNpub } from '@/lib/pubkey'
import { simplifyUrl } from '@/lib/url'
@ -10,7 +10,7 @@ import { useNostr } from '@/providers/NostrProvider' @@ -10,7 +10,7 @@ import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service'
import { Bell, BellOff, Code, Copy, Link, SatelliteDish, Trash2, TriangleAlert } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import { useMemo, useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import RelayIcon from '../RelayIcon'
@ -57,6 +57,27 @@ export function useMenuActions({ @@ -57,6 +57,27 @@ export function useMenuActions({
}, [currentBrowsingRelayUrls, favoriteRelays])
const { mutePubkeyPublicly, mutePubkeyPrivately, unmutePubkey, mutePubkeySet } = useMuteList()
const isMuted = useMemo(() => mutePubkeySet.has(event.pubkey), [mutePubkeySet, event])
// Check if this is a reply to a discussion event
const [isReplyToDiscussion, setIsReplyToDiscussion] = useState(false)
useEffect(() => {
const isDiscussion = event.kind === ExtendedKind.DISCUSSION
if (isDiscussion) return // Already a discussion event
const rootEventId = getRootEventHexId(event)
if (rootEventId) {
// Fetch the root event to check if it's a discussion
client.fetchEvent(rootEventId).then(rootEvent => {
if (rootEvent && rootEvent.kind === ExtendedKind.DISCUSSION) {
setIsReplyToDiscussion(true)
}
}).catch(() => {
// If we can't fetch the root event, assume it's not a discussion reply
setIsReplyToDiscussion(false)
})
}
}, [event.id, event.kind])
const broadcastSubMenu: SubMenuAction[] = useMemo(() => {
const items = []
@ -185,7 +206,7 @@ export function useMenuActions({ @@ -185,7 +206,7 @@ export function useMenuActions({
const isProtected = isProtectedEvent(event)
const isDiscussion = event.kind === ExtendedKind.DISCUSSION
if ((!isProtected || event.pubkey === pubkey) && !isDiscussion) {
if ((!isProtected || event.pubkey === pubkey) && !isDiscussion && !isReplyToDiscussion) {
actions.push({
icon: SatelliteDish,
label: t('Republish to ...'),

70
src/components/NoteStats/LikeButton.tsx

@ -4,8 +4,10 @@ import { @@ -4,8 +4,10 @@ import {
DropdownMenuContent,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { ExtendedKind } from '@/constants'
import { useNoteStatsById } from '@/hooks/useNoteStatsById'
import { createReactionDraftEvent } from '@/lib/draft-event'
import { getRootEventHexId } from '@/lib/event'
import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
@ -19,6 +21,7 @@ import { useTranslation } from 'react-i18next' @@ -19,6 +21,7 @@ import { useTranslation } from 'react-i18next'
import Emoji from '../Emoji'
import EmojiPicker from '../EmojiPicker'
import SuggestedEmojis from '../SuggestedEmojis'
import DiscussionEmojis from '../SuggestedEmojis/DiscussionEmojis'
import { formatCount } from './utils'
export default function LikeButton({ event }: { event: Event }) {
@ -30,14 +33,39 @@ export default function LikeButton({ event }: { event: Event }) { @@ -30,14 +33,39 @@ export default function LikeButton({ event }: { event: Event }) {
const [isEmojiReactionsOpen, setIsEmojiReactionsOpen] = useState(false)
const [isPickerOpen, setIsPickerOpen] = useState(false)
const noteStats = useNoteStatsById(event.id)
const { myLastEmoji, likeCount } = useMemo(() => {
const isDiscussion = event.kind === ExtendedKind.DISCUSSION
// Check if this is a reply to a discussion event
const [isReplyToDiscussion, setIsReplyToDiscussion] = useState(false)
useMemo(() => {
if (isDiscussion) return // Already a discussion event
const rootEventId = getRootEventHexId(event)
if (rootEventId) {
// Fetch the root event to check if it's a discussion
client.fetchEvent(rootEventId).then(rootEvent => {
if (rootEvent && rootEvent.kind === ExtendedKind.DISCUSSION) {
setIsReplyToDiscussion(true)
}
}).catch(() => {
// If we can't fetch the root event, assume it's not a discussion reply
setIsReplyToDiscussion(false)
})
}
}, [event.id, isDiscussion])
const { myLastEmoji, likeCount, hasVoted } = useMemo(() => {
const stats = noteStats || {}
const myLike = stats.likes?.find((like) => like.pubkey === pubkey)
const likes = hideUntrustedInteractions
? stats.likes?.filter((like) => isUserTrusted(like.pubkey))
: stats.likes
return { myLastEmoji: myLike?.emoji, likeCount: likes?.length }
}, [noteStats, pubkey, hideUntrustedInteractions])
// For discussion events and replies to discussions, check if user has voted (either up or down)
const hasVoted = (isDiscussion || isReplyToDiscussion) && myLike && (myLike.emoji === '⬆' || myLike.emoji === '⬇')
return { myLastEmoji: myLike?.emoji, likeCount: likes?.length, hasVoted }
}, [noteStats, pubkey, hideUntrustedInteractions, isDiscussion, isReplyToDiscussion])
const like = async (emoji: string | TEmoji) => {
checkLogin(async () => {
@ -68,8 +96,9 @@ export default function LikeButton({ event }: { event: Event }) { @@ -68,8 +96,9 @@ export default function LikeButton({ event }: { event: Event }) {
<button
className="flex items-center enabled:hover:text-primary gap-1 px-3 h-full text-muted-foreground"
title={t('Like')}
disabled={(isDiscussion || isReplyToDiscussion) && hasVoted}
onClick={() => {
if (isSmallScreen) {
if (isSmallScreen && !((isDiscussion || isReplyToDiscussion) && hasVoted)) {
setIsEmojiReactionsOpen(true)
}
}}
@ -97,14 +126,23 @@ export default function LikeButton({ event }: { event: Event }) { @@ -97,14 +126,23 @@ export default function LikeButton({ event }: { event: Event }) {
<Drawer open={isEmojiReactionsOpen} onOpenChange={setIsEmojiReactionsOpen}>
<DrawerOverlay onClick={() => setIsEmojiReactionsOpen(false)} />
<DrawerContent hideOverlay>
<EmojiPicker
onEmojiClick={(emoji) => {
setIsEmojiReactionsOpen(false)
if (!emoji) return
{(isDiscussion || isReplyToDiscussion) ? (
<DiscussionEmojis
onEmojiClick={(emoji) => {
setIsEmojiReactionsOpen(false)
like(emoji)
}}
/>
) : (
<EmojiPicker
onEmojiClick={(emoji) => {
setIsEmojiReactionsOpen(false)
if (!emoji) return
like(emoji)
}}
/>
like(emoji)
}}
/>
)}
</DrawerContent>
</Drawer>
</>
@ -115,6 +153,7 @@ export default function LikeButton({ event }: { event: Event }) { @@ -115,6 +153,7 @@ export default function LikeButton({ event }: { event: Event }) {
<DropdownMenu
open={isEmojiReactionsOpen}
onOpenChange={(open) => {
if ((isDiscussion || isReplyToDiscussion) && hasVoted) return // Don't open if user has already voted
setIsEmojiReactionsOpen(open)
if (open) {
setIsPickerOpen(false)
@ -122,7 +161,7 @@ export default function LikeButton({ event }: { event: Event }) { @@ -122,7 +161,7 @@ export default function LikeButton({ event }: { event: Event }) {
}}
>
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
<DropdownMenuContent side="top" className="p-0 w-fit">
<DropdownMenuContent side="top" className={(isDiscussion || isReplyToDiscussion) ? "p-0 w-fit min-w-0 max-w-fit" : "p-0 w-fit"} style={(isDiscussion || isReplyToDiscussion) ? { width: '60px', maxWidth: '60px', minWidth: '60px' } : undefined}>
{isPickerOpen ? (
<EmojiPicker
onEmojiClick={(emoji, e) => {
@ -130,6 +169,13 @@ export default function LikeButton({ event }: { event: Event }) { @@ -130,6 +169,13 @@ export default function LikeButton({ event }: { event: Event }) {
setIsEmojiReactionsOpen(false)
if (!emoji) return
like(emoji)
}}
/>
) : (isDiscussion || isReplyToDiscussion) ? (
<DiscussionEmojis
onEmojiClick={(emoji) => {
setIsEmojiReactionsOpen(false)
like(emoji)
}}
/>

32
src/components/NoteStats/index.tsx

@ -3,8 +3,10 @@ import { useNostr } from '@/providers/NostrProvider' @@ -3,8 +3,10 @@ import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import noteStatsService from '@/services/note-stats.service'
import { ExtendedKind } from '@/constants'
import { getRootEventHexId } from '@/lib/event'
import client from '@/services/client.service'
import { Event } from 'nostr-tools'
import { useEffect, useState } from 'react'
import { useEffect, useState, useMemo } from 'react'
import BookmarkButton from '../BookmarkButton'
import LikeButton from './LikeButton'
import Likes from './Likes'
@ -33,8 +35,26 @@ export default function NoteStats({ @@ -33,8 +35,26 @@ export default function NoteStats({
const { pubkey } = useNostr()
const [loading, setLoading] = useState(false)
// Hide repost button for discussion events
// Hide repost button for discussion events and replies to discussions
const isDiscussion = event.kind === ExtendedKind.DISCUSSION
const [isReplyToDiscussion, setIsReplyToDiscussion] = useState(false)
useMemo(() => {
if (isDiscussion) return // Already a discussion event
const rootEventId = getRootEventHexId(event)
if (rootEventId) {
// Fetch the root event to check if it's a discussion
client.fetchEvent(rootEventId).then(rootEvent => {
if (rootEvent && rootEvent.kind === ExtendedKind.DISCUSSION) {
setIsReplyToDiscussion(true)
}
}).catch(() => {
// If we can't fetch the root event, assume it's not a discussion reply
setIsReplyToDiscussion(false)
})
}
}, [event.id, isDiscussion])
useEffect(() => {
if (!fetchIfNotExisting) return
@ -60,8 +80,8 @@ export default function NoteStats({ @@ -60,8 +80,8 @@ export default function NoteStats({
onClick={(e) => e.stopPropagation()}
>
<ReplyButton event={event} />
{!isDiscussion && <RepostButton event={event} />}
{!isDiscussion && <LikeButton event={event} />}
{!isDiscussion && !isReplyToDiscussion && <RepostButton event={event} />}
<LikeButton event={event} />
<ZapButton event={event} />
<BookmarkButton event={event} />
<SeenOnButton event={event} />
@ -84,8 +104,8 @@ export default function NoteStats({ @@ -84,8 +104,8 @@ export default function NoteStats({
onClick={(e) => e.stopPropagation()}
>
<ReplyButton event={event} />
{!isDiscussion && <RepostButton event={event} />}
{!isDiscussion && <LikeButton event={event} />}
{!isDiscussion && !isReplyToDiscussion && <RepostButton event={event} />}
<LikeButton event={event} />
<ZapButton event={event} />
</div>
<div className="flex items-center" onClick={(e) => e.stopPropagation()}>

23
src/components/SuggestedEmojis/DiscussionEmojis.tsx

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
import { TEmoji } from '@/types'
const DISCUSSION_EMOJIS = ['⬆', '⬇']
export default function DiscussionEmojis({
onEmojiClick
}: {
onEmojiClick: (emoji: string | TEmoji) => void
}) {
return (
<div className="flex gap-1 p-1" style={{ width: '60px', maxWidth: '60px' }} onClick={(e) => e.stopPropagation()}>
{DISCUSSION_EMOJIS.map((emoji, index) => (
<div
key={index}
className="w-6 h-6 rounded-lg clickable flex justify-center items-center text-base hover:bg-muted flex-shrink-0"
onClick={() => onEmojiClick(emoji)}
>
{emoji}
</div>
))}
</div>
)
}
Loading…
Cancel
Save