diff --git a/app/.prettierignore b/app/.prettierignore
new file mode 100644
index 0000000..b85afe7
--- /dev/null
+++ b/app/.prettierignore
@@ -0,0 +1,24 @@
+# dependencies
+node_modules
+
+# testing
+/coverage
+
+# production
+build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
\ No newline at end of file
diff --git a/app/.prettierrc.json b/app/.prettierrc.json
new file mode 100644
index 0000000..1b2511f
--- /dev/null
+++ b/app/.prettierrc.json
@@ -0,0 +1,5 @@
+{
+ "semi": false,
+ "arrowParens": "always",
+ "trailingComma": "none"
+}
\ No newline at end of file
diff --git a/app/package.json b/app/package.json
index 050372e..24b5eb6 100644
--- a/app/package.json
+++ b/app/package.json
@@ -36,13 +36,18 @@
},
"scripts": {
"start": "react-scripts start",
- "build": "react-scripts build"
+ "build": "react-scripts build",
+ "prettier": "prettier -c src",
+ "prettier:fix": "prettier -w src"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
- ]
+ ],
+ "rules": {
+ "react-hooks/exhaustive-deps": 0
+ }
},
"browserslist": {
"production": [
@@ -79,6 +84,7 @@
"@types/react-redux": "^7.1.20",
"@types/redux": "^3.6.0",
"@types/redux-thunk": "^2.1.0",
- "@types/styled-components": "^5.1.15"
+ "@types/styled-components": "^5.1.15",
+ "prettier": "^2.6.2"
}
}
diff --git a/app/src/App.scss b/app/src/App.scss
index 027c894..d0b308c 100644
--- a/app/src/App.scss
+++ b/app/src/App.scss
@@ -38,10 +38,10 @@
}
}
-.modal-body {
+.modal-body {
padding: 40px;
}
-.modal-header {
- color: #475C7A;
+.modal-header {
+ color: #475c7a;
}
diff --git a/app/src/App.test.tsx b/app/src/App.test.tsx
index 2a68616..744673e 100644
--- a/app/src/App.test.tsx
+++ b/app/src/App.test.tsx
@@ -1,9 +1,9 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import App from './App';
+import React from "react"
+import { render, screen } from "@testing-library/react"
+import App from "./App"
-test('renders learn react link', () => {
- render();
- const linkElement = screen.getByText(/learn react/i);
- expect(linkElement).toBeInTheDocument();
-});
+test("renders learn react link", () => {
+ render()
+ const linkElement = screen.getByText(/learn react/i)
+ expect(linkElement).toBeInTheDocument()
+})
diff --git a/app/src/App.tsx b/app/src/App.tsx
index bec9572..b1e6583 100644
--- a/app/src/App.tsx
+++ b/app/src/App.tsx
@@ -1,6 +1,6 @@
-import "./App.scss";
-import { BrowserRouter } from "react-router-dom";
-import AppWrapper from "./components/AppWrapper";
+import "./App.scss"
+import { BrowserRouter } from "react-router-dom"
+import AppWrapper from "./components/AppWrapper"
function App() {
return (
@@ -11,7 +11,7 @@ function App() {
- );
+ )
}
-export default App;
+export default App
diff --git a/app/src/components/AppWrapper/index.tsx b/app/src/components/AppWrapper/index.tsx
index f519ff4..66141ec 100644
--- a/app/src/components/AppWrapper/index.tsx
+++ b/app/src/components/AppWrapper/index.tsx
@@ -1,69 +1,104 @@
-import React from "react";
-import { Routes, Route, Navigate } from "react-router-dom";
-import { useLocation, useNavigate } from "react-router";
-import RegisterOrRecover from "../RegisterOrRecover";
-import Dashboard from "../Dashboard";
-import { init, receive_message } from "rln-client-lib";
-import { useEffect } from "react";
-import { useAppDispatch } from "../../redux/hooks/useAppDispatch";
+import React from "react"
+import PublicRoomInvitedScreen from "../PublicRoomInvitedScreen"
+import RegisterOrRecover from "../RegisterOrRecover"
+import Dashboard from "../Dashboard"
+import SyncSpinner from "../Spinner"
+import { Routes, Route, Navigate } from "react-router-dom"
+import { useLocation, useNavigate } from "react-router"
+import { get_rooms, init, receive_message } from "rln-client-lib"
+import { useEffect } from "react"
+import { useAppDispatch } from "../../redux/hooks/useAppDispatch"
import {
addMessageToRoomAction,
- getChatHistoryAction,
- getRoomsAction
-} from "../../redux/actions/actionCreator";
-import PublicRoomInvitedScreen from "../PublicRoomInvitedScreen";
-import { roomTypes, serverUrl, socketUrl } from "../../constants/constants";
-import { ToastContainer } from 'react-toastify';
+ getRoomsAction,
+ loadMessagesForRooms,
+ runSyncMessageHistory
+} from "../../redux/actions/actionCreator"
+import { roomTypes, serverUrl, socketUrl } from "../../constants/constants"
+import { ToastContainer } from "react-toastify"
import 'react-toastify/dist/ReactToastify.css';
import { generateProof } from "../../util/util";
+import "react-toastify/dist/ReactToastify.css"
+import { IMessage } from "rln-client-lib/dist/src/chat/interfaces"
+import { IRooms } from "rln-client-lib/dist/src/profile/interfaces"
+import { useAppSelector } from "../../redux/hooks/useAppSelector"
const AppWrapper = () => {
- const navigate = useNavigate();
- const dispatch = useAppDispatch();
- const location = useLocation();
+ const navigate = useNavigate()
+ const dispatch = useAppDispatch()
+ const location = useLocation()
+
+ const isChatHistorySyncing = useAppSelector(
+ (state) => state.ChatReducer.chatHistorySyncing
+ )
useEffect(() => {
- initializeApp();
+ initializeApp()
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ }, [])
const initializeApp = async () => {
try {
await init({
serverUrl: serverUrl,
socketUrl: socketUrl
- },
- generateProof)
- .then(() => {
- if (!location.pathname.includes(roomTypes.public))
- navigate("/dashboard");
- dispatch(getRoomsAction());
- dispatch(getChatHistoryAction());
- })
- .then(async () => {
- await receive_message(receiveMessageCallback);
- });
+ }, generateProof).then(() => {
+ if (!location.pathname.includes(roomTypes.public))
+ navigate("/dashboard")
+ dispatch(getRoomsAction())
+ dispatch(
+ runSyncMessageHistory({
+ onSuccess: () => {
+ loadMessagesFromDb()
+ }
+ })
+ )
+ })
} catch (error) {
- navigate("/r-procedure");
+ navigate("/r-procedure")
}
- };
+ }
+
+ const loadMessagesFromDb = async () => {
+ const allRooms: IRooms = await get_rooms()
+ const roomIds: string[] = [
+ ...allRooms.direct.map((d) => d.id),
+ ...allRooms.private.map((d) => d.id),
+ ...allRooms.public.map((d) => d.id)
+ ]
+
+ const nowTimestamp: number = new Date().getTime()
+ dispatch(loadMessagesForRooms(roomIds, nowTimestamp))
+
+ await receive_message(receiveMessageCallback)
+ }
- const receiveMessageCallback = (message: any, roomId: string) => {
- dispatch(addMessageToRoomAction(message, roomId));
- };
+ const receiveMessageCallback = (message: IMessage, roomId: string) => {
+ dispatch(addMessageToRoomAction(message, roomId))
+ }
return (
-
- } />
- } />
- } />
- } />
-
-
+ {isChatHistorySyncing ? (
+
+ ) : (
+ <>
+ {" "}
+
+ } />
+ } />
+ }
+ />
+ } />
+
+
+ >
+ )}
- );
-};
+ )
+}
-export default AppWrapper;
+export default AppWrapper
diff --git a/app/src/components/ChatMessages/ChatMessagesWrapper.tsx b/app/src/components/ChatMessages/ChatMessagesWrapper.tsx
new file mode 100644
index 0000000..2bfad2f
--- /dev/null
+++ b/app/src/components/ChatMessages/ChatMessagesWrapper.tsx
@@ -0,0 +1,116 @@
+import React, { useEffect, useRef } from "react"
+import { Spinner } from "reactstrap"
+import styled from "styled-components"
+import * as Colors from "../../constants/colors"
+import { loadMessagesForRoom } from "../../redux/actions/actionCreator"
+import { useAppDispatch } from "../../redux/hooks/useAppDispatch"
+import { useAppSelector } from "../../redux/hooks/useAppSelector"
+
+const StyledSingleMessage = styled.div`
+ font-size: 16px;
+ color: black;
+ border-radius: 10px;
+ padding: 8px 12px;
+ margin-bottom: 16px;
+ width: fit-content;
+ text-align: left;
+`
+const StyledMessagesWrapper = styled.div`
+ overflow-y: scroll;
+ padding: 20px 40px;
+ scrollbar-width: none;
+ &::-webkit-scrollbar {
+ display: none;
+ }
+ height: 85%;
+ div:nth-child(odd) {
+ background: ${Colors.ANATRACITE};
+ color: white;
+ }
+ div:nth-child(even) {
+ background: #f0f2f5;
+ }
+`
+
+const StyledSpinnerWrapper = styled.div`
+ span {
+ color: ${Colors.ANATRACITE};
+ }
+ height: 85%;
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+`
+
+interface ChatMessagesProps {
+ chatHistory: any[]
+ chatRef: React.RefObject
+ currentActiveRoom: any
+}
+
+const ChatMessagesWrapper = ({
+ chatHistory,
+ chatRef,
+ currentActiveRoom
+}: ChatMessagesProps) => {
+ const dispatch = useAppDispatch()
+ const sortedHistory = chatHistory.sort((a, b) => a.epoch - b.epoch)
+ const chatHistoryRef = useRef(sortedHistory)
+ const loadingMessages = useAppSelector((state) => state.ChatReducer.loading)
+
+ const handleScroll = () => {
+ // @ts-ignore
+ if (chatRef.current.scrollTop === 0) {
+ fetchNewMessages()
+ }
+ }
+
+ const fetchNewMessages = () => {
+ const lastMessage = chatHistoryRef.current[0]
+
+ dispatch(
+ loadMessagesForRoom(
+ currentActiveRoom.id,
+ new Date(lastMessage.epoch).getTime(),
+ false,
+ {
+ onSuccess: (res: any) => {
+ chatHistoryRef.current = res.concat(sortedHistory)
+ }
+ }
+ )
+ )
+ }
+
+ useEffect(() => {
+ chatRef.current?.addEventListener("scroll", handleScroll)
+ return () => {
+ chatRef.current?.removeEventListener("scroll", handleScroll)
+ }
+ }, [])
+
+ return loadingMessages ? (
+
+ <>
+ {" "}
+ Loading messages
+ >
+
+ ) : (
+
+ {sortedHistory.length > 0 &&
+ sortedHistory.map((messageObj) => (
+
+ {messageObj.message_content}
+
+ ))}
+
+ )
+}
+
+export default ChatMessagesWrapper
diff --git a/app/src/components/ChatMessages/index.tsx b/app/src/components/ChatMessages/index.tsx
index 5644f20..8fbf35c 100644
--- a/app/src/components/ChatMessages/index.tsx
+++ b/app/src/components/ChatMessages/index.tsx
@@ -1,15 +1,18 @@
import React, { useEffect, useRef, useState } from "react";
import styled from "styled-components";
import * as Colors from "../../constants/colors";
-import { faTimes } from "@fortawesome/free-solid-svg-icons";
+import { faTimes, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { clientUrl, roomTypes } from "../../constants/constants";
import { useAppDispatch } from "../../redux/hooks/useAppDispatch";
-import { addActiveChatRoom } from "../../redux/actions/actionCreator";
+import { addActiveChatRoom, deleteMessagesForRoom } from "../../redux/actions/actionCreator";
import InviteModal from "../Modals/inviteModal";
import { useAppSelector } from "../../redux/hooks/useAppSelector";
import Input from "../Input";
import ReactTooltip from "react-tooltip";
+import ChatMessagesWrapper from "./ChatMessagesWrapper"
+import { delete_messages_for_room } from "rln-client-lib";
+
const StyledChatContainer = styled.div`
background: white;
@@ -17,32 +20,7 @@ const StyledChatContainer = styled.div`
border-radius: 18px;
box-shadow: 0px 8px 14px 0px #a0a0a0;
padding: 20px 40px;
-`;
-const StyledSingleMessage = styled.div`
- font-size: 16px;
- color: black;
- border-radius: 10px;
- padding: 8px 12px;
- margin-bottom: 16px;
- width: fit-content;
- text-align: left;
-`;
-const StyledMessagesWrapper = styled.div`
- overflow-y: scroll;
- padding: 20px 40px;
- scrollbar-width: none;
- &::-webkit-scrollbar {
- display: none;
- }
- height: 85%;
- div:nth-child(odd) {
- background: ${Colors.ANATRACITE};
- color: white;
- }
- div:nth-child(even) {
- background: #f0f2f5;
- }
-`;
+`
const StyledChatDetailsWrapper = styled.div`
display: flex;
@@ -53,12 +31,12 @@ const StyledChatDetailsWrapper = styled.div`
fill: ${Colors.ANATRACITE};
}
}
-`;
+`
const StyledChatRoomsTitle = styled.p`
color: ${Colors.ANATRACITE};
font-weight: 600;
font-size: 24px;
-`;
+`
const StyledButton = styled.button`
background: ${Colors.BERRY_PINK};
@@ -73,11 +51,19 @@ const StyledButton = styled.button`
box-shadow: 0px 0px 15px 0px ${Colors.BERRY_PINK};
}
width: 180px;
-`;
+`
+
+const StyledIconWrapper = styled.div`
+ margin: 0 30px;
+`
+const StyledChatCommandsWrapper = styled.div`
+ display: flex;
+ align-items: center;
+`
type ChatMessagesProps = {
- currentActiveRoom: any;
-};
+ currentActiveRoom: any
+}
const ChatMessages = ({ currentActiveRoom }: ChatMessagesProps) => {
const [toggleInviteModal, setToggleInviteModal] = useState(false);
@@ -86,39 +72,48 @@ const ChatMessages = ({ currentActiveRoom }: ChatMessagesProps) => {
);
//@ts-ignore
const chatHistoryByRoom: any[] = useAppSelector(
- state => state.ChatReducer.chatHistory[currentActiveRoom.id]
- );
- const chatRef = useRef(null);
- const dispatch = useAppDispatch();
+ (state) => state.ChatReducer.chatHistory[currentActiveRoom.id]
+ )
+ const stayOnBottom: boolean = useAppSelector(
+ (state) => state.ChatReducer.stayOnBottom
+ )
+ const chatRef = useRef(null)
+ const dispatch = useAppDispatch()
const scrollToBottom = () => {
if (chatRef.current) {
//Scroll to bottom
- chatRef.current.scrollTop = chatRef.current.scrollHeight;
+ chatRef.current.scrollTop = chatRef.current.scrollHeight
}
- };
+ }
useEffect(() => {
- scrollToBottom();
- }, [chatHistoryByRoom]);
+ if (stayOnBottom) scrollToBottom()
+ }, [chatHistoryByRoom, stayOnBottom])
const handleActiveChatClosing = () => {
- dispatch(addActiveChatRoom(undefined));
- };
+ dispatch(addActiveChatRoom(undefined))
+ }
const handleGenerateInvitePublicRoomLink = () => {
- const publicRoomInviteLink = `${clientUrl}/public/${currentActiveRoom.id}`;
+ const publicRoomInviteLink = `${clientUrl}/public/${currentActiveRoom.id}`
navigator.clipboard.writeText(publicRoomInviteLink).then(() => {
setIsPublicRoomInviteCopied(true)
- setTimeout(() => setIsPublicRoomInviteCopied(false), 4000);
- });
- };
+ setTimeout(() => setIsPublicRoomInviteCopied(false), 4000)
+ })
+ }
+
+ const handleMessagesDeleting = () => {
+ delete_messages_for_room(currentActiveRoom.id).then(() =>
+ dispatch(deleteMessagesForRoom(currentActiveRoom.id))
+ )
+ }
return (
{currentActiveRoom.name}
-
+
{currentActiveRoom.type.toLowerCase() === roomTypes.private && (
<>
setToggleInviteModal(true)}>
@@ -150,24 +145,37 @@ const ChatMessages = ({ currentActiveRoom }: ChatMessagesProps) => {
>
)}
+
+
+
+ Delete Message History
+
+
-
+
{" "}
-
- {chatHistoryByRoom?.length > 0 &&
- chatHistoryByRoom.map(messageObj => (
-
- {messageObj.message_content}
-
- ))}
-
+
- );
-};
+ )
+}
-export default ChatMessages;
+export default ChatMessages
diff --git a/app/src/components/ChatRooms/index.tsx b/app/src/components/ChatRooms/index.tsx
index 3d29a5a..c172ae2 100644
--- a/app/src/components/ChatRooms/index.tsx
+++ b/app/src/components/ChatRooms/index.tsx
@@ -1,14 +1,14 @@
-import React, { useEffect } from "react";
-import styled from "styled-components";
-import * as Colors from "../../constants/colors";
+import React, { useEffect } from "react"
+import styled from "styled-components"
+import * as Colors from "../../constants/colors"
import {
addActiveChatRoom,
getRoomsAction,
Room
-} from "../../redux/actions/actionCreator";
-import { useAppDispatch } from "../../redux/hooks/useAppDispatch";
-import { useAppSelector } from "../../redux/hooks/useAppSelector";
-import RoomHandlingButtons from "../RoomHandlingButtons";
+} from "../../redux/actions/actionCreator"
+import { useAppDispatch } from "../../redux/hooks/useAppDispatch"
+import { useAppSelector } from "../../redux/hooks/useAppSelector"
+import RoomHandlingButtons from "../RoomHandlingButtons"
const StyledChatRoomWrapper = styled.div`
background: white;
@@ -17,7 +17,7 @@ const StyledChatRoomWrapper = styled.div`
border-radius: 18px;
padding: 20px 40px;
margin-right: 16px;
-`;
+`
const StyledChatRoomCell = styled.div`
background: ${Colors.DARK_YELLOW};
border-radius: 8px;
@@ -31,13 +31,13 @@ const StyledChatRoomCell = styled.div`
transition: 0.15s;
box-shadow: 0px 0px 15px 0px ${Colors.DARK_YELLOW};
}
-`;
+`
export const StyledChatRoomsTitle = styled.p`
color: ${Colors.DARK_YELLOW};
font-weight: 600;
font-size: 24px;
text-transform: capitalize;
-`;
+`
const StyledChatRoomsWrapper = styled.div`
overflow-y: scroll;
padding: 20px 40px;
@@ -46,27 +46,27 @@ const StyledChatRoomsWrapper = styled.div`
display: none;
}
height: 85%;
-`;
+`
const ChatRooms = () => {
- const rooms: any = useAppSelector(state => state.ChatReducer.rooms);
- const dispatch = useAppDispatch();
+ const rooms: any = useAppSelector((state) => state.ChatReducer.rooms)
+ const dispatch = useAppDispatch()
useEffect(() => {
- dispatch(getRoomsAction());
+ dispatch(getRoomsAction())
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ }, [])
const handleChatRoomCellClick = (room: Room) => {
- dispatch(addActiveChatRoom(room));
- };
+ dispatch(addActiveChatRoom(room))
+ }
return (
<>
Chat rooms
- {Object.keys(rooms).map(key => (
+ {Object.keys(rooms).map((key) => (
{`${key}: ${rooms[key].length}`}
{rooms[key].map((room: Room) => (
@@ -83,7 +83,7 @@ const ChatRooms = () => {
>
- );
-};
+ )
+}
-export default ChatRooms;
+export default ChatRooms
diff --git a/app/src/components/Dashboard/index.tsx b/app/src/components/Dashboard/index.tsx
index 183311e..b5255f5 100644
--- a/app/src/components/Dashboard/index.tsx
+++ b/app/src/components/Dashboard/index.tsx
@@ -1,27 +1,33 @@
-import React from "react";
-import styled from "styled-components";
-import ChatRooms from "../ChatRooms";
-import ChatMessages from "../ChatMessages";
-import { useAppSelector } from "../../redux/hooks/useAppSelector";
+import React from "react"
+import styled from "styled-components"
+import ChatRooms from "../ChatRooms"
+import ChatMessages from "../ChatMessages"
+import { useAppSelector } from "../../redux/hooks/useAppSelector"
+import SyncSpinner from "../Spinner"
const StyledDashboardWrapper = styled.div`
height: 100vh;
padding: 1rem;
-`;
+`
const Dashboard = () => {
const currentActiveRoom: any = useAppSelector(
- state => state.ChatReducer.currentActiveRoom
- );
+ (state) => state.ChatReducer.currentActiveRoom
+ )
+ const isChatHistorySyncing = useAppSelector(
+ (state) => state.ChatReducer.chatHistorySyncing
+ )
- return (
+ return isChatHistorySyncing ? (
+
+ ) : (
{currentActiveRoom && (
)}
- );
-};
+ )
+}
-export default Dashboard;
+export default Dashboard
diff --git a/app/src/components/Input/index.tsx b/app/src/components/Input/index.tsx
index 57b9fae..ed49540 100644
--- a/app/src/components/Input/index.tsx
+++ b/app/src/components/Input/index.tsx
@@ -1,11 +1,11 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import React from "react";
-import styled from "styled-components";
-import * as Colors from "../../constants/colors";
-import { faPaperPlane } from "@fortawesome/free-solid-svg-icons";
-import { Room } from "../../redux/actions/actionCreator";
-import { send_message } from "rln-client-lib";
-import { toast } from 'react-toastify';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
+import React from "react"
+import styled from "styled-components"
+import * as Colors from "../../constants/colors"
+import { faPaperPlane } from "@fortawesome/free-solid-svg-icons"
+import { Room } from "../../redux/actions/actionCreator"
+import { send_message } from "rln-client-lib"
+import { toast } from "react-toastify"
const StyledInput = styled.input`
border: 1px solid #f0f2f5;
@@ -17,7 +17,7 @@ const StyledInput = styled.input`
&:active {
outline: none;
}
-`;
+`
const StyledChatFooterWrapper = styled.div`
display: flex;
@@ -30,60 +30,60 @@ const StyledChatFooterWrapper = styled.div`
fill: ${Colors.ANATRACITE};
}
}
-`;
+`
type InputProps = {
- currentActiveRoom: Room;
-};
+ currentActiveRoom: Room
+}
type InputState = {
- inputValue: string;
-};
+ inputValue: string
+}
class Input extends React.Component {
constructor(props: InputProps) {
- super(props);
+ super(props)
this.state = {
inputValue: ""
- };
+ }
}
componentDidMount() {
- window.addEventListener("keydown", this.handleKeyDown);
+ window.addEventListener("keydown", this.handleKeyDown)
}
componentWillUnmount() {
- window.removeEventListener("keydown", this.handleKeyDown);
+ window.removeEventListener("keydown", this.handleKeyDown)
}
- handleKeyDown = async(event: KeyboardEvent) => {
- if (event.code === "Enter")
- await this.handleMessageSubmit();
- };
+ handleKeyDown = async (event: KeyboardEvent) => {
+ if (event.code === "Enter") await this.handleMessageSubmit()
+ }
- handleMessageSubmit = async() => {
- const { inputValue } = this.state;
- const { currentActiveRoom } = this.props;
+ handleMessageSubmit = async () => {
+ const { inputValue } = this.state
+ const { currentActiveRoom } = this.props
if (inputValue) {
try {
- await send_message(currentActiveRoom.id, inputValue);
+ await send_message(currentActiveRoom.id, inputValue)
this.setState({ inputValue: "" })
} catch (error) {
- toast.error("Error while sending the message. You are either banned from the chat or deleted from InteRep");
+ console.log(error)
+ toast.error(
+ "Error while sending the message. You are either banned from the chat or deleted from InteRep"
+ )
}
}
- };
-
-
+ }
render() {
- const { inputValue } = this.state;
+ const { inputValue } = this.state
return (
this.setState({ inputValue: e.target.value })}
+ onChange={(e) => this.setState({ inputValue: e.target.value })}
/>{" "}
{" "}
@@ -92,10 +92,9 @@ class Input extends React.Component {
onClick={this.handleMessageSubmit}
/>{" "}
-
- );
+ )
}
}
-export default Input;
+export default Input
diff --git a/app/src/components/Modals/exchangeKeysModal.tsx b/app/src/components/Modals/exchangeKeysModal.tsx
new file mode 100644
index 0000000..5a3a710
--- /dev/null
+++ b/app/src/components/Modals/exchangeKeysModal.tsx
@@ -0,0 +1,155 @@
+import { useState } from "react"
+import { Modal, ModalBody, ModalHeader } from "reactstrap"
+import styled from "styled-components"
+import * as Colors from "../../constants/colors"
+import { faCopy } from "@fortawesome/free-solid-svg-icons"
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
+import ReactTooltip from "react-tooltip"
+import {
+ generate_encrypted_invite_direct_room,
+ update_direct_room_key
+} from "rln-client-lib"
+import { useAppSelector } from "../../redux/hooks/useAppSelector"
+
+const StyledButton = styled.button`
+ background: ${Colors.ANATRACITE};
+ border-radius: 8px;
+ border: none;
+ outline: none;
+ padding: 8px 12px;
+ margin: 8px;
+ margin-left: auto;
+ color: white;
+ &:hover {
+ transition: 0.15s;
+ box-shadow: 0px 0px 15px 0px ${Colors.ANATRACITE};
+ }
+ width: 200px;
+`
+
+const StyledTextarea = styled.textarea`
+ border: 1px solid #f0f2f5;
+ border-radius: 20px;
+ width: 100%;
+ position: relative;
+ margin-bottom: 10px;
+ padding: 8px 12px;
+ min-height: 40px;
+ &:focus,
+ &:active {
+ outline: none;
+ }
+`
+const StyledInviteCodeOuterWrapper = styled.div`
+ color: ${Colors.ANATRACITE};
+ margin: 8px;
+ display: flex;
+ align-items: center;
+
+ svg {
+ font-size: 30px;
+ cursor: pointer;
+ position: relative;
+ left: 10px;
+ }
+`
+
+type InviteModalProps = {
+ setToggleExchangeKeysModal: (shouldToggle: boolean) => void
+ toggleExchangeKeysModal: boolean
+}
+
+const ExcangeKeysModal = ({
+ setToggleExchangeKeysModal,
+ toggleExchangeKeysModal
+}: InviteModalProps) => {
+ const [roomKey, setRoomKey] = useState("")
+ const [recipientKey, setRecipientKey] = useState("")
+ const [isInviteCopied, setIsInviteCopied] = useState(false)
+ const currentActiveRoom = useAppSelector(
+ (state) => state.ChatReducer.currentActiveRoom
+ )
+
+ const handleInviteCopying = () => {
+ setIsInviteCopied(true)
+ navigator.clipboard.writeText(roomKey).then(() => {
+ setTimeout(() => setIsInviteCopied(false), 4000)
+ })
+ }
+
+ const handleGenerateRoomKey = async () => {
+ try {
+ //@ts-ignore
+ await generate_encrypted_invite_direct_room(currentActiveRoom?.id).then(
+ (invite) => setRoomKey(invite)
+ )
+ } catch (error) {
+ setRoomKey("Error while generating invite")
+ }
+ }
+
+ const handleUpdateKey = async () => {
+ try {
+ //@ts-ignore
+ await update_direct_room_key(currentActiveRoom?.id, recipientKey).then(
+ (invite) => {
+ setRoomKey("")
+ setRecipientKey("")
+ setToggleExchangeKeysModal(false)
+ }
+ )
+ } catch (error) {
+ console.log(error)
+ }
+ }
+
+ const handleModalClosing = () => {
+ setRoomKey("")
+ setRecipientKey("")
+ setToggleExchangeKeysModal(false)
+ }
+
+ return (
+
+ Exchange keys
+
+
+
+ {isInviteCopied ? "Key copied" : "Copy the key"}
+
+
+ {roomKey && (
+
+ )}
+
+
+ Generate key
+
+ setRecipientKey(e.target.value)}
+ placeholder="Enter your recipient's key..."
+ />
+ Update key
+
+
+ )
+}
+
+export default ExcangeKeysModal
diff --git a/app/src/components/Modals/generatePublicKey.tsx b/app/src/components/Modals/generatePublicKey.tsx
index d78da64..5e848c3 100644
--- a/app/src/components/Modals/generatePublicKey.tsx
+++ b/app/src/components/Modals/generatePublicKey.tsx
@@ -1,11 +1,11 @@
-import { useEffect, useState } from "react";
-import { Modal, ModalBody, ModalHeader } from "reactstrap";
-import styled from "styled-components";
-import * as Colors from "../../constants/colors";
-import { faCopy } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import ReactTooltip from "react-tooltip";
-import { get_public_key } from "rln-client-lib";
+import { useEffect, useState } from "react"
+import { Modal, ModalBody, ModalHeader } from "reactstrap"
+import styled from "styled-components"
+import * as Colors from "../../constants/colors"
+import { faCopy } from "@fortawesome/free-solid-svg-icons"
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
+import ReactTooltip from "react-tooltip"
+import { get_public_key } from "rln-client-lib"
const StyledTextarea = styled.textarea`
border: 1px solid #f0f2f5;
@@ -19,7 +19,7 @@ const StyledTextarea = styled.textarea`
&:active {
outline: none;
}
-`;
+`
const StyledInviteCodeOuterWrapper = styled.div`
color: ${Colors.ANATRACITE};
margin: 8px;
@@ -31,47 +31,47 @@ const StyledInviteCodeOuterWrapper = styled.div`
position: relative;
left: 10px;
}
-`;
+`
type InviteModalProps = {
- setToggleGeneratePublicKey: (shouldToggle: boolean) => void;
- toggleGeneratePublicKey: boolean;
-};
+ setToggleGeneratePublicKey: (shouldToggle: boolean) => void
+ toggleGeneratePublicKey: boolean
+}
const GeneratePublicKeyModal = ({
setToggleGeneratePublicKey,
toggleGeneratePublicKey
}: InviteModalProps) => {
- const [generatedInvite, setDisplayGeneratedInvite] = useState("");
- const [isInviteCopied, setIsInviteCopied] = useState(false);
+ const [generatedInvite, setDisplayGeneratedInvite] = useState("")
+ const [isInviteCopied, setIsInviteCopied] = useState(false)
useEffect(() => {
- if (toggleGeneratePublicKey) generatePublicKey();
- }, [toggleGeneratePublicKey]);
+ if (toggleGeneratePublicKey) generatePublicKey()
+ }, [toggleGeneratePublicKey])
const handleInviteCopying = () => {
navigator.clipboard.writeText(generatedInvite).then(() => {
- setIsInviteCopied(true);
- setTimeout(() => setIsInviteCopied(false), 4000);
- });
- };
+ setIsInviteCopied(true)
+ setTimeout(() => setIsInviteCopied(false), 4000)
+ })
+ }
const generatePublicKey = async () => {
try {
- get_public_key().then(key => {
- return setDisplayGeneratedInvite(key);
- });
+ get_public_key().then((key) => {
+ return setDisplayGeneratedInvite(key)
+ })
} catch (error) {
- console.log(error);
+ console.log(error)
}
- };
+ }
return (
{
- setDisplayGeneratedInvite("");
- setToggleGeneratePublicKey(false);
+ setDisplayGeneratedInvite("")
+ setToggleGeneratePublicKey(false)
}}
>
Generate Public Key
@@ -99,7 +99,7 @@ const GeneratePublicKeyModal = ({
- );
-};
+ )
+}
-export default GeneratePublicKeyModal;
+export default GeneratePublicKeyModal
diff --git a/app/src/components/Modals/index.tsx b/app/src/components/Modals/index.tsx
index e03a7ac..c3f4430 100644
--- a/app/src/components/Modals/index.tsx
+++ b/app/src/components/Modals/index.tsx
@@ -1,8 +1,8 @@
-import { useState } from "react";
-import { Modal, ModalBody, ModalFooter, ModalHeader } from "reactstrap";
-import styled from "styled-components";
-import * as Colors from "../../constants/colors";
-import { roomTypes } from "../../constants/constants";
+import { useState } from "react"
+import { Modal, ModalBody, ModalFooter, ModalHeader } from "reactstrap"
+import styled from "styled-components"
+import * as Colors from "../../constants/colors"
+import { roomTypes } from "../../constants/constants"
import {
createDirectRoomAction,
createPublicRoomAction,
@@ -12,7 +12,7 @@ import { useAppDispatch } from "../../redux/hooks/useAppDispatch";
import TrustedContactsList from "./trustedContactsList";
const StyledButton = styled.button`
- background: ${props => props.color};
+ background: ${(props) => props.color};
border-radius: 8px;
border: none;
outline: none;
@@ -21,10 +21,10 @@ const StyledButton = styled.button`
color: white;
&:hover {
transition: 0.15s;
- box-shadow: 0px 0px 15px 0px ${props => props.color};
+ box-shadow: 0px 0px 15px 0px ${(props) => props.color};
}
width: 200px;
-`;
+`
const StyledInput = styled.input`
border: 1px solid #f0f2f5;
@@ -37,53 +37,53 @@ const StyledInput = styled.input`
&:active {
outline: none;
}
-`;
+`
const StyledButtonsWrapper = styled.div`
display: flex;
-`;
+`
type RoomOptionsModalProps = {
- setToggleModal: (shouldToggle: boolean) => void;
- toggleModal: boolean;
-};
+ setToggleModal: (shouldToggle: boolean) => void
+ toggleModal: boolean
+}
type RoomsOptionsModalProps = {
- setToggleModal: (shouldToggle: boolean) => void;
- toggleModal: boolean;
- setModalContent: (modalContent: string) => void;
-};
+ setToggleModal: (shouldToggle: boolean) => void
+ toggleModal: boolean
+ setModalContent: (modalContent: string) => void
+}
const RoomOptionsModal = ({
toggleModal,
setToggleModal
}: RoomOptionsModalProps) => {
- const [modalContent, setModalContent] = useState("");
- const dispatch = useAppDispatch();
+ const [modalContent, setModalContent] = useState("")
+ const dispatch = useAppDispatch()
const handleRoomCreation = (roomType: string, data: any) => {
if (data.name) {
switch (roomType) {
case roomTypes.public:
- dispatch(createPublicRoomAction(data.name));
- break;
+ dispatch(createPublicRoomAction(data.name))
+ break
case roomTypes.private:
- dispatch(createPrivateRoomAction(data.name));
- break;
+ dispatch(createPrivateRoomAction(data.name))
+ break
case roomTypes.oneOnOne:
- dispatch(createDirectRoomAction(data.name, data.publicKey));
- break;
+ dispatch(createDirectRoomAction(data.name, data.publicKey))
+ break
default:
- return handleModalClosing();
+ return handleModalClosing()
}
}
- handleModalClosing();
- };
+ handleModalClosing()
+ }
const handleModalClosing = () => {
- setModalContent("");
- setToggleModal(false);
- };
+ setModalContent("")
+ setToggleModal(false)
+ }
const getModalContent = () => {
switch (modalContent) {
@@ -95,7 +95,7 @@ const RoomOptionsModal = ({
modalContent={modalContent}
handleModalClosing={handleModalClosing}
/>
- );
+ )
case roomTypes.private:
return (
@@ -105,7 +105,7 @@ const RoomOptionsModal = ({
modalContent={modalContent}
handleModalClosing={handleModalClosing}
/>
- );
+ )
case roomTypes.oneOnOne:
return (
@@ -115,7 +115,7 @@ const RoomOptionsModal = ({
modalContent={modalContent}
handleModalClosing={handleModalClosing}
/>
- );
+ )
default:
return (
- );
+ )
}
- };
- return getModalContent();
-};
+ }
+ return getModalContent()
+}
const RoomTypeOptions = ({
toggleModal,
@@ -163,15 +163,15 @@ const RoomTypeOptions = ({
{" "}
- );
-};
+ )
+}
type RoomNameInputProps = {
- toggleModal: boolean;
- handleRoomCreation: (roomType: string, data: any) => void;
- modalContent: string;
- handleModalClosing: () => void;
-};
+ toggleModal: boolean
+ handleRoomCreation: (roomType: string, data: any) => void
+ modalContent: string
+ handleModalClosing: () => void
+}
const RoomNameInput = ({
toggleModal,
@@ -179,7 +179,7 @@ const RoomNameInput = ({
modalContent,
handleModalClosing
}: RoomNameInputProps) => {
- const [roomName, setRoomName] = useState("");
+ const [roomName, setRoomName] = useState("")
return (
@@ -188,7 +188,7 @@ const RoomNameInput = ({
setRoomName(e.target.value)}
+ onChange={(e) => setRoomName(e.target.value)}
/>
@@ -200,25 +200,25 @@ const RoomNameInput = ({
- );
-};
+ )
+}
const OneOnOneModal = ({
toggleModal,
handleRoomCreation,
handleModalClosing
}: RoomNameInputProps) => {
- const [roomName, setRoomName] = useState("");
- const [publicKey, setPublicKey] = useState("");
+ const [roomName, setRoomName] = useState("")
+ const [publicKey, setPublicKey] = useState("")
const handleDirectRoomCreation = () => {
if (publicKey && roomName) {
handleRoomCreation(roomTypes.oneOnOne, {
name: roomName,
publicKey
- });
+ })
}
- };
+ }
return (
@@ -228,7 +228,7 @@ const OneOnOneModal = ({
setRoomName(e.target.value)}
+ onChange={(e) => setRoomName(e.target.value)}
/>
@@ -241,6 +241,6 @@ const OneOnOneModal = ({
- );
-};
-export default RoomOptionsModal;
+ )
+}
+export default RoomOptionsModal
diff --git a/app/src/components/Modals/inviteModal.tsx b/app/src/components/Modals/inviteModal.tsx
index 15f57de..77ac8c5 100644
--- a/app/src/components/Modals/inviteModal.tsx
+++ b/app/src/components/Modals/inviteModal.tsx
@@ -24,21 +24,21 @@ const StyledButton = styled.button`
box-shadow: 0px 0px 15px 0px ${Colors.ANATRACITE};
}
width: 200px;
-`;
+`
const StyledInviteCodeOuterWrapper = styled.div`
color: ${Colors.ANATRACITE};
margin: 8px;
display: flex;
align-items: center;
-
+
svg {
font-size: 30px;
cursor: pointer;
position: relative;
left: 10px;
}
-`;
+`
const StyledInviteCodeInnerWrapper = styled.div`
border: 1px solid #f0f2f5;
border-radius: 20px;
@@ -47,39 +47,40 @@ const StyledInviteCodeInnerWrapper = styled.div`
padding: 8px 12px;
margin: 10px 0;
word-break: break-all;
-`;
-
+`
+
type InviteModalProps = {
- setToggleInviteModal: (shouldToggle: boolean) => void;
- toggleInviteModal: boolean;
-};
+ setToggleInviteModal: (shouldToggle: boolean) => void
+ toggleInviteModal: boolean
+}
const InviteModal = ({
setToggleInviteModal,
toggleInviteModal
}: InviteModalProps) => {
- const [keyValue, setKeyValue] = useState("");
- const [generatedInvite, setDisplayGeneratedInvite] = useState("");
- const [isInviteCopied, setIsInviteCopied] = useState(false);
+ const [keyValue, setKeyValue] = useState("")
+ const [generatedInvite, setDisplayGeneratedInvite] = useState("")
+ const [isInviteCopied, setIsInviteCopied] = useState(false)
//@ts-ignore
- const currentActiveRoom: Room = useAppSelector(
- state => state.ChatReducer.currentActiveRoom
- );
+ const currentActiveRoom: Room = useAppSelector(
+ (state) => state.ChatReducer.currentActiveRoom
+ )
const handleInviteCopying = () => {
- setIsInviteCopied(true);
+ setIsInviteCopied(true)
navigator.clipboard.writeText(generatedInvite).then(() => {
- setTimeout(() => setIsInviteCopied(false), 4000);
- });
- };
+ setTimeout(() => setIsInviteCopied(false), 4000)
+ })
+ }
- const handleGenerateInvite = async () => {
+ const handleGenerateInvite = async () => {
try {
- await invite_private_room(currentActiveRoom?.id, keyValue).then(invite => setDisplayGeneratedInvite(invite)
- );
+ await invite_private_room(currentActiveRoom?.id, keyValue).then(
+ (invite) => setDisplayGeneratedInvite(invite)
+ )
} catch (error) {
console.log(error)
}
- };
+ }
return (
@@ -121,7 +122,7 @@ const InviteModal = ({
)}
- );
-};
+ )
+}
-export default InviteModal;
+export default InviteModal
diff --git a/app/src/components/Modals/privateRoomModal.tsx b/app/src/components/Modals/privateRoomModal.tsx
index a46d9f9..9b6c82d 100644
--- a/app/src/components/Modals/privateRoomModal.tsx
+++ b/app/src/components/Modals/privateRoomModal.tsx
@@ -1,9 +1,9 @@
-import { useState } from "react";
-import { Modal, ModalHeader, ModalBody, ModalFooter } from "reactstrap";
-import styled from "styled-components";
-import * as Colors from "../../constants/colors";
-import { joinPrivateRoomAction } from "../../redux/actions/actionCreator";
-import { useAppDispatch } from "../../redux/hooks/useAppDispatch";
+import { useState } from "react"
+import { Modal, ModalHeader, ModalBody, ModalFooter } from "reactstrap"
+import styled from "styled-components"
+import * as Colors from "../../constants/colors"
+import { joinPrivateRoomAction } from "../../redux/actions/actionCreator"
+import { useAppDispatch } from "../../redux/hooks/useAppDispatch"
const StyledTextarea = styled.textarea`
border: 1px solid #f0f2f5;
@@ -17,7 +17,7 @@ const StyledTextarea = styled.textarea`
&:active {
outline: none;
}
-`;
+`
const StyledButton = styled.button`
background: ${Colors.ANATRACITE};
@@ -32,26 +32,26 @@ const StyledButton = styled.button`
box-shadow: 0px 0px 15px 0px ${Colors.ANATRACITE};
}
width: 200px;
-`;
+`
const JoinPrivateRoomModal = ({
toggleJoinPrivateRoom,
setToggleJoinPrivateRoom
}: any) => {
- const [roomInvite, setRoomInvite] = useState("");
- const dispatch = useAppDispatch();
+ const [roomInvite, setRoomInvite] = useState("")
+ const dispatch = useAppDispatch()
const handleModalClosing = () => {
- setRoomInvite("");
- setToggleJoinPrivateRoom(false);
- };
+ setRoomInvite("")
+ setToggleJoinPrivateRoom(false)
+ }
const handleRoomCreation = () => {
if (roomInvite) {
- dispatch(joinPrivateRoomAction(roomInvite));
- handleModalClosing();
+ dispatch(joinPrivateRoomAction(roomInvite))
+ handleModalClosing()
}
- };
+ }
return (
@@ -60,7 +60,7 @@ const JoinPrivateRoomModal = ({
setRoomInvite(e.target.value)}
+ onChange={(e) => setRoomInvite(e.target.value)}
rows={2}
/>
@@ -70,7 +70,7 @@ const JoinPrivateRoomModal = ({
- );
-};
+ )
+}
-export default JoinPrivateRoomModal;
+export default JoinPrivateRoomModal
diff --git a/app/src/components/Modals/recoverModal.tsx b/app/src/components/Modals/recoverModal.tsx
index dabc66c..5d61f46 100644
--- a/app/src/components/Modals/recoverModal.tsx
+++ b/app/src/components/Modals/recoverModal.tsx
@@ -1,13 +1,24 @@
+import * as Colors from "../../constants/colors";
+import styled from "styled-components";
import { useState } from "react";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router";
import { Modal, ModalBody, ModalFooter, ModalHeader } from "reactstrap";
-import { init, receive_message, recover_profile } from "rln-client-lib";
-import styled from "styled-components";
-import * as Colors from "../../constants/colors";
+import {
+ get_rooms,
+ init,
+ receive_message,
+ recover_profile
+} from "rln-client-lib";
+import { IRooms } from "rln-client-lib/dist/src/profile/interfaces";
import { serverUrl, socketUrl } from "../../constants/constants";
-import { addMessageToRoomAction, getChatHistoryAction, getRoomsAction } from "../../redux/actions/actionCreator";
import { generateProof } from "../../util/util";
+import {
+ addMessageToRoomAction,
+ getRoomsAction,
+ loadMessagesForRooms,
+ runSyncMessageHistory
+} from "../../redux/actions/actionCreator";
const StyledButton = styled.button`
background: ${Colors.ANATRACITE};
@@ -50,21 +61,37 @@ const RecoverModal = ({
const dispatch = useDispatch();
const [userData, setUserData] = useState("");
+ const loadMessagesFromDb = async () => {
+ const allRooms: IRooms = await get_rooms();
+ const roomIds: string[] = [
+ ...allRooms.direct.map(d => d.id),
+ ...allRooms.private.map(d => d.id),
+ ...allRooms.public.map(d => d.id)
+ ];
+
+ const nowTimestamp: number = new Date().getTime();
+ dispatch(loadMessagesForRooms(roomIds, nowTimestamp));
+
+ await receive_message(receiveMessageCallback);
+ };
+
const initializeApp = async () => {
try {
await init({
serverUrl: serverUrl,
socketUrl: socketUrl
- },
- generateProof)
- .then(() => {
- navigate("/dashboard");
- dispatch(getRoomsAction());
- dispatch(getChatHistoryAction());
- })
- .then(async () => {
- await receive_message(receiveMessageCallback);
- });
+ }, generateProof).then(() => {
+ navigate("/dashboard");
+ dispatch(getRoomsAction());
+
+ dispatch(
+ runSyncMessageHistory({
+ onSuccess: () => {
+ loadMessagesFromDb();
+ }
+ })
+ );
+ });
} catch (error) {
navigate("/r-procedure");
}
@@ -98,6 +125,7 @@ const RecoverModal = ({
navigate("/r-procedure");
}
};
+
return (
setToggleRecoverModal(false)}>
diff --git a/app/src/components/Modals/trustedContactsModal.tsx b/app/src/components/Modals/trustedContactsModal.tsx
index fca6ced..d8b91ad 100644
--- a/app/src/components/Modals/trustedContactsModal.tsx
+++ b/app/src/components/Modals/trustedContactsModal.tsx
@@ -1,10 +1,10 @@
-import { useEffect, useState } from "react";
-import { Modal, ModalBody, ModalHeader } from "reactstrap";
-import styled from "styled-components";
-import * as Colors from "../../constants/colors";
-import { faPen, faTrash } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import ReactTooltip from "react-tooltip";
+import { useEffect, useState } from "react"
+import { Modal, ModalBody, ModalHeader } from "reactstrap"
+import styled from "styled-components"
+import * as Colors from "../../constants/colors"
+import { faPen, faTrash } from "@fortawesome/free-solid-svg-icons"
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
+import ReactTooltip from "react-tooltip"
import {
delete_contact,
get_contact,
@@ -28,7 +28,7 @@ const StyledTextarea = styled.textarea`
&:active {
outline: none;
}
-`;
+`
const StyledInput = styled.input`
border: 1px solid #f0f2f5;
@@ -40,7 +40,7 @@ const StyledInput = styled.input`
&:active {
outline: none;
}
-`;
+`
const StyledButton = styled.button`
background: ${Colors.ANATRACITE};
@@ -55,7 +55,7 @@ const StyledButton = styled.button`
box-shadow: 0px 0px 15px 0px ${Colors.ANATRACITE};
}
width: 200px;
-`;
+`
const StyledContactWrapper = styled.div`
display: flex;
@@ -69,7 +69,7 @@ const StyledContactWrapper = styled.div`
fill: ${Colors.ANATRACITE};
}
}
-`;
+`
const StyledContactName = styled.div`
background: ${Colors.BERRY_PINK};
@@ -81,40 +81,40 @@ const StyledContactName = styled.div`
color: white;
min-width: 300px;
text-align: center;
-`;
+`
const StyledButtonWrapper = styled.div`
display: flex;
justify-content: center;
margin-top: 20px;
-`;
+`
const StyledInputLabel = styled.div`
font-size: 12px;
padding: 0 10px;
color: ${Colors.ANATRACITE};
margin-top: 25px;
-`;
+`
type TrustedContactsProps = {
- setToggleTrustedContacts: (shouldToggle: boolean) => void;
- toggleTrustedContacts: boolean;
-};
+ setToggleTrustedContacts: (shouldToggle: boolean) => void
+ toggleTrustedContacts: boolean
+}
const TrustedContactsModal = ({
- setToggleTrustedContacts,
- toggleTrustedContacts
+ setToggleTrustedContacts,
+ toggleTrustedContacts
}: TrustedContactsProps) => {
- const [toggleAddEditModal, setToggleAddEditModal] = useState(false);
- const [editContactName, setEditContactName] = useState("");
- const trustedContacts = useAppSelector(
- state => state.ChatReducer.trustedContacts
- );
- const dispatch = useAppDispatch();
+ const [toggleAddEditModal, setToggleAddEditModal] = useState(false)
+ const [editContactName, setEditContactName] = useState("")
+ const trustedContacts = useAppSelector(
+ (state) => state.ChatReducer.trustedContacts
+ )
+ const dispatch = useAppDispatch()
- useEffect(() => {
- dispatch(getTrustedContacts());
- }, [toggleTrustedContacts]); // eslint-disable-line react-hooks/exhaustive-deps
+ useEffect(() => {
+ dispatch(getTrustedContacts())
+ }, [toggleTrustedContacts]) // eslint-disable-line react-hooks/exhaustive-deps
const handleContactDeleting = (name: string) => {
if (name) {
@@ -187,106 +187,106 @@ const TrustedContactsModal = ({
-
-
-
- );
-};
+
+
+
+ )
+}
type AddEditModalProps = {
- name?: string;
- setToggleAddEditModal: (shouldToggle: boolean) => void;
- toggleAddEditModal: boolean;
- setEditContactName: (name: string) => void;
-};
+ name?: string
+ setToggleAddEditModal: (shouldToggle: boolean) => void
+ toggleAddEditModal: boolean
+ setEditContactName: (name: string) => void
+}
const AddEditContactModal = ({
- name,
- setToggleAddEditModal,
- toggleAddEditModal,
- setEditContactName
+ name,
+ setToggleAddEditModal,
+ toggleAddEditModal,
+ setEditContactName
}: AddEditModalProps) => {
- const [contactName, setContactName] = useState("");
- const [publicKey, setPublicKey] = useState("");
- const dispatch = useAppDispatch();
+ const [contactName, setContactName] = useState("")
+ const [publicKey, setPublicKey] = useState("")
+ const dispatch = useAppDispatch()
- useEffect(() => {
- if (name) {
- get_contact(name)
- .then(contactDetails => {
- setContactName(contactDetails.name);
- setPublicKey(contactDetails.publicKey);
- })
- .catch(err => toast.error(err));
- }
- }, [toggleAddEditModal]); // eslint-disable-line react-hooks/exhaustive-deps
+ useEffect(() => {
+ if (name) {
+ get_contact(name)
+ .then((contactDetails) => {
+ setContactName(contactDetails.name)
+ setPublicKey(contactDetails.publicKey)
+ })
+ .catch((err) => toast.error(err))
+ }
+ }, [toggleAddEditModal]) // eslint-disable-line react-hooks/exhaustive-deps
- const handleContactSaving = () => {
- if (contactName && publicKey) {
- if (name) {
- update_contact(name, contactName, publicKey)
- .then(() => {
- dispatch(getTrustedContacts());
- setPublicKey("");
- setContactName("");
- setEditContactName("");
- setToggleAddEditModal(false);
- })
- .catch(err => toast.error(err));
- } else {
- insert_contact(contactName, publicKey)
- .then(() => {
- dispatch(getTrustedContacts());
- setPublicKey("");
- setContactName("");
- setEditContactName("");
- setToggleAddEditModal(false);
- })
- .catch(err => toast.error(err));
- }
- }
- };
+ const handleContactSaving = () => {
+ if (contactName && publicKey) {
+ if (name) {
+ update_contact(name, contactName, publicKey)
+ .then(() => {
+ dispatch(getTrustedContacts())
+ setPublicKey("")
+ setContactName("")
+ setEditContactName("")
+ setToggleAddEditModal(false)
+ })
+ .catch((err) => toast.error(err))
+ } else {
+ insert_contact(contactName, publicKey)
+ .then(() => {
+ dispatch(getTrustedContacts())
+ setPublicKey("")
+ setContactName("")
+ setEditContactName("")
+ setToggleAddEditModal(false)
+ })
+ .catch((err) => toast.error(err))
+ }
+ }
+ }
- return (
-
- {
- setContactName("");
- setPublicKey("");
- setEditContactName("");
- setToggleAddEditModal(false);
- }}
- >
- {name ? "Edit Contact" : "Create Trusted Contact"}
-
-
- Name
- setContactName(e.target.value)}
- />
+ return (
+
+ {
+ setContactName("")
+ setPublicKey("")
+ setEditContactName("")
+ setToggleAddEditModal(false)
+ }}
+ >
+ {name ? "Edit Contact" : "Create Trusted Contact"}
+
+
+ Name
+ setContactName(e.target.value)}
+ />
- Public Key
- setPublicKey(e.target.value)}
- />
-
-
- {name ? "Update" : "Create"}
-
-
-
-
- );
-};
+ Public Key
+ setPublicKey(e.target.value)}
+ />
+
+
+ {name ? "Update" : "Create"}
+
+
+
+
+ )
+}
-export default TrustedContactsModal;
+export default TrustedContactsModal
diff --git a/app/src/components/PublicRoomInvitedScreen/index.tsx b/app/src/components/PublicRoomInvitedScreen/index.tsx
index 57f877d..a3427bb 100644
--- a/app/src/components/PublicRoomInvitedScreen/index.tsx
+++ b/app/src/components/PublicRoomInvitedScreen/index.tsx
@@ -1,8 +1,8 @@
-import { useState } from "react";
-import { useNavigate, useParams } from "react-router";
-import { join_public_room } from "rln-client-lib";
-import styled from "styled-components";
-import * as Colors from "../../constants/colors";
+import { useState } from "react"
+import { useNavigate, useParams } from "react-router"
+import { join_public_room } from "rln-client-lib"
+import styled from "styled-components"
+import * as Colors from "../../constants/colors"
const StyledButton = styled.button`
background: ${Colors.BERRY_PINK};
@@ -16,32 +16,34 @@ const StyledButton = styled.button`
box-shadow: 0px 0px 15px 0px ${Colors.BERRY_PINK};
}
width: 180px;
-`;
+`
const StyledInvitedScreenWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
-`;
+`
const StyledNotificationWrapper = styled.div`
color: ${Colors.PASTEL_RED};
font-size: 24px;
margin: 20px 0;
-`;
+`
const successMessage = "Your new public room has been successfully added."
const PublicRoomInvitedScreen = () => {
- const params = useParams();
- const navigate = useNavigate();
- const [notificationMessage,setNotificationMessage] = useState("")
+ const params = useParams()
+ const navigate = useNavigate()
+ const [notificationMessage, setNotificationMessage] = useState("")
- const handlePublicRoomInvite=async()=>{
- try {
+ const handlePublicRoomInvite = async () => {
+ try {
//@ts-ignore
- await join_public_room(params.roomId).then(() => setNotificationMessage(successMessage))
+ await join_public_room(params.roomId).then(() =>
+ setNotificationMessage(successMessage)
+ )
} catch (error) {
setNotificationMessage(error as string)
}
@@ -50,25 +52,26 @@ const PublicRoomInvitedScreen = () => {
return (
- {notificationMessage ?
+ {notificationMessage ? (
<>
-
+
{notificationMessage}
navigate("/dashboard")}>
Go to Rooms
- > :
+ >
+ ) : (
<>
Would you like to join room with id: {params.roomId} ?
Join
>
- }
+ )}
- );
-};
+ )
+}
-export default PublicRoomInvitedScreen;
+export default PublicRoomInvitedScreen
diff --git a/app/src/components/RegisterOrRecover/index.tsx b/app/src/components/RegisterOrRecover/index.tsx
index 3a38214..c7afff4 100644
--- a/app/src/components/RegisterOrRecover/index.tsx
+++ b/app/src/components/RegisterOrRecover/index.tsx
@@ -1,15 +1,15 @@
-import React, { useState } from "react";
-import { useNavigate } from "react-router";
-import styled from "styled-components";
-import * as Colors from "../../constants/colors";
-import RecoverModal from "../Modals/recoverModal";
-import { init, receive_message } from "rln-client-lib";
-import { useDispatch } from "react-redux";
-import { addMessageToRoomAction, getChatHistoryAction, getRoomsAction } from "../../redux/actions/actionCreator";
+import React, { useState } from "react"
+import { useNavigate } from "react-router"
+import styled from "styled-components"
+import * as Colors from "../../constants/colors"
+import RecoverModal from "../Modals/recoverModal"
+import { init, receive_message } from "rln-client-lib"
+import { useDispatch } from "react-redux"
import {
- serverUrl,
- socketUrl
-} from "../../constants/constants";
+ addMessageToRoomAction,
+ getRoomsAction
+} from "../../redux/actions/actionCreator"
+import { serverUrl, socketUrl } from "../../constants/constants"
import { generateProof } from "../../util/util";
const StyledRegisterWrapper = styled.div`
@@ -17,7 +17,7 @@ const StyledRegisterWrapper = styled.div`
height: 100%;
display: flex;
align-items: center;
-`;
+`
const StyledButtonsContainer = styled.div`
margin: 0 auto;
@@ -25,10 +25,10 @@ const StyledButtonsContainer = styled.div`
border-radius: 27px;
display: flex;
flex-direction: column;
-`;
+`
const StyledRButton = styled.button`
- background: ${props => props.color};
+ background: ${(props) => props.color};
border-radius: 8px;
border: none;
outline: none;
@@ -37,22 +37,22 @@ const StyledRButton = styled.button`
color: ${Colors.ANATRACITE};
&:hover {
transition: 0.15s;
- box-shadow: 0px 0px 15px 0px ${props => props.color};
+ box-shadow: 0px 0px 15px 0px ${(props) => props.color};
}
-`;
+`
const RegisterOrRecover = () => {
- const [toggleRecoverModal, setToggleRecoverModal] = useState(false);
- const navigate = useNavigate();
- const dispatch = useDispatch();
+ const [toggleRecoverModal, setToggleRecoverModal] = useState(false)
+ const navigate = useNavigate()
+ const dispatch = useDispatch()
const handleRegisterClick = () => {
- initializeApp();
- };
+ initializeApp()
+ }
const initializeApp = async () => {
try {
- const identityCommitment = await getActiveIdentity();
+ const identityCommitment = await getActiveIdentity()
await init(
{
serverUrl,
@@ -60,30 +60,31 @@ const RegisterOrRecover = () => {
},
generateProof,
identityCommitment
- ).then(() => {
- navigate("/dashboard");
- dispatch(getRoomsAction());
- dispatch(getChatHistoryAction())
- })
- .then(async() => {
- await receive_message(receiveMessageCallback);
- });
+ )
+ .then(() => {
+ navigate("/dashboard")
+ dispatch(getRoomsAction())
+ // No need to sync the message history on Register, because the user doesn't have any room
+ })
+ .then(async () => {
+ await receive_message(receiveMessageCallback)
+ })
} catch (error) {
- navigate("/r-procedure");
+ navigate("/r-procedure")
}
- };
+ }
const getActiveIdentity = async () => {
- console.log('getting the identity from zk-keeper');
+ console.info("getting the identity from zk-keeper")
const { injected } = window as any
- const client = await injected.connect();
- const id = await client.getActiveIdentity(1, 2);
- return id;
+ const client = await injected.connect()
+ const id = await client.getActiveIdentity(1, 2)
+ return id
}
const receiveMessageCallback = (message: any, roomId: string) => {
- dispatch(addMessageToRoomAction(message, roomId));
- };
+ dispatch(addMessageToRoomAction(message, roomId))
+ }
return (
@@ -105,7 +106,7 @@ const RegisterOrRecover = () => {
/>
- );
-};
+ )
+}
-export default RegisterOrRecover;
+export default RegisterOrRecover
diff --git a/app/src/components/RoomHandlingButtons/index.tsx b/app/src/components/RoomHandlingButtons/index.tsx
index 361d695..6d9026b 100644
--- a/app/src/components/RoomHandlingButtons/index.tsx
+++ b/app/src/components/RoomHandlingButtons/index.tsx
@@ -1,19 +1,19 @@
-import React, { useState } from "react";
-import styled from "styled-components";
-import { faFileExport } from "@fortawesome/free-solid-svg-icons";
-import { faUserPlus } from "@fortawesome/free-solid-svg-icons";
-import { faKey } from "@fortawesome/free-solid-svg-icons";
-import { faPlusCircle, faUserLock } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import RoomOptionsModal from "../Modals";
-import ReactTooltip from "react-tooltip";
-import { saveAs } from "file-saver";
+import React, { useState } from "react"
+import styled from "styled-components"
+import { faFileExport } from "@fortawesome/free-solid-svg-icons"
+import { faUserPlus } from "@fortawesome/free-solid-svg-icons"
+import { faKey } from "@fortawesome/free-solid-svg-icons"
+import { faPlusCircle, faUserLock } from "@fortawesome/free-solid-svg-icons"
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
+import RoomOptionsModal from "../Modals"
+import ReactTooltip from "react-tooltip"
+import { saveAs } from "file-saver"
-import * as Colors from "../../constants/colors";
-import JoinPrivateRoomModal from "../Modals/privateRoomModal";
-import TrustedContactsModal from "../Modals/trustedContactsModal";
-import GeneratePublicKeyModal from "../Modals/generatePublicKey";
-import { export_profile } from "rln-client-lib";
+import * as Colors from "../../constants/colors"
+import JoinPrivateRoomModal from "../Modals/privateRoomModal"
+import TrustedContactsModal from "../Modals/trustedContactsModal"
+import GeneratePublicKeyModal from "../Modals/generatePublicKey"
+import { export_profile } from "rln-client-lib"
const StyledButtonsWrapper = styled.div`
display: flex;
@@ -27,26 +27,26 @@ const StyledButtonsWrapper = styled.div`
fill: ${Colors.BERRY_PINK};
}
}
-`;
+`
const RoomHandlingButtons = () => {
- const [toggleModal, setToggleModal] = useState(false);
- const [toggleJoinPrivateRoom, setToggleJoinPrivateRoom] = useState(false);
- const [toggleGeneratePublicKey, setToggleGeneratePublicKey] = useState(false);
- const [toggleTrustedContacts, setToggleTrustedContacts] = useState(false);
+ const [toggleModal, setToggleModal] = useState(false)
+ const [toggleJoinPrivateRoom, setToggleJoinPrivateRoom] = useState(false)
+ const [toggleGeneratePublicKey, setToggleGeneratePublicKey] = useState(false)
+ const [toggleTrustedContacts, setToggleTrustedContacts] = useState(false)
const handleExportProfileClick = async () => {
try {
- export_profile().then(json => {
+ export_profile().then((json) => {
var fileToSave = new Blob([json], {
type: "application/json"
- });
- return saveAs(fileToSave, "Profile.json");
- });
+ })
+ return saveAs(fileToSave, "Profile.json")
+ })
} catch (error) {
- console.log(error);
+ console.log(error)
}
- };
+ }
return (
@@ -137,7 +137,7 @@ const RoomHandlingButtons = () => {
setToggleTrustedContacts={setToggleTrustedContacts}
/>
- );
-};
+ )
+}
-export default RoomHandlingButtons;
+export default RoomHandlingButtons
diff --git a/app/src/components/Spinner/index.tsx b/app/src/components/Spinner/index.tsx
new file mode 100644
index 0000000..617ca5a
--- /dev/null
+++ b/app/src/components/Spinner/index.tsx
@@ -0,0 +1,30 @@
+import React from "react";
+import { Spinner } from "reactstrap";
+import styled from "styled-components";
+
+const StyledSpinnerWrapper = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100vh;
+ flex-direction: column;
+ color: white;
+ div {
+ height: 5rem;
+ width: 5rem;
+ }
+`;
+
+const SyncSpinner = () => (
+
+
+ Loading messages...
+
+);
+
+export default SyncSpinner;
diff --git a/app/src/constants/colors.ts b/app/src/constants/colors.ts
index 1463e52..543ba55 100644
--- a/app/src/constants/colors.ts
+++ b/app/src/constants/colors.ts
@@ -1,5 +1,5 @@
-export const ANATRACITE = "#475C7A";
-export const BERRY_PURPLE = "#685B79";
-export const BERRY_PINK = "#AB6C82";
-export const PASTEL_RED = "#D8737F";
-export const DARK_YELLOW = "#FCBB6D";
+export const ANATRACITE = "#475C7A"
+export const BERRY_PURPLE = "#685B79"
+export const BERRY_PINK = "#AB6C82"
+export const PASTEL_RED = "#D8737F"
+export const DARK_YELLOW = "#FCBB6D"
diff --git a/app/src/constants/constants.ts b/app/src/constants/constants.ts
index dc1d7c9..f840c63 100644
--- a/app/src/constants/constants.ts
+++ b/app/src/constants/constants.ts
@@ -2,7 +2,7 @@ export const roomTypes = {
public: "public",
private: "private",
oneOnOne: "direct"
-};
+}
export const identitySecret = [
"292199313053995090417273958361373808790528937259298737824851232628819755919",
@@ -15,13 +15,13 @@ export const identitySecret = [
"163334061393674327105174944950480885010102997192279119202426081876815328325",
"11437531512502006611249802511569299406013863708736381461713283259250234558",
"216536453410511343986078619172555451653395528739428071816041036609205931217"
-];
+]
export const identityCommitment =
- "17653365708849444179865362482568296819146357340229089950066221313927057063266";
+ "17653365708849444179865362482568296819146357340229089950066221313927057063266"
-export const clientUrl = `${process.env.REACT_APP_ENV}`;
+export const clientUrl = `${process.env.REACT_APP_ENV}`
-export const serverUrl = `${process.env.REACT_APP_SERVER_HOST}`;
+export const serverUrl = `${process.env.REACT_APP_SERVER_HOST}`
export const socketUrl = `${process.env.REACT_APP_SOCKET_HOST}`
diff --git a/app/src/index.css b/app/src/index.css
index ec2585e..4a1df4d 100644
--- a/app/src/index.css
+++ b/app/src/index.css
@@ -1,13 +1,13 @@
body {
margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
- 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
+ "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+ font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
diff --git a/app/src/index.tsx b/app/src/index.tsx
index ee803f5..c352c08 100644
--- a/app/src/index.tsx
+++ b/app/src/index.tsx
@@ -1,11 +1,11 @@
-import React from "react";
-import ReactDOM from "react-dom";
-import "./index.css";
-import App from "./App";
-import reportWebVitals from "./reportWebVitals";
-import { Provider } from "react-redux";
-import store from "./redux/store";
-import "bootstrap/dist/css/bootstrap.min.css";
+import React from "react"
+import ReactDOM from "react-dom"
+import "./index.css"
+import App from "./App"
+import reportWebVitals from "./reportWebVitals"
+import { Provider } from "react-redux"
+import store from "./redux/store"
+import "bootstrap/dist/css/bootstrap.min.css"
ReactDOM.render(
@@ -14,9 +14,9 @@ ReactDOM.render(
,
document.getElementById("root")
-);
+)
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
-reportWebVitals();
+reportWebVitals()
diff --git a/app/src/redux/actions/actionCreator.ts b/app/src/redux/actions/actionCreator.ts
index 551e6b5..72cc58b 100644
--- a/app/src/redux/actions/actionCreator.ts
+++ b/app/src/redux/actions/actionCreator.ts
@@ -2,71 +2,105 @@ import {
create_direct_room,
create_private_room,
create_public_room,
- get_chat_history,
get_rooms,
join_private_room,
- get_contacts
-} from "rln-client-lib";
+ get_contacts,
+ delete_messages_for_room,
+ get_messages_for_room,
+ get_messages_for_rooms,
+ sync_message_history
+} from "rln-client-lib"
+import { IMessage } from "rln-client-lib/dist/src/chat/interfaces"
import {
IDirectRoom,
IPrivateRoom,
IPublicRoom
-} from "rln-client-lib/dist/src/room/interfaces";
+} from "rln-client-lib/dist/src/room/interfaces"
-export type Room = IDirectRoom | IPublicRoom | IPrivateRoom;
+export type Room = IDirectRoom | IPublicRoom | IPrivateRoom
-export const ADD_ACTIVE_CHAT_ROOM = "ADD_ACTIVE_CHAT_ROOM";
+export const ADD_ACTIVE_CHAT_ROOM = "ADD_ACTIVE_CHAT_ROOM"
export const addActiveChatRoom = (room: Room | undefined) => ({
type: ADD_ACTIVE_CHAT_ROOM,
meta: room
-});
+})
-export const GET_ROOMS = "GET_ROOMS";
+export const GET_ROOMS = "GET_ROOMS"
export const getRoomsAction = () => ({
type: GET_ROOMS,
promise: get_rooms()
-});
+})
-export const CREATE_PUBLIC_ROOM = "CREATE_PUBLIC_ROOM";
+export const CREATE_PUBLIC_ROOM = "CREATE_PUBLIC_ROOM"
export const createPublicRoomAction = (name: string) => ({
type: CREATE_PUBLIC_ROOM,
promise: create_public_room(name)
-});
+})
-export const CREATE_PRIVATE_ROOM = "CREATE_PRIVATE_ROOM";
+export const CREATE_PRIVATE_ROOM = "CREATE_PRIVATE_ROOM"
export const createPrivateRoomAction = (name: string) => ({
type: CREATE_PRIVATE_ROOM,
promise: create_private_room(name)
-});
+})
export const joinPrivateRoomAction = (invite: string) => ({
type: CREATE_PRIVATE_ROOM,
promise: join_private_room(invite)
-});
+})
-export const CREATE_DIRECT_ROOM = "CREATE_DIRECT_ROOM";
+export const CREATE_DIRECT_ROOM = "CREATE_DIRECT_ROOM"
export const createDirectRoomAction = (
name: string,
receiver_public_key: string
) => ({
type: CREATE_DIRECT_ROOM,
promise: create_direct_room(name, receiver_public_key)
-});
+})
-export const ADD_MESSAGE_TO_ROOM = "ADD_MESSAGE_TO_ROOM";
-export const addMessageToRoomAction = (message: string, roomId: string) => ({
+export const ADD_MESSAGE_TO_ROOM = "ADD_MESSAGE_TO_ROOM"
+export const addMessageToRoomAction = (message: IMessage, roomId: string) => ({
type: ADD_MESSAGE_TO_ROOM,
meta: { message, roomId }
-});
+})
-export const GET_CHAT_HISTORY = "GET_CHAT_HISTORY";
-export const getChatHistoryAction = () => ({
- type: GET_CHAT_HISTORY,
- promise: get_chat_history()
-});
+export const RUN_SYNC_MESSAGE_HISTORY = "RUN_SYNC_MESSAGE_HISTORY"
+export const runSyncMessageHistory = (meta?: any) => ({
+ type: RUN_SYNC_MESSAGE_HISTORY,
+ promise: sync_message_history(),
+ meta
+})
-export const GET_TRUSTED_CONTACTS = "GET_TRUSTED_CONTACTS";
+export const LOAD_MESSAGES_FOR_ROOM = "LOAD_MESSAGES_FOR_ROOM"
+export const loadMessagesForRoom = (
+ roomId: string,
+ fromTimestamp: number,
+ shouldReset: boolean = false,
+ meta: any
+) => ({
+ type: LOAD_MESSAGES_FOR_ROOM,
+ meta: { roomId, shouldReset, ...meta },
+ promise: get_messages_for_room(roomId, fromTimestamp)
+})
+
+export const LOAD_MESSAGES_FOR_ROOMS = "LOAD_MESSAGES_FOR_ROOMS"
+export const loadMessagesForRooms = (
+ roomIds: string[],
+ fromTimestamp: number
+) => ({
+ type: LOAD_MESSAGES_FOR_ROOMS,
+ meta: { roomIds },
+ promise: get_messages_for_rooms(roomIds, fromTimestamp)
+})
+
+export const DELETE_MESSAGES_FOR_ROOM = "DELETE_MESSAGES_FOR_ROOM"
+export const deleteMessagesForRoom = (roomId: string) => ({
+ type: DELETE_MESSAGES_FOR_ROOM,
+ meta: { roomId },
+ promise: delete_messages_for_room(roomId)
+})
+
+export const GET_TRUSTED_CONTACTS = "GET_TRUSTED_CONTACTS"
export const getTrustedContacts = () => ({
type: GET_TRUSTED_CONTACTS,
promise: get_contacts()
-});
\ No newline at end of file
+})
diff --git a/app/src/redux/actions/actionTypes.ts b/app/src/redux/actions/actionTypes.ts
index 8cec2e9..336ce12 100644
--- a/app/src/redux/actions/actionTypes.ts
+++ b/app/src/redux/actions/actionTypes.ts
@@ -1 +1 @@
-export {};
\ No newline at end of file
+export {}
diff --git a/app/src/redux/history/index.ts b/app/src/redux/history/index.ts
index 30eab10..53e9619 100644
--- a/app/src/redux/history/index.ts
+++ b/app/src/redux/history/index.ts
@@ -1,3 +1,3 @@
-import { createBrowserHistory } from "history";
+import { createBrowserHistory } from "history"
-export default createBrowserHistory();
\ No newline at end of file
+export default createBrowserHistory()
diff --git a/app/src/redux/hooks/useAppDispatch/index.ts b/app/src/redux/hooks/useAppDispatch/index.ts
index 82396ba..8af1b09 100644
--- a/app/src/redux/hooks/useAppDispatch/index.ts
+++ b/app/src/redux/hooks/useAppDispatch/index.ts
@@ -1,6 +1,7 @@
-import { Dispatch } from "react";
-import { useDispatch } from "react-redux";
-import { AnyAction } from "redux";
-import { AppDispatch } from "../../store";
+import { Dispatch } from "react"
+import { useDispatch } from "react-redux"
+import { AnyAction } from "redux"
+import { AppDispatch } from "../../store"
-export const useAppDispatch = (): Dispatch => useDispatch();
+export const useAppDispatch = (): Dispatch =>
+ useDispatch()
diff --git a/app/src/redux/hooks/useAppSelector/index.ts b/app/src/redux/hooks/useAppSelector/index.ts
index 293330e..b92b4ed 100644
--- a/app/src/redux/hooks/useAppSelector/index.ts
+++ b/app/src/redux/hooks/useAppSelector/index.ts
@@ -1,4 +1,4 @@
-import { TypedUseSelectorHook, useSelector } from "react-redux";
-import { RootState } from "../../store";
+import { TypedUseSelectorHook, useSelector } from "react-redux"
+import { RootState } from "../../store"
-export const useAppSelector: TypedUseSelectorHook = useSelector;
+export const useAppSelector: TypedUseSelectorHook = useSelector
diff --git a/app/src/redux/reducers/chat.ts b/app/src/redux/reducers/chat.ts
index 0a99ca1..c458955 100644
--- a/app/src/redux/reducers/chat.ts
+++ b/app/src/redux/reducers/chat.ts
@@ -1,5 +1,5 @@
-import { handle } from "redux-pack";
-import { AnyAction } from "redux";
+import { handle } from "redux-pack"
+import { AnyAction } from "redux"
import {
ADD_ACTIVE_CHAT_ROOM,
GET_ROOMS,
@@ -8,49 +8,59 @@ import {
CREATE_DIRECT_ROOM,
Room,
ADD_MESSAGE_TO_ROOM,
- GET_CHAT_HISTORY,
- GET_TRUSTED_CONTACTS
+ GET_TRUSTED_CONTACTS,
+ DELETE_MESSAGES_FOR_ROOM,
+ LOAD_MESSAGES_FOR_ROOM,
+ LOAD_MESSAGES_FOR_ROOMS,
+ RUN_SYNC_MESSAGE_HISTORY
} from "../actions/actionCreator";
import {
IRooms,
ITrustedContact
} from "rln-client-lib/dist/src/profile/interfaces";
+import { IMessage } from "rln-client-lib/dist/src/chat/interfaces";
interface RoomsState {
rooms: IRooms;
currentActiveRoom: Room | undefined;
chatHistory: Messages;
trustedContacts: ITrustedContact[];
+ chatHistorySyncing: boolean
+ stayOnBottom: boolean
+ loading: boolean
}
export type Messages = {
- [id: string]: any[];
-};
+ [id: string]: IMessage[]
+}
const defaultState: RoomsState = {
rooms: { public: [], private: [], direct: [] },
currentActiveRoom: undefined,
chatHistory: {},
- trustedContacts: []
-};
+ trustedContacts: [],
+ chatHistorySyncing: false,
+ stayOnBottom: true,
+ loading: false
+}
const ChatReducer = (state = defaultState, action: AnyAction): RoomsState => {
- const { type, payload, meta } = action;
+ const { type, payload, meta } = action
switch (type) {
case GET_ROOMS: {
return handle(state, action, {
- start: prevState => ({ ...prevState }),
- success: prevState => ({
+ start: (prevState) => ({ ...prevState }),
+ success: (prevState) => ({
...prevState,
rooms: payload
}),
- finish: prevState => ({ ...prevState })
- });
+ finish: (prevState) => ({ ...prevState })
+ })
}
case CREATE_PUBLIC_ROOM: {
return handle(state, action, {
- start: prevState => ({ ...prevState }),
- success: prevState => ({
+ start: (prevState) => ({ ...prevState }),
+ success: (prevState) => ({
...prevState,
rooms: {
...prevState.rooms,
@@ -58,13 +68,13 @@ const ChatReducer = (state = defaultState, action: AnyAction): RoomsState => {
},
currentActiveRoom: payload
}),
- finish: prevState => ({ ...prevState })
- });
+ finish: (prevState) => ({ ...prevState })
+ })
}
case CREATE_PRIVATE_ROOM: {
return handle(state, action, {
- start: prevState => ({ ...prevState }),
- success: prevState => ({
+ start: (prevState) => ({ ...prevState }),
+ success: (prevState) => ({
...prevState,
rooms: {
...prevState.rooms,
@@ -72,13 +82,13 @@ const ChatReducer = (state = defaultState, action: AnyAction): RoomsState => {
},
currentActiveRoom: payload
}),
- finish: prevState => ({ ...prevState })
- });
+ finish: (prevState) => ({ ...prevState })
+ })
}
case CREATE_DIRECT_ROOM: {
return handle(state, action, {
- start: prevState => ({ ...prevState }),
- success: prevState => ({
+ start: (prevState) => ({ ...prevState }),
+ success: (prevState) => ({
...prevState,
rooms: {
...prevState.rooms,
@@ -86,15 +96,15 @@ const ChatReducer = (state = defaultState, action: AnyAction): RoomsState => {
},
currentActiveRoom: payload
}),
- finish: prevState => ({ ...prevState })
- });
+ finish: (prevState) => ({ ...prevState })
+ })
}
case ADD_ACTIVE_CHAT_ROOM: {
return {
...state,
currentActiveRoom: meta
- };
+ }
}
case ADD_MESSAGE_TO_ROOM: {
@@ -105,35 +115,81 @@ const ChatReducer = (state = defaultState, action: AnyAction): RoomsState => {
[meta.roomId]: state.chatHistory[meta.roomId]
? [...state.chatHistory[meta.roomId], meta.message]
: [meta.message]
- }
- };
+ },
+ stayOnBottom: true
+ }
+ }
+
+ case RUN_SYNC_MESSAGE_HISTORY: {
+ return handle(state, action, {
+ start: (prevState) => ({
+ ...prevState,
+ chatHistorySyncing: true
+ }),
+ success: (prevState) => ({ ...prevState }),
+ finish: (prevState) => ({
+ ...prevState,
+ chatHistorySyncing: false
+ })
+ })
+ }
+
+ case LOAD_MESSAGES_FOR_ROOM: {
+ return handle(state, action, {
+ start: (prevState) => ({ ...prevState, loading: true }),
+ success: (prevState) => ({
+ ...prevState,
+ chatHistory: {
+ ...prevState.chatHistory,
+ [meta.roomId]: meta.shouldReset
+ ? [...payload]
+ : [...payload, ...prevState.chatHistory[meta.roomId]]
+ },
+ stayOnBottom: false
+ }),
+ finish: (prevState) => ({ ...prevState, loading: false })
+ })
+ }
+
+ case LOAD_MESSAGES_FOR_ROOMS: {
+ return handle(state, action, {
+ start: (prevState) => ({ ...prevState }),
+ success: (prevState) => ({
+ ...prevState,
+ chatHistory: {
+ ...prevState.chatHistory,
+ ...payload
+ }
+ }),
+ finish: (prevState) => ({ ...prevState })
+ })
}
- case GET_CHAT_HISTORY: {
+ case DELETE_MESSAGES_FOR_ROOM: {
return handle(state, action, {
- start: prevState => ({ ...prevState }),
- success: prevState => ({
+ start: (prevState) => ({ ...prevState }),
+ success: (prevState) => ({
...prevState,
- chatHistory: payload
+ [meta.roomId]: []
}),
- finish: prevState => ({ ...prevState })
- });
+ finish: (prevState) => ({ ...prevState })
+ })
}
case GET_TRUSTED_CONTACTS: {
return handle(state, action, {
- start: prevState => ({ ...prevState }),
- success: prevState => ({
+ start: (prevState) => ({ ...prevState }),
+ success: (prevState) => ({
...prevState,
trustedContacts: Object.values(payload)
}),
- finish: prevState => ({ ...prevState })
- });
+ finish: (prevState) => ({ ...prevState })
+ })
}
default:
- return state;
+ return state
}
-};
+}
-export default ChatReducer;
+export default ChatReducer
diff --git a/app/src/redux/reducers/index.ts b/app/src/redux/reducers/index.ts
index d901004..8307885 100644
--- a/app/src/redux/reducers/index.ts
+++ b/app/src/redux/reducers/index.ts
@@ -1,15 +1,12 @@
-import { AnyAction, combineReducers } from "redux";
-import ChatReducer from "./chat";
+import { AnyAction, combineReducers } from "redux"
+import ChatReducer from "./chat"
const appReducer = combineReducers({
ChatReducer
-});
+})
const rootReducer = (state: any, action: AnyAction) => {
- // if (action.type === RESET_APP_STORE) {
- // state = undefined;
- // }
- return appReducer(state, action);
-};
+ return appReducer(state, action)
+}
-export default rootReducer;
+export default rootReducer
diff --git a/app/src/redux/store/index.ts b/app/src/redux/store/index.ts
index e682bf1..743af12 100644
--- a/app/src/redux/store/index.ts
+++ b/app/src/redux/store/index.ts
@@ -1,19 +1,19 @@
-import { applyMiddleware, createStore } from "redux";
-import thunk from "redux-thunk";
-import rootReducer from "../reducers";
-import { composeWithDevTools } from "redux-devtools-extension/developmentOnly";
-import { middleware as reduxPackMiddleware } from "redux-pack";
+import { applyMiddleware, createStore } from "redux"
+import thunk from "redux-thunk"
+import rootReducer from "../reducers"
+import { composeWithDevTools } from "redux-devtools-extension/developmentOnly"
+import { middleware as reduxPackMiddleware } from "redux-pack"
-const initialState = {};
+const initialState = {}
const store = createStore(
rootReducer,
initialState,
composeWithDevTools(applyMiddleware(...[thunk, reduxPackMiddleware]))
-);
+)
-export default store;
+export default store
// Infer the `RootState` and `AppDispatch` types from the store itself
-export type RootState = ReturnType;
-export type AppDispatch = typeof store.dispatch;
+export type RootState = ReturnType
+export type AppDispatch = typeof store.dispatch
diff --git a/app/src/reportWebVitals.ts b/app/src/reportWebVitals.ts
index 49a2a16..8c8400b 100644
--- a/app/src/reportWebVitals.ts
+++ b/app/src/reportWebVitals.ts
@@ -1,15 +1,15 @@
-import { ReportHandler } from 'web-vitals';
+import { ReportHandler } from "web-vitals"
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
- import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
- getCLS(onPerfEntry);
- getFID(onPerfEntry);
- getFCP(onPerfEntry);
- getLCP(onPerfEntry);
- getTTFB(onPerfEntry);
- });
+ import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
+ getCLS(onPerfEntry)
+ getFID(onPerfEntry)
+ getFCP(onPerfEntry)
+ getLCP(onPerfEntry)
+ getTTFB(onPerfEntry)
+ })
}
-};
+}
-export default reportWebVitals;
+export default reportWebVitals
diff --git a/app/src/setupTests.ts b/app/src/setupTests.ts
index 8f2609b..6a0fd12 100644
--- a/app/src/setupTests.ts
+++ b/app/src/setupTests.ts
@@ -2,4 +2,4 @@
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
-import '@testing-library/jest-dom';
+import "@testing-library/jest-dom"
diff --git a/rln-client-lib/jest.config.js b/rln-client-lib/jest.config.js
index 5dd918d..d5e8ccb 100644
--- a/rln-client-lib/jest.config.js
+++ b/rln-client-lib/jest.config.js
@@ -19,7 +19,10 @@ module.exports = {
'!src/communication/websocket.ts',
'!src/hasher.ts'
],
- setupFilesAfterEnv: ['./tests/jest.setup.ts'],
+ setupFilesAfterEnv: [
+ './tests/jest.setup.ts',
+ 'fake-indexeddb/auto'
+ ],
coverageThreshold: {
'global': {
"branches": 80,
diff --git a/rln-client-lib/package.json b/rln-client-lib/package.json
index 7269de8..8e48a58 100644
--- a/rln-client-lib/package.json
+++ b/rln-client-lib/package.json
@@ -10,6 +10,7 @@
"@zk-kit/protocols": "^1.8.2",
"axios": "^0.24.0",
"crypto-js": "^4.1.1",
+ "dexie": "^3.2.1",
"dotenv": "^10.0.0",
"uuid": "^8.3.2",
"ws": "^8.3.0"
@@ -23,6 +24,7 @@
"@types/node": "^16.11.12",
"@types/uuid": "^8.3.3",
"babel-jest": "26.6.0",
+ "fake-indexeddb": "^3.1.7",
"jest": "27.0.3",
"jest-localstorage-mock": "^2.4.18",
"mockdate": "^3.0.5",
diff --git a/rln-client-lib/src/chat/db.ts b/rln-client-lib/src/chat/db.ts
new file mode 100644
index 0000000..09c3d48
--- /dev/null
+++ b/rln-client-lib/src/chat/db.ts
@@ -0,0 +1,113 @@
+import Dexie from 'dexie';
+import { IChatHistoryDB, IMessage } from "./interfaces";
+
+/**
+ * Uses IndexedDB as a persistent database to store all the user's messages in the browser, while using {@link Dexie} as
+ * an abstraction layer for interacting with IndexedDB.
+ */
+export class LocalChatDB implements IChatHistoryDB {
+
+ public static MAX_NUMBER_OF_MESSAGES_TO_FETCH_FROM_DB = 100;
+
+ public static DB_NAME = "ZKChatHistory";
+ private db: ZKChatDB;
+
+ constructor() {
+ this.db = new ZKChatDB();
+ }
+
+ /**
+ * Save message to IndexedDB.
+ */
+ public async saveMessage(roomId: string, message: IMessage) {
+ await this.db.messages.put({
+ uuid: message.uuid,
+ roomId: roomId,
+ epoch: message.epoch,
+ chat_type: message.chat_type,
+ message_content: message.message_content
+ })
+ }
+
+ /**
+ * Returns messages for a given room with a maximum limit of {@link LocalChatDB.MAX_NUMBER_OF_MESSAGES_TO_FETCH_FROM_DB},
+ * ordered in reverse starting from the message with the nearest epoch to the provided {@link fromTimestamp}.
+ */
+ public async getMessagesForRoom(roomId: string, fromTimestamp: number): Promise {
+ const messages: IZkMessage[] = await this.db.messages
+ .orderBy('epoch')
+ .reverse()
+ .and(message => {
+ return message.roomId == roomId && new Date(message.epoch) < new Date(fromTimestamp)
+ })
+ .limit(LocalChatDB.MAX_NUMBER_OF_MESSAGES_TO_FETCH_FROM_DB)
+ .toArray();
+
+ return messages.map(message => {
+ return {
+ uuid: message.uuid,
+ epoch: message.epoch,
+ chat_type: message.chat_type,
+ message_content: message.message_content
+ }
+ });
+
+ }
+
+ /**
+ * Returns messages for the given rooms with a maximum limit of {@link LocalChatDB.MAX_NUMBER_OF_MESSAGES_TO_FETCH_FROM_DB},
+ * ordered in reverse starting from the message with the nearest epoch to the provided {@link fromTimestamp}.
+ */
+ public async getMessagesForRooms(roomIds: string[], fromTimestamp: number): Promise<{ [key: string]: IMessage[] }> {
+ const messagesForRooms: { [key: string]: IMessage[] } = {};
+
+ for (let rId of roomIds) {
+ const roomMessages: IMessage[] = await this.getMessagesForRoom(rId, fromTimestamp);
+ messagesForRooms[rId] = roomMessages;
+ }
+
+ return messagesForRooms;
+ }
+
+ /**
+ * From all the stored messages in IndexedDB, this method returns the maximum timestamp. In case no messages are found in the local database, -1 is returned.
+ */
+ public async getMaxTimestampForAllMessages(): Promise {
+ const mostRecentMessage = await this.db.messages.orderBy('epoch').reverse().first();
+
+ if (mostRecentMessage != undefined) {
+ return mostRecentMessage.epoch;
+ }
+
+ return -1;
+ }
+
+ /**
+ * Deletes all messages for a given room.
+ */
+ public async deleteAllMessagesForRoom(roomId: string) {
+ await this.db.messages.where('roomId').equals(roomId).delete();
+ }
+}
+
+export class ZKChatDB extends Dexie {
+
+ messages!: Dexie.Table;
+
+ constructor() {
+ super(LocalChatDB.DB_NAME);
+
+ this.version(1).stores({
+ messages: '++id, roomId, epoch'
+ });
+ }
+}
+
+interface IZkMessage {
+ id?: string;
+ uuid: string;
+ roomId: string;
+ epoch: number;
+ chat_type: string;
+ message_content: string;
+}
\ No newline at end of file
diff --git a/rln-client-lib/src/chat/index.ts b/rln-client-lib/src/chat/index.ts
index ad9868f..5cd1077 100644
--- a/rln-client-lib/src/chat/index.ts
+++ b/rln-client-lib/src/chat/index.ts
@@ -1,8 +1,9 @@
+import { RLNFullProof } from "@zk-kit/protocols";
+import { IChatHistoryDB, IMessage, ITimeRangeMessages } from './interfaces';
import { ICryptography } from '../crypto/interfaces';
import { ServerCommunication } from '../communication/index';
import ProfileManager from "../profile";
import Hasher from "../hasher";
-import { RLNFullProof } from "@zk-kit/protocols";
/**
* The core component that is responsible for creating valid ZK proofs for a message, encrypting and dispatching it, as well as receiving and decrypting messages
@@ -16,11 +17,14 @@ class ChatManager {
private profile_manager: ProfileManager;
private communication_manager: ServerCommunication;
private cryptography: ICryptography;
+ private message_db: IChatHistoryDB;
private hasher: Hasher;
private message_callback;
+ private chatHistoryIsSyncing: boolean = false;
+
/**
* Same RLN identifier as the one in server.
*/
@@ -28,10 +32,11 @@ class ChatManager {
public static NUM_SHARES: number = 2;
- constructor(profile_manager: ProfileManager, communication_manager: ServerCommunication, cryptography: ICryptography) {
+ constructor(profile_manager: ProfileManager, communication_manager: ServerCommunication, cryptography: ICryptography, message_db: IChatHistoryDB) {
this.profile_manager = profile_manager;
this.communication_manager = communication_manager;
this.cryptography = cryptography;
+ this.message_db = message_db;
this.hasher = new Hasher();
}
@@ -89,7 +94,7 @@ class ChatManager {
this.communication_manager.sendMessage(JSON.stringify(message));
}
- public async registerReceiveMessageHandler(receive_msg_callback: (message: any, chat_room_id: string) => void) {
+ public async registerReceiveMessageHandler(receive_msg_callback: (message: IMessage, chat_room_id: string) => void) {
this.message_callback = receive_msg_callback;
this.communication_manager.receiveMessage(this.messageHandlerForRooms.bind(this))
}
@@ -98,11 +103,19 @@ class ChatManager {
const [decryptedMessage, room_id] = await this.decryptMessage(JSON.parse(message));
if (decryptedMessage != null && room_id != null) {
- this.message_callback(decryptedMessage, room_id);
+ // Save the message to the local DB.
+ this.message_db.saveMessage(room_id, decryptedMessage);
+
+ // When the chat history sync in underway, the callback function
+ // (which would indicate additional interaction with the client) is not called.
+ if (!this.chatHistoryIsSyncing) {
+ // Return the message to the calling function, usually the UI app, only if history sync is not in progress.
+ this.message_callback(decryptedMessage, room_id);
+ }
}
}
- public async decryptMessage(message: any): Promise {
+ public async decryptMessage(message: IMessage): Promise<[IMessage | null, string | null]> {
const room_type = message.chat_type;
const user_rooms_for_type: any[] = await this.profile_manager.getUserRoomsForChatType(room_type);
@@ -129,6 +142,84 @@ class ChatManager {
return [null, null];
}
+ /**
+ * Loads all messages from the server, from the max timestamp of the messages stored locally until the
+ * provided toTimestamp, and only stores locally the messages that can be decrypted.
+ *
+ * A time-range pagination is implemented, where the server returns a number of messages, no more than a certain limit in a single call.
+ * The pagination loop ends when the number of returned messages is less than the limit.
+ */
+ public async syncMessagesForAllRooms(toTimestamp: number): Promise {
+ // The messages will be loaded starting from MAX_TIMESTAMP of the stored messages + 1ms.
+ let fromTimestamp: number = await this.message_db.getMaxTimestampForAllMessages() + 1;
+
+ if (fromTimestamp == -1) {
+ fromTimestamp = toTimestamp - 24 * 60 * 60 * 100;
+ // If there are no messages stored locally, load only message history for the given day.
+ }
+
+ console.info("Syncing chat history");
+ this.chatHistoryIsSyncing = true;
+ let messageData: ITimeRangeMessages | null = await this.getAndSaveMessagesForTimeRange(fromTimestamp, toTimestamp);
+
+ while (1) {
+ // The server can be unavailable and the returned payload can be null.
+ if (messageData == null) {
+ break;
+ }
+
+ if (messageData.messages.length == messageData.limit) {
+ fromTimestamp = parseInt(messageData.returnedToTimestamp) + 1;
+ messageData = await this.getAndSaveMessagesForTimeRange(fromTimestamp, toTimestamp);
+ } else {
+ break;
+ }
+ }
+
+ console.info("Chat history synced");
+ this.chatHistoryIsSyncing = false;
+ }
+
+ private async getAndSaveMessagesForTimeRange(fromTimestamp: number, toTimestamp: number): Promise {
+ let messageData: ITimeRangeMessages = await this.communication_manager.getTimeRangeChatHistory(fromTimestamp, toTimestamp);
+
+ if (messageData == null || messageData == undefined) {
+ return null;
+ }
+
+ for (let message of messageData.messages) {
+ const [decryptedMessage, room_id] = await this.decryptMessage(message);
+
+ if (decryptedMessage != null && room_id != null) {
+ // Save the decrypted message to the local DB.
+ this.message_db.saveMessage(room_id, decryptedMessage);
+ }
+ }
+
+ return messageData;
+ }
+
+ /**
+ * Removes all messages for a given room from the local database.
+ */
+ public async deleteMessageHistoryForRoom(roomId: string) {
+ await this.message_db.deleteAllMessagesForRoom(roomId);
+ }
+
+ /**
+ * Returns the messages with a timestamp lower than fromTimestamp (the returned results are limited) for a given room id.
+ */
+ public async loadMessagesForRoom(roomId: string, fromTimestamp: number): Promise {
+ return this.message_db.getMessagesForRoom(roomId, fromTimestamp);
+ }
+
+ /**
+ * Returns the messages with a timestamp lower than fromTimestamp (the returned results are limited) for the given room ids.
+ */
+ public async loadMessagesForRooms(roomIds: string[], fromTimestamp: number): Promise<{ [key: string]: IMessage[] }> {
+ return this.message_db.getMessagesForRooms(roomIds, fromTimestamp);
+ }
+
/**
* Refresh root hash and auth path when needed, only if the root is obsolete.
*/
diff --git a/rln-client-lib/src/chat/interfaces.ts b/rln-client-lib/src/chat/interfaces.ts
new file mode 100644
index 0000000..0b4d8ce
--- /dev/null
+++ b/rln-client-lib/src/chat/interfaces.ts
@@ -0,0 +1,55 @@
+/**
+ * Indicates an infite persistant storage used to store the decrypted messages locally.
+ */
+export interface IChatHistoryDB {
+
+ /**
+ * Save the decrypted message for the given room.
+ */
+ saveMessage(roomId: string, message: IMessage);
+
+ /**
+ * Return all messages for a room with timestamps lower than the provided {@link fromTimestamp} up to a certain limit.
+ * This method is used to ensure time-based pagination.
+ */
+ getMessagesForRoom(roomId: string, fromTimestamp: number): Promise;
+
+ /**
+ * Return all messages for the given rooms with timestamps lower than the provided {@link fromTimestamp} up to a certain limit.
+ * This method is used to ensure time-based pagination.
+ */
+ getMessagesForRooms(roomIds: string[], fromTimestamp: number): Promise<{ [key: string]: IMessage[] }>;
+
+ /**
+ * Returns the maximum timestamp from all the stored messages.
+ */
+ getMaxTimestampForAllMessages(): Promise;
+
+ /**
+ * Deletes all messages for a given room.
+ */
+ deleteAllMessagesForRoom(roomId: string);
+
+}
+
+/**
+ * Core interface of the messages that are broadcast by the server. This is the type of all messages received by clients.
+ */
+export interface IMessage {
+ uuid: string;
+ epoch: number;
+ chat_type: string;
+ message_content: string;
+}
+
+/**
+ * Core interface of the message returned by the server that contains time-based paginated messages.
+ */
+export interface ITimeRangeMessages {
+ requestedFromTimestamp: string;
+ requestedToTimestamp: string;
+ returnedFromTimestamp: string;
+ returnedToTimestamp: string;
+ messages: IMessage[];
+ limit: number;
+}
\ No newline at end of file
diff --git a/rln-client-lib/src/communication/api.ts b/rln-client-lib/src/communication/api.ts
index 7b5b8d8..e8cd127 100644
--- a/rln-client-lib/src/communication/api.ts
+++ b/rln-client-lib/src/communication/api.ts
@@ -81,6 +81,26 @@ class RLNServerApi {
}
};
+ public getTimeRangeChatHistory = async (fromTimestamp: number, toTimestamp: number): Promise => {
+ try {
+ const res = await axios({
+ method: 'POST',
+ url: this.server_url + "/api/v1/chat/time_range_chat_history",
+ data: {
+ from: fromTimestamp,
+ to: toTimestamp
+ },
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ }
+ });
+ return res.data;
+ } catch (e) {
+ return null;
+ }
+ };
+
public getRlnRoot = async (): Promise => {
try {
const res = await axios({
diff --git a/rln-client-lib/src/communication/index.ts b/rln-client-lib/src/communication/index.ts
index ba4329f..62a2846 100644
--- a/rln-client-lib/src/communication/index.ts
+++ b/rln-client-lib/src/communication/index.ts
@@ -47,6 +47,10 @@ export class ServerCommunication {
return await this.rln_server.getChatHistory();
}
+ public async getTimeRangeChatHistory(from: number, to: number) {
+ return await this.rln_server.getTimeRangeChatHistory(from, to);
+ }
+
public async getRlnRoot() {
return await this.rln_server.getRlnRoot();
}
diff --git a/rln-client-lib/src/index.ts b/rln-client-lib/src/index.ts
index 6e026b3..6959d84 100644
--- a/rln-client-lib/src/index.ts
+++ b/rln-client-lib/src/index.ts
@@ -15,6 +15,8 @@ import { IPrivateRoom, IDirectRoom } from './room/interfaces';
import WebsocketClient from './communication/websocket';
import WsSocketClient from './communication/ws-socket';
import KeyExchangeManager from './key-exchange';
+import { IChatHistoryDB, IMessage } from './chat/interfaces';
+import { LocalChatDB } from './chat/db';
let communication: ServerCommunication | null = null;
let generated_storage_provider: StorageProvider | null = null;
@@ -22,6 +24,7 @@ let generated_cryptography: ICryptography | null = null;
let profile_manager: ProfileManager | null = null;
let key_exchange_manager: KeyExchangeManager | null = null;
let chat_manager: ChatManager;
+let message_db: IChatHistoryDB;
let get_proof_callback: (nullifier: string, signal: string, storage_artifacts: any, rln_identitifer: any) => Promise ;
@@ -43,6 +46,10 @@ const init = async (
communication = new ServerCommunication(new RLNServerApi(server_config.serverUrl), socket_client);
await communication.init();
}
+
+ if (message_db == null) {
+ message_db = new LocalChatDB();
+ }
if (storage_provider) {
generated_storage_provider = storage_provider;
@@ -57,7 +64,7 @@ const init = async (
}
profile_manager = new ProfileManager(generated_storage_provider, generated_cryptography);
- chat_manager = new ChatManager(profile_manager, communication, generated_cryptography);
+ chat_manager = new ChatManager(profile_manager, communication, generated_cryptography, message_db);
const root: string = await communication.getRlnRoot();
const leaves: string[] = await communication.getLeaves();
@@ -98,7 +105,7 @@ const send_message = async (chat_room_id: string, raw_message: string) => {
await chat_manager.sendMessage(chat_room_id, raw_message, get_proof_callback);
}
-const receive_message = async(receive_msg_callback: (message: any, chat_room_id: string) => void) => {
+const receive_message = async(receive_msg_callback: (message: IMessage, chat_room_id: string) => void) => {
if (profile_manager == null || chat_manager == null)
throw "init() not called";
@@ -247,6 +254,9 @@ const create_direct_room = async (name: string, receiver_public_key: string) =>
return room;
}
+/**
+ * @deprecated use persistent storage for chat history.
+ */
const get_chat_history = async () => {
if (generated_cryptography == null || communication == null || profile_manager == null)
throw "init() not called";
@@ -268,6 +278,36 @@ const get_chat_history = async () => {
return groupBy(decrypted_messages, "room_id");
}
+const sync_message_history = async () => {
+ if (generated_cryptography == null || communication == null || profile_manager == null)
+ throw "init() not called";
+
+ const timestampNow: number = new Date().getTime();
+
+ await chat_manager.syncMessagesForAllRooms(timestampNow);
+}
+
+const delete_messages_for_room = async(room_id: string) => {
+ if (generated_cryptography == null || communication == null || profile_manager == null)
+ throw "init() not called";
+
+ await chat_manager.deleteMessageHistoryForRoom(room_id);
+}
+
+const get_messages_for_room = async(room_id: string, from_timestamp: number) => {
+ if (generated_cryptography == null || communication == null || profile_manager == null)
+ throw "init() not called";
+
+ return await chat_manager.loadMessagesForRoom(room_id, from_timestamp);
+}
+
+const get_messages_for_rooms = async (room_ids: string[], from_timestamp: number) => {
+ if (generated_cryptography == null || communication == null || profile_manager == null)
+ throw "init() not called";
+
+ return await chat_manager.loadMessagesForRooms(room_ids, from_timestamp);
+}
+
const get_public_key = async() => {
if (profile_manager == null)
throw "init() not called";
@@ -354,6 +394,10 @@ export {
update_direct_room_key,
create_direct_room,
get_chat_history,
+ sync_message_history,
+ delete_messages_for_room,
+ get_messages_for_room,
+ get_messages_for_rooms,
get_public_key,
export_profile,
recover_profile,
diff --git a/rln-client-lib/src/main-test.ts b/rln-client-lib/src/main-test.ts
index f7a112c..3b29df8 100644
--- a/rln-client-lib/src/main-test.ts
+++ b/rln-client-lib/src/main-test.ts
@@ -1,5 +1,6 @@
import { Strategy, ZkIdentity } from "@zk-kit/identity";
import ChatManager from "./chat";
+import { IChatHistoryDB, IMessage } from "./chat/interfaces";
import { ServerCommunication } from "./communication";
import RLNServerApi from "./communication/api";
import SocketClient from "./communication/ws-socket";
@@ -79,13 +80,38 @@ class LocalTestCryptography implements ICryptography {
}
}
+class LocalTestMessageDB implements IChatHistoryDB {
+
+ async saveMessage(roomId: string, message: IMessage) {
+
+ }
+
+ async getMessagesForRoom(roomId: string, fromTimestamp: number): Promise {
+ return [];
+ }
+
+ async getMessagesForRooms(roomIds: string[], fromTimestamp: number): Promise {
+ return {};
+ }
+
+ async getMaxTimestampForAllMessages(): Promise {
+ return 1;
+ }
+
+ async deleteAllMessagesForRoom(roomId: string) {
+
+ }
+
+}
+
const comm_manager = new ServerCommunication(new RLNServerApi("http://localhost:8080"), new SocketClient("ws://localhost:8081"));
const cryptography = new LocalTestCryptography();
const storageProvider = new TestStorageProvider();
const profile = new ProfileManager(storageProvider, cryptography);
+const message_db = new LocalTestMessageDB();
-const chat = new ChatManager(profile, comm_manager, cryptography);
+const chat = new ChatManager(profile, comm_manager, cryptography, message_db);
const main = async () => {
let zkIdentity: ZkIdentity = new ZkIdentity(Strategy.SERIALIZED ,`{
diff --git a/rln-client-lib/tests/chat/chat.test.ts b/rln-client-lib/tests/chat/chat.test.ts
index 304f20a..9882709 100644
--- a/rln-client-lib/tests/chat/chat.test.ts
+++ b/rln-client-lib/tests/chat/chat.test.ts
@@ -1,14 +1,16 @@
-import { StorageProvider } from '../../src/storage/interfaces';
-import { jest, test, expect, describe, beforeAll, beforeEach } from '@jest/globals'
-import { ICryptography, IKeyPair } from '../../src/crypto/interfaces';
-import ProfileManager from '../../src/profile';
-import { ServerCommunication } from '../../src/communication';
+import "../../src/hasher";
import RLNServerApi from '../../src/communication/api';
import WsSocketClient from '../../src/communication/ws-socket';
import ChatManager from '../../src/chat/index';
import MockDate from 'mockdate';
-
-import "../../src/hasher";
+import ProfileManager from '../../src/profile';
+import { IMessage } from './../../../server/dist/src/persistence/model/message/message.types.d';
+import { StorageProvider } from '../../src/storage/interfaces';
+import { jest, test, expect, describe, beforeAll, beforeEach } from '@jest/globals'
+import { ICryptography, IKeyPair } from '../../src/crypto/interfaces';
+import { ServerCommunication } from '../../src/communication';
+import { IChatHistoryDB } from '../../src/chat/interfaces';
+import { deepClone } from '../../src/util';
const ws = require("ws");
@@ -128,6 +130,35 @@ export class LocalTestCryptography implements ICryptography {
}
+class LocalTestMessageDB implements IChatHistoryDB {
+
+ private messages = {};
+
+ async saveMessage(roomId: string, message: IMessage) {
+ if (this.messages[roomId] == undefined) {
+ this.messages[roomId] = [deepClone(message)];
+ } else {
+ this.messages[roomId].push(deepClone(message));
+ }
+ }
+
+ async getMessagesForRoom(roomId: string, fromTimestamp: number): Promise {
+ return this.messages[roomId];
+ }
+
+ async getMessagesForRooms(roomIdS: string[], fromTimestamp: number): Promise {
+ return {}
+ }
+
+ async getMaxTimestampForAllMessages(): Promise {
+ return 0;
+ }
+
+ async deleteAllMessagesForRoom(roomId: string) {
+ this.messages[roomId] = [];
+ }
+
+}
describe('Chat test', () => {
@@ -141,6 +172,7 @@ describe('Chat test', () => {
let profileManager: ProfileManager;
let chatManager: ChatManager;
+ let chatDB: IChatHistoryDB;
const proof_generator_callback = async (nullifier: string, signal: string, storage_artifacts: any, rln_identitifer: any): Promise => {
return JSON.stringify({
@@ -172,8 +204,9 @@ describe('Chat test', () => {
server = new RLNServerApi("");
socketClient = new WsSocketClient("");
communication = new ServerCommunication(server, socketClient);
+ chatDB = new LocalTestMessageDB();
- chatManager = new ChatManager(profileManager, communication, crypto);
+ chatManager = new ChatManager(profileManager, communication, crypto, chatDB);
MockDate.reset();
});
@@ -247,7 +280,12 @@ describe('Chat test', () => {
test('decrypt message - no rooms', async () => {
jest.spyOn(profileManager, "getUserRoomsForChatType").mockResolvedValue([]);
- const decrypted = await chatManager.decryptMessage("test message");
+ const decrypted = await chatManager.decryptMessage({
+ chat_type: "DIRECT",
+ uuid: "1",
+ epoch: 12345,
+ message_content: "test content"
+ });
expect(decrypted).toEqual([null, null]);
});
@@ -346,4 +384,147 @@ describe('Chat test', () => {
expect(decrypted).toEqual([null, null]);
});
+ test('sync messages for all rooms', async () => {
+ jest.spyOn(profileManager, "getUserRoomsForChatType").mockResolvedValue([
+ {
+ "id": "test-1",
+ "type": "PUBLIC",
+ "symmetric_key": "test_symmetric_key"
+ }
+ ]);
+
+ jest.spyOn(communication, "getTimeRangeChatHistory").mockImplementation(async (from: number, to: number) => {
+
+ if (from == 1) {
+ return {
+ requestedFromTimestamp: 0,
+ requestedToTimestamp: 10000,
+ returnedFromTimestamp: 0,
+ returnedToTimestamp: 999,
+ messages: [
+ {
+ uuid: "1",
+ epoch: 100,
+ chat_type: "PUBLIC",
+ message_content: "content 1"
+ },
+ {
+ uuid: "2",
+ epoch: 400,
+ chat_type: "PUBLIC",
+ message_content: "content 2"
+ },
+ {
+ uuid: "3",
+ epoch: 500,
+ chat_type: "PUBLIC",
+ message_content: "content 3"
+ },
+ {
+ uuid: "4",
+ epoch: 800,
+ chat_type: "PUBLIC",
+ message_content: "content 4"
+ },
+ {
+ uuid: "5",
+ epoch: 999,
+ chat_type: "PUBLIC",
+ message_content: "content 5"
+ }
+ ],
+ limit: 5
+ };
+ } else if (from == 1000) {
+ return {
+ requestedFromTimestamp: 1000,
+ requestedToTimestamp: 10000,
+ returnedFromTimestamp: 1000,
+ returnedToTimestamp: 2000,
+ messages: [
+ {
+ uuid: "6",
+ epoch: 1800,
+ chat_type: "PUBLIC",
+ message_content: "content 6"
+ },
+ {
+ uuid: "7",
+ epoch: 2000,
+ chat_type: "PUBLIC",
+ message_content: "content 7"
+ }
+ ],
+ limit: 5
+ };
+ }
+
+ });
+
+ jest.spyOn(crypto, "decryptMessageSymmetric").mockImplementation(async (cyphertext: string, symm_key: string) => {
+ return cyphertext;
+ });
+
+ jest.spyOn(chatDB, "getMaxTimestampForAllMessages").mockResolvedValue(0);
+
+ const toTimestamp = 10000;
+ await chatManager.syncMessagesForAllRooms(toTimestamp)
+
+ const allMessages: IMessage[] = await chatDB.getMessagesForRoom("test-1", 1);
+ expect(allMessages.length).toEqual(7);
+
+ });
+
+ test('delete messages for a given room', async () => {
+ await chatDB.saveMessage('room-1', {
+ uuid: "1",
+ epoch: 100,
+ chat_type: "PUBLIC",
+ message_content: "content 1"
+ });
+ await chatDB.saveMessage('room-1', {
+ uuid: "2",
+ epoch: 102,
+ chat_type: "PUBLIC",
+ message_content: "content 2"
+ });
+ await chatDB.saveMessage('room-2', {
+ uuid: "3",
+ epoch: 105,
+ chat_type: "PUBLIC",
+ message_content: "content 3"
+ });
+
+ const messagesForRoomBeforeDelete: IMessage[] = await chatManager.loadMessagesForRoom('room-2', 1);
+ expect(messagesForRoomBeforeDelete.length).toEqual(1);
+
+ await chatManager.deleteMessageHistoryForRoom('room-2');
+ const messagesForRoomAfterDelete: IMessage[] = await chatManager.loadMessagesForRoom('room-2', 1);
+ expect(messagesForRoomAfterDelete.length).toEqual(0);
+ });
+
+ test('load messages for a given room', async () => {
+ await chatDB.saveMessage('room-1', {
+ uuid: "1",
+ epoch: 100,
+ chat_type: "PUBLIC",
+ message_content: "content 1"
+ });
+ await chatDB.saveMessage('room-1', {
+ uuid: "2",
+ epoch: 102,
+ chat_type: "PUBLIC",
+ message_content: "content 2"
+ });
+ await chatDB.saveMessage('room-2', {
+ uuid: "3",
+ epoch: 105,
+ chat_type: "PUBLIC",
+ message_content: "content 3"
+ });
+
+ const messages: IMessage[] = await chatManager.loadMessagesForRoom('room-1', 1);
+ expect(messages.length).toEqual(2);
+ })
+
});
\ No newline at end of file
diff --git a/rln-client-lib/tests/chat/db.test.ts b/rln-client-lib/tests/chat/db.test.ts
new file mode 100644
index 0000000..969cd67
--- /dev/null
+++ b/rln-client-lib/tests/chat/db.test.ts
@@ -0,0 +1,140 @@
+import { LocalChatDB, ZKChatDB } from './../../src/chat/db';
+import { jest, test, expect, describe, beforeAll, beforeEach } from '@jest/globals'
+import { IMessage } from '../../src/chat/interfaces';
+
+describe('Message db test', () => {
+
+ let chatDB: LocalChatDB;
+ let testChatDB: ZKChatDB;
+
+ beforeEach(async () => {
+ testChatDB = new ZKChatDB();
+ chatDB = new LocalChatDB();
+
+ await testChatDB.messages.toCollection().delete();
+ })
+
+ test('save message', async () => {
+ await chatDB.saveMessage('testRoom', {
+ uuid: 'id-1',
+ epoch: 123,
+ chat_type: "PUBLIC",
+ message_content: "test message"
+ })
+
+ const messageCount = await testChatDB.messages.where('roomId').equals('testRoom').count();
+ expect(messageCount).toEqual(1);
+ });
+
+ test('get messages for room - room has no messages', async () => {
+ const endTimestampOfMessagesInDB = (2 * LocalChatDB.MAX_NUMBER_OF_MESSAGES_TO_FETCH_FROM_DB - 1) * 10 * 1000;
+ const messagesForRoom: IMessage[] = await chatDB.getMessagesForRoom('testRoom', endTimestampOfMessagesInDB);
+ expect(messagesForRoom.length).toEqual(0);
+ });
+
+ test('get messages for room - room has more messages than limit', async () => {
+ await saveMessagesForRoom('testRoom', 2 * LocalChatDB.MAX_NUMBER_OF_MESSAGES_TO_FETCH_FROM_DB);
+
+ // End timestamp would be (2 * LocalChatDB.MAX_NUMBER_OF_MESSAGES_TO_FETCH_FROM_DB - 1) * 10 * 1000;
+
+ const endTimestampOfMessagesInDB = (2 * LocalChatDB.MAX_NUMBER_OF_MESSAGES_TO_FETCH_FROM_DB - 1) * 10 * 1000;
+ const messagesForRoom: IMessage[] = await chatDB.getMessagesForRoom('testRoom', endTimestampOfMessagesInDB);
+ expect(messagesForRoom.length).toEqual(LocalChatDB.MAX_NUMBER_OF_MESSAGES_TO_FETCH_FROM_DB);
+
+ expect(messagesForRoom[0].epoch).toEqual(endTimestampOfMessagesInDB - 10 * 1000);
+ expect(messagesForRoom[99].epoch).toEqual(endTimestampOfMessagesInDB - 100 * 1000 * 10);
+ });
+
+ test('get messages for room - room has less messages than limit', async () => {
+ await saveMessagesForRoom('testRoom', 10);
+
+ // End timestamp would be (10 - 1) * 10 * 1000;
+
+ const endTimestampOfMessagesInDB = 10 * 10 * 1000;
+ const messagesForRoom: IMessage[] = await chatDB.getMessagesForRoom('testRoom', endTimestampOfMessagesInDB);
+
+ expect(messagesForRoom.length).toEqual(10);
+ expect(messagesForRoom[0].epoch).toEqual((messagesForRoom.length - 1) * 10 * 1000);
+ expect(messagesForRoom[9].epoch).toEqual(0);
+ });
+
+ test('get messages for rooms - room has less messages than limit', async () => {
+ saveMessagesForRoom('testRoom1', 10);
+ saveMessagesForRoom('testRoom2', 10);
+
+ // End timestamp would be (10 - 1) * 10 * 1000;
+
+ const endTimestampOfMessagesInDB = 10 * 10 * 1000;
+ const messagesForRoom: {[key: string]: IMessage[]} = await chatDB.getMessagesForRooms(['testRoom1', 'testRoom2'], endTimestampOfMessagesInDB);
+
+ expect(Object.keys(messagesForRoom).length).toEqual(2);
+ expect(Object.keys(messagesForRoom)).toContain('testRoom1');
+ expect(Object.keys(messagesForRoom)).toContain('testRoom2');
+
+
+ });
+
+ test('get max timestamp', async () => {
+ let timestamp = 0; // starting timestamp
+ for (let i = 0; i < 100; i++) {
+ await chatDB.saveMessage('testRoom', {
+ uuid: 'id-' + i,
+ epoch: timestamp,
+ chat_type: "PUBLIC",
+ message_content: "test message " + i
+ });
+
+ timestamp += 10 * 1000; // Increase timestamp by 10 seconds for every stored message
+ }
+
+ const maxTimestamp = await chatDB.getMaxTimestampForAllMessages();
+
+ expect(maxTimestamp).toEqual((100 - 1) * 10 * 1000);
+ });
+
+ test('delete messages - room has messages', async () => {
+ await chatDB.saveMessage('testRoom1', {
+ uuid: 'id-1',
+ epoch: 1,
+ chat_type: "PUBLIC",
+ message_content: "test message 1"
+ });
+
+ await chatDB.saveMessage('testRoom2', {
+ uuid: 'id-2',
+ epoch: 2,
+ chat_type: "PUBLIC",
+ message_content: "test message 2"
+ });
+
+ await chatDB.saveMessage('testRoom2', {
+ uuid: 'id-3',
+ epoch: 3,
+ chat_type: "PUBLIC",
+ message_content: "test message 3"
+ });
+
+ await chatDB.deleteAllMessagesForRoom('testRoom2');
+
+ const messagesForRoom1: IMessage[] = await chatDB.getMessagesForRoom('testRoom1', 5);
+ expect(messagesForRoom1.length).toEqual(1);
+
+ const messagesForRoom2: IMessage[] = await chatDB.getMessagesForRoom('testRoom2', 5);
+ expect(messagesForRoom2.length).toEqual(0);
+ });
+
+ const saveMessagesForRoom = async(roomId: string, messageCount: number) => {
+ let timestamp = 0; // starting timestamp
+ for (let i = 0; i < messageCount; i++) {
+ await chatDB.saveMessage(roomId, {
+ uuid: 'id-' + i,
+ epoch: timestamp,
+ chat_type: "PUBLIC",
+ message_content: "test message " + i
+ });
+
+ timestamp += 10 * 1000; // Increase timestamp by 10 seconds for every stored message
+ }
+ }
+
+});
\ No newline at end of file
diff --git a/rln-client-lib/tests/communication/api.test.ts b/rln-client-lib/tests/communication/api.test.ts
index f4eb338..be4f1e1 100644
--- a/rln-client-lib/tests/communication/api.test.ts
+++ b/rln-client-lib/tests/communication/api.test.ts
@@ -1,3 +1,4 @@
+import { ITimeRangeMessages } from './../../../server/src/services/chat.service';
import axios, { AxiosStatic } from 'axios'
import { jest, test, expect, describe, beforeAll } from '@jest/globals'
import RLNServerApi from '../../src/communication/api';
@@ -221,5 +222,53 @@ describe('Test api', () => {
const res = await rlnServerApi.deleteKeyExchangeBundles(proof, "test", "test", ["1", "2"]);
expect(res).toEqual(dataToReturn)
});
+ test('get time range message history', async() => {
+ const history: ITimeRangeMessages = {
+ requestedFromTimestamp: 0,
+ requestedToTimestamp: 10000,
+ returnedFromTimestamp: 0,
+ returnedToTimestamp: 999,
+ messages: [
+ {
+ uuid: "1",
+ epoch: 100,
+ chat_type: "PUBLIC",
+ message_content: "content 1"
+ },
+ {
+ uuid: "2",
+ epoch: 400,
+ chat_type: "PUBLIC",
+ message_content: "content 2"
+ },
+ {
+ uuid: "3",
+ epoch: 500,
+ chat_type: "PUBLIC",
+ message_content: "content 3"
+ },
+ {
+ uuid: "4",
+ epoch: 800,
+ chat_type: "PUBLIC",
+ message_content: "content 4"
+ },
+ {
+ uuid: "5",
+ epoch: 999,
+ chat_type: "PUBLIC",
+ message_content: "content 5"
+ }
+ ],
+ limit: 5
+ };
+
+ mockAxios.mockResolvedValue({
+ data: history
+ });
+
+ const historyFromServer = await rlnServerApi.getTimeRangeChatHistory(1, 2);
+ expect(history).toEqual(historyFromServer);
+ })
});
\ No newline at end of file
diff --git a/rln-client-lib/tests/communication/index.test.ts b/rln-client-lib/tests/communication/index.test.ts
index e5a27e9..d02d196 100644
--- a/rln-client-lib/tests/communication/index.test.ts
+++ b/rln-client-lib/tests/communication/index.test.ts
@@ -128,6 +128,14 @@ describe('Test server communication', () => {
expect(testSpy).toHaveBeenCalled();
});
+ test('get time range chat history', async () => {
+ const testSpy = jest.spyOn(server, "getTimeRangeChatHistory");
+ testSpy.mockResolvedValue([]);
+ await communication.getTimeRangeChatHistory(1, 2);
+
+ expect(testSpy).toHaveBeenCalledWith(1, 2);
+ })
+
test('get rln root', async () => {
const testSpy = jest.spyOn(server, "getRlnRoot");
testSpy.mockResolvedValue("test root");
@@ -151,6 +159,7 @@ describe('Test server communication', () => {
expect(testSpy).toHaveBeenCalled();
});
+
test('save key exchange bundle', async () => {
const testSpy = jest.spyOn(server, "saveKeyExchangeBundle");
diff --git a/rln-client-lib/tests/index.test.ts b/rln-client-lib/tests/index.test.ts
index 275bb0a..2a3cb9e 100644
--- a/rln-client-lib/tests/index.test.ts
+++ b/rln-client-lib/tests/index.test.ts
@@ -16,6 +16,9 @@ import {
join_private_room,
create_direct_room,
get_chat_history,
+ sync_message_history,
+ delete_messages_for_room,
+ get_messages_for_room,
get_public_key,
export_profile,
recover_profile,
@@ -159,7 +162,6 @@ class LocalTestCryptography implements ICryptography {
}
}
-
describe('Test main', () => {
const proof_generator_callback = async (nullifier: string, signal: string, storage_artifacts: any, rln_identitifer: any): Promise => {
@@ -210,7 +212,6 @@ describe('Test main', () => {
}
});
-
test('init - default params, profile exists', async () => {
jest.spyOn(ServerCommunication.prototype, "init").mockImplementation(() => {
return new Promise((res, rej) => { res() });
@@ -577,7 +578,7 @@ describe('Test main', () => {
test('get chat history', async () => {
// No profile
try {
- await get_public_key();
+ await get_chat_history();
expect(true).toBeFalsy();
} catch (e) {
expect(true).toBeTruthy();
@@ -608,26 +609,30 @@ describe('Test main', () => {
if (message.uuid == 'id-1') {
return [{
uuid: "id-1",
- message: "Decrypted 1",
+ chat_type: "PUBLIC",
+ message_content: "Decrypted 1",
epoch: 1
}, 'room-1'];
}
if (message.uuid == 'id-2') {
return [{
uuid: "id-2",
- message: "Decrypted 2",
+ chat_type: "PUBLIC",
+ message_content: "Decrypted 2",
epoch: 2
}, 'room-1'];
}
if (message.uuid == 'id-3') {
return [{
uuid: "id-3",
- message: "Decrypted 3",
+ chat_type: "PUBLIC",
+ message_content: "Decrypted 3",
epoch: 3
}, 'room-2'];
}
+
+ return [null, null];
})
-
const chatHistory = await get_chat_history();
expect(Object.keys(chatHistory)).toEqual(['room-1', 'room-2']);
@@ -635,6 +640,77 @@ describe('Test main', () => {
expect(chatHistory['room-2'].length).toEqual(1);
});
+ test('sync message history', async() => {
+ // No profile
+ try {
+ await sync_message_history();
+ expect(true).toBeFalsy();
+ } catch (e) {
+ expect(true).toBeTruthy();
+ }
+
+ // With profile
+ await init_new_profile();
+
+ const spy = jest.spyOn(ChatManager.prototype, "syncMessagesForAllRooms").mockResolvedValue();
+
+ await sync_message_history();
+
+ expect(spy).toHaveBeenCalled();
+ })
+
+ test('delete messages for room', async () => {
+ // No profile
+ try {
+ await delete_messages_for_room("id-1");
+ expect(true).toBeFalsy();
+ } catch (e) {
+ expect(true).toBeTruthy();
+ }
+
+ // With profile
+ await init_new_profile();
+
+ const spy = jest.spyOn(ChatManager.prototype, "deleteMessageHistoryForRoom").mockResolvedValue();
+
+ await delete_messages_for_room("id-1");
+
+ expect(spy).toHaveBeenCalled();
+ })
+
+ test('get messages for room', async () => {
+ // No profile
+ try {
+ await get_messages_for_room("id-1", 1);
+ expect(true).toBeFalsy();
+ } catch (e) {
+ expect(true).toBeTruthy();
+ }
+
+ // With profile
+ await init_new_profile();
+
+ const spy = jest.spyOn(ChatManager.prototype, "loadMessagesForRoom").mockResolvedValue(
+ [
+ {
+ uuid: "1",
+ epoch: 100,
+ chat_type: "PUBLIC",
+ message_content: "content 1"
+ },
+ {
+ uuid: "2",
+ epoch: 102,
+ chat_type: "PUBLIC",
+ message_content: "content 2"
+ }
+ ]);
+
+ await get_messages_for_room("id-1", 1);
+
+ expect(spy).toHaveBeenCalled();
+ })
+
test('get public key', async () => {
// No profile
try {
diff --git a/rln-client-lib/tests/jest.setup.ts b/rln-client-lib/tests/jest.setup.ts
index ae3ce79..6c87f1a 100644
--- a/rln-client-lib/tests/jest.setup.ts
+++ b/rln-client-lib/tests/jest.setup.ts
@@ -1,4 +1,5 @@
-import { Crypto } from "@peculiar/webcrypto"
+import { Crypto } from "@peculiar/webcrypto";
import 'jest-localstorage-mock';
global.crypto = new Crypto()
+
diff --git a/rln-client-lib/tests/key-exchange/key-exchange.test.ts b/rln-client-lib/tests/key-exchange/key-exchange.test.ts
index 2a36f75..949d399 100644
--- a/rln-client-lib/tests/key-exchange/key-exchange.test.ts
+++ b/rln-client-lib/tests/key-exchange/key-exchange.test.ts
@@ -9,6 +9,8 @@ import ProfileManager from '../../src/profile';
import "../../src/hasher";
import { ICryptography, IKeyPair } from '../../src/crypto/interfaces';
import { StorageProvider } from '../../src/storage/interfaces';
+import { IChatHistoryDB, IMessage } from '../../src/chat/interfaces';
+import { deepClone } from '../../src/util';
const ws = require("ws");
@@ -128,6 +130,31 @@ export class LocalTestCryptography implements ICryptography {
}
+class LocalTestMessageDB implements IChatHistoryDB {
+
+ private messages = {};
+
+ async saveMessage(roomId: string, message: IMessage) {
+
+ }
+
+ async getMessagesForRoom(roomId: string, fromTimestamp: number): Promise {
+ return this.messages[roomId];
+ }
+
+ async getMessagesForRooms(roomIdS: string[], fromTimestamp: number): Promise {
+ return {}
+ }
+
+ async getMaxTimestampForAllMessages(): Promise {
+ return 0;
+ }
+
+ async deleteAllMessagesForRoom(roomId: string) {
+ this.messages[roomId] = [];
+ }
+
+}
describe('Key exchange test', () => {
@@ -141,6 +168,7 @@ describe('Key exchange test', () => {
let profileManager: ProfileManager;
let storageProvider: TestStorageProvider;
let cryptography: LocalTestCryptography;
+ let chatDB: IChatHistoryDB;
let chatManager: ChatManager;
beforeEach(() => {
@@ -151,7 +179,8 @@ describe('Key exchange test', () => {
server = new RLNServerApi("");
socketClient = new WsSocketClient("");
communication = new ServerCommunication(server, socketClient);
- chatManager = new ChatManager(profileManager, communication, cryptography);
+ chatDB = new LocalTestMessageDB();
+ chatManager = new ChatManager(profileManager, communication, cryptography, chatDB);
keyExchangeManager = new KeyExchangeManager(communication, cryptography, chatManager, profileManager, proofGeneratorCallback);
})
diff --git a/server/src/controllers/chat.ts b/server/src/controllers/chat.ts
index e7fddb0..323e127 100644
--- a/server/src/controllers/chat.ts
+++ b/server/src/controllers/chat.ts
@@ -1,8 +1,12 @@
import express from "express";
+import { body, validationResult } from 'express-validator';
import { chatService } from "../services";
const router = express.Router();
+/**
+* @deprecated use the endpoint to get messages by time range
+*/
router.get("/chat_history", async (req, res) => {
try {
@@ -13,4 +17,26 @@ router.get("/chat_history", async (req, res) => {
}
});
+router.post("/time_range_chat_history",
+ body('from').notEmpty(),
+ body('to').notEmpty(),
+ async (req, res) => {
+
+ // Finds the validation errors in this request and wraps them in an object with handy functions
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(500).json({ errors: errors.array() });
+ }
+
+ try {
+ const fromTimestamp = new Date(req.body.from)
+ const toTimestamp = new Date(req.body.to)
+ const data = await chatService.getMessagesInTimeRange(fromTimestamp, toTimestamp)
+ res.status(200).json(data);
+ } catch (e) {
+ console.log(e);
+ res.status(500).json(e);
+ }
+});
+
export default router;
\ No newline at end of file
diff --git a/server/src/interrep/api.ts b/server/src/interrep/api.ts
index b44d1fb..00c2625 100644
--- a/server/src/interrep/api.ts
+++ b/server/src/interrep/api.ts
@@ -29,28 +29,51 @@ const getMembersForGroup = async (provider: string, name: string, limit: number
const res = await axios({
method: 'GET',
timeout: 5000,
- url: config.INTERREP_V2 + `/groups/${provider}/${name}?members=true&limit=${limit}&offset=${offset}`,
+ url: config.INTERREP_V2 + `/groups/${provider}/${name}/members?limit=${limit}&offset=${offset}`,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
}
});
- return res.data.data.members.map((el, index) => {
+ return res.data.data.map((el, index) => {
return {
index: offset + index,
identityCommitment: el
}
});
} catch (e) {
- console.log("Exception while loading group: ", provider, name);
+ console.log("Exception while loading members of the group: ", provider, name);
+ return [];
+ }
+}
+
+/**
+ * Returns an ordered list of the leaf indexes of removed members in the group.
+ */
+const getRemovedMembersForGroup = async (provider: string, name: string, limit: number = 100, offset: number = 0): Promise => {
+ try {
+ const res = await axios({
+ method: 'GET',
+ timeout: 5000,
+ url: config.INTERREP_V2 + `/groups/${provider}/${name}/removed-members?limit=${limit}&offset=${offset}`,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ }
+ });
+
+ return res.data.data;
+ } catch (e) {
+ console.log("Exception while loading removed members of the group: ", provider, name);
return [];
}
}
const apiFunctions = {
getAllGroups,
- getMembersForGroup
+ getMembersForGroup,
+ getRemovedMembersForGroup
}
export default apiFunctions
\ No newline at end of file
diff --git a/server/src/interrep/index.ts b/server/src/interrep/index.ts
index 3c2e59a..e493371 100644
--- a/server/src/interrep/index.ts
+++ b/server/src/interrep/index.ts
@@ -46,7 +46,7 @@ class InterRepSynchronizer {
let tree_root_changed = false;
// 2. For each group, check the status in database. Only load new members for group if the size in db is different than the new size of the group
- for (let g of allGroupsOnNet) {
+ for (let g of allGroupsOnNet) {
const g_id = g.provider + "_" + g.name;
const groupInDb: IGroup | undefined = groupsInDb.find(x => x.group_id == g_id && x.name == g.name && x.provider == g.provider);
@@ -57,26 +57,43 @@ class InterRepSynchronizer {
// Add all members to the tree
await this.userService.appendUsers(groupMembers, g_id);
// Persist the group
- await this.groupService.saveGroup(g_id, g.provider, g.name, g.numberOfLeaves);
+ await this.groupService.saveGroup(g_id, g.provider, g.name, g.size, g.numberOfLeaves);
tree_root_changed = true;
} catch (e) {
console.log("Unknown error while saving group", e);
}
} else {
- // Group exists, load new members only if sizes differ (new members got added in InterRep)
- if (g.numberOfLeaves > groupInDb.size) {
+ // Group exists locally, load new members only if the number of leaves in interep is > number of leaves stored locally
+ if (g.numberOfLeaves > groupInDb.number_of_leaves) {
// Load members from groupInDb.size up to g.numberOfLeaves
- const groupMembers: IGroupMember[] = await this.loadGroupMembersWithPagination(g.provider, g.name, groupInDb.size, g.numberOfLeaves);
+ const groupMembers: IGroupMember[] = await this.loadGroupMembersWithPagination(g.provider, g.name, groupInDb.number_of_leaves, g.numberOfLeaves);
try {
// Add group members to the tree
await this.userService.appendUsers(groupMembers, g_id);
- // Update group in DB
- await this.groupService.updateSize(g_id, g.numberOfLeaves);
+ // Update group leaf count in DB
+ await this.groupService.updateNumberOfLeaves(g_id, g.numberOfLeaves);
tree_root_changed = true;
} catch (e) {
- console.log("Unknown error while saving group", e);
+ console.log("Unknown error while saving group - appending new members", e);
+ }
+ }
+
+ // Group exists locally, delete members that were removed from interep.
+ if (g.size != groupInDb.size) {
+ // Load all deleted indexes from interep
+ const indexesOfRemovedMembers: number[] = await this.loadRemovedGroupMembersWithPagination(g.provider, g.name, 0, g.size);
+ try {
+ // Remove members from the tree
+ await this.userService.removeUsersByIndexes(indexesOfRemovedMembers, groupInDb.group_id);
+
+ // Update group size in DB
+ await this.groupService.updateSize(g_id, g.size);
+
+ tree_root_changed = true;
+ } catch (e) {
+ console.log("Unknown error while saving group - removing deleted members", e);
}
}
}
@@ -106,6 +123,24 @@ class InterRepSynchronizer {
return loadedMembers;
}
+ private async loadRemovedGroupMembersWithPagination(provider: string, name: string, offset: number, to: number): Promise {
+ let indexesOfDeletedMembers: number[] = [];
+
+ const limit: number = 100;
+
+ let indexes: number[] = await interRepFunctions.getRemovedMembersForGroup(provider, name, limit, offset);
+
+ indexesOfDeletedMembers = indexesOfDeletedMembers.concat(indexes);
+
+ while (indexes.length + limit <= to) {
+ offset += limit;
+ indexes = await interRepFunctions.getRemovedMembersForGroup(provider, name, limit, offset);
+ indexesOfDeletedMembers = indexesOfDeletedMembers.concat(indexes);
+ }
+
+ return indexesOfDeletedMembers;
+ }
+
private async continuousSync() {
setInterval(async() => {
console.log("Syncing with InterRep!");
diff --git a/server/src/persistence/model/group/group.schema.ts b/server/src/persistence/model/group/group.schema.ts
index c9cb6f4..acb5544 100644
--- a/server/src/persistence/model/group/group.schema.ts
+++ b/server/src/persistence/model/group/group.schema.ts
@@ -1,16 +1,18 @@
import { Schema } from "mongoose";
-import { getAllGroups, updateGroupSize } from "./group.statics";
+import { getAllGroups, updateGroupSize, updateGroupLeafCount } from "./group.statics";
import { IGroup, IGroupDocument, IGroupModel } from "./group.types";
const GroupSchemaField: Record = {
group_id: { type: String, required: true, unique: true },
provider: { type: String, required: true, unique: false },
name: { type: String, required: true, unique: false },
- size: { type: Number, required: true, unique: false }
+ size: { type: Number, required: true, unique: false },
+ number_of_leaves: { type: Number, required: true, unique: false }
};
const GroupSchema = new Schema(GroupSchemaField);
GroupSchema.statics.getAllGroups = getAllGroups;
GroupSchema.statics.updateGroupSize = updateGroupSize;
+GroupSchema.statics.updateGroupLeafCount = updateGroupLeafCount;
export default GroupSchema;
diff --git a/server/src/persistence/model/group/group.statics.ts b/server/src/persistence/model/group/group.statics.ts
index 4218719..80cd0fe 100644
--- a/server/src/persistence/model/group/group.statics.ts
+++ b/server/src/persistence/model/group/group.statics.ts
@@ -1,11 +1,14 @@
import Group from "./group.model";
import { IGroup } from "./group.types";
-
export async function getAllGroups(this: typeof Group,): Promise {
return this.find({});
}
export async function updateGroupSize(this: typeof Group, id: string, new_size: number): Promise {
return this.findOneAndUpdate({group_id: id}, {size: new_size});
+}
+
+export async function updateGroupLeafCount(this: typeof Group, id: string, new_leaf_count: number): Promise {
+ return this.findOneAndUpdate({ group_id: id }, { number_of_leaves: new_leaf_count });
}
\ No newline at end of file
diff --git a/server/src/persistence/model/group/group.types.ts b/server/src/persistence/model/group/group.types.ts
index 6f000c1..c4db98c 100644
--- a/server/src/persistence/model/group/group.types.ts
+++ b/server/src/persistence/model/group/group.types.ts
@@ -1,11 +1,12 @@
import { Model, Document } from "mongoose";
-import { getAllGroups, updateGroupSize } from "./group.statics";
+import { getAllGroups, updateGroupSize, updateGroupLeafCount } from "./group.statics";
export interface IGroup {
group_id: string;
provider: string;
name: string;
size: number;
+ number_of_leaves: number;
}
export interface IGroupDocument extends IGroup, Document { }
@@ -13,4 +14,5 @@ export interface IGroupDocument extends IGroup, Document { }
export interface IGroupModel extends Model {
getAllGroups: typeof getAllGroups;
updateGroupSize: typeof updateGroupSize;
+ updateGroupLeafCount: typeof updateGroupLeafCount;
}
diff --git a/server/src/persistence/model/message/message.schema.ts b/server/src/persistence/model/message/message.schema.ts
index fbb009b..422aade 100644
--- a/server/src/persistence/model/message/message.schema.ts
+++ b/server/src/persistence/model/message/message.schema.ts
@@ -1,6 +1,6 @@
import { IMessage, IMessageDocument, IMessageModel } from './message.types';
import { Schema } from "mongoose";
-import { getDailyMessages } from './message.statics';
+import { getDailyMessages, getMessagesInTimeRange } from './message.statics';
const MessageSchemaFields: Record = {
uuid: { type: String, required: true, unique: true },
@@ -14,5 +14,6 @@ const MessageSchema = new Schema(
);
MessageSchema.statics.getDailyMessages = getDailyMessages;
+MessageSchema.statics.getMessagesInTimeRange = getMessagesInTimeRange;
export default MessageSchema;
\ No newline at end of file
diff --git a/server/src/persistence/model/message/message.statics.ts b/server/src/persistence/model/message/message.statics.ts
index 2c4bb8a..aaf64d9 100644
--- a/server/src/persistence/model/message/message.statics.ts
+++ b/server/src/persistence/model/message/message.statics.ts
@@ -1,9 +1,28 @@
+import { ITimeRangeMessages } from "../../../services/chat.service";
import Message from "./message.model";
import { IMessage } from "./message.types";
/**
* @returns all the messages that were stored for the day when the function gets called
+ * @deprecated use the endpoint to get messages by time range
*/
export async function getDailyMessages(this: typeof Message,): Promise {
return this.find({ epoch: { $gte: new Date(new Date().setHours(0, 0, 0, 0)).getTime() }});
}
+
+export async function getMessagesInTimeRange(this: typeof Message, from: Date, to: Date, limit: number): Promise {
+
+ const foundMessagesOrdered: IMessage[] = await this.find({ epoch: { $gte: from.getTime(), $lt: to.getTime() } }, null, {limit: limit, sort: {epoch: 1}}).exec();
+
+ const returnedFromTimestamp: number = foundMessagesOrdered.length > 0 ? foundMessagesOrdered[0].epoch : from.getTime();
+ const returnedToTimestamp: number = foundMessagesOrdered.length > 0 ? foundMessagesOrdered[foundMessagesOrdered.length - 1].epoch : to.getTime();
+
+ return {
+ requestedFromTimestamp: from.getTime(),
+ requestedToTimestamp: to.getTime(),
+ returnedFromTimestamp: returnedFromTimestamp,
+ returnedToTimestamp: returnedToTimestamp,
+ messages: foundMessagesOrdered,
+ limit: limit
+ }
+}
\ No newline at end of file
diff --git a/server/src/persistence/model/message/message.types.ts b/server/src/persistence/model/message/message.types.ts
index 33be7f7..b1d8dd5 100644
--- a/server/src/persistence/model/message/message.types.ts
+++ b/server/src/persistence/model/message/message.types.ts
@@ -1,5 +1,5 @@
import { Model, Document } from "mongoose";
-import { getDailyMessages } from "./message.statics";
+import { getDailyMessages, getMessagesInTimeRange } from "./message.statics";
export interface IMessage {
uuid: string;
@@ -11,4 +11,5 @@ export interface IMessage {
export interface IMessageDocument extends IMessage, Document {}
export interface IMessageModel extends Model {
getDailyMessages: typeof getDailyMessages;
+ getMessagesInTimeRange: typeof getMessagesInTimeRange;
}
\ No newline at end of file
diff --git a/server/src/services/chat.service.ts b/server/src/services/chat.service.ts
index c8775c3..e9bbd5f 100644
--- a/server/src/services/chat.service.ts
+++ b/server/src/services/chat.service.ts
@@ -6,6 +6,14 @@ import { IMessage } from './../persistence/model/message/message.types';
*/
class ChatService {
+ /**
+ * The number of messages to return in one request.
+ */
+ public static MESSAGE_COUNT_LIMIT = 1000;
+
+ /**
+ * @deprecated use the endpoint to get messages by time range
+ */
public async getDailyMessages(): Promise {
return (await Message.getDailyMessages())
.map(message => {
@@ -18,6 +26,27 @@ class ChatService {
});
}
+ public async getMessagesInTimeRange(from: Date, to: Date): Promise {
+ if (from >= to) {
+ throw Error("Please select valid date range");
+ }
+
+ return await Message.getMessagesInTimeRange(from, to, ChatService.MESSAGE_COUNT_LIMIT);
+ }
+
+}
+
+/**
+ * When messages for a specified time range are requested, time-based pagination is required. The metadata for the time-based pagination
+ * is included in this message, which is returned by the server, along with a subset of all the messages within that range.
+ */
+export interface ITimeRangeMessages {
+ requestedFromTimestamp: number;
+ requestedToTimestamp: number;
+ returnedFromTimestamp: number;
+ returnedToTimestamp: number;
+ messages: IMessage[];
+ limit: number;
}
export default ChatService
\ No newline at end of file
diff --git a/server/src/services/group.service.ts b/server/src/services/group.service.ts
index db34207..ecccc08 100644
--- a/server/src/services/group.service.ts
+++ b/server/src/services/group.service.ts
@@ -10,12 +10,13 @@ class GroupService {
return await Group.getAllGroups();
}
- public async saveGroup(group_id: string, provider: string, name: string, size: number): Promise {
+ public async saveGroup(group_id: string, provider: string, name: string, size: number, number_of_leaves: number): Promise {
const group = await Group.create({
group_id: group_id,
provider: provider,
name: name,
- size: size
+ size: size,
+ number_of_leaves: number_of_leaves
})
return await group.save();
}
@@ -24,6 +25,10 @@ class GroupService {
return await Group.updateGroupSize(group_id, new_size);
}
+ public async updateNumberOfLeaves(group_id: string, new_number_of_leaves: number): Promise {
+ return await Group.updateGroupLeafCount(group_id, new_number_of_leaves);
+ }
+
public async containsGroup(group_id: string): Promise {
const group = await Group.findOne({group_id: group_id})
return group != null ? true : false;
diff --git a/server/src/services/message_handler_service.ts b/server/src/services/message_handler_service.ts
index 855d456..4211b69 100644
--- a/server/src/services/message_handler_service.ts
+++ b/server/src/services/message_handler_service.ts
@@ -70,7 +70,7 @@ class MessageHandlerService {
const user = getUserFromShares(validMessage.zk_proof, validMessage.x_share, this.hasher, requestStats);
// Ban User
- await this.userService.removeUser(user.idCommitment, user.secret);
+ await this.userService.banUser(user.idCommitment, user.secret);
// Set user leaf to 0 and recalculate merkle tree
await this.userService.updateUser(user.idCommitment);
diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts
index ad7ba80..2c07d7e 100644
--- a/server/src/services/user.service.ts
+++ b/server/src/services/user.service.ts
@@ -157,6 +157,31 @@ class UserService {
return "Done";
}
+ public async removeUsersByIndexes(indexes: number[], groupId: string): Promise {
+ if (indexes.length == 0)
+ return "";
+
+ // Get the zero hashes.
+ const zeros = await MerkleTreeZero.findZeros();
+
+ if (!zeros || zeros.length === 0) {
+ throw `The zero hashes have not yet been created`;
+ }
+
+ for (let index of indexes) {
+
+ let node = await MerkleTreeNode.findLeafByGroupIdAndIndexInGroup(groupId, index)
+
+ if (!node) {
+ throw new Error(`The node with the given index does not exist`)
+ }
+
+ await this.updateUser(node.hash);
+ }
+
+ return "Done";
+ }
+
public async updateUser(leafHash: string, newValue: string = config.ZERO_VALUE.toString()) {
let node = await MerkleTreeNode.findLeafByHash(leafHash);
@@ -183,7 +208,10 @@ class UserService {
}
- public async removeUser(idCommitment: string, secret: bigint): Promise {
+ /**
+ * Bans a user with the given identity commitment.
+ */
+ public async banUser(idCommitment: string, secret: bigint): Promise {
const treeNode = await MerkleTreeNode.findLeafByHash(idCommitment);
if (!treeNode) {
@@ -198,6 +226,7 @@ class UserService {
await bannedUser.save();
return idCommitment
}
+
}
export default UserService
\ No newline at end of file
diff --git a/server/tests/interrep/api.test.ts b/server/tests/interrep/api.test.ts
index 8b47dfa..11233cd 100644
--- a/server/tests/interrep/api.test.ts
+++ b/server/tests/interrep/api.test.ts
@@ -62,15 +62,7 @@ describe('Test interrep sync - subgraph', () => {
mockAxios.mockResolvedValue({
data: {
- data: {
- provider: "twitter",
- name: "not_sufficient",
- depth: 20,
- root: "3282736528510229708245753028800701559160032734733920390753117377915762630937",
- size: 6,
- numberOfLeaves: 6,
- members: testMembers_g1
- }
+ data: testMembers_g1
}
});
@@ -81,4 +73,20 @@ describe('Test interrep sync - subgraph', () => {
expect(members[5]).toEqual({ index: 5, identityCommitment: 'id-5' });
});
+ test('test get deleted members of group - defaults', async () => {
+ const deletedIndexes = [0, 1, 2]
+
+ mockAxios.mockResolvedValue({
+ data: {
+ data: deletedIndexes
+ }
+ });
+
+ const removedIndexes: number[] = await apiFunctions.getRemovedMembersForGroup("twitter", "not_sufficient");
+ expect(removedIndexes.length).toEqual(3);
+ expect(removedIndexes[0]).toEqual(0);
+ expect(removedIndexes[1]).toEqual(1);
+ expect(removedIndexes[2]).toEqual(2);
+ });
+
});
\ No newline at end of file
diff --git a/server/tests/interrep/synchronizer.test.ts b/server/tests/interrep/synchronizer.test.ts
index c8a0414..5f01ecb 100644
--- a/server/tests/interrep/synchronizer.test.ts
+++ b/server/tests/interrep/synchronizer.test.ts
@@ -13,21 +13,21 @@ const testGroups: IInterRepGroupV2[] = [
root: "1",
provider: "github",
name: "GOLD",
- size: 6,
+ size: 4, // 2 deleted members.
numberOfLeaves: 6
},
{
root: "2",
provider: "twitter",
name: "GOLD",
- size: 10,
+ size: 9, // 1 deleted member
numberOfLeaves: 10
},
{
root: "3",
provider: "reddit",
name: "GOLD",
- size: 50,
+ size: 45, // 5 deleted members
numberOfLeaves: 50
}
];
@@ -57,14 +57,34 @@ const getAllGroupsMock = async (): Promise => {
return testGroups
};
-const getMembersForGroupMock = async (groupid: string): Promise => {
- if (groupid == '1')
+const getMembersForGroupMock = async (provider: string, name: string): Promise => {
+ // Init deleted members
+ testMembers_g1[1].identityCommitment = "0";
+ testMembers_g1[2].identityCommitment = "0";
+
+ testMembers_g2[5].identityCommitment = "0";
+
+ testMembers_g3[0].identityCommitment = "0";
+ testMembers_g3[20].identityCommitment = "0";
+ testMembers_g3[21].identityCommitment = "0";
+ testMembers_g3[30].identityCommitment = "0";
+ testMembers_g3[45].identityCommitment = "0";
+
+ if (provider == 'github' && name == 'GOLD')
return testMembers_g1;
- else if (groupid == '2')
+ else if (provider == 'twitter' && name == 'GOLD')
return testMembers_g2;
return testMembers_g3;
}
+const getRemovedMembersForGroupMock = async (provider: string, name: string): Promise => {
+ if (provider == 'github' && name == 'GOLD')
+ return [1, 2];
+ else if (provider == 'twitter' && name == 'GOLD')
+ return [5];
+ return [0, 20, 21, 30, 45];
+}
+
describe('Test interrep synchronizer', () => {
let testPubSub: PubSub;
@@ -80,6 +100,7 @@ describe('Test interrep synchronizer', () => {
subgraphFunctions.getAllGroups = getAllGroupsMock;
subgraphFunctions.getMembersForGroup = getMembersForGroupMock;
+ subgraphFunctions.getRemovedMembersForGroup = getRemovedMembersForGroupMock;
synchronizer = new InterRepSynchronizer(testPubSub, groupService, userService);
})
@@ -99,7 +120,8 @@ describe('Test interrep synchronizer', () => {
group_id: 'test',
provider: 'test',
name: 'test',
- size: 1
+ size: 1,
+ number_of_leaves: 1
}
});
@@ -119,33 +141,38 @@ describe('Test interrep synchronizer', () => {
group_id: "github_GOLD",
provider: "github",
name: "GOLD",
- size: 6
+ size: 4,
+ number_of_leaves: 6
},
{
group_id: "twitter_GOLD",
provider: "twitter",
name: "GOLD",
- size: 10
+ size: 9,
+ number_of_leaves: 10
},
{
group_id: "reddit_GOLD",
provider: "reddit",
name: "GOLD",
- size: 50
+ size: 45,
+ number_of_leaves: 50
}
]);
let saveGroupSpy = jest.spyOn(groupService, 'saveGroup').mockResolvedValue({
group_id: 'test',
provider: 'test',
name: 'test',
- size: 1
+ size: 1,
+ number_of_leaves: 1
});
let updateSizeSpy = jest.spyOn(groupService, 'updateSize').mockResolvedValue({
group_id: 'test',
provider: 'test',
name: 'test',
- size: 1
+ size: 1,
+ number_of_leaves: 1
});
let appendUsersSpy = jest.spyOn(userService, 'appendUsers').mockResolvedValue("success");
@@ -158,20 +185,22 @@ describe('Test interrep synchronizer', () => {
expect(appendUsersSpy).not.toHaveBeenCalled();
});
- test('test sync - partial db', async () => {
+ test('test sync - partial db no deleted records', async () => {
let getGroupsSpy = jest.spyOn(groupService, 'getGroups').mockResolvedValue(
[
{
group_id: "github_GOLD",
provider: "github",
name: "GOLD",
- size: 6
+ size: 4,
+ number_of_leaves: 6
},
{
group_id: "twitter_GOLD",
provider: "twitter",
name: "GOLD",
- size: 8 // Less records in db
+ size: 9, // Same as db
+ number_of_leaves: 8 // Less records in db
}
]);
@@ -179,14 +208,16 @@ describe('Test interrep synchronizer', () => {
group_id: 'test',
provider: 'test',
name: 'test',
- size: 1
+ size: 1,
+ number_of_leaves: 1
});
- let updateSizeSpy = jest.spyOn(groupService, 'updateSize').mockResolvedValue({
+ let updateSizeSpy = jest.spyOn(groupService, 'updateNumberOfLeaves').mockResolvedValue({
group_id: 'test',
provider: 'test',
name: 'test',
- size: 1
+ size: 1,
+ number_of_leaves: 1
});
let appendUsersSpy = jest.spyOn(userService, 'appendUsers').mockResolvedValue("success");
@@ -199,4 +230,57 @@ describe('Test interrep synchronizer', () => {
expect(appendUsersSpy).toHaveBeenCalledTimes(2);
});
+ test('test sync - partial db with deleted records', async () => {
+ let getGroupsSpy = jest.spyOn(groupService, 'getGroups').mockResolvedValue(
+ [
+ {
+ group_id: "github_GOLD",
+ provider: "github",
+ name: "GOLD",
+ size: 4,
+ number_of_leaves: 6
+ },
+ {
+ group_id: "twitter_GOLD",
+ provider: "twitter",
+ name: "GOLD",
+ size: 8, // Less records in db, some member was deleted
+ number_of_leaves: 10 // Same as db
+ },
+ {
+ group_id: "reddit_GOLD",
+ provider: "reddit",
+ name: "GOLD",
+ size: 45,
+ number_of_leaves: 50
+ }
+ ]);
+
+ let saveGroupSpy = jest.spyOn(groupService, 'saveGroup').mockResolvedValue({
+ group_id: 'test',
+ provider: 'test',
+ name: 'test',
+ size: 1,
+ number_of_leaves: 1
+ });
+
+ let updateSizeSpy = jest.spyOn(groupService, 'updateSize').mockResolvedValue({
+ group_id: 'test',
+ provider: 'test',
+ name: 'test',
+ size: 1,
+ number_of_leaves: 1
+ });
+
+ let removeUsersByIndexesSpy = jest.spyOn(userService, 'removeUsersByIndexes').mockResolvedValue("success");
+
+ await synchronizer.syncCommitmentsFromInterRep();
+
+ expect(saveGroupSpy).not.toHaveBeenCalled();
+ expect(getGroupsSpy).toHaveBeenCalled();
+ expect(updateSizeSpy).toHaveBeenCalledTimes(1);
+ expect(removeUsersByIndexesSpy).toHaveBeenCalledTimes(1);
+ expect(removeUsersByIndexesSpy).toHaveBeenCalledWith([5], 'twitter_GOLD');
+ });
+
});
\ No newline at end of file
diff --git a/server/tests/services/chat.service.test.ts b/server/tests/services/chat.service.test.ts
index 9a15e4b..e85939a 100644
--- a/server/tests/services/chat.service.test.ts
+++ b/server/tests/services/chat.service.test.ts
@@ -1,14 +1,15 @@
import { clearDatabase } from '../jest.setup';
-import { test, expect, describe, afterEach } from '@jest/globals'
+import { test, expect, describe, afterEach, beforeEach } from '@jest/globals'
import Message from '../../src/persistence/model/message/message.model';
import { IMessage } from '../../src/persistence/model/message/message.types';
-import ChatService from '../../src/services/chat.service'
+import ChatService, { ITimeRangeMessages } from '../../src/services/chat.service'
import MockDate from 'mockdate';
describe('Test chat service', () => {
const timestampTodayMs = 1637837920000;
+
const msecondsPerDay = 86400000;
afterEach(async () => {
@@ -48,6 +49,128 @@ describe('Test chat service', () => {
expect(data.length).toEqual(1);
});
+ test('get messages in time range - invalid range', async () => {
+ jest.setTimeout(30000);
+ const chatService = new ChatService();
+
+ try {
+ await chatService.getMessagesInTimeRange(new Date(timestampTodayMs), new Date(timestampTodayMs - 10));
+ expect(false).toBeTruthy();
+ } catch (e: any) {
+ expect(true).toBeTruthy();
+ expect(e.message).toEqual("Please select valid date range");
+ }
+ })
+
+ test('get messages in time range - 1', async () => {
+ jest.setTimeout(30000);
+ const chatService = new ChatService();
+
+ for (let i = 0; i < 100; i++) {
+ const timestamp = timestampTodayMs + 60000 * i; // 1 minute apart
+ await insertMessage(i, timestamp);
+ }
+
+ const lastItemTimestamp = timestampTodayMs + 9 * 60000
+ const messageData: ITimeRangeMessages = await chatService.getMessagesInTimeRange(
+ new Date(timestampTodayMs),
+ new Date(lastItemTimestamp + 100));
+
+ expect(messageData.messages.length).toEqual(10);
+ expect(messageData.returnedFromTimestamp).toEqual(timestampTodayMs);
+ expect(messageData.returnedToTimestamp).toEqual(lastItemTimestamp);
+ })
+
+ test('get messages in time range - 2', async () => {
+ jest.setTimeout(30000);
+ const chatService = new ChatService();
+
+ for (let i = 0; i < 100; i++) {
+ const timestamp = timestampTodayMs + 60000 * i; // 1 minute apart
+ await insertMessage(i, timestamp);
+ }
+
+ const lastItemTimestamp = timestampTodayMs + 9 * 60000
+ const messageData: ITimeRangeMessages = await chatService.getMessagesInTimeRange(
+ new Date(timestampTodayMs + 100),
+ new Date(lastItemTimestamp + 100));
+
+ expect(messageData.messages.length).toEqual(10 - 1); // the first one is not returned
+ expect(messageData.returnedFromTimestamp).toEqual((timestampTodayMs + 60000));
+ expect(messageData.returnedToTimestamp).toEqual(lastItemTimestamp);
+ })
+
+ test('get messages in time range - pagination single page', async () => {
+ jest.setTimeout(30000);
+ const chatService = new ChatService();
+
+ for (let i = 0; i < ChatService.MESSAGE_COUNT_LIMIT + 100; i++) {
+ const timestamp = timestampTodayMs + 60000 * i; // 1 minute apart
+ await insertMessage(i, timestamp);
+ }
+
+ const numberOfMessagesMoreThanLimit = 10;
+ const lastItemTimestamp = timestampTodayMs + (ChatService.MESSAGE_COUNT_LIMIT + numberOfMessagesMoreThanLimit) * 60000
+ const messageData: ITimeRangeMessages = await chatService.getMessagesInTimeRange(
+ new Date(timestampTodayMs),
+ new Date(lastItemTimestamp + 100));
+
+ expect(messageData.messages.length).toEqual(ChatService.MESSAGE_COUNT_LIMIT); // no more than limit gets returned
+
+ expect(messageData.returnedFromTimestamp).toEqual(timestampTodayMs);
+
+ expect(messageData.returnedToTimestamp).toEqual((lastItemTimestamp - (numberOfMessagesMoreThanLimit + 1) * 60000));
+ })
+
+ test('get messages in time range - pagination single page no items', async () => {
+ jest.setTimeout(30000);
+ const chatService = new ChatService();
+
+ const numberOfMessagesMoreThanLimit = 10;
+ const lastItemTimestamp = timestampTodayMs + (ChatService.MESSAGE_COUNT_LIMIT + numberOfMessagesMoreThanLimit) * 60000
+ const messageData: ITimeRangeMessages = await chatService.getMessagesInTimeRange(
+ new Date(timestampTodayMs),
+ new Date(lastItemTimestamp + 100));
+
+ expect(messageData.messages.length).toEqual(0);
+
+ expect(messageData.returnedFromTimestamp).toEqual(timestampTodayMs);
+ expect(messageData.returnedToTimestamp).toEqual(lastItemTimestamp + 100);
+ })
+
+ test('get messages in time range - pagination all pages', async () => {
+ jest.setTimeout(30000);
+ const chatService = new ChatService();
+
+ for (let i = 0; i < ChatService.MESSAGE_COUNT_LIMIT + 100; i++) {
+ const timestamp = timestampTodayMs + 60000 * i; // 1 minute apart
+ await insertMessage(i, timestamp);
+ }
+
+ let messages: IMessage[] = [];
+ const numberOfMessagesMoreThanLimit = 10;
+ const lastItemTimestamp = timestampTodayMs + (ChatService.MESSAGE_COUNT_LIMIT + numberOfMessagesMoreThanLimit) * 60000;
+
+ // Pagination needs to load all until now.
+ const toTimestamp = new Date();
+ let fromTimestamp = new Date(timestampTodayMs);
+ let messageData: ITimeRangeMessages = await chatService.getMessagesInTimeRange(fromTimestamp, toTimestamp);
+ messages = messages.concat(messageData.messages);
+
+ while(1) {
+ if (messageData.messages.length == messageData.limit) {
+ fromTimestamp = new Date(messageData.returnedToTimestamp + 1)
+ messageData = await chatService.getMessagesInTimeRange(fromTimestamp, toTimestamp);
+ messages = messages.concat(messageData.messages);
+ } else {
+ break;
+ }
+ }
+
+ expect(messages.length).toEqual(ChatService.MESSAGE_COUNT_LIMIT + 100);
+ })
+
+
});
const insertMessage = async(id: number, epoch: number) => {
diff --git a/server/tests/services/group_service.test.ts b/server/tests/services/group_service.test.ts
index fbfb270..c2a8add 100644
--- a/server/tests/services/group_service.test.ts
+++ b/server/tests/services/group_service.test.ts
@@ -12,7 +12,7 @@ describe('Test group service', () => {
test('create group', async () => {
const groupService = new GroupService();
- await groupService.saveGroup('id-1', 'github', 'test_1', 10);
+ await groupService.saveGroup('id-1', 'github', 'test_1', 1, 10);
const allGroups = await Group.find({});
expect(allGroups.length).toEqual(1);
@@ -21,19 +21,33 @@ describe('Test group service', () => {
test('update size', async () => {
const groupService = new GroupService();
- await groupService.saveGroup('id-1', 'github', 'test_1', 10);
+ await groupService.saveGroup('id-1', 'github', 'test_1', 10, 10);
await groupService.updateSize('id-1', 15);
const allGroups = await Group.find({});
expect(allGroups.length).toEqual(1);
expect(allGroups[0].size).toEqual(15);
+ expect(allGroups[0].number_of_leaves).toEqual(10);
+ });
+
+ test('update leaf count', async () => {
+ const groupService = new GroupService();
+
+ await groupService.saveGroup('id-1', 'github', 'test_1', 10, 10);
+
+ await groupService.updateNumberOfLeaves('id-1', 15);
+
+ const allGroups = await Group.find({});
+ expect(allGroups.length).toEqual(1);
+ expect(allGroups[0].size).toEqual(10);
+ expect(allGroups[0].number_of_leaves).toEqual(15);
});
test('contains group', async () => {
const groupService = new GroupService();
- await groupService.saveGroup('id-1', 'github', 'test_1', 10);
+ await groupService.saveGroup('id-1', 'github', 'test_1', 1, 10);
const containsGroup1 = await groupService.containsGroup("id-1");
expect(containsGroup1).toBeTruthy();
@@ -48,8 +62,8 @@ describe('Test group service', () => {
const groups = await groupService.getGroups();
expect(groups.length).toEqual(0);
- await groupService.saveGroup('id-1', 'github', 'test_1', 10);
- await groupService.saveGroup('id-2', 'github', 'test_2', 10);
+ await groupService.saveGroup('id-1', 'github', 'test_1',1, 10);
+ await groupService.saveGroup('id-2', 'github', 'test_2', 1, 10);
const groups2 = await groupService.getGroups();
expect(groups2.length).toEqual(2);
diff --git a/server/tests/services/message_handle.service.test.ts b/server/tests/services/message_handle.service.test.ts
index 70042d4..0c94c1d 100644
--- a/server/tests/services/message_handle.service.test.ts
+++ b/server/tests/services/message_handle.service.test.ts
@@ -188,7 +188,7 @@ describe('Test message handle service', () => {
]);
jest.spyOn(userService, "getRoot").mockResolvedValue(BigInt(123).toString());
jest.spyOn(userService, "updateUser").mockResolvedValue();
- const removeUserSpy = jest.spyOn(userService, "removeUser").mockResolvedValue("success");
+ const removeUserSpy = jest.spyOn(userService, "banUser").mockResolvedValue("success");
jest.spyOn(hasher, "verifyProof").mockResolvedValue(true);
jest.spyOn(hasher, "retrieveSecret").mockReturnValue(BigInt(100001));
jest.spyOn(hasher, "poseidonHash").mockReturnValue(BigInt(122010101));
diff --git a/server/tests/services/user.service.test.ts b/server/tests/services/user.service.test.ts
index ca76a3b..751d40d 100644
--- a/server/tests/services/user.service.test.ts
+++ b/server/tests/services/user.service.test.ts
@@ -1,11 +1,11 @@
import { clearDatabase } from '../jest.setup';
import { test, expect, describe, afterEach } from '@jest/globals'
-import BannedUser from "../../src/persistence/model/banned_user/banned_user.model";
import { IBannedUser } from "../../src/persistence/model/banned_user/banned_user.types";
-import UserService from '../../src/services/user.service';
-import config from "../../src/config"
import { MerkleTreeNode, MerkleTreeZero } from '../../src/persistence/model/merkle_tree/merkle_tree.model';
import Hasher from '../../src/util/hasher';
+import UserService from '../../src/services/user.service';
+import config from "../../src/config"
+import BannedUser from "../../src/persistence/model/banned_user/banned_user.model";
describe('Test user service', () => {
@@ -178,7 +178,7 @@ describe('Test user service', () => {
expect(allNodes.length).toEqual(17); // user exists, was not added
});
- test('remove user - exists', async () => {
+ test('ban user - exists', async () => {
const user_service = new UserService();
await testSeedZeros(BigInt(0));
@@ -199,16 +199,16 @@ describe('Test user service', () => {
expect(result2).toEqual("Done");
- await user_service.removeUser(BigInt(2 ^ 244).toString(), BigInt(13^120));
+ await user_service.banUser(BigInt(2 ^ 244).toString(), BigInt(13^120));
const allBannedUsers: IBannedUser[] = await user_service.getAllBannedUsers();
expect(allBannedUsers.length).toEqual(1);
});
- test('remove user - not exists', async () => {
+ test('ban user - not exists', async () => {
const user_service = new UserService();
try {
- await user_service.removeUser(BigInt(2 ^ 244).toString(), BigInt(11111));
+ await user_service.banUser(BigInt(2 ^ 244).toString(), BigInt(11111));
expect(false).toBeTruthy();
} catch(e) {
expect(e).toEqual("The user doesn't exists");
@@ -255,6 +255,30 @@ describe('Test user service', () => {
}
});
+ test('remove users by indexes - exists', async () => {
+ const user_service = new UserService();
+ await testSeedZeros(BigInt(0));
+
+ const hash1 = BigInt(2 ^ 244).toString();
+ const hash2 = BigInt(3 ^ 244).toString();
+
+ await user_service.appendUsers([{
+ index: 1,
+ identityCommitment: hash1
+ }], "id-1");
+
+ await user_service.appendUsers([{
+ index: 2,
+ identityCommitment: hash2
+ }], "id-1");
+
+ await user_service.removeUsersByIndexes([1], 'id-1');
+
+ let deletedUser = await MerkleTreeNode.findByLevelAndIndex(0, 1);
+ expect(deletedUser).not.toBeNull();
+ expect(deletedUser!.hash).not.toEqual(hash1);
+ });
+
});
const insertBannedUser = async (id: number) => {
diff --git a/yarn.lock b/yarn.lock
index 345e403..1976bc9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3957,6 +3957,11 @@ base-x@^3.0.2, base-x@^3.0.8:
dependencies:
safe-buffer "^5.0.1"
+base64-arraybuffer-es6@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/base64-arraybuffer-es6/-/base64-arraybuffer-es6-0.7.0.tgz#dbe1e6c87b1bf1ca2875904461a7de40f21abc86"
+ integrity sha512-ESyU/U1CFZDJUdr+neHRhNozeCv72Y7Vm0m1DCbjX3KBjT6eYocvAJlSk6+8+HkVwXlT1FNxhGW6q3UKAlCvvw==
+
base64-js@^1.0.2, base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
@@ -5093,6 +5098,11 @@ core-js@^2.4.0:
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
+core-js@^3.4:
+ version "3.21.1"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.21.1.tgz#f2e0ddc1fc43da6f904706e8e955bc19d06a0d94"
+ integrity sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig==
+
core-js@^3.6.5:
version "3.19.3"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.19.3.tgz#6df8142a996337503019ff3235a7022d7cdf4559"
@@ -5718,6 +5728,11 @@ detect-port-alt@1.1.6:
address "^1.0.1"
debug "^2.6.0"
+dexie@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.2.1.tgz#ef21456d725e700c1ab7ac4307896e4fdabaf753"
+ integrity sha512-Y8oz3t2XC9hvjkP35B5I8rUkKKwM36GGRjWQCMjzIYScg7W+GHKDXobSYswkisW7CxL1/tKQtggMDsiWqDUc1g==
+
diff-sequences@^26.6.2:
version "26.6.2"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1"
@@ -5845,6 +5860,13 @@ domelementtype@^2.0.1, domelementtype@^2.2.0:
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57"
integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
+domexception@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
+ integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==
+ dependencies:
+ webidl-conversions "^4.0.2"
+
domexception@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304"
@@ -6733,6 +6755,13 @@ extsprintf@^1.2.0:
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07"
integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==
+fake-indexeddb@^3.1.7:
+ version "3.1.7"
+ resolved "https://registry.yarnpkg.com/fake-indexeddb/-/fake-indexeddb-3.1.7.tgz#d9efbeade113c15efbe862e4598a4b0a1797ed9f"
+ integrity sha512-CUGeCzCOVjmeKi2C0pcvSh6NDU6uQIaS+7YyR++tO/atJJujkBYVhDvfePdz/U8bD33BMVWirsr1MKczfAqbjA==
+ dependencies:
+ realistic-structured-clone "^2.0.1"
+
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
@@ -12202,6 +12231,11 @@ prepend-http@^2.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
+prettier@^2.6.2:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.6.2.tgz#e26d71a18a74c3d0f0597f55f01fb6c06c206032"
+ integrity sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==
+
pretty-bytes@^5.3.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
@@ -12793,6 +12827,16 @@ readline@^1.3.0:
resolved "https://registry.yarnpkg.com/readline/-/readline-1.3.0.tgz#c580d77ef2cfc8752b132498060dc9793a7ac01c"
integrity sha1-xYDXfvLPyHUrEySYBg3JeTp6wBw=
+realistic-structured-clone@^2.0.1:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/realistic-structured-clone/-/realistic-structured-clone-2.0.4.tgz#7eb4c2319fc3cb72f4c8d3c9e888b11647894b50"
+ integrity sha512-lItAdBIFHUSe6fgztHPtmmWqKUgs+qhcYLi3wTRUl4OTB3Vb8aBVSjGfQZUvkmJCKoX3K9Wf7kyLp/F/208+7A==
+ dependencies:
+ core-js "^3.4"
+ domexception "^1.0.1"
+ typeson "^6.1.0"
+ typeson-registry "^1.0.0-alpha.20"
+
recursive-readdir@2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f"
@@ -14734,6 +14778,20 @@ typescript@^4.1.2, typescript@^4.5.2:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.2.tgz#8ac1fba9f52256fdb06fb89e4122fa6a346c2998"
integrity sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==
+typeson-registry@^1.0.0-alpha.20:
+ version "1.0.0-alpha.39"
+ resolved "https://registry.yarnpkg.com/typeson-registry/-/typeson-registry-1.0.0-alpha.39.tgz#9e0f5aabd5eebfcffd65a796487541196f4b1211"
+ integrity sha512-NeGDEquhw+yfwNhguLPcZ9Oj0fzbADiX4R0WxvoY8nGhy98IbzQy1sezjoEFWOywOboj/DWehI+/aUlRVrJnnw==
+ dependencies:
+ base64-arraybuffer-es6 "^0.7.0"
+ typeson "^6.0.0"
+ whatwg-url "^8.4.0"
+
+typeson@^6.0.0, typeson@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/typeson/-/typeson-6.1.0.tgz#5b2a53705a5f58ff4d6f82f965917cabd0d7448b"
+ integrity sha512-6FTtyGr8ldU0pfbvW/eOZrEtEkczHRUtduBnA90Jh9kMPCiFNnXIon3vF41N0S4tV1HHQt4Hk1j4srpESziCaA==
+
uWebSockets.js@uNetworking/uWebSockets.js#v20.4.0:
version "20.4.0"
resolved "https://codeload.github.com/uNetworking/uWebSockets.js/tar.gz/65f39bdff763be3883e6cf18e433dd4fec155845"
@@ -15427,6 +15485,11 @@ webcrypto-core@^1.4.0:
pvtsutils "^1.2.0"
tslib "^2.3.1"
+webidl-conversions@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
+ integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==
+
webidl-conversions@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-5.0.0.tgz#ae59c8a00b121543a2acc65c0434f57b0fc11aff"
@@ -15598,7 +15661,7 @@ whatwg-url@^11.0.0:
tr46 "^3.0.0"
webidl-conversions "^7.0.0"
-whatwg-url@^8.0.0, whatwg-url@^8.5.0:
+whatwg-url@^8.0.0, whatwg-url@^8.4.0, whatwg-url@^8.5.0:
version "8.7.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-8.7.0.tgz#656a78e510ff8f3937bc0bcbe9f5c0ac35941b77"
integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==