diff --git a/backend/jitsi-rooms.cabal b/backend/jitsi-rooms.cabal index e789efa..13ee870 100644 --- a/backend/jitsi-rooms.cabal +++ b/backend/jitsi-rooms.cabal @@ -1,6 +1,6 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.35.1. +-- This file has been generated from package.yaml by hpack version 0.35.0. -- -- see: https://github.com/sol/hpack @@ -35,10 +35,12 @@ library Types.Participant Types.RoomData Types.RoomsState + Types.User Types.UsersData Types.WebEnv Types.WebSocketMessages.WebSocketMessages WebServer + WebSocket.AllChat WebSocket.Messages WebSocket.MonadWebSocketSession WebSocket.Server @@ -90,4 +92,3 @@ executable jitsi-rooms-exe , warp , websockets default-language: Haskell2010 - diff --git a/backend/package.yaml b/backend/package.yaml index 2444dbe..d35a14c 100644 --- a/backend/package.yaml +++ b/backend/package.yaml @@ -58,14 +58,4 @@ executables: dependencies: - jitsi-rooms -tests: - jitsi-rooms-test: - main: Spec.hs - source-dirs: test - ghc-options: - - -threaded - - -rtsopts - - -with-rtsopts=-N - dependencies: - - jitsi-rooms default-extensions: NoImplicitPrelude,OverloadedStrings,ImportQualifiedPost diff --git a/backend/src/BroadcastUserData.hs b/backend/src/BroadcastUserData.hs index b1a0a54..bc00f48 100644 --- a/backend/src/BroadcastUserData.hs +++ b/backend/src/BroadcastUserData.hs @@ -5,15 +5,14 @@ module BroadcastUserData ) where -import ClassyPrelude -import Data.Aeson (encode) -import qualified Network.WebSockets as WS -import State.ConnectedClientsState (MonadConnectedClientsRead (getConnctedClients)) -import State.RoomDataState (MonadRoomDataStateRead (getRoomDataState)) -import Types.AppTypes (HasConnectedClientState (..)) -import Types.ConnectionState (Client (..), ConnectedClients) -import Types.RoomsState (HasRoomsState (..)) -import Types.UsersData (UsersData (..)) +import ClassyPrelude +import Data.Aeson (encode) +import Network.WebSockets qualified as WS +import State.ConnectedClientsState (MonadConnectedClientsRead (getConnctedClients)) +import State.RoomDataState (MonadRoomDataStateRead (getRoomDataState)) +import Types.ConnectionState (Client (..), ConnectedClients) +import Types.User (User, clientToUser) +import Types.UsersData (UsersData (..)) class (Monad m, MonadConnectedClientsRead m) => MonadBroadcast m where broadCastToClients :: Text -> m () @@ -32,8 +31,8 @@ broadcastUserData = do getUsersWithoutRoom :: ( MonadConnectedClientsRead m ) => - m [Text] -getUsersWithoutRoom = map name . filter (not . joinedRoom) <$> getConnctedClients + m [User] +getUsersWithoutRoom = map clientToUser . filter (not . joinedRoom) <$> getConnctedClients broadCastToClientsGeneric :: ( MonadIO m, diff --git a/backend/src/Types/User.hs b/backend/src/Types/User.hs new file mode 100644 index 0000000..f773587 --- /dev/null +++ b/backend/src/Types/User.hs @@ -0,0 +1,21 @@ +{-# LANGUAGE DeriveGeneric #-} + +module Types.User (User, clientToUser) where + +import ClassyPrelude +import Data.Aeson (FromJSON, ToJSON) +import Data.UUID (UUID) +import Types.ConnectionState qualified as C + +data User = User + { uuid :: UUID, + name :: Text + } + deriving (Generic, Show, Eq) + +clientToUser :: C.Client -> User +clientToUser C.Client {C.uuid = userUuid, C.name = userName, C.joinedRoom = _, C.conn = _} = User userUuid userName + +instance ToJSON User + +instance FromJSON User diff --git a/backend/src/Types/UsersData.hs b/backend/src/Types/UsersData.hs index 04ffe1a..fa8ad52 100644 --- a/backend/src/Types/UsersData.hs +++ b/backend/src/Types/UsersData.hs @@ -5,16 +5,17 @@ module Types.UsersData ) where -import ClassyPrelude -import Data.Aeson (ToJSON) -import Types.RoomData (RoomsData) +import ClassyPrelude +import Data.Aeson (ToJSON) +import Types.RoomData (RoomsData) +import Types.User (User) data UsersData = UsersData - { roomsData :: RoomsData, + { roomsData :: RoomsData, usersWithOutRoom :: UsersWithoutRoom } deriving (Generic, Show) instance ToJSON UsersData -type UsersWithoutRoom = [Text] +type UsersWithoutRoom = [User] diff --git a/backend/src/Types/WebSocketMessages/WebSocketMessages.hs b/backend/src/Types/WebSocketMessages/WebSocketMessages.hs index 9269cb2..30049ba 100644 --- a/backend/src/Types/WebSocketMessages/WebSocketMessages.hs +++ b/backend/src/Types/WebSocketMessages/WebSocketMessages.hs @@ -1,9 +1,12 @@ {-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DuplicateRecordFields #-} module Types.WebSocketMessages.WebSocketMessages ( WebSocketMessage (..), SetClientInfo (..), JoinRoom (..), + AllChatMessageIncoming (..), + AllChatMessageOutgoing (..), ) where @@ -12,14 +15,13 @@ import Data.Aeson ( FromJSON (parseJSON), Options (sumEncoding), SumEncoding (..), - decode, + ToJSON, defaultOptions, genericParseJSON, - withObject, - (.:), ) +import Types.User (User) -data WebSocketMessage = ClientInfoMessage SetClientInfo | JoinRoomMessage JoinRoom +data WebSocketMessage = ClientInfoMessage SetClientInfo | JoinRoomMessage JoinRoom | AllChatMessageIncomingMessage AllChatMessageIncoming deriving (Generic) instance FromJSON WebSocketMessage where @@ -38,3 +40,18 @@ data JoinRoom = JoinRoom deriving (Generic, Show) instance FromJSON JoinRoom + +data AllChatMessageIncoming = AllChatMessageIncoming + { content :: Text + } + deriving (Generic, Show) + +instance FromJSON AllChatMessageIncoming + +data AllChatMessageOutgoing = AllChatMessageOutgoing + { content :: Text, + sender :: User + } + deriving (Generic, Show) + +instance ToJSON AllChatMessageOutgoing diff --git a/backend/src/WebSocket/AllChat.hs b/backend/src/WebSocket/AllChat.hs new file mode 100644 index 0000000..fce86ef --- /dev/null +++ b/backend/src/WebSocket/AllChat.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE LambdaCase #-} + +module WebSocket.AllChat (broadCastAllChatMessage) where + +import BroadcastUserData (MonadBroadcast (..)) +import ClassyPrelude +import Data.Aeson (encode) +import Types.User (clientToUser) +import Types.WebSocketMessages.WebSocketMessages (AllChatMessageIncoming (..), AllChatMessageOutgoing (AllChatMessageOutgoing)) +import WebSocket.MonadWebSocketSession (MonadWebSocketSession (getClient)) + +broadCastAllChatMessage :: (MonadBroadcast m, MonadWebSocketSession m) => AllChatMessageIncoming -> m () +broadCastAllChatMessage AllChatMessageIncoming {content = message} = do + getClient >>= \case + Nothing -> return () + Just client -> do + let broadCastValue = AllChatMessageOutgoing message (clientToUser client) + broadCastToClients $ (decodeUtf8 . toStrict . encode) broadCastValue diff --git a/backend/src/WebSocket/MonadWebSocketSession.hs b/backend/src/WebSocket/MonadWebSocketSession.hs index 7893af5..8ef1698 100644 --- a/backend/src/WebSocket/MonadWebSocketSession.hs +++ b/backend/src/WebSocket/MonadWebSocketSession.hs @@ -15,6 +15,7 @@ import Data.Aeson import Data.UUID (UUID) import State.ConnectedClientsState ( MonadConnectedClientsModify, + MonadConnectedClientsRead (getConnctedClients), removeWSClient, ) import State.RoomDataState (MonadRoomDataStateRead) @@ -23,9 +24,10 @@ import Types.WebSocketMessages.WebSocketMessages (SetClientInfo (..)) import WebSocket.Messages import WebSocket.WSReaderTApp -class Monad m => MonadWebSocketSession m where +class MonadConnectedClientsRead m => MonadWebSocketSession m where getTypedWSMessage :: FromJSON a => m a getSesssionId :: m UUID + getClient :: m (Maybe Client) instance MonadWebSocketSession (WSApp WSEnv) where getTypedWSMessage = do @@ -36,6 +38,9 @@ instance MonadWebSocketSession (WSApp WSEnv) where sendMessage $ "Bad message: " <> pack err getTypedWSMessage getSesssionId = getClientId <$> ask + getClient = do + id' <- getSesssionId + find ((== id') . uuid) <$> getConnctedClients class (Monad m) => MonadWebSocketSessionInit m where newClient :: SetClientInfo -> m Client diff --git a/backend/src/WebSocket/WSApp.hs b/backend/src/WebSocket/WSApp.hs index d2eb9f7..aea7ad3 100644 --- a/backend/src/WebSocket/WSApp.hs +++ b/backend/src/WebSocket/WSApp.hs @@ -13,6 +13,7 @@ import Types.WebSocketMessages.WebSocketMessages ( SetClientInfo (displayName), WebSocketMessage (..), ) +import WebSocket.AllChat (broadCastAllChatMessage) import WebSocket.MonadWebSocketSession import WebSocket.WSReaderTApp @@ -35,7 +36,8 @@ wsApp = do handleWSAction :: ( MonadWebSocketSession m, - MonadConnectedClientsModify m + MonadConnectedClientsModify m, + MonadBroadcast m ) => m () handleWSAction = do @@ -45,6 +47,8 @@ handleWSAction = do joinRoom ClientInfoMessage clientInfo -> do updateClientName clientInfo + AllChatMessageIncomingMessage incomingMessage -> do + broadCastAllChatMessage incomingMessage joinRoom :: ( MonadConnectedClientsModify m, diff --git a/backend/test/Spec.hs b/backend/test/Spec.hs index cd4753f..e69de29 100644 --- a/backend/test/Spec.hs +++ b/backend/test/Spec.hs @@ -1,2 +0,0 @@ -main :: IO () -main = putStrLn "Test suite not yet implemented" diff --git a/frontend/dev-with-remote-backend.sh b/frontend/dev-with-remote-backend.sh old mode 100644 new mode 100755 diff --git a/frontend/package.json b/frontend/package.json index 19a0215..5b9bf01 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@jitsi/react-sdk": "^1.3.0", + "jotai": "^2.0.3", "just-curry-it": "^5.3.0", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5901c6c..92bf62a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,3 +1,4 @@ +import { Provider } from "jotai"; import { useState } from "react"; import "./App.css"; import Meeting from "./components/meeting/Meeting"; @@ -9,7 +10,6 @@ import { useRoomName } from "./hooks/useRoomName"; function App() { const { userInfo, setUserInfo } = useLocalUser(); - const { roomName, updateRoomName, updateAndSubmitRoomName, submitRoomName } = useRoomName(); const { roomData, sendMessage } = useBackendData(userInfo); const { conferenceData, setConferenceData } = useConferenceData( sendMessage, @@ -23,20 +23,12 @@ function App() { return (
{ - updateAndSubmitRoomName(roomName) - setMeetingStarted(true) - }} + sendMessage={sendMessage} />
); diff --git a/frontend/src/background/types/roomData.ts b/frontend/src/background/types/roomData.ts index 02ae94d..55579cd 100644 --- a/frontend/src/background/types/roomData.ts +++ b/frontend/src/background/types/roomData.ts @@ -10,7 +10,13 @@ export interface Participant { email: string; } +export interface User { + uuid: string, + name: string + +} + export interface UsersData { roomsData: RoomData[]; - usersWithOutRoom: string[]; + usersWithOutRoom: User[]; } diff --git a/frontend/src/components/chat/Chat.css b/frontend/src/components/chat/Chat.css new file mode 100644 index 0000000..2b530f8 --- /dev/null +++ b/frontend/src/components/chat/Chat.css @@ -0,0 +1,20 @@ +.chat-input{ +max-width: 50%; +} + + +.chat-bubble{ + background-color: #3d3d5c; + overflow-wrap: break-word; + margin-bottom: 7px; + + white-space: -moz-pre-wrap !important; /* Mozilla, since 1999 */ + white-space: -pre-wrap; /* Opera 4-6 */ + white-space: -o-pre-wrap; /* Opera 7 */ + white-space: pre-wrap; /* css-3 */ + word-wrap: break-word; /* Internet Explorer 5.5+ */ + white-space: -webkit-pre-wrap; /* Newer versions of Chrome/Safari*/ + word-break: break-all; + white-space: normal; +} + diff --git a/frontend/src/components/chat/Chat.tsx b/frontend/src/components/chat/Chat.tsx new file mode 100644 index 0000000..cc8484b --- /dev/null +++ b/frontend/src/components/chat/Chat.tsx @@ -0,0 +1,53 @@ +import { useState } from "react"; +import { FormEventHandler } from "react"; +import useAllChat from "../../hooks/useAllChat"; +import "./Chat.css" + +interface Props { + sendMessage: Function +} + + + +function Chat({ sendMessage }: Props) { + + const [chatInput, setChatInput] = useState("") + + const { chatMesages } = useAllChat() + + const onInput: React.ChangeEventHandler = (event) => { + setChatInput(event.target.value); + event.preventDefault(); + }; + + const onSubmit: FormEventHandler = (event) => { + event.preventDefault(); + sendMessage(JSON.stringify({ content: chatInput })); + setChatInput("") + } + + return ( +
+ {chatMesages.map(message => ( +
{message.sender.name}: {message.content}
+ )) + + } +
+ + +
+
+ ); + + +} + + +export default Chat diff --git a/frontend/src/components/meeting/Meeting.tsx b/frontend/src/components/meeting/Meeting.tsx index f767894..23c2f9a 100644 --- a/frontend/src/components/meeting/Meeting.tsx +++ b/frontend/src/components/meeting/Meeting.tsx @@ -1,5 +1,6 @@ import { useCallback, useState } from "react"; import { ConferenceData } from "../../background/jitsi/eventListeners"; +import useMeetingStarted from "../../hooks/useMeetingStarted"; import { useRoomName } from "../../hooks/useRoomName"; import JitsiEntrypoint from "../jitsi/JitsiEntrypoint"; import { UserInfo } from "../jitsi/types"; @@ -9,17 +10,12 @@ interface Props { conferenceData: ConferenceData | undefined; setConferenceData: (newData: ConferenceData) => void; userInfo: UserInfo; - //@ts-ignore - roomName, updateRoomName, submitRoomName - meetingStarted: Boolean, setMeetingStarted: Function } -function Meeting({ conferenceData, setConferenceData, userInfo, roomName, updateRoomName, submitRoomName, meetingStarted, setMeetingStarted }: Props) { +function Meeting({ conferenceData, setConferenceData, userInfo }: Props) { - const startMeeting = useCallback(() => { - submitRoomName(); - setMeetingStarted(true); - }, [submitRoomName, setMeetingStarted]); + const [meetingStarted, setMeetingStarted] = useMeetingStarted() + const { roomName } = useRoomName() if (meetingStarted) { return ( @@ -35,8 +31,6 @@ function Meeting({ conferenceData, setConferenceData, userInfo, roomName, update return ( ); diff --git a/frontend/src/components/meeting/MeetingNameInput.tsx b/frontend/src/components/meeting/MeetingNameInput.tsx index b8aa7dd..38a3531 100644 --- a/frontend/src/components/meeting/MeetingNameInput.tsx +++ b/frontend/src/components/meeting/MeetingNameInput.tsx @@ -1,26 +1,35 @@ import { FormEventHandler } from "react"; +import useMeetingStarted from "../../hooks/useMeetingStarted"; +import { useRoomName } from "../../hooks/useRoomName"; import "./MeetingNameInput.css"; function MeetingNameInput(props: { - roomName: string; - setName: (name: string) => void; - submit: FormEventHandler; - currentUser: string; + roomName: string; currentUser: string; }) { + + const { roomName, updateRoomName, updateAndSubmitRoomName, submitRoomName } = useRoomName() + const [_, setMeetingStarted] = useMeetingStarted() + const onInput: React.ChangeEventHandler = (event) => { - props.setName(event.target.value); + updateRoomName(event.target.value); event.preventDefault(); }; + const onSubmit: FormEventHandler = (event) => { + submitRoomName() + setMeetingStarted(true) + event.preventDefault(); + } + console.log("[Rooms] MeetingName input comp"); return (

Greetings {props.currentUser}

-
+ diff --git a/frontend/src/components/sidebar/Sidebar.css b/frontend/src/components/sidebar/Sidebar.css index 964b43e..435e1f0 100644 --- a/frontend/src/components/sidebar/Sidebar.css +++ b/frontend/src/components/sidebar/Sidebar.css @@ -25,7 +25,11 @@ max-width: 220px; } -.sidebar-hidden > .sidebar-footer { +.sidebar-hidden { + width: 0; +} + +.sidebar-hidden > .sidebar-footer > .sidebar-toggle { position: absolute; bottom: 0; } diff --git a/frontend/src/components/sidebar/Sidebar.tsx b/frontend/src/components/sidebar/Sidebar.tsx index 17d5414..251ad88 100644 --- a/frontend/src/components/sidebar/Sidebar.tsx +++ b/frontend/src/components/sidebar/Sidebar.tsx @@ -2,15 +2,20 @@ import SidebarHeader from "./SidebarHeader"; import useSidebarVisibility from "./useSidebarVisibility"; import "./Sidebar.css"; import { UsersData } from "../../background/types/roomData"; +import useMeetingStarted from "../../hooks/useMeetingStarted"; +import { useRoomName } from "../../hooks/useRoomName"; +import Chat from "../chat/Chat"; interface Props { usersData: UsersData; - updateAndSubmitRoomName: Function + sendMessage: Function } function Sidebar(props: Props) { const { sidebarVisibility, toggleSidebarVisibility, sidebarToggleText } = useSidebarVisibility(); + const [_, setMeetingStarted] = useMeetingStarted() + const { updateAndSubmitRoomName: updateAndSubmitRoomName } = useRoomName(); return (
@@ -21,13 +26,14 @@ function Sidebar(props: Props) { <>

{ - props.updateAndSubmitRoomName(roomData.roomName) + updateAndSubmitRoomName(roomData.roomName) + setMeetingStarted(true) }}> {roomData.roomName}

{roomData.participants.map((participant) => ( -
{participant.displayName}
+
{participant.displayName}
))} ); @@ -35,12 +41,13 @@ function Sidebar(props: Props) {

No room

- {props.usersData.usersWithOutRoom.map((username) => ( -
{username}
+ {props.usersData.usersWithOutRoom.map((user) => ( +
{user.name}
))}
- + +
); diff --git a/frontend/src/hooks/useAllChat.tsx b/frontend/src/hooks/useAllChat.tsx new file mode 100644 index 0000000..d75d202 --- /dev/null +++ b/frontend/src/hooks/useAllChat.tsx @@ -0,0 +1,27 @@ +import { atom, useAtom } from "jotai"; +import { atomWithReducer, useReducerAtom } from 'jotai/utils' +import { User } from "../background/types/roomData"; + +interface ChatMessage { + content: string + sender: User + +} + +export const allChatMessagesAtom = atomWithReducer([], (list: ChatMessage[], item: ChatMessage) => list.concat(item)) + + +const useAllChat = () => { + const [chatMessages, addChatMessage] = useAtom(allChatMessagesAtom) + + + return { chatMesages: chatMessages, addChatMessage } + + + +} + + +export default useAllChat + + diff --git a/frontend/src/hooks/useBackendData.ts b/frontend/src/hooks/useBackendData.ts index 4befc80..3231f4b 100644 --- a/frontend/src/hooks/useBackendData.ts +++ b/frontend/src/hooks/useBackendData.ts @@ -1,5 +1,6 @@ import { useEffect } from "react"; import { UserInfo } from "../components/jitsi/types"; +import useAllChat from "./useAllChat"; import useRoomData from "./useRoomData"; import useWebSocketConnection from "./useWebSocketConnection"; @@ -10,12 +11,15 @@ function useBackendData(userInfo: UserInfo) { const { roomData, setRoomData } = useRoomData(); + const { chatMesages, addChatMessage } = useAllChat() + useEffect(() => { onMessage((messageString) => { console.log("[Rooms] message from ws", messageString); const messageObject = JSON.parse(messageString); !!messageObject.roomsData && setRoomData(messageObject); + !!messageObject.content && addChatMessage(messageObject); return disconnect; }); }, [onMessage, setRoomData, disconnect]); diff --git a/frontend/src/hooks/useMeetingStarted.ts b/frontend/src/hooks/useMeetingStarted.ts new file mode 100644 index 0000000..2187ec7 --- /dev/null +++ b/frontend/src/hooks/useMeetingStarted.ts @@ -0,0 +1,7 @@ +import { atom, useAtom } from "jotai"; + +const meetingStarted = atom(false) + +const useMeetingStarted = () => useAtom(meetingStarted); + +export default useMeetingStarted diff --git a/frontend/src/hooks/useRoomName.ts b/frontend/src/hooks/useRoomName.ts index 1639237..dfdc685 100644 --- a/frontend/src/hooks/useRoomName.ts +++ b/frontend/src/hooks/useRoomName.ts @@ -1,7 +1,10 @@ +import { atom, useAtom } from "jotai"; import { useCallback, useState } from "react"; +const roomNameAtom = atom(getRoomNameFromUrl()) + function useRoomName() { - const [roomName, setRoomName] = useState(() => getRoomNameFromUrl()); + const [roomName, setRoomName] = useAtom(roomNameAtom) const updateRoomName = useCallback( (newName: string) => { diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 791f139..2ed612a 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,3 +1,4 @@ +import { Provider } from 'jotai' import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' @@ -5,6 +6,8 @@ import './index.css' ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - + + + , ) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 08008e3..b3b1a74 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -543,6 +543,11 @@ is-core-module@^2.9.0: dependencies: has "^1.0.3" +jotai@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.0.3.tgz#3b67cda9f6d5feb70a14db0b842a9873aacda8b5" + integrity sha512-MMjhSPAL3RoeZD9WbObufRT2quThEAEknHHridf2ma8Ml7ZVQmUiHk0ssdbR3F0h3kcwhYqSGJ59OjhPge7RRg== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"