| { | |||||
| "ExpandedNodes": [ | |||||
| "" | |||||
| ], | |||||
| "PreviewInSolutionExplorer": false | |||||
| } |
| 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(); | |||||
| //} | |||||
| } | |||||
| } |
| namespace Diligent.WebAPI.Host.Hubs | |||||
| { | |||||
| public class SenderObj | |||||
| { | |||||
| public string Id { get; set; } | |||||
| public string ConnId { get; set; } | |||||
| } | |||||
| } |
| namespace Diligent.WebAPI.Host.Hubs | |||||
| { | |||||
| public class StatusMessage | |||||
| { | |||||
| public string Id { get; set; } | |||||
| public string M { get; set; } | |||||
| } | |||||
| } |
| app.SetupData(); | app.SetupData(); | ||||
| app.MapHub<ChatHub>("/chatHub"); | app.MapHub<ChatHub>("/chatHub"); | ||||
| app.MapHub<ConnectionHub>("/statusHub"); | |||||
| app.Run(); | app.Run(); |
| "@csstools/postcss-stepped-value-functions": "^1.0.0", | "@csstools/postcss-stepped-value-functions": "^1.0.0", | ||||
| "@csstools/postcss-trigonometric-functions": "^1.0.1", | "@csstools/postcss-trigonometric-functions": "^1.0.1", | ||||
| "@csstools/postcss-unset-value": "^1.0.1", | "@csstools/postcss-unset-value": "^1.0.1", | ||||
| "autoprefixer": "10.4.5", | |||||
| "autoprefixer": "^10.4.7", | |||||
| "browserslist": "^4.21.0", | "browserslist": "^4.21.0", | ||||
| "css-blank-pseudo": "^3.0.3", | "css-blank-pseudo": "^3.0.3", | ||||
| "css-has-pseudo": "^3.0.4", | "css-has-pseudo": "^3.0.4", |
| import { UserContext } from "../contexts/userContext"; | import { UserContext } from "../contexts/userContext"; | ||||
| import { useSelector, useDispatch } from "react-redux"; | import { useSelector, useDispatch } from "react-redux"; | ||||
| import { chatActions } from "../store/chat-slice"; | import { chatActions } from "../store/chat-slice"; | ||||
| import { BsCircleFill } from 'react-icons/bs' | |||||
| const ChatWindow = ({ room }) => { | const ChatWindow = ({ room }) => { | ||||
| const messagesEndRef = useRef(null); | const messagesEndRef = useRef(null); | ||||
| }, [dispatch, room.messages]); | }, [dispatch, room.messages]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| scrollToBottom(); | |||||
| // scrollToBottom(); | |||||
| }, []); | }, []); | ||||
| const leftRoomHandler = async () => { | const leftRoomHandler = async () => { | ||||
| setMessage(""); | setMessage(""); | ||||
| }; | }; | ||||
| const status = useSelector((s) => s.status); | |||||
| const { activeUsers } = status; | |||||
| return ( | return ( | ||||
| <div className="px-3 bg-light-transparent rounded h-100 d-flex flex-column"> | <div className="px-3 bg-light-transparent rounded h-100 d-flex flex-column"> | ||||
| <div style={{ height: "80px" }}> | <div style={{ height: "80px" }}> | ||||
| {/* {n.message} */} | {/* {n.message} */} | ||||
| {n.content} | {n.content} | ||||
| </p> | </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> | </div> | ||||
| )) | )) | ||||
| .reverse()} | .reverse()} |
| 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 ( | 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> | </div> | ||||
| ) | |||||
| } | |||||
| ); | |||||
| }; | |||||
| export default MiddleContainer | |||||
| export default MiddleContainer; |
| import { createContext, useEffect, useState } from "react"; | import { createContext, useEffect, useState } from "react"; | ||||
| import { useNavigate } from "react-router-dom"; | import { useNavigate } from "react-router-dom"; | ||||
| import axios from "axios"; | import axios from "axios"; | ||||
| import { useSelector } from "react-redux"; | |||||
| import { HubConnectionBuilder } from "@microsoft/signalr"; | |||||
| export const UserContext = createContext(); | export const UserContext = createContext(); | ||||
| export const UserProvider = (props) => { | export const UserProvider = (props) => { | ||||
| const [user, setUser] = useState(null); | const [user, setUser] = useState(null); | ||||
| const navigate = useNavigate(); | 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'))) { | if (JSON.parse(localStorage.getItem('user'))) { | ||||
| axios.defaults.headers.common['Authorization'] = `Bearer ${JSON.parse(localStorage.getItem('user')).token}`; | axios.defaults.headers.common['Authorization'] = `Bearer ${JSON.parse(localStorage.getItem('user')).token}`; | ||||
| } | } | ||||
| }, []); | }, []); | ||||
| 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 = () => { | const logOut = () => { | ||||
| localStorage.removeItem("user"); | localStorage.removeItem("user"); | ||||
| localStorage.removeItem("activeOnes"); | |||||
| disconnect(); | |||||
| setUser(null); | setUser(null); | ||||
| navigate("/login"); | navigate("/login"); | ||||
| }; | }; |
| import { uiReducers } from "./ui-slice"; | import { uiReducers } from "./ui-slice"; | ||||
| import { chatReducers } from "./chat-slice"; | import { chatReducers } from "./chat-slice"; | ||||
| import { requestsReducers } from "./request-slice"; | import { requestsReducers } from "./request-slice"; | ||||
| import { statusReducers } from "./status-slice"; | |||||
| const store = configureStore({ | const store = configureStore({ | ||||
| reducer: { ui: uiReducers, chat: chatReducers, requests: requestsReducers }, | |||||
| reducer: { ui: uiReducers, chat: chatReducers, requests: requestsReducers, status: statusReducers }, | |||||
| middleware: (getDefaultMiddleware) => | middleware: (getDefaultMiddleware) => | ||||
| getDefaultMiddleware({ | getDefaultMiddleware({ | ||||
| serializableCheck: false, | serializableCheck: false, |
| 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; |