diff --git a/src/components/app-body/nostr-chat/channel-item.js b/src/components/app-body/nostr-chat/channel-item.js index af652a4..49bc137 100644 --- a/src/components/app-body/nostr-chat/channel-item.js +++ b/src/components/app-body/nostr-chat/channel-item.js @@ -4,7 +4,7 @@ // Global npm libraries import React, { useCallback, useEffect, useState } from 'react' -import { ListGroup, Spinner } from 'react-bootstrap' +import { Spinner } from 'react-bootstrap' export default function ChannelItem (props) { const { channel, selectedChannel, onChangeChannel, channelsData } = props @@ -20,40 +20,86 @@ export default function ChannelItem (props) { } }, [chInfo, channelsData, channel]) + const handleClick = () => { + if (onChangeChannel) { + onChangeChannel(channel) + } + } + return ( - { onChangeChannel(channel) }} - key={channel} - className='border-0 bg-transparent text-dark' +
{ + if (selectedChannel !== channel) { + e.currentTarget.style.backgroundColor = '#e9ecef' + e.currentTarget.style.borderColor = '#dee2e6' + } + }} + onMouseLeave={(e) => { + if (selectedChannel !== channel) { + e.currentTarget.style.backgroundColor = 'transparent' + e.currentTarget.style.borderColor = 'transparent' + } }} > - {chInfo?.name && -
- {chInfo.picture && {chInfo.name}} - + {chInfo?.picture + ? ( + {chInfo.name + ) + : ( +
+ # +
+ )} +
+ + {/* Channel Info */} +
+
+ {chInfo?.name || getShortName(channel)} +
+ {!chInfo?.name && ( +
- {chInfo?.name || getShortName(channel)} - -
} - {!chInfo?.name && -
- {getShortName(channel)} -
} - + Loading channel... +
+ )} +
+ + {/* Loading indicator */} + {!chInfo?.name && ( +
+ +
+ )} + ) } diff --git a/src/components/app-body/nostr-chat/channel-list.js b/src/components/app-body/nostr-chat/channel-list.js index d9b68f2..2c776f4 100644 --- a/src/components/app-body/nostr-chat/channel-list.js +++ b/src/components/app-body/nostr-chat/channel-list.js @@ -7,9 +7,9 @@ import React from 'react' import { ListGroup } from 'react-bootstrap' import ChannelItem from './channel-item' function ChannelList (props) { - const { channels, selectedChannel } = props + const { groupChannels, selectedChannel } = props - if (!channels || channels.length === 0) { + if (!groupChannels || groupChannels.length === 0) { return (
No channels available @@ -19,7 +19,7 @@ function ChannelList (props) { return ( - {channels.map((channel, i) => ( + {groupChannels.map((channel, i) => ( ))} diff --git a/src/components/app-body/nostr-chat/chat-header.js b/src/components/app-body/nostr-chat/chat-header.js index 76bbf08..7cb1deb 100644 --- a/src/components/app-body/nostr-chat/chat-header.js +++ b/src/components/app-body/nostr-chat/chat-header.js @@ -9,7 +9,8 @@ function ChatHeader (props) { const { selectedChannel, channelsData } = props const [chInfo, setChInfo] = useState() // channel data const getShortName = useCallback((str) => { - return str.slice(0, 8) + '...' + str.slice(-5) + if (str.length < 20) return str + return str.slice(0, 4) + '...' + str.slice(-4) }, []) // Restore ch info @@ -27,9 +28,9 @@ function ChatHeader (props) {
{chInfo &&
- {chInfo.picture && {chInfo.name}} + {chInfo.picture && {chInfo.name}}
-
{chInfo?.name || getShortName(selectedChannel)}
+
{getShortName(chInfo?.name || selectedChannel)}
{chInfo?.about || ''}
diff --git a/src/components/app-body/nostr-chat/chat-sidebar.js b/src/components/app-body/nostr-chat/chat-sidebar.js index eacce60..70e2f8f 100644 --- a/src/components/app-body/nostr-chat/chat-sidebar.js +++ b/src/components/app-body/nostr-chat/chat-sidebar.js @@ -7,9 +7,10 @@ import React from 'react' // Local libraries import ChannelList from './channel-list' +import DMList from './dm-list' function ChatSidebar (props) { - const { channels, selectedChannel } = props + const { dmChannels, profiles } = props return (
@@ -22,10 +23,22 @@ function ChatSidebar (props) {
+ + {/* DMs Section */} +
+
+
+ DMs +
+
+ +
diff --git a/src/components/app-body/nostr-chat/dm-item.js b/src/components/app-body/nostr-chat/dm-item.js new file mode 100644 index 0000000..a9df2bb --- /dev/null +++ b/src/components/app-body/nostr-chat/dm-item.js @@ -0,0 +1,113 @@ +/* + Component for displaying direct message item +*/ + +// Global npm libraries +import React, { useCallback } from 'react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faUser } from '@fortawesome/free-solid-svg-icons' + +export default function DMItem (props) { + const { dm, profiles, selectedChannel, onChangeChannel } = props + const { pubKey } = dm + const profile = profiles[pubKey] + const isSelected = selectedChannel === pubKey + + const getShortName = useCallback((str) => { + if (str.length < 20) return str + return str.slice(0, 4) + '...' + str.slice(-4) + }, []) + + const handleClick = () => { + if (onChangeChannel) { + onChangeChannel(pubKey) + } + } + + return ( +
{ + if (!isSelected) { + e.currentTarget.style.backgroundColor = '#e9ecef' + e.currentTarget.style.borderColor = '#dee2e6' + } + }} + onMouseLeave={(e) => { + if (!isSelected) { + e.currentTarget.style.backgroundColor = 'transparent' + e.currentTarget.style.borderColor = 'transparent' + } + }} + > + {/* User Avatar */} +
+ {profile?.picture + ? ( + {`${profile.name { + e.target.style.display = 'none' + e.target.nextSibling.style.display = 'flex' + }} + /> + ) + : } +
+ + {/* User Info */} +
+
0 ? 'fw-bold' : 'fw-medium'}`} + style={{ fontSize: '14px' }} + > + {getShortName(profile?.name) || 'Unknown User'} +
+
0 ? 'fw-bold' : ''}`} + style={{ fontSize: '12px' }} + > + No messages yet +
+
+ + {/* Unread indicator */} + {/* {dm.unreadCount > 0 && ( +
+ + {dm.unreadCount > 99 ? '99+' : dm.unreadCount} + +
+ )} */} +
+ ) +} diff --git a/src/components/app-body/nostr-chat/dm-list.js b/src/components/app-body/nostr-chat/dm-list.js new file mode 100644 index 0000000..72d2911 --- /dev/null +++ b/src/components/app-body/nostr-chat/dm-list.js @@ -0,0 +1,44 @@ +/* + Component for displaying a list of direct messages +*/ + +// Global npm libraries +import React from 'react' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faUser } from '@fortawesome/free-solid-svg-icons' +import DMItem from './dm-item' + +function DMList (props) { + const { dmChannels, profiles, selectedChannel, onChangeChannel } = props + + if (dmChannels.length === 0) { + return ( +
+
+ +
+
No direct messages yet
+
+ Start a conversation to see it here +
+
+ ) + } + + return ( +
+ {dmChannels.map((pubKey) => ( + + ))} +
+ ) +} + +export default DMList diff --git a/src/components/app-body/nostr-chat/index.js b/src/components/app-body/nostr-chat/index.js index dae8c29..8569eef 100644 --- a/src/components/app-body/nostr-chat/index.js +++ b/src/components/app-body/nostr-chat/index.js @@ -20,7 +20,8 @@ function NostrChat (props) { const [profiles, setProfiles] = useState({}) const [channelsData, setChannelsData] = useState({}) const [channelsLoaded, setChannelsLoaded] = useState(false) - const [channels] = useState(config.chatsId) + const [groupChannels] = useState(config.chatsId) + const [dmChannels, setDmChannels] = useState([]) const [selectedChannel, setSelectedChannel] = useState(config.chatsId[0]) const profilesRef = useRef({}) @@ -59,6 +60,10 @@ function NostrChat (props) { const nostrProfile = await appData.nostrQueries.getProfile(pubKey) const profile = nostrProfile || defaultProfile + const npub = await appData.nostrQueries.hexToNpub(pubKey) + // add public key formats to profile object + profile.pubKey = pubKey + profile.npub = npub // Update profiles state setProfiles(currentProfiles => { const newProfiles = { ...currentProfiles } @@ -108,8 +113,8 @@ function NostrChat (props) { useEffect(() => { const loadChData = async () => { const loadedChannels = [] - for (let i = 0; i < channels.length; i++) { - const ch = channels[i] + for (let i = 0; i < groupChannels.length; i++) { + const ch = groupChannels[i] const exist = loadedChannels.find((val) => { return val === ch }) if (exist) { continue } @@ -120,7 +125,7 @@ function NostrChat (props) { if (!channelData) channelData = { name: ch.slice(0, 8) + '...' + ch.slice(-5) } loadedChannels.push(channelData) - // Update profile state + // Update channels data state setChannelsData(currentChs => { const newChs = { ...currentChs } newChs[ch] = channelData @@ -132,14 +137,40 @@ function NostrChat (props) { // loadMessages() loadChData() - }, [selectedChannel, appData, channels]) + }, [selectedChannel, appData, groupChannels]) + + const addPrivateMessage = async (profile) => { + try { + const exist = dmChannels.find(val => val === profile.pubKey) + + setSelectedChannel(profile.pubKey) + if (exist) return + setDmChannels(currentChs => { + const newChs = [...currentChs] + newChs.push(profile.pubKey) + return newChs + }) + setChannelsData(currentChs => { + const newChs = { ...currentChs } + newChs[profile.pubKey] = profile + return newChs + }) + + setMessages([]) + + // setSelectedChannel(profile.pubKey) + } catch (error) { + console.warn(error) + } + } // Reset states on change channel const onChangeChannel = useCallback((ch) => { + if (selectedChannel === ch) return setSelectedChannel(ch) setMessages([]) setLoadedMessages(false) - }, []) + }, [selectedChannel]) return ( <> @@ -147,7 +178,8 @@ function NostrChat (props) { diff --git a/src/components/app-body/nostr-chat/message-input.js b/src/components/app-body/nostr-chat/message-input.js index 893365c..bf3c4b3 100644 --- a/src/components/app-body/nostr-chat/message-input.js +++ b/src/components/app-body/nostr-chat/message-input.js @@ -12,7 +12,7 @@ import { hexToBytes } from '@noble/hashes/utils' // already an installed depende import { Relay } from 'nostr-tools/relay' function MessageInput (props) { - const { appData, selectedChannel } = props + const { appData, selectedChannel, profiles } = props const { bchWalletState, writeRelays } = appData const [message, setMessage] = useState('') const [onFetch, setOnFetch] = useState(false) @@ -82,7 +82,7 @@ function MessageInput (props) {