Преглед изворни кода

Online status for user

feature/314_online_status_for_user
mer1s пре 3 година
родитељ
комит
79d13c03cb

+ 6
- 0
.vs/VSWorkspaceState.json Прегледај датотеку

@@ -0,0 +1,6 @@
{
"ExpandedNodes": [
""
],
"PreviewInSolutionExplorer": false
}

BIN
.vs/WebAPISignalRChat/v17/.suo Прегледај датотеку



+ 44
- 0
Backend/Diligent.WebAPI.Host/Hubs/ConnectionHub.cs Прегледај датотеку

@@ -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();
//}
}
}

+ 8
- 0
Backend/Diligent.WebAPI.Host/Hubs/SenderObj.cs Прегледај датотеку

@@ -0,0 +1,8 @@
namespace Diligent.WebAPI.Host.Hubs
{
public class SenderObj
{
public string Id { get; set; }
public string ConnId { get; set; }
}
}

+ 8
- 0
Backend/Diligent.WebAPI.Host/Hubs/StatusMessage.cs Прегледај датотеку

@@ -0,0 +1,8 @@
namespace Diligent.WebAPI.Host.Hubs
{
public class StatusMessage
{
public string Id { get; set; }
public string M { get; set; }
}
}

+ 1
- 0
Backend/Diligent.WebAPI.Host/Program.cs Прегледај датотеку

@@ -17,5 +17,6 @@ app.UseAuthorization();
app.SetupData();

app.MapHub<ChatHub>("/chatHub");
app.MapHub<ConnectionHub>("/statusHub");

app.Run();

+ 1
- 1
Frontend/package-lock.json Прегледај датотеку

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

+ 17
- 2
Frontend/src/components/ChatWindow.js Прегледај датотеку

@@ -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()}

+ 63
- 12
Frontend/src/components/MiddleContainer.js Прегледај датотеку

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

+ 40
- 0
Frontend/src/contexts/userContext.js Прегледај датотеку

@@ -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
- 1
Frontend/src/store/index.js Прегледај датотеку

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

+ 53
- 0
Frontend/src/store/status-slice.js Прегледај датотеку

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

Loading…
Откажи
Сачувај