Refactor useMessenger (#89)

This commit is contained in:
Szymon Szlachtowicz 2021-10-20 15:57:10 +02:00 committed by GitHub
parent 255d1d30f9
commit 819b89562c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 304 additions and 249 deletions

View File

@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useState } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { useNarrow } from "../contexts/narrowProvider"; import { useNarrow } from "../contexts/narrowProvider";
import { useMessenger } from "../hooks/useMessenger"; import { useMessenger } from "../hooks/messenger/useMessenger";
import { ChannelData } from "../models/ChannelData"; import { ChannelData } from "../models/ChannelData";
import { Metadata } from "../models/Metadata"; import { Metadata } from "../models/Metadata";
import { Theme } from "../styles/themes"; import { Theme } from "../styles/themes";
@ -37,7 +37,7 @@ export function Chat({ theme, communityKey, fetchMetadata }: ChatProps) {
sendMessage, sendMessage,
notifications, notifications,
clearNotifications, clearNotifications,
loadNextDay, loadPrevDay,
loadingMessages, loadingMessages,
community, community,
} = useMessenger(activeChannel?.id ?? "", communityKey); } = useMessenger(activeChannel?.id ?? "", communityKey);
@ -120,7 +120,7 @@ export function Chat({ theme, communityKey, fetchMetadata }: ChatProps) {
showMembers={showMembers} showMembers={showMembers}
community={communityData} community={communityData}
showCommunity={!showChannels} showCommunity={!showChannels}
loadNextDay={() => loadNextDay(activeChannel.name)} loadPrevDay={() => loadPrevDay(activeChannel.name)}
onCommunityClick={showModal} onCommunityClick={showModal}
fetchMetadata={fetchMetadata} fetchMetadata={fetchMetadata}
loadingMessages={loadingMessages} loadingMessages={loadingMessages}

View File

@ -33,7 +33,7 @@ interface ChatBodyProps {
notifications: { [id: string]: number }; notifications: { [id: string]: number };
setActiveChannel: (val: ChannelData) => void; setActiveChannel: (val: ChannelData) => void;
activeChannelId: string; activeChannelId: string;
loadNextDay: () => void; loadPrevDay: () => void;
onCommunityClick: () => void; onCommunityClick: () => void;
fetchMetadata?: (url: string) => Promise<Metadata | undefined>; fetchMetadata?: (url: string) => Promise<Metadata | undefined>;
loadingMessages: boolean; loadingMessages: boolean;
@ -54,7 +54,7 @@ export function ChatBody({
notifications, notifications,
setActiveChannel, setActiveChannel,
activeChannelId, activeChannelId,
loadNextDay, loadPrevDay,
onCommunityClick, onCommunityClick,
fetchMetadata, fetchMetadata,
loadingMessages, loadingMessages,
@ -131,7 +131,7 @@ export function ChatBody({
messenger ? ( messenger ? (
<ChatMessages <ChatMessages
messages={messages} messages={messages}
loadNextDay={loadNextDay} loadPrevDay={loadPrevDay}
fetchMetadata={fetchMetadata} fetchMetadata={fetchMetadata}
loadingMessages={loadingMessages} loadingMessages={loadingMessages}
/> />

View File

@ -13,14 +13,14 @@ import { ChatMessageContent } from "./ChatMessageContent";
type ChatMessagesProps = { type ChatMessagesProps = {
messages: ChatMessage[]; messages: ChatMessage[];
loadNextDay: () => void; loadPrevDay: () => void;
fetchMetadata?: (url: string) => Promise<Metadata | undefined>; fetchMetadata?: (url: string) => Promise<Metadata | undefined>;
loadingMessages: boolean; loadingMessages: boolean;
}; };
export function ChatMessages({ export function ChatMessages({
messages, messages,
loadNextDay, loadPrevDay,
fetchMetadata, fetchMetadata,
loadingMessages, loadingMessages,
}: ChatMessagesProps) { }: ChatMessagesProps) {
@ -38,7 +38,7 @@ export function ChatMessages({
if ( if (
(ref?.current?.clientHeight ?? 0) >= (ref?.current?.scrollHeight ?? 0) (ref?.current?.clientHeight ?? 0) >= (ref?.current?.scrollHeight ?? 0)
) { ) {
loadNextDay(); loadPrevDay();
} }
} }
}, [messages, messages.length, loadingMessages]); }, [messages, messages.length, loadingMessages]);
@ -47,7 +47,7 @@ export function ChatMessages({
const setScroll = () => { const setScroll = () => {
if (ref && ref.current) { if (ref && ref.current) {
if (ref.current.scrollTop <= 0) { if (ref.current.scrollTop <= 0) {
loadNextDay(); loadPrevDay();
} }
if ( if (
ref.current.scrollTop + ref.current.clientHeight == ref.current.scrollTop + ref.current.clientHeight ==

View File

@ -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 };
}

View File

@ -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,
};
}

View File

@ -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<Messenger | undefined>(undefined);
const { addMessage, clearNotifications, notifications, messages } =
useMessages(chatId);
const [community, setCommunity] = useState<Community | undefined>(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,
};
}

View File

@ -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 };
}

View File

@ -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<T>(
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<Messenger | undefined>(undefined);
const [messages, setMessages] = useState<{ [chatId: string]: ChatMessage[] }>(
{}
);
const [community, setCommunity] = useState<Community | undefined>(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,
};
}

View File

@ -1,6 +1,41 @@
export type ChatMessage = { import { ApplicationMetadataMessage } from "status-communities/dist/cjs";
import { uintToImgUrl } from "../utils";
export class ChatMessage {
content: string; content: string;
date: Date; date: Date;
sender: string; sender: string;
image?: 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;
}
}
}

View File

@ -0,0 +1,21 @@
export function binarySetInsert<T>(
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;
}

View File

@ -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";