diff --git a/packages/react-chat/src/components/Chat.tsx b/packages/react-chat/src/components/Chat.tsx index 8928643b..c06255a0 100644 --- a/packages/react-chat/src/components/Chat.tsx +++ b/packages/react-chat/src/components/Chat.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useState } from "react"; import styled from "styled-components"; import { useNarrow } from "../contexts/narrowProvider"; -import { useMessenger } from "../hooks/useMessenger"; +import { useMessenger } from "../hooks/messenger/useMessenger"; import { ChannelData } from "../models/ChannelData"; import { Metadata } from "../models/Metadata"; import { Theme } from "../styles/themes"; @@ -37,7 +37,7 @@ export function Chat({ theme, communityKey, fetchMetadata }: ChatProps) { sendMessage, notifications, clearNotifications, - loadNextDay, + loadPrevDay, loadingMessages, community, } = useMessenger(activeChannel?.id ?? "", communityKey); @@ -120,7 +120,7 @@ export function Chat({ theme, communityKey, fetchMetadata }: ChatProps) { showMembers={showMembers} community={communityData} showCommunity={!showChannels} - loadNextDay={() => loadNextDay(activeChannel.name)} + loadPrevDay={() => loadPrevDay(activeChannel.name)} onCommunityClick={showModal} fetchMetadata={fetchMetadata} loadingMessages={loadingMessages} diff --git a/packages/react-chat/src/components/Chat/ChatBody.tsx b/packages/react-chat/src/components/Chat/ChatBody.tsx index 33f90eea..26ab7bc6 100644 --- a/packages/react-chat/src/components/Chat/ChatBody.tsx +++ b/packages/react-chat/src/components/Chat/ChatBody.tsx @@ -33,7 +33,7 @@ interface ChatBodyProps { notifications: { [id: string]: number }; setActiveChannel: (val: ChannelData) => void; activeChannelId: string; - loadNextDay: () => void; + loadPrevDay: () => void; onCommunityClick: () => void; fetchMetadata?: (url: string) => Promise; loadingMessages: boolean; @@ -54,7 +54,7 @@ export function ChatBody({ notifications, setActiveChannel, activeChannelId, - loadNextDay, + loadPrevDay, onCommunityClick, fetchMetadata, loadingMessages, @@ -131,7 +131,7 @@ export function ChatBody({ messenger ? ( diff --git a/packages/react-chat/src/components/Chat/ChatMessages.tsx b/packages/react-chat/src/components/Chat/ChatMessages.tsx index d4d5eaef..40f84d9d 100644 --- a/packages/react-chat/src/components/Chat/ChatMessages.tsx +++ b/packages/react-chat/src/components/Chat/ChatMessages.tsx @@ -13,14 +13,14 @@ import { ChatMessageContent } from "./ChatMessageContent"; type ChatMessagesProps = { messages: ChatMessage[]; - loadNextDay: () => void; + loadPrevDay: () => void; fetchMetadata?: (url: string) => Promise; loadingMessages: boolean; }; export function ChatMessages({ messages, - loadNextDay, + loadPrevDay, fetchMetadata, loadingMessages, }: ChatMessagesProps) { @@ -38,7 +38,7 @@ export function ChatMessages({ if ( (ref?.current?.clientHeight ?? 0) >= (ref?.current?.scrollHeight ?? 0) ) { - loadNextDay(); + loadPrevDay(); } } }, [messages, messages.length, loadingMessages]); @@ -47,7 +47,7 @@ export function ChatMessages({ const setScroll = () => { if (ref && ref.current) { if (ref.current.scrollTop <= 0) { - loadNextDay(); + loadPrevDay(); } if ( ref.current.scrollTop + ref.current.clientHeight == diff --git a/packages/react-chat/src/hooks/messenger/useLoadPrevDay.ts b/packages/react-chat/src/hooks/messenger/useLoadPrevDay.ts new file mode 100644 index 00000000..7600428c --- /dev/null +++ b/packages/react-chat/src/hooks/messenger/useLoadPrevDay.ts @@ -0,0 +1,52 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { Messenger } from "status-communities/dist/cjs"; + +const _MS_PER_DAY = 1000 * 60 * 60 * 24; + +export function useLoadPrevDay( + chatId: string, + messenger: Messenger | undefined +) { + const loadingPreviousMessages = useRef<{ + [chatId: string]: boolean; + }>({}); + const lastLoadTime = useRef<{ + [chatId: string]: Date; + }>({}); + const [loadingMessages, setLoadingMessages] = useState(false); + + useEffect(() => { + setLoadingMessages(loadingPreviousMessages.current[chatId]); + }, [chatId]); + + const loadPrevDay = useCallback( + async (id: string) => { + if (messenger) { + const endTime = lastLoadTime.current[id] ?? new Date(); + const startTime = new Date(endTime.getTime() - _MS_PER_DAY); + const timeDiff = Math.floor( + (new Date().getTime() - endTime.getTime()) / _MS_PER_DAY + ); + if (timeDiff < 30) { + if (!loadingPreviousMessages.current[id]) { + loadingPreviousMessages.current[id] = true; + setLoadingMessages(true); + const amountOfMessages = await messenger.retrievePreviousMessages( + id, + startTime, + endTime + ); + lastLoadTime.current[id] = startTime; + loadingPreviousMessages.current[id] = false; + setLoadingMessages(false); + if (amountOfMessages === 0) { + loadPrevDay(id); + } + } + } + } + }, + [lastLoadTime, messenger] + ); + return { loadingMessages, loadPrevDay }; +} diff --git a/packages/react-chat/src/hooks/messenger/useMessages.ts b/packages/react-chat/src/hooks/messenger/useMessages.ts new file mode 100644 index 00000000..dab8ff71 --- /dev/null +++ b/packages/react-chat/src/hooks/messenger/useMessages.ts @@ -0,0 +1,48 @@ +import { useCallback, useMemo, useState } from "react"; +import { ApplicationMetadataMessage } from "status-communities/dist/cjs"; + +import { ChatMessage } from "../../models/ChatMessage"; +import { binarySetInsert } from "../../utils"; + +import { useNotifications } from "./useNotifications"; + +export function useMessages(chatId: string) { + const [messages, setMessages] = useState<{ [chatId: string]: ChatMessage[] }>( + {} + ); + const { notifications, incNotification, clearNotifications } = + useNotifications(); + + const addMessage = useCallback( + (msg: ApplicationMetadataMessage, id: string, date: Date) => { + const newMessage = ChatMessage.fromMetadataMessage(msg, date); + if (newMessage) { + setMessages((prev) => { + return { + ...prev, + [id]: binarySetInsert( + prev?.[id] ?? [], + newMessage, + (a, b) => a.date < b.date, + (a, b) => a.date.getTime() === b.date.getTime() + ), + }; + }); + incNotification(id); + } + }, + [] + ); + + const activeMessages = useMemo( + () => messages?.[chatId] ?? [], + [messages, chatId] + ); + + return { + messages: activeMessages, + addMessage, + notifications, + clearNotifications, + }; +} diff --git a/packages/react-chat/src/hooks/messenger/useMessenger.ts b/packages/react-chat/src/hooks/messenger/useMessenger.ts new file mode 100644 index 00000000..59cfc0fc --- /dev/null +++ b/packages/react-chat/src/hooks/messenger/useMessenger.ts @@ -0,0 +1,105 @@ +// import { StoreCodec } from "js-waku"; +import { StoreCodec } from "js-waku"; +import { useCallback, useEffect, useState } from "react"; +import { Community, Identity, Messenger } from "status-communities/dist/cjs"; + +import { loadIdentity, saveIdentity } from "../../utils"; + +import { useLoadPrevDay } from "./useLoadPrevDay"; +import { useMessages } from "./useMessages"; + +export function useMessenger(chatId: string, communityKey: string) { + const [messenger, setMessenger] = useState(undefined); + const { addMessage, clearNotifications, notifications, messages } = + useMessages(chatId); + const [community, setCommunity] = useState(undefined); + const { loadPrevDay, loadingMessages } = useLoadPrevDay(chatId, messenger); + + useEffect(() => { + const createMessenger = async () => { + // Test password for now + // Need design for password input + + let identity = await loadIdentity("test"); + if (!identity) { + identity = Identity.generate(); + await saveIdentity(identity, "test"); + } + const messenger = await Messenger.create(identity, { + libp2p: { + config: { + pubsub: { + enabled: true, + emitSelf: true, + }, + }, + }, + }); + await new Promise((resolve) => { + messenger.waku.libp2p.peerStore.on( + "change:protocols", + ({ protocols }) => { + if (protocols.includes(StoreCodec)) { + resolve(""); + } + } + ); + }); + const community = await Community.instantiateCommunity( + communityKey, + messenger.waku + ); + setCommunity(community); + await Promise.all( + Array.from(community.chats.values()).map(async (chat) => { + await messenger.joinChat(chat); + messenger.addObserver( + (msg, date) => addMessage(msg, chat.id, date), + chat.id + ); + clearNotifications(chat.id); + }) + ); + setMessenger(messenger); + }; + createMessenger(); + }, []); + + useEffect(() => { + if (messenger && community?.chats) { + Array.from(community?.chats.values()).forEach(({ id }) => + loadPrevDay(id) + ); + } + }, [messenger]); + + const sendMessage = useCallback( + async (messageText?: string, image?: Uint8Array) => { + if (messageText) { + await messenger?.sendMessage(chatId, { + text: messageText, + contentType: 0, + }); + } + if (image) { + await messenger?.sendMessage(chatId, { + image, + imageType: 1, + contentType: 2, + }); + } + }, + [chatId, messenger] + ); + + return { + messenger, + messages, + sendMessage, + notifications, + clearNotifications, + loadPrevDay, + loadingMessages, + community, + }; +} diff --git a/packages/react-chat/src/hooks/messenger/useNotifications.ts b/packages/react-chat/src/hooks/messenger/useNotifications.ts new file mode 100644 index 00000000..a0b393a7 --- /dev/null +++ b/packages/react-chat/src/hooks/messenger/useNotifications.ts @@ -0,0 +1,24 @@ +import { useCallback, useState } from "react"; + +export function useNotifications() { + const [notifications, setNotifications] = useState<{ + [chatId: string]: number; + }>({}); + const incNotification = useCallback((id: string) => { + setNotifications((prevNotifications) => { + return { + ...prevNotifications, + [id]: prevNotifications[id] + 1, + }; + }); + }, []); + const clearNotifications = useCallback((id: string) => { + setNotifications((prevNotifications) => { + return { + ...prevNotifications, + [id]: 0, + }; + }); + }, []); + return { notifications, incNotification, clearNotifications }; +} diff --git a/packages/react-chat/src/hooks/useMessenger.ts b/packages/react-chat/src/hooks/useMessenger.ts deleted file mode 100644 index 3a86d259..00000000 --- a/packages/react-chat/src/hooks/useMessenger.ts +++ /dev/null @@ -1,237 +0,0 @@ -// import { StoreCodec } from "js-waku"; -import { StoreCodec } from "js-waku"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Community, Identity, Messenger } from "status-communities/dist/cjs"; -import { ApplicationMetadataMessage } from "status-communities/dist/cjs"; - -import { ChatMessage } from "../models/ChatMessage"; -import { loadIdentity, saveIdentity } from "../utils/identityStorage"; -import { uintToImgUrl } from "../utils/uintToImgUrl"; - -const _MS_PER_DAY = 1000 * 60 * 60 * 24; - -function binarySetInsert( - arr: T[], - val: T, - compFunc: (a: T, b: T) => boolean, - eqFunc: (a: T, b: T) => boolean -) { - let low = 0; - let high = arr.length; - while (low < high) { - const mid = (low + high) >> 1; - if (compFunc(arr[mid], val)) { - low = mid + 1; - } else { - high = mid; - } - } - if (arr.length === low || !eqFunc(arr[low], val)) { - arr.splice(low, 0, val); - } - return arr; -} - -export function useMessenger(chatId: string, communityKey: string) { - const [messenger, setMessenger] = useState(undefined); - const [messages, setMessages] = useState<{ [chatId: string]: ChatMessage[] }>( - {} - ); - const [community, setCommunity] = useState(undefined); - const [notifications, setNotifications] = useState<{ - [chatId: string]: number; - }>({}); - const loadingPreviousMessages = useRef<{ - [chatId: string]: boolean; - }>({}); - const lastLoadTime = useRef<{ - [chatId: string]: Date; - }>({}); - const [lastMessage, setLastMessage] = useState(new Date()); - const [loadingMessages, setLoadingMessages] = useState(false); - - useEffect(() => { - if (lastLoadTime.current?.[chatId]) { - setLastMessage(lastLoadTime.current?.[chatId]); - } - }, [chatId]); - const clearNotifications = useCallback((id: string) => { - setNotifications((prevNotifications) => { - return { - ...prevNotifications, - [id]: 0, - }; - }); - }, []); - - const addNewMessage = useCallback( - (msg: ApplicationMetadataMessage, id: string, date: Date) => { - if ( - msg.signer && - (msg.chatMessage?.text || msg.chatMessage?.image) && - msg.chatMessage.clock - ) { - const content = msg.chatMessage.text ?? ""; - let image: string | undefined = undefined; - if (msg.chatMessage?.image) { - image = uintToImgUrl(msg.chatMessage?.image.payload); - } - const sender = msg.signer.reduce( - (p: string, c: number): string => p + c.toString(16), - "0x" - ); - const newMessage = { sender, content, date, image }; - setMessages((prev) => { - return { - ...prev, - [id]: binarySetInsert( - prev?.[id] ?? [], - newMessage, - (a, b) => a.date < b.date, - (a, b) => a.date.getTime() === b.date.getTime() - ), - }; - }); - setNotifications((prev) => { - return { - ...prev, - [id]: prev[id] + 1, - }; - }); - } - }, - [] - ); - - const loadNextDay = useCallback( - async (id: string) => { - if (messenger) { - const endTime = lastLoadTime.current[id] ?? new Date(); - const startTime = new Date(endTime.getTime() - _MS_PER_DAY); - const timeDiff = Math.floor( - (new Date().getTime() - endTime.getTime()) / _MS_PER_DAY - ); - if (timeDiff < 30) { - if (!loadingPreviousMessages.current[id]) { - loadingPreviousMessages.current[id] = true; - setLoadingMessages(true); - const amountOfMessages = await messenger.retrievePreviousMessages( - id, - startTime, - endTime - ); - lastLoadTime.current[id] = startTime; - if (id === chatId) { - setLastMessage(startTime); - } - loadingPreviousMessages.current[id] = false; - setLoadingMessages(false); - if (amountOfMessages === 0) { - loadNextDay(id); - } - } - } - } - }, - [lastLoadTime, messenger, chatId] - ); - - useEffect(() => { - const createMessenger = async () => { - // Test password for now - // Need design for password input - - let identity = await loadIdentity("test"); - if (!identity) { - identity = Identity.generate(); - await saveIdentity(identity, "test"); - } - const messenger = await Messenger.create(identity, { - libp2p: { - config: { - pubsub: { - enabled: true, - emitSelf: true, - }, - }, - }, - }); - await new Promise((resolve) => { - messenger.waku.libp2p.peerStore.on( - "change:protocols", - ({ protocols }) => { - if (protocols.includes(StoreCodec)) { - resolve(""); - } - } - ); - }); - const community = await Community.instantiateCommunity( - communityKey, - messenger.waku - ); - setCommunity(community); - await Promise.all( - Array.from(community.chats.values()).map(async (chat) => { - await messenger.joinChat(chat); - lastLoadTime.current[chat.id] = new Date(); - messenger.addObserver( - (msg, date) => addNewMessage(msg, chat.id, date), - chat.id - ); - clearNotifications(chat.id); - }) - ); - setMessenger(messenger); - }; - createMessenger(); - }, []); - - useEffect(() => { - if (messenger && community?.chats) { - Array.from(community?.chats.values()).forEach(({ id }) => - loadNextDay(id) - ); - } - }, [messenger]); - - const sendMessage = useCallback( - async (messageText?: string, image?: Uint8Array) => { - if (messageText) { - await messenger?.sendMessage(chatId, { - text: messageText, - contentType: 0, - }); - } - if (image) { - await messenger?.sendMessage(chatId, { - image, - imageType: 1, - contentType: 2, - }); - } - }, - [chatId, messenger] - ); - - const activeMessages = useMemo( - () => messages?.[chatId] ?? [], - [messages, chatId] - ); - - useEffect(() => { - setLoadingMessages(loadingPreviousMessages.current[chatId]); - }, [chatId]); - - return { - messenger, - messages: activeMessages, - sendMessage, - notifications, - clearNotifications, - loadNextDay, - lastMessage, - loadingMessages, - community, - }; -} diff --git a/packages/react-chat/src/models/ChatMessage.ts b/packages/react-chat/src/models/ChatMessage.ts index 327ce507..91e29fb0 100644 --- a/packages/react-chat/src/models/ChatMessage.ts +++ b/packages/react-chat/src/models/ChatMessage.ts @@ -1,6 +1,41 @@ -export type ChatMessage = { +import { ApplicationMetadataMessage } from "status-communities/dist/cjs"; + +import { uintToImgUrl } from "../utils"; + +export class ChatMessage { content: string; date: Date; sender: string; image?: string; -}; + + constructor(content: string, date: Date, sender: string, image?: string) { + this.content = content; + this.date = date; + this.sender = sender; + this.image = image; + } + + public static fromMetadataMessage( + msg: ApplicationMetadataMessage, + date: Date + ) { + if ( + msg.signer && + (msg.chatMessage?.text || msg.chatMessage?.image) && + msg.chatMessage.clock + ) { + const content = msg.chatMessage.text ?? ""; + let image: string | undefined = undefined; + if (msg.chatMessage?.image) { + image = uintToImgUrl(msg.chatMessage?.image.payload); + } + const sender = msg.signer.reduce( + (p: string, c: number): string => p + c.toString(16), + "0x" + ); + return new ChatMessage(content, date, sender, image); + } else { + return undefined; + } + } +} diff --git a/packages/react-chat/src/utils/binarySetInsert.ts b/packages/react-chat/src/utils/binarySetInsert.ts new file mode 100644 index 00000000..de163844 --- /dev/null +++ b/packages/react-chat/src/utils/binarySetInsert.ts @@ -0,0 +1,21 @@ +export function binarySetInsert( + arr: T[], + val: T, + compFunc: (a: T, b: T) => boolean, + eqFunc: (a: T, b: T) => boolean +) { + let low = 0; + let high = arr.length; + while (low < high) { + const mid = (low + high) >> 1; + if (compFunc(arr[mid], val)) { + low = mid + 1; + } else { + high = mid; + } + } + if (arr.length === low || !eqFunc(arr[low], val)) { + arr.splice(low, 0, val); + } + return arr; +} diff --git a/packages/react-chat/src/utils/index.ts b/packages/react-chat/src/utils/index.ts new file mode 100644 index 00000000..d3ea8080 --- /dev/null +++ b/packages/react-chat/src/utils/index.ts @@ -0,0 +1,7 @@ +export { binarySetInsert } from "./binarySetInsert"; +export { copy } from "./copy"; +export { copyImg } from "./copyImg"; +export { downloadImg } from "./downloadImg"; +export { saveIdentity, loadIdentity } from "./identityStorage"; +export { reduceString } from "./reduceString"; +export { uintToImgUrl } from "./uintToImgUrl";