diff --git a/src/@context/Orbis.tsx b/src/@context/Orbis.tsx index 6a8d28de6..cfaf75da3 100644 --- a/src/@context/Orbis.tsx +++ b/src/@context/Orbis.tsx @@ -21,16 +21,21 @@ interface INewConversation { recipients: string[] } +export interface IConversationWithNotifsCount extends IOrbisConversation { + notifications_count: number +} + type IOrbisProvider = { orbis: IOrbis account: IOrbisProfile hasLit: boolean openConversations: boolean conversationId: string - conversations: IOrbisConversation[] - notifications: Record + conversations: IConversationWithNotifsCount[] activeConversationTitle: string newConversation: INewConversation + notifsLastRead: Record> + totalNotifications: number connectOrbis: (options: { address: string lit?: boolean @@ -52,9 +57,9 @@ type IOrbisProvider = { setConversationId: (conversationId: string) => void getConversationByDid: (userDid: string) => Promise createConversation: (recipients: string[]) => Promise - clearMessageNotifs: (conversationId: string) => void getConversationTitle: (conversationId: string) => Promise getDid: (address: string) => Promise + clearConversationNotifs: (conversationId: string) => void } const OrbisContext = createContext({} as IOrbisProvider) @@ -72,15 +77,17 @@ function OrbisProvider({ children }: { children: ReactNode }): ReactElement { 'ocean-ceramic-sessions', [] ) - const [usersNotifications, setUsersNotifications] = useLocalStorage< - Record> - >('ocean-convo-notifs', {}) + const [notifsLastRead, setNotifsLastRead] = useLocalStorage< + Record> + >('ocean-notifs-last-read', {}) const [account, setAccount] = useState(null) const [hasLit, setHasLit] = useState(false) const [openConversations, setOpenConversations] = useState(false) const [conversationId, setConversationId] = useState(null) - const [conversations, setConversations] = useState([]) + const [conversations, setConversations] = useState< + IConversationWithNotifsCount[] + >([]) const [activeConversationTitle, setActiveConversationTitle] = useState(null) const [newConversation, setNewConversation] = useState(null) @@ -211,69 +218,78 @@ function OrbisProvider({ children }: { children: ReactNode }): ReactElement { return _did } - const getMessageNotifs = async () => { + const getConversationNotifications: ( + conversations: IConversationWithNotifsCount[] + ) => Promise = async (conversations) => { + if (!conversations.length || !orbis) return + let did = account?.did if (!did && accountId) { did = await getDid(accountId) } - const address = didToAddress(did) + const _newConversations = await Promise.all( + conversations.map(async (conversation) => { + // Get timestamp of last read notification + const lastRead = + notifsLastRead[accountId]?.[conversation.stream_id] || 0 - const { data, error } = await orbis.api.rpc('orbis_f_notifications', { - user_did: did || 'none', - notif_type: 'messages' - }) + const { data, error } = await orbis.api + .rpc('orbis_f_count_notifications_alpha', { + user_did: did, + notif_type: 'messages', + q_context: CONVERSATION_CONTEXT, + q_conversation_id: conversation.stream_id, + q_last_read: lastRead + }) + .single() - if (!error && data.length > 0) { - const _usersNotifications = { ...usersNotifications } - const _conversationIds = conversations.map((o) => o.stream_id) + if (error) { + console.log(error) + return conversation + } - // Only show new notifications from existing conversations - const _unreads = data.filter((o: IOrbisNotification) => { - return ( - _conversationIds.includes(o.content.conversation_id) && - o.status === 'new' - ) - }) - _unreads.forEach((o: IOrbisNotification) => { - const conversationId = o.content.conversation_id - const { encryptedString } = o.content.encryptedMessage - - // Add address if not exists - if (!_usersNotifications[address]) _usersNotifications[address] = {} - - // Add conversationId if not exists - if (!_usersNotifications[address][conversationId]) - _usersNotifications[address][conversationId] = [] - - // Add encryptedString if not exists - if ( - !_usersNotifications[address][conversationId].includes( - encryptedString - ) - ) { - _usersNotifications[address][conversationId].push(encryptedString) + if (data) { + const newNotifsCount = data.count_new_notifications + // Get conversation by stream_id + conversation.notifications_count = newNotifsCount + return conversation } }) - setUsersNotifications(_usersNotifications) + ) - if (_unreads.length > 0) { - // Get unix timestamp in seconds - const timestamp = Math.floor(Date.now() / 1000) - // Set read time - await orbis.setNotificationsReadTime('messages', timestamp) - } - } + setConversations(_newConversations) } - const clearMessageNotifs = (conversationId: string) => { - const _usersNotifications = { ...usersNotifications } - const address = didToAddress(account?.did) - if (_usersNotifications[address]) { - delete _usersNotifications[address][conversationId] + const clearConversationNotifs = async (conversationId: string) => { + if (!accountId || !conversationId) return + + const _notifsLastRead = { ...notifsLastRead } + + // Add address if not exists + if (!_notifsLastRead[accountId]) { + _notifsLastRead[accountId] = {} } - setUsersNotifications(_usersNotifications) + + // Add conversationId if not exists + if (!_notifsLastRead[accountId][conversationId]) { + _notifsLastRead[accountId][conversationId] = 0 + } + + // Update last read + _notifsLastRead[accountId][conversationId] = Math.floor(Date.now() / 1000) + setNotifsLastRead(_notifsLastRead) + + // Set conversation notifications count to 0 + const _conversations = conversations.map((conversation) => { + if (conversation.stream_id === conversationId) { + conversation.notifications_count = 0 + } + return conversation + }) + + setConversations(_conversations) } const getConversations = async (did: string = null) => { @@ -285,8 +301,8 @@ function OrbisProvider({ children }: { children: ReactNode }): ReactElement { }) // Only show conversations with unique recipients - const filteredConversations: IOrbisConversation[] = [] - data.forEach((conversation: IOrbisConversation) => { + const filteredConversations: IConversationWithNotifsCount[] = [] + data.forEach((conversation: IConversationWithNotifsCount) => { if (conversation.recipients.length > 1) { // Sort recipients by alphabetical order and stringify const sortedRecipients = conversation.recipients.sort() @@ -294,7 +310,7 @@ function OrbisProvider({ children }: { children: ReactNode }): ReactElement { // Check if conversation already exists based on sorted and stringified recipients const found = filteredConversations.find( - (o: IOrbisConversation) => + (o: IConversationWithNotifsCount) => o.recipients.length > 1 && o.recipients.sort().join(',') === stringifiedRecipients ) @@ -306,7 +322,7 @@ function OrbisProvider({ children }: { children: ReactNode }): ReactElement { }) // Also fetch message notifications - await getMessageNotifs() + await getConversationNotifications(filteredConversations) setConversations(filteredConversations) return filteredConversations @@ -319,7 +335,7 @@ function OrbisProvider({ children }: { children: ReactNode }): ReactElement { await sleep(2000) await getConversation(conversationId) } else { - return data + return data as IConversationWithNotifsCount } } @@ -330,7 +346,7 @@ function OrbisProvider({ children }: { children: ReactNode }): ReactElement { if (!_conversations.length) return null const filteredConversations = _conversations.filter( - (conversation: IOrbisConversation) => { + (conversation: IConversationWithNotifsCount) => { return ( conversation.recipients.length === 2 && conversation.recipients.includes(userDid) @@ -361,61 +377,6 @@ function OrbisProvider({ children }: { children: ReactNode }): ReactElement { } } - // const createConversation = async (userDid: string) => { - // let _account = account - // if (!_account) { - // // Check connection and force connect - // _account = await checkOrbisConnection({ - // address: accountId, - // autoConnect: true - // }) - // } - - // if (!hasLit) { - // const res = await connectLit() - // if (res.status !== 200) { - // alert('Error connecting to Lit.') - // return - // } - // } - - // let existingConversation = conversations.find( - // (conversation: IOrbisConversation) => { - // return conversation.recipients.includes(userDid) - // } - // ) - - // // Refetch to make sure we have the latest conversations - // if (!existingConversation) { - // const _conversations = await getConversations(_account?.did) - // existingConversation = _conversations.find( - // (conversation: IOrbisConversation) => { - // return conversation.recipients.includes(userDid) - // } - // ) - // } - - // if (existingConversation) { - // setConversationId(existingConversation.stream_id) - // setOpenConversations(true) - // } else { - // const res = await orbis.createConversation({ - // recipients: [userDid], - // context: CONVERSATION_CONTEXT - // }) - // if (res.status === 200) { - // setTimeout(async () => { - // const { data, error } = await orbis.getConversation(res.doc) - // if (!error && data) { - // setConversations([data, ...conversations]) - // } - // setConversationId(res.doc) - // setOpenConversations(true) - // }, 2000) - // } - // } - // } - const getConversationTitle = async (conversationId: string) => { if (conversationId && conversations.length) { // Get conversation based on conversationId @@ -438,13 +399,17 @@ function OrbisProvider({ children }: { children: ReactNode }): ReactElement { } } - const notifications = useMemo(() => { - let _notifications = {} - if (accountId && accountId.toLowerCase() in usersNotifications) { - _notifications = usersNotifications[accountId.toLowerCase()] - } - return _notifications - }, [accountId, usersNotifications]) + const totalNotifications = useMemo(() => { + if (!conversations.length) return 0 + + // Loop through conversations and count notifications + let count = 0 + conversations.forEach((conversation: IConversationWithNotifsCount) => { + count += conversation?.notifications_count || 0 + }) + + return count + }, [conversations]) useInterval(async () => { await getConversations(account?.did) @@ -477,9 +442,10 @@ function OrbisProvider({ children }: { children: ReactNode }): ReactElement { openConversations, conversationId, conversations, - notifications, activeConversationTitle, newConversation, + notifsLastRead, + totalNotifications, connectOrbis, disconnectOrbis, checkOrbisConnection, @@ -490,9 +456,9 @@ function OrbisProvider({ children }: { children: ReactNode }): ReactElement { setConversationId, getConversationByDid, createConversation, - clearMessageNotifs, getConversationTitle, - getDid + getDid, + clearConversationNotifs }} > {children} diff --git a/src/components/@shared/Orbis/DirectMessages/Conversation.module.css b/src/components/@shared/Orbis/DirectMessages/Conversation.module.css index 5a62a59dd..308b58498 100644 --- a/src/components/@shared/Orbis/DirectMessages/Conversation.module.css +++ b/src/components/@shared/Orbis/DirectMessages/Conversation.module.css @@ -17,9 +17,11 @@ padding: calc(var(--spacer) / 4); text-align: center; color: var(--color-secondary); - -webkit-animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; + /* -webkit-animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; -moz-animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; - animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; */ + background-color: var(--background-content); + z-index: 2; } .connectLit { diff --git a/src/components/@shared/Orbis/DirectMessages/Conversation.tsx b/src/components/@shared/Orbis/DirectMessages/Conversation.tsx index c7bc283b3..178521faa 100644 --- a/src/components/@shared/Orbis/DirectMessages/Conversation.tsx +++ b/src/components/@shared/Orbis/DirectMessages/Conversation.tsx @@ -15,12 +15,13 @@ export default function DmConversation() { conversationId, hasLit, connectLit, - clearMessageNotifs + clearConversationNotifs } = useOrbis() const messagesWrapper = useRef(null) const [isInitialized, setIsInitialized] = useState(false) const [isLoading, setIsLoading] = useState(false) + const [isCreatingConvo, setIsCreatingConvo] = useState(false) const [messages, setMessages] = useState([]) const [currentPage, setCurrentPage] = useState(0) const [hasMore, setHasMore] = useState(true) @@ -58,7 +59,7 @@ export default function DmConversation() { _messages = [...data, ..._messages] setMessages(_messages) if (currentPage === 0) { - clearMessageNotifs(conversationId) + clearConversationNotifs(conversationId) scrollToBottom() } setCurrentPage(_page + 1) @@ -116,7 +117,7 @@ export default function DmConversation() { Math.ceil(el.scrollTop) >= Math.floor(el.scrollHeight - el.offsetHeight) ) { setNewMessages(0) - clearMessageNotifs(conversationId) + clearConversationNotifs(conversationId) } // Remove scroll listener @@ -172,6 +173,11 @@ export default function DmConversation() { ) : ( <> {isLoading &&
Loading...
} + {isCreatingConvo && ( +
+ Creating conversation and sending message. Please wait... +
+ )}
{!isLoading && messages.length === 0 ? (
No message yet
@@ -215,7 +221,11 @@ export default function DmConversation() { )}
- + )} diff --git a/src/components/@shared/Orbis/DirectMessages/Header.module.css b/src/components/@shared/Orbis/DirectMessages/Header.module.css index eec96da39..1f518113c 100644 --- a/src/components/@shared/Orbis/DirectMessages/Header.module.css +++ b/src/components/@shared/Orbis/DirectMessages/Header.module.css @@ -88,8 +88,8 @@ .notificationCount { background-color: var(--color-primary); - width: 20px; - height: 20px; + width: 24px; + height: 24px; color: var(--brand-white); border-radius: 100%; display: flex; diff --git a/src/components/@shared/Orbis/DirectMessages/Header.tsx b/src/components/@shared/Orbis/DirectMessages/Header.tsx index d1ac26b69..2d024bfd3 100644 --- a/src/components/@shared/Orbis/DirectMessages/Header.tsx +++ b/src/components/@shared/Orbis/DirectMessages/Header.tsx @@ -14,9 +14,9 @@ export default function Header() { conversations, conversationId, openConversations, - notifications, activeConversationTitle, newConversation, + totalNotifications, setActiveConversationTitle, setNewConversation, getConversationTitle, @@ -78,9 +78,9 @@ export default function Header() { Direct Messages - {Object.values(notifications).flat().length > 0 && ( + {totalNotifications > 0 && ( - {Object.values(notifications).flat().length} + {totalNotifications} )} diff --git a/src/components/@shared/Orbis/DirectMessages/List.tsx b/src/components/@shared/Orbis/DirectMessages/List.tsx index d3431150d..f88be3941 100644 --- a/src/components/@shared/Orbis/DirectMessages/List.tsx +++ b/src/components/@shared/Orbis/DirectMessages/List.tsx @@ -1,27 +1,24 @@ import React from 'react' -import { useOrbis } from '@context/Orbis' +import { useOrbis, IConversationWithNotifsCount } from '@context/Orbis' import ListItem from './ListItem' import ChatBubble from '@images/chatbubble.svg' import styles from './List.module.css' export default function List() { - const { conversations, notifications, setConversationId } = useOrbis() - - const getConversationUnreads = (conversationId: string) => { - return notifications[conversationId]?.length || 0 - } + const { conversations, setConversationId } = useOrbis() return (
{conversations.length > 0 ? ( - conversations.map((conversation: IOrbisConversation, index: number) => ( - - )) + conversations.map( + (conversation: IConversationWithNotifsCount, index: number) => ( + + ) + ) ) : (
diff --git a/src/components/@shared/Orbis/DirectMessages/ListItem.module.css b/src/components/@shared/Orbis/DirectMessages/ListItem.module.css index 0603adb67..ea2bf18e9 100644 --- a/src/components/@shared/Orbis/DirectMessages/ListItem.module.css +++ b/src/components/@shared/Orbis/DirectMessages/ListItem.module.css @@ -30,8 +30,8 @@ bottom: -4px; right: -4px; background-color: var(--color-primary); - width: 20px; - height: 20px; + width: 24px; + height: 24px; color: var(--brand-white); border-radius: 100%; display: flex; @@ -39,9 +39,6 @@ align-items: center; font-size: var(--font-size-mini); font-weight: 600; - position: absolute; - bottom: -4px; - right: -4px; } .accountInfo { diff --git a/src/components/@shared/Orbis/DirectMessages/ListItem.tsx b/src/components/@shared/Orbis/DirectMessages/ListItem.tsx index e5c68069c..bf03da185 100644 --- a/src/components/@shared/Orbis/DirectMessages/ListItem.tsx +++ b/src/components/@shared/Orbis/DirectMessages/ListItem.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react' import { useCancelToken } from '@hooks/useCancelToken' -import { useOrbis } from '@context/Orbis' +import { useOrbis, IConversationWithNotifsCount } from '@context/Orbis' import { didToAddress } from '@utils/orbis' import Avatar from '@shared/atoms/Avatar' import Time from '@shared/atoms/Time' @@ -8,11 +8,9 @@ import styles from './ListItem.module.css' export default function ConversationItem({ conversation, - unreads, setConversationId }: { - conversation: IOrbisConversation - unreads: number + conversation: IConversationWithNotifsCount setConversationId: (value: string) => void }) { const { account, getConversationTitle } = useOrbis() @@ -45,8 +43,10 @@ export default function ConversationItem({ >
- {unreads > 0 && ( -
{unreads}
+ {conversation.notifications_count > 0 && ( +
+ {conversation.notifications_count} +
)}
diff --git a/src/components/@shared/Orbis/DirectMessages/Postbox.tsx b/src/components/@shared/Orbis/DirectMessages/Postbox.tsx index 2cb5bd794..4520654b1 100644 --- a/src/components/@shared/Orbis/DirectMessages/Postbox.tsx +++ b/src/components/@shared/Orbis/DirectMessages/Postbox.tsx @@ -8,10 +8,14 @@ import { didToAddress, sleep } from '@utils/orbis' export default function Postbox({ replyTo = null, + isCreatingConvo, + setIsCreatingConvo, cancelReplyTo, callback }: { replyTo?: IOrbisMessage + isCreatingConvo: boolean + setIsCreatingConvo: (isCreatingConvo: boolean) => void cancelReplyTo?: () => void callback: (value: IOrbisMessage) => void }) { @@ -49,8 +53,12 @@ export default function Postbox({ setIsSending(true) + const body = postBoxArea.current.innerText + postBoxArea.current.innerText = '' + let _conversationId = conversationId if (_conversationId.startsWith('new-') && newConversation) { + setIsCreatingConvo(true) const _newConversation = await createConversation( newConversation.recipients ) @@ -60,10 +68,9 @@ export default function Postbox({ setNewConversation(null) await sleep(1000) } + setIsCreatingConvo(false) } - const body = postBoxArea.current.innerText - const content: IOrbisMessageContent & { decryptedMessage?: string } = { encryptedMessage: null, decryptedMessage: body, @@ -91,7 +98,6 @@ export default function Postbox({ } if (callback) callback(_callbackContent) - postBoxArea.current.innerText = '' const res = await orbis.sendMessage({ conversation_id: _conversationId, @@ -109,7 +115,12 @@ export default function Postbox({ const handleKeyDown = (e: KeyboardEvent) => { if (!e.key) return - if (e.key === 'Enter' && !e.shiftKey && !isSending) { + if (isSending && isCreatingConvo) { + e.preventDefault() + return + } + + if (e.key === 'Enter' && !e.shiftKey) { // Don't generate a new line e.preventDefault() share() @@ -151,6 +162,9 @@ export default function Postbox({ onKeyDown={handleKeyDown} onKeyUp={saveCaretPos} onMouseUp={saveCaretPos} + style={{ + pointerEvents: isSending || isCreatingConvo ? 'none' : 'auto' + }} />