| @@ -1,7 +0,0 @@ | |||
| namespace Diligent.WebAPI.Host.Hubs | |||
| { | |||
| public class Attachment | |||
| { | |||
| public string Name { get; set; } | |||
| } | |||
| } | |||
| @@ -30,7 +30,7 @@ namespace Diligent.WebAPI.Host.Hubs | |||
| // All other users will receive notification | |||
| 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 })); | |||
| // Find other users in room and save notification in database | |||
| @@ -59,7 +59,7 @@ namespace Diligent.WebAPI.Host.Hubs | |||
| { | |||
| await Groups.AddToGroupAsync(Context.ConnectionId, userConnection.RoomId); | |||
| _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 | |||
| { | |||
| @@ -75,7 +75,7 @@ namespace Diligent.WebAPI.Host.Hubs | |||
| { | |||
| // Find user connection in connections dictionary and delete it | |||
| _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)); | |||
| } | |||
| } | |||
| @@ -6,10 +6,10 @@ | |||
| public string? User { get; set; } | |||
| public string Message { get; set; } | |||
| public DateTime Created { get; set; } = DateTime.Now; | |||
| public Attachment? Attachment { get; set; } | |||
| public bool IsAccessMessage { get; set; } = false; | |||
| // Context.ConnectionId generated by SignalR | |||
| public string ConnId { get; set; } | |||
| public string RoomId { get; set; } | |||
| public string Username { get; set; } | |||
| } | |||
| } | |||
| @@ -0,0 +1,46 @@ | |||
| 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)); | |||
| }; | |||
| @@ -78,6 +78,7 @@ const ChatList = () => { | |||
| }; | |||
| const showRoomMessagesHandler = (n) => { | |||
| console.log("??"); | |||
| dispatch(chatActions.readNotifications(n.id)); | |||
| dispatch(chatActions.setRoom(n)); | |||
| }; | |||
| @@ -97,7 +98,6 @@ const ChatList = () => { | |||
| dispatch( | |||
| chatActions.saveContextId({ connId: data.connId, userId: user.id }) | |||
| ); | |||
| console.log("Join room", data); | |||
| setChatMessage(data); | |||
| } | |||
| }); | |||
| @@ -113,7 +113,9 @@ const ChatList = () => { | |||
| } | |||
| }); | |||
| // 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) => { | |||
| // On close connection | |||
| @@ -143,7 +145,6 @@ const ChatList = () => { | |||
| chatActions.saveContextId({ connId: data.connId, userId: user.id }) | |||
| ); | |||
| } | |||
| console.log("Send group message", data); | |||
| setChatMessage(data); | |||
| }); | |||
| } | |||
| @@ -154,14 +155,17 @@ const ChatList = () => { | |||
| if (chatMessage && activeRoom.id === chatMessage.roomId) { | |||
| dispatch( | |||
| 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, | |||
| }) | |||
| ); | |||
| } | |||
| @@ -233,11 +237,14 @@ const ChatList = () => { | |||
| <div | |||
| className="border-bottom roomsBtn d-flex bg-light" | |||
| key={index} | |||
| onClick={() => joinRoom(room)} | |||
| onClick={() => { | |||
| joinRoom(room); | |||
| showRoomMessagesHandler(room); | |||
| }} | |||
| > | |||
| <button | |||
| 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} | |||
| </button> | |||
| @@ -255,7 +262,10 @@ const ChatList = () => { | |||
| <div | |||
| className="border-bottom roomsBtn d-flex bg-light" | |||
| key={index} | |||
| onClick={() => joinRoom(n)} | |||
| onClick={() => { | |||
| joinRoom(n); | |||
| showRoomMessagesHandler(n); | |||
| }} | |||
| > | |||
| {notificationCounter(n) && ( | |||
| <div className="notification rounded-circle my-auto ms-3"> | |||
| @@ -264,7 +274,7 @@ const ChatList = () => { | |||
| )} | |||
| <button | |||
| 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} | |||
| </button> | |||
| @@ -5,6 +5,8 @@ import { useSelector, useDispatch } from "react-redux"; | |||
| import { chatActions } from "../store/chat-slice"; | |||
| import { BsCircleFill } from "react-icons/bs"; | |||
| import TypingBar from "./TypingBar"; | |||
| import { fetchChatRoomsAsync } from "../store/chat-slice"; | |||
| import { getDate, getMonth } from "../Helpers"; | |||
| const ChatWindow = ({ room }) => { | |||
| const messagesEndRef = useRef(null); | |||
| @@ -14,12 +16,81 @@ const ChatWindow = ({ room }) => { | |||
| const connection = useSelector((state) => state.chat.connection); | |||
| const connections = useSelector((state) => state.chat.connections); | |||
| const activeRoom = useSelector((state) => state.chat.activeRoom); | |||
| const roomStatus = useSelector((state) => state.chat.status); | |||
| const messages = useSelector((state) => state.chat.messages); | |||
| const trackedRoom = useSelector((state) => state.chat.trackedRoom); | |||
| const [proba, setProba] = useState([]); | |||
| const dispatch = useDispatch(); | |||
| 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 userToFetch = connections.filter( | |||
| @@ -67,92 +138,116 @@ const ChatWindow = ({ room }) => { | |||
| }); | |||
| }; | |||
| const calculateMinutes = (createdAt) => { | |||
| const date = getDate(createdAt); | |||
| const minutes = date.getMinutes(); | |||
| return minutes < 10 ? "0" + minutes : minutes; | |||
| }; | |||
| return ( | |||
| <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> | |||
| <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> | |||
| ); | |||
| }; | |||
| @@ -12,6 +12,8 @@ const initialState = { | |||
| connections: [], | |||
| notifications: [], | |||
| typings: [], | |||
| trackedRoom: null, | |||
| }; | |||
| export const fetchChatRoomsAsync = createAsyncThunk( | |||
| @@ -88,11 +90,19 @@ const chatSlice = createSlice({ | |||
| }, | |||
| // New message sent from user | |||
| 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) => { | |||
| // Check is empty array | |||
| @@ -123,15 +133,12 @@ const chatSlice = createSlice({ | |||
| state.messages = action.payload; | |||
| }, | |||
| addNotification: (state, action) => { | |||
| console.log(1, action.payload); | |||
| if (state.notifications.length !== 0) { | |||
| console.log(2); | |||
| const room = state.notifications.find( | |||
| (notification) => notification.roomId === action.payload | |||
| ); | |||
| if (room) { | |||
| console.log(3); | |||
| room.notificationCount++; | |||
| } else { | |||
| state.notifications.push({ | |||
| @@ -140,7 +147,6 @@ const chatSlice = createSlice({ | |||
| }); | |||
| } | |||
| } else { | |||
| console.log(4); | |||
| state.notifications.push({ | |||
| roomId: action.payload, | |||
| notificationCount: 1, | |||
| @@ -180,6 +186,9 @@ const chatSlice = createSlice({ | |||
| 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) => { | |||
| // Fetch chat rooms | |||
| @@ -189,7 +198,7 @@ const chatSlice = createSlice({ | |||
| }); | |||
| builder.addCase(fetchChatRoomsAsync.fulfilled, (state, action) => { | |||
| state.rooms = action.payload; | |||
| state.status = "idle"; | |||
| state.status = "fetchRoomsFulfilled idle"; | |||
| state.error = null; | |||
| }); | |||
| builder.addCase(fetchChatRoomsAsync.rejected, (state, action) => { | |||