| @@ -0,0 +1,6 @@ | |||
| { | |||
| "ExpandedNodes": [ | |||
| "" | |||
| ], | |||
| "PreviewInSolutionExplorer": false | |||
| } | |||
| @@ -0,0 +1,44 @@ | |||
| using Microsoft.AspNetCore.SignalR; | |||
| namespace Diligent.WebAPI.Host.Hubs | |||
| { | |||
| public class ConnectionHub : Hub | |||
| { | |||
| private static Dictionary<string, string> IDs { get; set; } = new(); | |||
| public override async Task OnDisconnectedAsync(Exception exception) | |||
| { | |||
| var msg = new StatusMessage { Id = IDs[Context.ConnectionId], M = "unsubscription" }; | |||
| IDs.Remove(Context.ConnectionId); | |||
| await Clients.All.SendAsync("Notify", msg); | |||
| } | |||
| [HubMethodName("Subscribe")] | |||
| public async Task Subscribe(string id) | |||
| { | |||
| if (!IDs.Any(n => n.Value == id)) | |||
| IDs[Context.ConnectionId] = id; | |||
| string[] ids = new string[IDs.Count]; | |||
| IDs.Values.CopyTo(ids, 0); | |||
| await Clients.Caller.SendAsync("ReceiveList", ids); | |||
| var msg = new StatusMessage { Id = id, M = "subscription" }; | |||
| await Clients.Others.SendAsync("Notify", msg); | |||
| } | |||
| [HubMethodName("Unsubscribe")] | |||
| public async Task Unsubscribe(SenderObj s) | |||
| { | |||
| IDs.Remove(s.ConnId); | |||
| var msg = new StatusMessage { Id = s.Id, M = "unsubscription" }; | |||
| //breakpoint here ! | |||
| await Clients.Others.SendAsync("Notify", msg); | |||
| } | |||
| //public override async Task OnDisconnectedAsync(Exception exception, string id) | |||
| //{ | |||
| // IDs.Remove(id); | |||
| // var msg = new StatusMessage { Id = id, M = "unsubscription" }; | |||
| // await Clients.Others.SendAsync("Notify", msg); | |||
| // await base(); | |||
| //} | |||
| } | |||
| } | |||
| @@ -0,0 +1,8 @@ | |||
| namespace Diligent.WebAPI.Host.Hubs | |||
| { | |||
| public class SenderObj | |||
| { | |||
| public string Id { get; set; } | |||
| public string ConnId { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,8 @@ | |||
| namespace Diligent.WebAPI.Host.Hubs | |||
| { | |||
| public class StatusMessage | |||
| { | |||
| public string Id { get; set; } | |||
| public string M { get; set; } | |||
| } | |||
| } | |||
| @@ -17,5 +17,6 @@ app.UseAuthorization(); | |||
| app.SetupData(); | |||
| app.MapHub<ChatHub>("/chatHub"); | |||
| app.MapHub<ConnectionHub>("/statusHub"); | |||
| app.Run(); | |||
| @@ -27517,7 +27517,7 @@ | |||
| "@csstools/postcss-stepped-value-functions": "^1.0.0", | |||
| "@csstools/postcss-trigonometric-functions": "^1.0.1", | |||
| "@csstools/postcss-unset-value": "^1.0.1", | |||
| "autoprefixer": "10.4.5", | |||
| "autoprefixer": "^10.4.7", | |||
| "browserslist": "^4.21.0", | |||
| "css-blank-pseudo": "^3.0.3", | |||
| "css-has-pseudo": "^3.0.4", | |||
| @@ -3,6 +3,7 @@ import { Button, Form, FormControl, InputGroup } from "react-bootstrap"; | |||
| import { UserContext } from "../contexts/userContext"; | |||
| import { useSelector, useDispatch } from "react-redux"; | |||
| import { chatActions } from "../store/chat-slice"; | |||
| import { BsCircleFill } from 'react-icons/bs' | |||
| const ChatWindow = ({ room }) => { | |||
| const messagesEndRef = useRef(null); | |||
| @@ -24,7 +25,7 @@ const ChatWindow = ({ room }) => { | |||
| }, [dispatch, room.messages]); | |||
| useEffect(() => { | |||
| scrollToBottom(); | |||
| // scrollToBottom(); | |||
| }, []); | |||
| const leftRoomHandler = async () => { | |||
| @@ -56,6 +57,9 @@ const ChatWindow = ({ room }) => { | |||
| setMessage(""); | |||
| }; | |||
| const status = useSelector((s) => s.status); | |||
| const { activeUsers } = status; | |||
| return ( | |||
| <div className="px-3 bg-light-transparent rounded h-100 d-flex flex-column"> | |||
| <div style={{ height: "80px" }}> | |||
| @@ -97,7 +101,18 @@ const ChatWindow = ({ room }) => { | |||
| {/* {n.message} */} | |||
| {n.content} | |||
| </p> | |||
| <p className="text-muted small m-0 p-0 mb-4">{n.username}</p> | |||
| <p className="text-muted small m-0 p-0 mb-4"> | |||
| {n.senderId !== user?.id ? ( | |||
| activeUsers.some((m) => m === n.senderId) ? ( | |||
| <BsCircleFill className="me-2 text-success" /> | |||
| ) : ( | |||
| <BsCircleFill className="me-2 text-danger" /> | |||
| ) | |||
| ) : ( | |||
| "" | |||
| )} | |||
| {n.username} | |||
| </p> | |||
| </div> | |||
| )) | |||
| .reverse()} | |||
| @@ -1,17 +1,68 @@ | |||
| import React from 'react' | |||
| import Activity from './Activity' | |||
| import ChatList from './ChatList' | |||
| import Requests from './Requests/Requests' | |||
| import React from "react"; | |||
| import Activity from "./Activity"; | |||
| import ChatList from "./ChatList"; | |||
| import Requests from "./Requests/Requests"; | |||
| import { useContext, useEffect, useState } from "react"; | |||
| import { HubConnectionBuilder } from "@microsoft/signalr"; | |||
| import { UserContext } from "../contexts/userContext"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { statusActions } from "../store/status-slice"; | |||
| const MiddleContainer = ({showTerm}) => { | |||
| const MiddleContainer = ({ showTerm }) => { | |||
| // const [conn, setConn] = useState(''); | |||
| // window.addEventListener('beforeunload', ()=>{ | |||
| // }) | |||
| const { user } = useContext(UserContext); | |||
| const dispatch = useDispatch(); | |||
| function connect() { | |||
| const connection = new HubConnectionBuilder() | |||
| .withUrl("http://localhost:5116/statusHub") | |||
| .withAutomaticReconnect() | |||
| .build(); | |||
| dispatch(statusActions.setStatusConn(connection)); | |||
| // setConn(connection) | |||
| // connection.onclose(() => { | |||
| // connection.send("Unsubscribe", user.id); | |||
| // }); | |||
| connection.on("Notify", (data) => { | |||
| console.log(data) | |||
| if (data.m === "subscription") { | |||
| dispatch(statusActions.addToActiveUsers(data.id)); | |||
| } else { | |||
| dispatch(statusActions.removeFromActiveUsers(data.id)); | |||
| } | |||
| }); | |||
| connection.on("ReceiveList", (data) => { | |||
| dispatch(statusActions.setActiveUsers(data)); | |||
| }); | |||
| const fulfilled = () => { | |||
| connection.send("Subscribe", user.id); | |||
| }; | |||
| const rejected = () => {}; | |||
| connection.start().then(fulfilled, rejected); | |||
| } | |||
| useEffect(() => { | |||
| connect(); | |||
| }, []); | |||
| return ( | |||
| <div className='w-25 mh-100-vh px-3 bg-light'> | |||
| {showTerm === 'chats' && <ChatList />} | |||
| {showTerm === 'notifications' && <Activity />} | |||
| {showTerm === 'requests' && <Requests/>} | |||
| <div className="w-25 mh-100-vh px-3 bg-light"> | |||
| {showTerm === "chats" && <ChatList />} | |||
| {showTerm === "notifications" && <Activity />} | |||
| {showTerm === "requests" && <Requests />} | |||
| </div> | |||
| ) | |||
| } | |||
| ); | |||
| }; | |||
| export default MiddleContainer | |||
| export default MiddleContainer; | |||
| @@ -1,12 +1,25 @@ | |||
| import { createContext, useEffect, useState } from "react"; | |||
| import { useNavigate } from "react-router-dom"; | |||
| import axios from "axios"; | |||
| import { useSelector } from "react-redux"; | |||
| import { HubConnectionBuilder } from "@microsoft/signalr"; | |||
| export const UserContext = createContext(); | |||
| export const UserProvider = (props) => { | |||
| const [user, setUser] = useState(null); | |||
| const navigate = useNavigate(); | |||
| const status = useSelector((s) => s.status); | |||
| window.addEventListener("beforeunload", () => { | |||
| // that means the user probably shut the browser down without loging out | |||
| if (localStorage.getItem("activeOnes")) { | |||
| // status.connection.stop(); | |||
| disconnect(); | |||
| } | |||
| }); | |||
| if (JSON.parse(localStorage.getItem('user'))) { | |||
| axios.defaults.headers.common['Authorization'] = `Bearer ${JSON.parse(localStorage.getItem('user')).token}`; | |||
| @@ -18,8 +31,35 @@ export const UserProvider = (props) => { | |||
| } | |||
| }, []); | |||
| function disconnect() { | |||
| const connection = new HubConnectionBuilder() | |||
| .withUrl("http://localhost:5116/statusHub") | |||
| .withAutomaticReconnect() | |||
| .build(); | |||
| const fulfilled = () => { | |||
| console.log(user.id) | |||
| console.log(status.connection.connectionId) | |||
| connection | |||
| .send("Unsubscribe", { | |||
| id: user.id, | |||
| connId: status.connection.connectionId, | |||
| }) | |||
| .then(() => console.log("bye")); | |||
| // connection.stop(); | |||
| }; | |||
| const rejected = () => { | |||
| console.log("nope"); | |||
| }; | |||
| connection.start().then(fulfilled, rejected); | |||
| } | |||
| const logOut = () => { | |||
| localStorage.removeItem("user"); | |||
| localStorage.removeItem("activeOnes"); | |||
| disconnect(); | |||
| setUser(null); | |||
| navigate("/login"); | |||
| }; | |||
| @@ -2,9 +2,10 @@ import { configureStore } from "@reduxjs/toolkit"; | |||
| import { uiReducers } from "./ui-slice"; | |||
| import { chatReducers } from "./chat-slice"; | |||
| import { requestsReducers } from "./request-slice"; | |||
| import { statusReducers } from "./status-slice"; | |||
| const store = configureStore({ | |||
| reducer: { ui: uiReducers, chat: chatReducers, requests: requestsReducers }, | |||
| reducer: { ui: uiReducers, chat: chatReducers, requests: requestsReducers, status: statusReducers }, | |||
| middleware: (getDefaultMiddleware) => | |||
| getDefaultMiddleware({ | |||
| serializableCheck: false, | |||
| @@ -0,0 +1,53 @@ | |||
| import { createSlice } from "@reduxjs/toolkit"; | |||
| const initialState = { | |||
| activeUsers: localStorage.getItem("activeOnes") | |||
| ? JSON.parse(localStorage.getItem("activeOnes")) | |||
| : [], | |||
| //maybe needed later | |||
| connection: '' | |||
| }; | |||
| const statusSlice = createSlice({ | |||
| name: "status", | |||
| initialState, | |||
| reducers: { | |||
| // when an user logs in signalR delivers the list of all active users to him, so we | |||
| // just set it into our state menagement | |||
| setActiveUsers: (state, action) => { | |||
| state.activeUsers = action.payload; | |||
| localStorage.setItem("activeOnes", JSON.stringify(action.payload)); | |||
| }, | |||
| // when another user logs in, signalR hub sends his id to all other | |||
| // clients so we just add his id to the array of active users ids | |||
| addToActiveUsers: (state, action) => { | |||
| state.activeUsers.push(action.payload); | |||
| localStorage.setItem( | |||
| "activeOnes", | |||
| JSON.stringify([...state.activeUsers, action.payload]) | |||
| ); | |||
| }, | |||
| // when another user logs out, signalR hub sends his id to all other clients | |||
| // so we just filter the array of active user ids so that we take the id of the | |||
| // user who left out | |||
| removeFromActiveUsers: (state, action) => { | |||
| state.activeUsers = state.activeUsers.filter((n) => n !== action.payload); | |||
| localStorage.setItem( | |||
| "activeOnes", | |||
| JSON.stringify([...state.activeUsers, action.payload]) | |||
| ); | |||
| }, | |||
| // maybe needed later | |||
| resetActiveUsers: (state, action) => { | |||
| state.activeUsers = []; | |||
| localStorage.removeItem("activeOnes"); | |||
| }, | |||
| // connection may be needed later | |||
| setStatusConn: (state, action) => { | |||
| state.connection = action.payload; | |||
| } | |||
| }, | |||
| }); | |||
| export const statusActions = statusSlice.actions; | |||
| export const statusReducers = statusSlice.reducer; | |||