From 96f874f93d2db77204587b449fb0c89c64043d14 Mon Sep 17 00:00:00 2001 From: Daniel Gonzalez Date: Mon, 8 Sep 2025 13:07:48 -0400 Subject: [PATCH] feat(nostr): Added functionality to message button in profile --- .../app-body/nostr-chat/chat-main.js | 5 +- src/components/app-body/nostr-chat/dm-item.js | 2 + src/components/app-body/nostr-chat/index.js | 144 ++++++++++++------ .../app-body/nostr-chat/message-list.js | 1 + .../app-body/nostr/profile/profile-read.js | 15 +- src/hooks/state.js | 8 +- src/services/nostr-queries.js | 2 +- 7 files changed, 124 insertions(+), 53 deletions(-) diff --git a/src/components/app-body/nostr-chat/chat-main.js b/src/components/app-body/nostr-chat/chat-main.js index 17399c9..606e94e 100644 --- a/src/components/app-body/nostr-chat/chat-main.js +++ b/src/components/app-body/nostr-chat/chat-main.js @@ -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 && ( -
+
-

Select a channel to start chatting

+
)} diff --git a/src/components/app-body/nostr-chat/dm-item.js b/src/components/app-body/nostr-chat/dm-item.js index b99fb08..c81259c 100644 --- a/src/components/app-body/nostr-chat/dm-item.js +++ b/src/components/app-body/nostr-chat/dm-item.js @@ -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 && }
{/*
0 ? 'fw-bold' : ''}`} diff --git a/src/components/app-body/nostr-chat/index.js b/src/components/app-body/nostr-chat/index.js index 46025e6..095736e 100644 --- a/src/components/app-body/nostr-chat/index.js +++ b/src/components/app-body/nostr-chat/index.js @@ -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 ( <> diff --git a/src/components/app-body/nostr-chat/message-list.js b/src/components/app-body/nostr-chat/message-list.js index 5884bfa..cef3650 100644 --- a/src/components/app-body/nostr-chat/message-list.js +++ b/src/components/app-body/nostr-chat/message-list.js @@ -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 diff --git a/src/components/app-body/nostr/profile/profile-read.js b/src/components/app-body/nostr/profile/profile-read.js index 5fd6d0c..65ee226 100644 --- a/src/components/app-body/nostr/profile/profile-read.js +++ b/src/components/app-body/nostr/profile/profile-read.js @@ -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 ( {/* 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} > Message diff --git a/src/hooks/state.js b/src/hooks/state.js index 8792360..e534cfd 100644 --- a/src/hooks/state.js +++ b/src/hooks/state.js @@ -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 } } diff --git a/src/services/nostr-queries.js b/src/services/nostr-queries.js index 6a8ebfa..0b36508 100644 --- a/src/services/nostr-queries.js +++ b/src/services/nostr-queries.js @@ -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]