| namespace Diligent.WebAPI.Host.Hubs | |||||
| { | |||||
| public class Attachment | |||||
| { | |||||
| public string Name { get; set; } | |||||
| } | |||||
| } |
| // All other users will receive notification | // All other users will receive notification | ||||
| if (_connections.TryGetValue(message.ConnId, out UserConnection room)) | if (_connections.TryGetValue(message.ConnId, out UserConnection room)) | ||||
| { | { | ||||
| await Clients.Group(room.RoomId).ReceiveMessage(new ChatMessage { UserId = room.UserId, User = room.UserId, Message = message.Message, RoomId = room.RoomId }); | |||||
| await Clients.Group(room.RoomId).ReceiveMessage(new ChatMessage { UserId = room.UserId, User = room.UserId, Message = message.Message, RoomId = room.RoomId, Username = room.Username }); | |||||
| await _mediator.Send(new AddMessageCommand(room.RoomId, new Message { Content = message.Message, SenderId = message.UserId, Username = room.Username })); | await _mediator.Send(new AddMessageCommand(room.RoomId, new Message { Content = message.Message, SenderId = message.UserId, Username = room.Username })); | ||||
| // Find other users in room and save notification in database | // Find other users in room and save notification in database | ||||
| { | { | ||||
| await Groups.AddToGroupAsync(Context.ConnectionId, userConnection.RoomId); | await Groups.AddToGroupAsync(Context.ConnectionId, userConnection.RoomId); | ||||
| _connections[Context.ConnectionId] = userConnection; | _connections[Context.ConnectionId] = userConnection; | ||||
| await Clients.Group(userConnection.RoomId).ReceiveMessage(new ChatMessage { UserId = userConnection.UserId, User = userConnection.UserId, RoomId = userConnection.RoomId, Message = $"{userConnection.Username} has joined room", ConnId = Context.ConnectionId,IsAccessMessage = true }); | |||||
| await Clients.Group(userConnection.RoomId).ReceiveMessage(new ChatMessage { UserId = userConnection.UserId, User = userConnection.UserId, RoomId = userConnection.RoomId, Message = $"{userConnection.Username} has joined room", ConnId = Context.ConnectionId,IsAccessMessage = true, Username = userConnection.Username }); | |||||
| } | } | ||||
| else | else | ||||
| { | { | ||||
| { | { | ||||
| // Find user connection in connections dictionary and delete it | // Find user connection in connections dictionary and delete it | ||||
| _connections.Remove(Context.ConnectionId); | _connections.Remove(Context.ConnectionId); | ||||
| await Clients.OthersInGroup(room.RoomId).ReceiveMessage(new ChatMessage { UserId = room.UserId, User = room.UserId, Message = $"{room.Username} has left room", RoomId = room.RoomId,IsAccessMessage = true }); | |||||
| await Clients.OthersInGroup(room.RoomId).ReceiveMessage(new ChatMessage { UserId = room.UserId, User = room.UserId, Message = $"{room.Username} has left room", RoomId = room.RoomId,IsAccessMessage = true, Username = room.Username }); | |||||
| await _mediator.Send(new RemoveUserFromGroupCommand(room.RoomId, room.UserId)); | await _mediator.Send(new RemoveUserFromGroupCommand(room.RoomId, room.UserId)); | ||||
| } | } | ||||
| } | } |
| public string? User { get; set; } | public string? User { get; set; } | ||||
| public string Message { get; set; } | public string Message { get; set; } | ||||
| public DateTime Created { get; set; } = DateTime.Now; | public DateTime Created { get; set; } = DateTime.Now; | ||||
| public Attachment? Attachment { get; set; } | |||||
| public bool IsAccessMessage { get; set; } = false; | public bool IsAccessMessage { get; set; } = false; | ||||
| // Context.ConnectionId generated by SignalR | // Context.ConnectionId generated by SignalR | ||||
| public string ConnId { get; set; } | public string ConnId { get; set; } | ||||
| public string RoomId { get; set; } | public string RoomId { get; set; } | ||||
| public string Username { get; set; } | |||||
| } | } | ||||
| } | } |
| export const getMonth = (monthNumber) => { | |||||
| switch (monthNumber) { | |||||
| case 0: | |||||
| return "January"; | |||||
| case 1: | |||||
| return "February"; | |||||
| case 2: | |||||
| return "March"; | |||||
| case 3: | |||||
| return "April"; | |||||
| case 4: | |||||
| return "May"; | |||||
| case 5: | |||||
| return "June"; | |||||
| case 6: | |||||
| return "July"; | |||||
| case 7: | |||||
| return "August"; | |||||
| case 8: | |||||
| return "September"; | |||||
| case 9: | |||||
| return "October"; | |||||
| case 10: | |||||
| return "November"; | |||||
| case 11: | |||||
| return "December"; | |||||
| default: | |||||
| return "January"; | |||||
| } | |||||
| }; | |||||
| export const getDate = (date) => { | |||||
| return new Date(Date.parse(date)); | |||||
| }; |
| }; | }; | ||||
| const showRoomMessagesHandler = (n) => { | const showRoomMessagesHandler = (n) => { | ||||
| console.log("??"); | |||||
| dispatch(chatActions.readNotifications(n.id)); | dispatch(chatActions.readNotifications(n.id)); | ||||
| dispatch(chatActions.setRoom(n)); | dispatch(chatActions.setRoom(n)); | ||||
| }; | }; | ||||
| dispatch( | dispatch( | ||||
| chatActions.saveContextId({ connId: data.connId, userId: user.id }) | chatActions.saveContextId({ connId: data.connId, userId: user.id }) | ||||
| ); | ); | ||||
| console.log("Join room", data); | |||||
| setChatMessage(data); | setChatMessage(data); | ||||
| } | } | ||||
| }); | }); | ||||
| } | } | ||||
| }); | }); | ||||
| // When user changed room, array with messages from previous room will be deleted from redux | // When user changed room, array with messages from previous room will be deleted from redux | ||||
| dispatch(chatActions.newMessage({ changedRoom: true })); | |||||
| // dispatch( | |||||
| // chatActions.newMessage({ changedRoom: true, room: activeRoom.id }) | |||||
| // ); | |||||
| connection.onclose((e) => { | connection.onclose((e) => { | ||||
| // On close connection | // On close connection | ||||
| chatActions.saveContextId({ connId: data.connId, userId: user.id }) | chatActions.saveContextId({ connId: data.connId, userId: user.id }) | ||||
| ); | ); | ||||
| } | } | ||||
| console.log("Send group message", data); | |||||
| setChatMessage(data); | setChatMessage(data); | ||||
| }); | }); | ||||
| } | } | ||||
| if (chatMessage && activeRoom.id === chatMessage.roomId) { | if (chatMessage && activeRoom.id === chatMessage.roomId) { | ||||
| dispatch( | dispatch( | ||||
| chatActions.newMessage({ | chatActions.newMessage({ | ||||
| content: chatMessage.message, | |||||
| createdAtUtc: new Date(), | |||||
| deletedAtUtc: null, | |||||
| id: null, | |||||
| senderId: chatMessage.userId, | |||||
| updatedAtUtc: null, | |||||
| username: user.username, | |||||
| isAccessMessage: chatMessage.isAccessMessage, | |||||
| message: { | |||||
| content: chatMessage.message, | |||||
| createdAtUtc: new Date(), | |||||
| deletedAtUtc: null, | |||||
| id: null, | |||||
| senderId: chatMessage.userId, | |||||
| updatedAtUtc: null, | |||||
| username: chatMessage.username, | |||||
| isAccessMessage: chatMessage.isAccessMessage, | |||||
| }, | |||||
| room: activeRoom.id, | |||||
| }) | }) | ||||
| ); | ); | ||||
| } | } | ||||
| <div | <div | ||||
| className="border-bottom roomsBtn d-flex bg-light" | className="border-bottom roomsBtn d-flex bg-light" | ||||
| key={index} | key={index} | ||||
| onClick={() => joinRoom(room)} | |||||
| onClick={() => { | |||||
| joinRoom(room); | |||||
| showRoomMessagesHandler(room); | |||||
| }} | |||||
| > | > | ||||
| <button | <button | ||||
| className="text-start w-100 py-2 px-3 btn btn-light h-100" | className="text-start w-100 py-2 px-3 btn btn-light h-100" | ||||
| onClick={showRoomMessagesHandler.bind(this, room)} | |||||
| // onClick={showRoomMessagesHandler.bind(this, room)} | |||||
| > | > | ||||
| {room.name} | {room.name} | ||||
| </button> | </button> | ||||
| <div | <div | ||||
| className="border-bottom roomsBtn d-flex bg-light" | className="border-bottom roomsBtn d-flex bg-light" | ||||
| key={index} | key={index} | ||||
| onClick={() => joinRoom(n)} | |||||
| onClick={() => { | |||||
| joinRoom(n); | |||||
| showRoomMessagesHandler(n); | |||||
| }} | |||||
| > | > | ||||
| {notificationCounter(n) && ( | {notificationCounter(n) && ( | ||||
| <div className="notification rounded-circle my-auto ms-3"> | <div className="notification rounded-circle my-auto ms-3"> | ||||
| )} | )} | ||||
| <button | <button | ||||
| className="text-start w-100 py-2 px-3 btn btn-light h-100" | className="text-start w-100 py-2 px-3 btn btn-light h-100" | ||||
| onClick={showRoomMessagesHandler.bind(this, n)} | |||||
| // onClick={showRoomMessagesHandler.bind(this, n)} | |||||
| > | > | ||||
| {n.name} | {n.name} | ||||
| </button> | </button> |
| import { chatActions } from "../store/chat-slice"; | import { chatActions } from "../store/chat-slice"; | ||||
| import { BsCircleFill } from "react-icons/bs"; | import { BsCircleFill } from "react-icons/bs"; | ||||
| import TypingBar from "./TypingBar"; | import TypingBar from "./TypingBar"; | ||||
| import { fetchChatRoomsAsync } from "../store/chat-slice"; | |||||
| import { getDate, getMonth } from "../Helpers"; | |||||
| const ChatWindow = ({ room }) => { | const ChatWindow = ({ room }) => { | ||||
| const messagesEndRef = useRef(null); | const messagesEndRef = useRef(null); | ||||
| const connection = useSelector((state) => state.chat.connection); | const connection = useSelector((state) => state.chat.connection); | ||||
| const connections = useSelector((state) => state.chat.connections); | const connections = useSelector((state) => state.chat.connections); | ||||
| const activeRoom = useSelector((state) => state.chat.activeRoom); | const activeRoom = useSelector((state) => state.chat.activeRoom); | ||||
| const roomStatus = useSelector((state) => state.chat.status); | |||||
| const messages = useSelector((state) => state.chat.messages); | const messages = useSelector((state) => state.chat.messages); | ||||
| const trackedRoom = useSelector((state) => state.chat.trackedRoom); | |||||
| const [proba, setProba] = useState([]); | |||||
| const dispatch = useDispatch(); | const dispatch = useDispatch(); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| dispatch(chatActions.setMessages(room.messages)); | |||||
| }, [dispatch, room.messages]); | |||||
| if (trackedRoom) { | |||||
| showDate(); | |||||
| } | |||||
| }, [trackedRoom, messages]); | |||||
| const showDate = () => { | |||||
| let temp = [...trackedRoom.messages]; | |||||
| if (temp[0] !== undefined) { | |||||
| let date = getDate(temp[0].createdAtUtc); | |||||
| let d = date.getDate(); | |||||
| let m = date.getMonth(); | |||||
| let y = date.getFullYear(); | |||||
| temp.splice(0, 0, { | |||||
| content: getMonth(m) + " " + d.toString() + ", " + y.toString(), | |||||
| senderId: undefined, //is information about date not message | |||||
| username: undefined, | |||||
| id: null, | |||||
| createdAtUtc: undefined, | |||||
| }); | |||||
| for (let index = 0; index < temp.length; index++) { | |||||
| const element = temp[index]; | |||||
| let day = getDate(element.createdAtUtc).getDate(); | |||||
| if (temp[index + 1] !== undefined) { | |||||
| if (temp[index + 1].createdAtUtc !== undefined) { | |||||
| if (temp[index].isAccessMessage === undefined) { | |||||
| if (getDate(temp[index + 1].createdAtUtc).getDate() > day) { | |||||
| let elem = temp[index + 1].createdAtUtc; | |||||
| let d = getDate(elem).getDate(); | |||||
| let m = getDate(elem).getMonth(); | |||||
| let y = getDate(elem).getFullYear(); | |||||
| temp.splice(index + 1, 0, { | |||||
| content: | |||||
| getMonth(m) + " " + d.toString() + ", " + y.toString(), | |||||
| senderId: undefined, //is information about date not message | |||||
| username: undefined, | |||||
| id: null, | |||||
| createdAtUtc: undefined, | |||||
| }); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| setProba(temp); | |||||
| }; | |||||
| useEffect(() => { | |||||
| // dispatch(chatActions.setMessages(room.messages)); | |||||
| if (user) { | |||||
| dispatch(fetchChatRoomsAsync(user.id)); | |||||
| } | |||||
| }, [dispatch, room, user]); | |||||
| useEffect(() => { | |||||
| if (roomStatus.includes("fetchRoomsFulfilled")) { | |||||
| dispatch(chatActions.setTrackedRoom(room.id)); | |||||
| } | |||||
| }, [roomStatus]); | |||||
| const leftRoomHandler = async () => { | const leftRoomHandler = async () => { | ||||
| const userToFetch = connections.filter( | const userToFetch = connections.filter( | ||||
| }); | }); | ||||
| }; | }; | ||||
| const calculateMinutes = (createdAt) => { | |||||
| const date = getDate(createdAt); | |||||
| const minutes = date.getMinutes(); | |||||
| return minutes < 10 ? "0" + minutes : minutes; | |||||
| }; | |||||
| return ( | return ( | ||||
| <div className="p-2 position-relative bg-main rounded h-100 d-flex flex-column"> | <div className="p-2 position-relative bg-main rounded h-100 d-flex flex-column"> | ||||
| <div className="p-2 px-3 pt-3 position-relative bg-light rounded h-100 d-flex flex-column"> | |||||
| <div style={{ height: "60px" }}> | |||||
| <h2 className="p-0 m-0">{room.name}</h2> | |||||
| <button | |||||
| onClick={leftRoomHandler} | |||||
| className='btn btn-danger mb-2 leave-btn' | |||||
| <div className="p-2 px-3 pt-3 position-relative bg-light rounded h-100 d-flex flex-column"> | |||||
| <div style={{ height: "60px" }}> | |||||
| <h2 className="p-0 m-0">{room.name}</h2> | |||||
| <button | |||||
| onClick={leftRoomHandler} | |||||
| className="btn btn-danger mb-2 leave-btn" | |||||
| > | |||||
| Leave Room | |||||
| </button> | |||||
| </div> | |||||
| <div className="messages bg-white border "> | |||||
| <div className="overlay p-3 px-4 d-flex flex-column-reverse"> | |||||
| {/* mapirane poruke */} | |||||
| {user && | |||||
| trackedRoom && | |||||
| proba | |||||
| .map((n, index) => ( | |||||
| <div | |||||
| key={index} | |||||
| className={ | |||||
| n.isAccessMessage === true || n.senderId === undefined | |||||
| ? "d-flex flex-column align-items-center" | |||||
| : n.senderId === user.id | |||||
| ? "d-flex flex-column align-items-end" | |||||
| : "d-flex flex-column align-items-start" | |||||
| } | |||||
| > | |||||
| <div | |||||
| className={`p-2 px-4 mb-0 rounded message ${ | |||||
| n.isAccessMessage === true || n.senderId === undefined | |||||
| ? "text-muted small" | |||||
| : n.senderId !== user.id | |||||
| ? "bg-main-primary text-light chatMsg" | |||||
| : "bg-light text-dark chatMsg" | |||||
| }`} | |||||
| > | |||||
| <div style={{ display: "flex", flexDirection: "column" }}> | |||||
| {n.content} | |||||
| {n.senderId !== undefined ? ( | |||||
| <div style={{ fontSize: 12, alignSelf: "flex-end" }}> | |||||
| {getDate(n.createdAtUtc).getHours().toString()}: | |||||
| {calculateMinutes(n.createdAtUtc)} | |||||
| </div> | |||||
| ) : ( | |||||
| "" | |||||
| )} | |||||
| </div> | |||||
| </div> | |||||
| <p className="text-muted small m-0 p-0 mb-4"> | |||||
| {n.senderId !== user?.id && | |||||
| !n.isAccessMessage && | |||||
| n.senderId !== undefined ? ( | |||||
| activeUsers.some((m) => m === n.senderId) ? ( | |||||
| <BsCircleFill className="me-2 text-success" /> | |||||
| ) : ( | |||||
| <BsCircleFill className="me-2 text-danger" /> | |||||
| ) | |||||
| ) : ( | |||||
| "" | |||||
| )} | |||||
| {n.isAccessMessage !== true && | |||||
| n.senderId !== user?.id && | |||||
| n.username} | |||||
| </p> | |||||
| </div> | |||||
| )) | |||||
| .reverse()} | |||||
| <div ref={messagesEndRef} /> | |||||
| </div> | |||||
| </div> | |||||
| <TypingBar id={activeRoom.id} /> | |||||
| <Form | |||||
| style={{ height: "80px" }} | |||||
| className="d-flex align-items-center" | |||||
| onSubmit={onSendMessageToGroupHandler} | |||||
| > | > | ||||
| Leave Room | |||||
| </button> | |||||
| </div> | |||||
| <div className="messages bg-white border "> | |||||
| <div className="overlay p-3 px-4 d-flex flex-column-reverse"> | |||||
| {/* mapirane poruke */} | |||||
| {user && | |||||
| messages | |||||
| .map((n, index) => ( | |||||
| <div | |||||
| key={index} | |||||
| className={ | |||||
| n.isAccessMessage === true | |||||
| ? "d-flex flex-column align-items-center" | |||||
| : n.senderId === user.id | |||||
| ? "d-flex flex-column align-items-end" | |||||
| : "d-flex flex-column align-items-start" | |||||
| } | |||||
| > | |||||
| {console.log("Message", n, "User id", user.id)} | |||||
| <p | |||||
| className={`p-2 px-4 mb-0 rounded message ${ | |||||
| n.isAccessMessage === true | |||||
| ? "text-muted small" | |||||
| : n.senderId !== user.id | |||||
| ? "bg-main-primary text-light chatMsg" | |||||
| : "bg-light text-dark chatMsg" | |||||
| }`} | |||||
| > | |||||
| {n.content} | |||||
| </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.isAccessMessage !== true && n.senderId !== user?.id && n.username} | |||||
| </p> | |||||
| </div> | |||||
| )) | |||||
| .reverse()} | |||||
| <div ref={messagesEndRef} /></div> | |||||
| <InputGroup className="mb-3"> | |||||
| <FormControl | |||||
| placeholder="Enter your messagge..." | |||||
| aria-label="Enter your messagge..." | |||||
| aria-describedby="basic-addon2" | |||||
| onChange={(e) => messageOnChangeHandler(e)} | |||||
| value={message} | |||||
| /> | |||||
| <Button | |||||
| className="px-5" | |||||
| variant="outline-secondary" | |||||
| id="button-addon2" | |||||
| type="submit" | |||||
| > | |||||
| Send | |||||
| </Button> | |||||
| </InputGroup> | |||||
| </Form> | |||||
| </div> | </div> | ||||
| <TypingBar id={activeRoom.id} /> | |||||
| <Form | |||||
| style={{ height: "80px" }} | |||||
| className="d-flex align-items-center" | |||||
| onSubmit={onSendMessageToGroupHandler} | |||||
| > | |||||
| <InputGroup className="mb-3"> | |||||
| <FormControl | |||||
| placeholder="Enter your messagge..." | |||||
| aria-label="Enter your messagge..." | |||||
| aria-describedby="basic-addon2" | |||||
| onChange={(e) => messageOnChangeHandler(e)} | |||||
| value={message} | |||||
| /> | |||||
| <Button | |||||
| className="px-5" | |||||
| variant="outline-secondary" | |||||
| id="button-addon2" | |||||
| type="submit" | |||||
| > | |||||
| Send | |||||
| </Button> | |||||
| </InputGroup> | |||||
| </Form> | |||||
| </div> | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| }; | }; |
| connections: [], | connections: [], | ||||
| notifications: [], | notifications: [], | ||||
| typings: [], | typings: [], | ||||
| trackedRoom: null, | |||||
| }; | }; | ||||
| export const fetchChatRoomsAsync = createAsyncThunk( | export const fetchChatRoomsAsync = createAsyncThunk( | ||||
| }, | }, | ||||
| // New message sent from user | // New message sent from user | ||||
| newMessage: (state, action) => { | newMessage: (state, action) => { | ||||
| if (action.payload.changedRoom) { | |||||
| state.messages = []; | |||||
| } else { | |||||
| state.messages = [...state.messages, action.payload]; | |||||
| } | |||||
| // if (action.payload.changedRoom) { | |||||
| // state.messages = []; | |||||
| // } else { | |||||
| // state.messages = [...state.messages, action.payload]; | |||||
| // } | |||||
| console.log(action.payload); | |||||
| const room = state.rooms.find((r) => r.id === action.payload.room); | |||||
| room.messages.push(action.payload.message); | |||||
| state.trackedRoom.messages = [ | |||||
| ...state.trackedRoom.messages, | |||||
| action.payload.message, | |||||
| ]; | |||||
| }, | }, | ||||
| saveContextId: (state, action) => { | saveContextId: (state, action) => { | ||||
| // Check is empty array | // Check is empty array | ||||
| state.messages = action.payload; | state.messages = action.payload; | ||||
| }, | }, | ||||
| addNotification: (state, action) => { | addNotification: (state, action) => { | ||||
| console.log(1, action.payload); | |||||
| if (state.notifications.length !== 0) { | if (state.notifications.length !== 0) { | ||||
| console.log(2); | |||||
| const room = state.notifications.find( | const room = state.notifications.find( | ||||
| (notification) => notification.roomId === action.payload | (notification) => notification.roomId === action.payload | ||||
| ); | ); | ||||
| if (room) { | if (room) { | ||||
| console.log(3); | |||||
| room.notificationCount++; | room.notificationCount++; | ||||
| } else { | } else { | ||||
| state.notifications.push({ | state.notifications.push({ | ||||
| }); | }); | ||||
| } | } | ||||
| } else { | } else { | ||||
| console.log(4); | |||||
| state.notifications.push({ | state.notifications.push({ | ||||
| roomId: action.payload, | roomId: action.payload, | ||||
| notificationCount: 1, | notificationCount: 1, | ||||
| state.typings = state.typings.filter((n) => n.message !== f.message); | state.typings = state.typings.filter((n) => n.message !== f.message); | ||||
| } | } | ||||
| }, | }, | ||||
| setTrackedRoom: (state, action) => { | |||||
| state.trackedRoom = state.rooms.find((r) => r.id === action.payload); | |||||
| }, | |||||
| }, | }, | ||||
| extraReducers: (builder) => { | extraReducers: (builder) => { | ||||
| // Fetch chat rooms | // Fetch chat rooms | ||||
| }); | }); | ||||
| builder.addCase(fetchChatRoomsAsync.fulfilled, (state, action) => { | builder.addCase(fetchChatRoomsAsync.fulfilled, (state, action) => { | ||||
| state.rooms = action.payload; | state.rooms = action.payload; | ||||
| state.status = "idle"; | |||||
| state.status = "fetchRoomsFulfilled idle"; | |||||
| state.error = null; | state.error = null; | ||||
| }); | }); | ||||
| builder.addCase(fetchChatRoomsAsync.rejected, (state, action) => { | builder.addCase(fetchChatRoomsAsync.rejected, (state, action) => { |