mirror of
https://github.com/Permissionless-Software-Foundation/bch-dex-taker-v2.git
synced 2026-09-22 09:12:00 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3dd9f3d07 | ||
|
|
5e6a90f560 | ||
|
|
ce410ddc9c | ||
|
|
67bd90be8f |
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Card } from 'react-bootstrap'
|
||||
import { Card, Spinner } from 'react-bootstrap'
|
||||
import Jdenticon from '@chris.troutner/react-jdenticon'
|
||||
import CopyOnClick from '../../bch-wallet/copy-on-click.js'
|
||||
import FollowBtn from './follow-btn.js'
|
||||
@@ -11,9 +11,10 @@ import FollowBtn from './follow-btn.js'
|
||||
function ContentCard (props) {
|
||||
const { creator, followList, refreshFollowList } = props
|
||||
const [profile, setProfile] = useState(null)
|
||||
const [imageError, setImageError] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setProfile(creator?.profile || {})
|
||||
setProfile(creator?.profile)
|
||||
}, [creator.profile])
|
||||
|
||||
const goToProfile = () => {
|
||||
@@ -21,6 +22,10 @@ function ContentCard (props) {
|
||||
window.open(profileUrl, '_blank')
|
||||
}
|
||||
|
||||
const handleImageError = (type) => {
|
||||
setImageError(true)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<>
|
||||
@@ -34,9 +39,33 @@ function ContentCard (props) {
|
||||
</div>
|
||||
<div className='d-none d-md-flex align-items-center gap-4 cursor-pointer'>
|
||||
{/* Profile Picture */}
|
||||
<div className='flex-shrink-0' onClick={goToProfile}>
|
||||
<Jdenticon size='140' value={creator.npub} />
|
||||
</div>
|
||||
{(profile && profile.picture && !imageError) && (
|
||||
<div className='flex-shrink-0 max-w-50' onClick={goToProfile}>
|
||||
<img
|
||||
src={profile.picture}
|
||||
alt='Profile'
|
||||
className='rounded-circle shadow'
|
||||
style={{ objectFit: 'cover', width: '140px' }}
|
||||
onError={() => handleImageError('picture')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
)}
|
||||
{/* Default profile Picture */}
|
||||
{((profile && !profile.picture) || imageError) && (
|
||||
<div className='flex-shrink-0' onClick={goToProfile}>
|
||||
<Jdenticon size='140' value={creator.npub} />
|
||||
</div>
|
||||
)}
|
||||
{/** */}
|
||||
{!profile && (
|
||||
<div
|
||||
className='flex-shrink-0'
|
||||
style={{ width: '140px', height: '140px', display: 'flex', justifyContent: 'center', alignItems: 'center' }}
|
||||
>
|
||||
<Spinner size='140' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Creator Info */}
|
||||
<div className='flex-grow-1'>
|
||||
@@ -113,9 +142,33 @@ function ContentCard (props) {
|
||||
{/* Mobile Layout - Vertical */}
|
||||
<div className='d-flex d-md-none flex-column align-items-center text-center'>
|
||||
{/* Profile Picture */}
|
||||
<div className='mb-3'>
|
||||
<Jdenticon size='100' value={creator.npub} onClick={goToProfile} />
|
||||
</div>
|
||||
{(profile && profile.picture && !imageError) && (
|
||||
<div className='flex-shrink-0 max-w-50 mb-2' onClick={goToProfile}>
|
||||
<img
|
||||
src={profile.picture}
|
||||
alt='Profile'
|
||||
className='rounded-circle shadow'
|
||||
style={{ objectFit: 'cover', width: '100px' }}
|
||||
onError={() => handleImageError('picture')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
)}
|
||||
{/* Default profile Picture */}
|
||||
{((profile && !profile.picture) || imageError) && (
|
||||
<div className='flex-shrink-0 mb-2' onClick={goToProfile}>
|
||||
<Jdenticon size='100' value={creator.npub} />
|
||||
</div>
|
||||
)}
|
||||
{/** */}
|
||||
{!profile && (
|
||||
<div
|
||||
className='flex-shrink-0'
|
||||
style={{ width: '100px', height: '100px', display: 'flex', justifyContent: 'center', alignItems: 'center' }}
|
||||
>
|
||||
<Spinner size='100' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Creator Info */}
|
||||
<div className='w-100 mb-3'>
|
||||
|
||||
@@ -5,22 +5,87 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { Card, Spinner } from 'react-bootstrap'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faUser } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faUser, faHeart as faHeartSolid } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faHeart } from '@fortawesome/free-regular-svg-icons'
|
||||
import * as nip19 from 'nostr-tools/nip19'
|
||||
import NostrFormat from '../nostr-format'
|
||||
import { finalizeEvent } from 'nostr-tools/pure'
|
||||
import { Relay } from 'nostr-tools/relay'
|
||||
import { hexToBytes } from '@noble/hashes/utils' // already an installed dependency
|
||||
import { RelayPool } from 'nostr'
|
||||
|
||||
function FeedCard (props) {
|
||||
const { post, appData, profiles } = props
|
||||
const { nostrKeyPair } = appData.bchWalletState
|
||||
|
||||
const [profile, setProfile] = useState(profiles[post.pubkey])
|
||||
|
||||
const [npub, setNpub] = useState('')
|
||||
const [isClicked, setIsClicked] = useState(false)
|
||||
const [isLiked, setIsLiked] = useState(false)
|
||||
const [likesCount, setLikesCount] = useState(null)
|
||||
const [likesFetched, setLikesFetched] = useState(false)
|
||||
|
||||
// function to fetch a post likes reaction
|
||||
const handleLikes = useCallback(async (post, userPubKey) => {
|
||||
try {
|
||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||
const res = await new Promise((resolve) => {
|
||||
let likesCnt = 0
|
||||
let userLikedCnt = 0
|
||||
const pool = RelayPool([psf])
|
||||
pool.on('open', relay => {
|
||||
relay.subscribe('subid', { kinds: [7], '#e': [post.id] })
|
||||
})
|
||||
|
||||
pool.on('eose', relay => {
|
||||
relay.close()
|
||||
resolve({
|
||||
count: likesCnt,
|
||||
userLiked: userLikedCnt > 0
|
||||
})
|
||||
})
|
||||
|
||||
pool.on('event', (relay, subId, ev) => {
|
||||
try {
|
||||
// Count likes
|
||||
if (ev.content === '+') {
|
||||
likesCnt++
|
||||
// Count user likes
|
||||
if (ev.pubkey === userPubKey) {
|
||||
userLikedCnt++
|
||||
}
|
||||
}
|
||||
// Count dislikes
|
||||
if (ev.content === '-') {
|
||||
likesCnt--
|
||||
// Count user dislikes.
|
||||
if (ev.pubkey === userPubKey) {
|
||||
userLikedCnt--
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// skip error
|
||||
}
|
||||
})
|
||||
})
|
||||
setLikesCount(res.count)
|
||||
setIsLiked(res.userLiked)
|
||||
setLikesFetched(true)
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
}
|
||||
}, [])
|
||||
// Get npub from pubkey
|
||||
useEffect(() => {
|
||||
const npub = nip19.npubEncode(post.pubkey)
|
||||
setNpub(npub)
|
||||
}, [post])
|
||||
|
||||
// Handle post likes
|
||||
if (!likesFetched && nostrKeyPair.pubHex) {
|
||||
handleLikes(post, nostrKeyPair.pubHex)
|
||||
}
|
||||
}, [post, nostrKeyPair, handleLikes, likesFetched])
|
||||
|
||||
// Verify if pubkey profile exists into the profiles state
|
||||
useEffect(() => {
|
||||
@@ -41,6 +106,58 @@ function FeedCard (props) {
|
||||
return npub.slice(0, 8) + '...' + npub.slice(-5)
|
||||
}, [])
|
||||
|
||||
const submitLike = async (e) => {
|
||||
// Post on nostr network
|
||||
e.preventDefault()
|
||||
try {
|
||||
const { nostrKeyPair } = appData.bchWalletState
|
||||
|
||||
setLikesFetched(false)
|
||||
// Convert private key to binary
|
||||
const privateKeyBin = hexToBytes(nostrKeyPair.privHex)
|
||||
|
||||
// Relay list
|
||||
const psf = 'wss://nostr-relay.psfoundation.info'
|
||||
|
||||
// Define if like or dislike
|
||||
let content = '+'
|
||||
if (isLiked) { content = '-' }
|
||||
|
||||
// Generate a post.
|
||||
const eventTemplate = {
|
||||
kind: 7,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['e', post.id, psf],
|
||||
['p', psf, nostrKeyPair.pubHex]
|
||||
],
|
||||
content
|
||||
}
|
||||
console.log(`eventTemplate: ${JSON.stringify(eventTemplate, null, 2)}`)
|
||||
|
||||
// Sign the post
|
||||
const signedEvent = finalizeEvent(eventTemplate, privateKeyBin)
|
||||
console.log('signedEvent: ', signedEvent)
|
||||
|
||||
// Connect to a relay.
|
||||
const relay = await Relay.connect(psf)
|
||||
console.log(`connected to ${relay.url}`)
|
||||
|
||||
// Publish the message to the relay.
|
||||
const result = await relay.publish(signedEvent)
|
||||
console.log('result: ', result)
|
||||
|
||||
// Close the connection to the relay.
|
||||
relay.close()
|
||||
|
||||
await handleLikes(post, nostrKeyPair.pubHex)
|
||||
setLikesFetched(true)
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
setLikesFetched(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className='mb-4 bg-light rounded-4 shadow-sm border-0'>
|
||||
<Card.Body className='p-3'>
|
||||
@@ -91,6 +208,50 @@ function FeedCard (props) {
|
||||
<NostrFormat content={post.content} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='d-flex justify-content-end'>
|
||||
{likesFetched && (
|
||||
<button
|
||||
onClick={submitLike}
|
||||
className='btn btn-link text-decoration-none p-0 d-flex align-items-center gap-2'
|
||||
style={{
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
transition: 'all 0.2s ease',
|
||||
color: isLiked ? '#e31b23' : '#536471',
|
||||
fontSize: '14px',
|
||||
fontWeight: '400'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.target.style.transform = 'scale(1.05)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.target.style.transform = 'scale(1)'
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={isLiked ? faHeartSolid : faHeart}
|
||||
style={{
|
||||
fontSize: '16px',
|
||||
color: isLiked ? '#e31b23' : '#536471',
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: '400',
|
||||
color: isLiked ? '#e31b23' : '#536471',
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
>
|
||||
{likesCount || 0}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
{!likesFetched && (
|
||||
<Spinner animation='border' size='sm' className='ms-2' />
|
||||
)}
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ function ProfileRead (props) {
|
||||
? (
|
||||
<img
|
||||
src={post.picture}
|
||||
alt='Profile picture'
|
||||
alt='Profile'
|
||||
className='rounded-circle shadow w-100 h-100'
|
||||
style={{ objectFit: 'cover' }}
|
||||
onError={() => handleImageError('picture')}
|
||||
|
||||
Reference in New Issue
Block a user