feat(nostr): Added functionality to message button in profile

This commit is contained in:
Daniel Gonzalez
2025-09-08 13:07:48 -04:00
parent a1d31253c6
commit 96f874f93d
7 changed files with 124 additions and 53 deletions
@@ -9,6 +9,7 @@ import React from 'react'
import ChatHeader from './chat-header'
import MessageList from './message-list'
import MessageInput from './message-input'
import { Spinner } from 'react-bootstrap'
function ChatMain (props) {
const { selectedChannel, messages } = props
@@ -16,9 +17,9 @@ function ChatMain (props) {
return (
<>
{!selectedChannel && (
<div className='h-100 d-flex align-items-center justify-content-center' style={{ backgroundColor: '#f8f9fa' }}>
<div className='h-100 d-flex align-items-center justify-content-center' style={{ backgroundColor: '#f8f9fa', minHeight: '200px' }}>
<div className='text-center text-muted'>
<h4>Select a channel to start chatting</h4>
<Spinner animation='border' size='sm' />
</div>
</div>
)}
@@ -6,6 +6,7 @@
import React, { useCallback, useEffect, useState } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUser } from '@fortawesome/free-solid-svg-icons'
import { Spinner } from 'react-bootstrap'
export default function DMItem (props) {
const { dm, profiles, selectedChannel, onChangeChannel } = props
@@ -97,6 +98,7 @@ export default function DMItem (props) {
style={{ fontSize: '14px' }}
>
{getShortName(profile?.name) || 'Unknown User'}
{!profile?.name && <Spinner animation='border' size='sm' style={{ marginLeft: '5px' }} />}
</div>
{/* <div
className={`small ${isSelected ? 'text-white-50' : 'text-muted'} ${dm.unreadCount > 0 ? 'fw-bold' : ''}`}
+97 -47
View File
@@ -14,7 +14,8 @@ import config from '../../../config'
function NostrChat (props) {
const { appData } = props
const { nostrQueries, bchWalletState } = appData
const { nostrQueries, bchWalletState, startChannelChat } = appData
const [messages, setMessages] = useState([])
const [loadedMessages, setLoadedMessages] = useState(false)
const [profiles, setProfiles] = useState({})
@@ -24,12 +25,86 @@ function NostrChat (props) {
const [dmChannels, setDmChannels] = useState([])
const [dmListLoaded, setDmListLoaded] = useState(false)
const [selectedChannel, setSelectedChannel] = useState(config.chatsId[0])
const [selectedChannel, setSelectedChannel] = useState(null)
const [selectedChannelIsDm, setSelectedChannelIsDm] = useState(false)
const profilesRef = useRef({})
const dmChannelsRef = useRef([])
// Reset states on change channel
const onChangeChannel = useCallback((ch) => {
if (selectedChannel === ch) return
const profiles = profilesRef.current
setSelectedChannelIsDm(!!profiles[ch])
setMessages([])
setLoadedMessages(false)
setSelectedChannel(ch)
}, [selectedChannel])
// Add a new DM to the list
const addPrivateMessage = useCallback(async (profile) => {
try {
const exist = dmChannelsRef.current.find(val => val === profile.pubKey)
setMessages([])
setLoadedMessages(false)
onChangeChannel(profile.pubKey)
if (exist) return
setDmChannels(currentChs => {
let newChs = [...currentChs]
newChs.push(profile.pubKey)
newChs = newChs.filter((val, i, list) => {
const existingIndex = list.findIndex(value => value === val)
return existingIndex === i
})
dmChannelsRef.current = newChs
return newChs
})
setChannelsData(currentChs => {
const newChs = { ...currentChs }
newChs[profile.pubKey] = profile
return newChs
})
} catch (error) {
console.warn(error)
}
}, [onChangeChannel])
// Define starter chat
useEffect(() => {
// Start dm if a initial chat is provided from props
const startDM = async (pubKey) => {
const npub = await appData.nostrQueries.hexToNpub(pubKey)
const defaultProfile = { name: npub } // default profile
profilesRef.current[pubKey] = defaultProfile // update ref , to prevent fetch this profile again.
// Fetch profile
const nostrProfile = await appData.nostrQueries.getProfile(pubKey)
const profile = nostrProfile || defaultProfile
// add public key formats to profile object
profile.pubKey = pubKey
profile.npub = npub
// Update profiles state
setProfiles(currentProfiles => {
const newProfiles = { ...currentProfiles }
newProfiles[pubKey] = profile
profilesRef.current = newProfiles
return newProfiles
})
await addPrivateMessage(profile)
}
if (selectedChannel) return
if (!startChannelChat) {
// Set group chat as initial chat
setSelectedChannel(config.chatsId[0])
setSelectedChannelIsDm(false)
} else if (dmListLoaded) {
// Set provided profile as initial chat
startDM(startChannelChat)
}
}, [appData, startChannelChat, dmListLoaded, addPrivateMessage, selectedChannel])
// Handle read messages
const onMsgRead = useCallback(async (ev) => {
try {
@@ -127,7 +202,7 @@ function NostrChat (props) {
})
pool.on('event', (relay, subId, ev) => {
console.log('post retrieved from ', relay.url, ev.content)
console.log('Group post retrieved from ', relay.url, ev.content)
onMsgRead(ev)
})
@@ -141,13 +216,11 @@ function NostrChat (props) {
// Handle nostr pool for dm channels
useEffect(() => {
// fetch messages when channel selected and channel metadata are loaded
console.log('selectedChannel', selectedChannel)
console.log('selectedChannelIsDm', selectedChannelIsDm)
if (!selectedChannel || !selectedChannelIsDm) return
const { nostrKeyPair } = bchWalletState
const dmPubKey = selectedChannel
console.log('dmPubKey', selectedChannel)
// Load messages for group channel
const relays = nostrQueries.relays
@@ -172,7 +245,7 @@ function NostrChat (props) {
})
pool.on('event', (relay, subId, ev) => {
console.log('post retrieved from ', relay.url, ev.content)
console.log('DM post retrieved from ', relay.url, ev.content)
// decrpt message
if (ev.pubkey === nostrKeyPair.pubHex) {
// Sent messages
@@ -211,6 +284,7 @@ function NostrChat (props) {
setDmChannels(currentChs => {
const newChs = [...currentChs]
newChs.push(profile.pubKey)
dmChannelsRef.current = newChs
return newChs
})
@@ -254,18 +328,30 @@ function NostrChat (props) {
// Load Dm channels
useEffect(() => {
const loadCurrentDms = async () => {
const { nostrQueries, bchWalletState } = appData
const { nostrKeyPair } = bchWalletState
const dms = await appData.nostrQueries.getDms(nostrKeyPair.pubHex)
setDmChannels(currentChs => {
let newChs = [...currentChs, ...dms]
newChs = newChs.filter((val, i, list) => {
const existingIndex = list.findIndex(value => value === val)
return existingIndex === i
})
dmChannelsRef.current = newChs
return newChs
})
setDmListLoaded(true)
console.log('dm list', dms)
for (let i = 0; i < dms.length; i++) {
const pubKey = dms[i]
const npub = await appData.nostrQueries.hexToNpub(pubKey)
const npub = await nostrQueries.hexToNpub(pubKey)
const defaultProfile = { name: npub } // default profile
profilesRef.current[pubKey] = defaultProfile // update ref , to prevent fetch this profile again.
// Fetch profile
const nostrProfile = await appData.nostrQueries.getProfile(pubKey)
const nostrProfile = await nostrQueries.getProfile(pubKey)
const profile = nostrProfile || defaultProfile
// add public key formats to profile object
@@ -279,13 +365,10 @@ function NostrChat (props) {
return newProfiles
})
}
setDmChannels(dms)
setDmListLoaded(true)
dmChannelsRef.current = dms
}
loadCurrentDms()
}, [bchWalletState, appData.nostrQueries])
if (!dmListLoaded) loadCurrentDms()
}, [appData, dmListLoaded])
// Load public channels data
useEffect(() => {
@@ -313,42 +396,9 @@ function NostrChat (props) {
}
}
// loadMessages()
loadChData()
}, [selectedChannel, appData, groupChannels])
const addPrivateMessage = async (profile) => {
try {
const exist = dmChannels.find(val => val === profile.pubKey)
setMessages([])
setLoadedMessages(false)
onChangeChannel(profile.pubKey)
if (exist) return
setDmChannels(currentChs => {
const newChs = [...currentChs]
newChs.push(profile.pubKey)
dmChannelsRef.current = newChs
return newChs
})
setChannelsData(currentChs => {
const newChs = { ...currentChs }
newChs[profile.pubKey] = profile
return newChs
})
} catch (error) {
console.warn(error)
}
}
// Reset states on change channel
const onChangeChannel = useCallback((ch) => {
if (selectedChannel === ch) return
setSelectedChannelIsDm(!!profiles[ch])
setMessages([])
setLoadedMessages(false)
setSelectedChannel(ch)
}, [selectedChannel, profiles])
return (
<>
<Container fluid className='h-100 p-0 mb-5 '>
@@ -12,6 +12,7 @@ import { Spinner } from 'react-bootstrap'
function MessageList (props) {
const { messages, loadedMessages } = props
console.log('loadedMessages', loadedMessages)
const [groupedMessages, setGroupedMessages] = useState([])
const msgContainerRef = useRef()
// Group messages by date
@@ -10,6 +10,7 @@ import CopyOnClick from '../../bch-wallet/copy-on-click.js'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUser, faGlobe } from '@fortawesome/free-solid-svg-icons'
import * as nip19 from 'nostr-tools/nip19'
import { useNavigate } from 'react-router-dom'
// Local libraries
import NostrFormat from '../nostr-format'
@@ -18,14 +19,17 @@ function ProfileRead (props) {
const { npub, appData } = props
const { onProfileRead } = props
const [profile, setProfile] = useState({})
const [pubKey, setPubKey] = useState({})
const [loaded, setLoaded] = useState(false)
const [imageError, setImageError] = useState({ picture: false, banner: false })
const navigate = useNavigate()
useEffect(() => {
const start = async () => {
const pubHexData = nip19.decode(npub)
const pubHex = pubHexData.data
setPubKey(pubHex)
const profile = await appData.nostrQueries.getProfile(pubHex)
if (profile) {
onProfileRead(profile)
@@ -48,6 +52,13 @@ function ProfileRead (props) {
setImageError(prev => ({ ...prev, [type]: false }))
}
const handleMessage = () => {
console.log('appData', appData)
const { setStartChannelChat } = appData
setStartChannelChat(pubKey)
navigate('/nostr-chat')
}
return (
<Container>
{/* Banner Section */}
@@ -146,6 +157,8 @@ function ProfileRead (props) {
variant='primary'
className='px-3 px-md-4 py-2 rounded-pill fw-semibold'
style={{ minWidth: '120px' }}
onClick={handleMessage}
disabled={!loaded}
>
<i className='bi bi-chat-dots me-2' />
Message
+6 -2
View File
@@ -58,6 +58,9 @@ function useAppState () {
// Nostr queries service
const nostrQueriesRef = useRef(new NostrQueries({ relays: readRelays }))
// ProfileDM
const [startChannelChat, setStartChannelChat] = useState('')
// The wallet state makes this a true progressive web app (PWA). As
// balances, UTXOs, and tokens are retrieved, this state is updated.
// properties are enumerated here for the purpose of documentation.
@@ -205,8 +208,9 @@ function useAppState () {
updateRelaysData,
restoreRelaysData,
readRelays,
writeRelays
writeRelays,
startChannelChat,
setStartChannelChat
}
}
+1 -1
View File
@@ -401,7 +401,7 @@ export default class NostrQueries {
})
pool.on('event', (relay, subId, ev) => {
console.log('post retrieved from ', relay.url, ev.sig)
// console.log('post retrieved from ', relay.url, ev.sig)
if (ev.pubkey === pubKey) {
const pk = ev.tags[0][1]
list = [...list, pk]