diff --git a/Messenger.BusinessLogic/ApiCommands/Chats/CreateChatCommandHandler.cs b/Messenger.BusinessLogic/ApiCommands/Chats/CreateChatCommandHandler.cs index 8b0b3f5..59eeefe 100644 --- a/Messenger.BusinessLogic/ApiCommands/Chats/CreateChatCommandHandler.cs +++ b/Messenger.BusinessLogic/ApiCommands/Chats/CreateChatCommandHandler.cs @@ -75,6 +75,7 @@ public async Task> Handle(CreateChatCommand request, Cancellatio AvatarLink = avatarLink, MembersCount = 1, CanSendMedia = true, + OwnerId = requester.Id, IsOwner = true, IsMember = true }; diff --git a/Messenger.BusinessLogic/ApiCommands/Chats/JoinToChatCommandHandler.cs b/Messenger.BusinessLogic/ApiCommands/Chats/JoinToChatCommandHandler.cs index bcb832a..e31be03 100644 --- a/Messenger.BusinessLogic/ApiCommands/Chats/JoinToChatCommandHandler.cs +++ b/Messenger.BusinessLogic/ApiCommands/Chats/JoinToChatCommandHandler.cs @@ -79,6 +79,7 @@ public async Task> Handle(JoinToChatCommand request, Cancellatio ? chat.LastMessage.Owner.DisplayName : null, LastMessageDateOfCreate = chat.LastMessage?.DateOfCreate, + OwnerId = chat.OwnerId, IsOwner = chat.OwnerId == request.RequesterId, IsMember = true, MembersCount = memberCount, diff --git a/Messenger.BusinessLogic/ApiCommands/Chats/LeaveFromChatCommandHandler.cs b/Messenger.BusinessLogic/ApiCommands/Chats/LeaveFromChatCommandHandler.cs index c7c9407..71224f9 100644 --- a/Messenger.BusinessLogic/ApiCommands/Chats/LeaveFromChatCommandHandler.cs +++ b/Messenger.BusinessLogic/ApiCommands/Chats/LeaveFromChatCommandHandler.cs @@ -58,6 +58,7 @@ public async Task> Handle(LeaveFromChatCommand request, Cancella : null, LastMessageDateOfCreate = chatUser.Chat.LastMessage?.DateOfCreate, CanSendMedia = chatUser.CanSendMedia, + OwnerId = chatUser.Chat.OwnerId, IsOwner = chatUser.Chat.OwnerId == request.RequesterId, IsMember = false, MuteDateOfExpire = chatUser.MuteDateOfExpire, diff --git a/Messenger.BusinessLogic/ApiCommands/Chats/UpdateChatAvatarCommandHandler.cs b/Messenger.BusinessLogic/ApiCommands/Chats/UpdateChatAvatarCommandHandler.cs index 9694677..4bfb231 100644 --- a/Messenger.BusinessLogic/ApiCommands/Chats/UpdateChatAvatarCommandHandler.cs +++ b/Messenger.BusinessLogic/ApiCommands/Chats/UpdateChatAvatarCommandHandler.cs @@ -83,6 +83,7 @@ public async Task> Handle(UpdateChatAvatarCommand request, Cance ? chatUserByRequester.Chat.LastMessage.Owner.DisplayName : null, LastMessageDateOfCreate = chatUserByRequester.Chat.LastMessage?.DateOfCreate, + OwnerId = chatUserByRequester.Chat.OwnerId, IsOwner = chatUserByRequester.Chat.OwnerId == request.RequesterId, IsMember = true, MuteDateOfExpire = chatUserByRequester.MuteDateOfExpire, diff --git a/Messenger.BusinessLogic/ApiCommands/Chats/UpdateChatDataCommandHandler.cs b/Messenger.BusinessLogic/ApiCommands/Chats/UpdateChatDataCommandHandler.cs index 4aefceb..472d9de 100644 --- a/Messenger.BusinessLogic/ApiCommands/Chats/UpdateChatDataCommandHandler.cs +++ b/Messenger.BusinessLogic/ApiCommands/Chats/UpdateChatDataCommandHandler.cs @@ -84,6 +84,7 @@ public async Task> Handle(UpdateChatDataCommand request, Cancell LastMessageDateOfCreate = chatUserByRequester.Chat.LastMessage?.DateOfCreate, MembersCount = chatUserByRequester.Chat.ChatUsers.Count, CanSendMedia = chatUserByRequester.CanSendMedia, + OwnerId = chatUserByRequester.Chat.OwnerId, IsOwner = chatUserByRequester.Chat.OwnerId == request.RequesterId, IsMember = true, MuteDateOfExpire = chatUserByRequester.MuteDateOfExpire, diff --git a/Messenger.BusinessLogic/ApiQueries/Chats/GetChatListBySearchQueryHandler.cs b/Messenger.BusinessLogic/ApiQueries/Chats/GetChatListBySearchQueryHandler.cs index 91258ae..a33675d 100644 --- a/Messenger.BusinessLogic/ApiQueries/Chats/GetChatListBySearchQueryHandler.cs +++ b/Messenger.BusinessLogic/ApiQueries/Chats/GetChatListBySearchQueryHandler.cs @@ -53,6 +53,7 @@ where EF.Functions.Like(chat.Title, $"%{request.SearchText}%") || LastMessageDateOfCreate = chat.LastMessage != null ? chat.LastMessage.DateOfCreate : null, MembersCount = chat.ChatUsers.Count, CanSendMedia = chatUsersItem != null && chatUsersItem.CanSendMedia, + OwnerId = chat.OwnerId, IsOwner = chat.OwnerId == request.RequesterId, IsMember = chatUsersItem != null, MuteDateOfExpire = chatUsersItem != null ? chatUsersItem.MuteDateOfExpire : null, diff --git a/Messenger.BusinessLogic/ApiQueries/Chats/GetChatListQueryHandler.cs b/Messenger.BusinessLogic/ApiQueries/Chats/GetChatListQueryHandler.cs index 0d5e3bc..5489259 100644 --- a/Messenger.BusinessLogic/ApiQueries/Chats/GetChatListQueryHandler.cs +++ b/Messenger.BusinessLogic/ApiQueries/Chats/GetChatListQueryHandler.cs @@ -62,6 +62,7 @@ orderby chat.LastMessage.DateOfCreate descending LastMessageDateOfCreate = chat.LastMessage != null ? chat.LastMessage.DateOfCreate : null, MembersCount = chat.ChatUsers.Count, CanSendMedia = chatUsersItem.CanSendMedia, + OwnerId = chat.OwnerId, IsOwner = chat.OwnerId == request.RequesterId, IsMember = chatUsersItem != null, MuteDateOfExpire = chatUsersItem != null ? chatUsersItem.MuteDateOfExpire : null, diff --git a/Messenger.BusinessLogic/ApiQueries/Chats/GetChatQueryHandler.cs b/Messenger.BusinessLogic/ApiQueries/Chats/GetChatQueryHandler.cs index 5086c82..e879d2d 100644 --- a/Messenger.BusinessLogic/ApiQueries/Chats/GetChatQueryHandler.cs +++ b/Messenger.BusinessLogic/ApiQueries/Chats/GetChatQueryHandler.cs @@ -53,6 +53,7 @@ from banUserByChatItem in banUserByChatEnumerable.DefaultIfEmpty() LastMessageDateOfCreate = chat.LastMessage != null ? chat.LastMessage.DateOfCreate : null, MembersCount = chat.ChatUsers.Count, CanSendMedia = chatUsersItem != null && chatUsersItem.CanSendMedia, + OwnerId = chat.OwnerId, IsOwner = chat.OwnerId == request.RequesterId, IsMember = chatUsersItem != null, MuteDateOfExpire = chatUsersItem != null ? chatUsersItem.MuteDateOfExpire : null, diff --git a/Messenger.BusinessLogic/ApiQueries/Conversations/GetUserPermissionsQuery.cs b/Messenger.BusinessLogic/ApiQueries/Conversations/GetUserPermissionsQuery.cs new file mode 100644 index 0000000..9db7561 --- /dev/null +++ b/Messenger.BusinessLogic/ApiQueries/Conversations/GetUserPermissionsQuery.cs @@ -0,0 +1,7 @@ +using MediatR; +using Messenger.BusinessLogic.Models; +using Messenger.BusinessLogic.Responses; + +namespace Messenger.BusinessLogic.ApiQueries.Conversations; + +public record GetUserPermissionsQuery(Guid ChatId, Guid UserId) : IRequest>; \ No newline at end of file diff --git a/Messenger.BusinessLogic/ApiQueries/Conversations/GetUserPermissionsQueryHandler.cs b/Messenger.BusinessLogic/ApiQueries/Conversations/GetUserPermissionsQueryHandler.cs new file mode 100644 index 0000000..32951d3 --- /dev/null +++ b/Messenger.BusinessLogic/ApiQueries/Conversations/GetUserPermissionsQueryHandler.cs @@ -0,0 +1,32 @@ +using MediatR; +using Messenger.BusinessLogic.Models; +using Messenger.BusinessLogic.Responses; +using Messenger.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace Messenger.BusinessLogic.ApiQueries.Conversations; + +public class GetUserPermissionsQueryHandler : IRequestHandler> +{ + private readonly DatabaseContext _context; + + public GetUserPermissionsQueryHandler(DatabaseContext context) + { + _context = context; + } + + public async Task> Handle(GetUserPermissionsQuery request, CancellationToken cancellationToken) + { + var userPermissions = await _context.ChatUsers + .Where(x => x.UserId == request.UserId && x.ChatId == request.ChatId) + .Select(c => new PermissionDto(c)) + .FirstOrDefaultAsync(cancellationToken); + + if (userPermissions == null) + { + return new Result(new DbEntityNotFoundError("user permissions not found")); + } + + return new Result(userPermissions); + } +} \ No newline at end of file diff --git a/Messenger.BusinessLogic/Models/ChatDto.cs b/Messenger.BusinessLogic/Models/ChatDto.cs index 1d9f8a3..d551b7a 100644 --- a/Messenger.BusinessLogic/Models/ChatDto.cs +++ b/Messenger.BusinessLogic/Models/ChatDto.cs @@ -26,6 +26,8 @@ public class ChatDto public bool CanSendMedia { get; init; } + public Guid? OwnerId { get; set; } + public bool IsOwner { get; init; } public bool IsMember { get; init; } diff --git a/Messenger.BusinessLogic/Models/PermissionDto.cs b/Messenger.BusinessLogic/Models/PermissionDto.cs index 07e8ce3..00413bc 100644 --- a/Messenger.BusinessLogic/Models/PermissionDto.cs +++ b/Messenger.BusinessLogic/Models/PermissionDto.cs @@ -4,12 +4,18 @@ namespace Messenger.BusinessLogic.Models; public class PermissionDto { - public bool CanSendMedia { get; set; } + public Guid UserId { get; private set; } - public DateTime? MuteDateOfExpire { get; set; } + public Guid ChatId { get; private set; } + + public bool CanSendMedia { get; private set; } + + public DateTime? MuteDateOfExpire { get; private set; } public PermissionDto(ChatUserEntity chatUser) { + UserId = chatUser.UserId; + ChatId = chatUser.ChatId; CanSendMedia = chatUser.CanSendMedia; MuteDateOfExpire = chatUser.MuteDateOfExpire; } diff --git a/Messenger.BusinessLogic/Models/Requests/CreateOrUpdateRoleUserRequest.cs b/Messenger.BusinessLogic/Models/Requests/CreateOrUpdateRoleUserRequest.cs new file mode 100644 index 0000000..dffde15 --- /dev/null +++ b/Messenger.BusinessLogic/Models/Requests/CreateOrUpdateRoleUserRequest.cs @@ -0,0 +1,22 @@ +using Messenger.Domain.Enums; + +namespace Messenger.BusinessLogic.Models.Requests; + +public class CreateOrUpdateRoleUserRequest +{ + public Guid ChatId { get; set; } + + public Guid UserId { get; set; } + + public string RoleTitle { get; set; } + + public RoleColor RoleColor { get; set; } + + public bool CanBanUser { get; set; } + + public bool CanChangeChatData { get; set; } + + public bool CanAddAndRemoveUserToConversation { get; set; } + + public bool CanGivePermissionToUser { get; set; } +} \ No newline at end of file diff --git a/Messenger.BusinessLogic/Models/Requests/CreatePermissionsUserInConversationRequest.cs b/Messenger.BusinessLogic/Models/Requests/CreatePermissionsUserInConversationRequest.cs new file mode 100644 index 0000000..5d4701c --- /dev/null +++ b/Messenger.BusinessLogic/Models/Requests/CreatePermissionsUserInConversationRequest.cs @@ -0,0 +1,9 @@ +namespace Messenger.BusinessLogic.Models.Requests; + +public class CreatePermissionsUserInConversationRequest +{ + public Guid ChatId { get; set; } + public Guid UserId { get; set; } + public bool CanSendMedia { get; set; } + public int? MuteMinutes { get; set; } +} \ No newline at end of file diff --git a/Messenger.Client/src/App.scss b/Messenger.Client/src/App.scss index b3c07c1..091f962 100644 --- a/Messenger.Client/src/App.scss +++ b/Messenger.Client/src/App.scss @@ -12,7 +12,7 @@ body { $light-grey-border: rgb(185, 185, 185); $grey-font-page:rgb(56, 56, 56); $light-grey-font-page:rgb(150, 150, 150); -$light-grey: #efefef; +$light-grey: #f5f5f5; $light-grey-for-session-item: rgb(92, 90, 90); $grey: #dbdbdb; @@ -43,4 +43,22 @@ $purple-transparent-button-hover: #9533e6b7; $pink-transparent: rgba(243, 237, 245, 0.781); -$blue-transparent-for-session-item: rgb(174, 180, 243); \ No newline at end of file +$blue-transparent-for-session-item: rgb(174, 180, 243); + +$role-color-blue: rgba(0, 4, 255, 0.13); +$role-color-blue-hover: rgba(0, 4, 255, 0.18); + +$role-color-green: rgba(81, 255, 0, 0.13); +$role-color-green-hover: rgba(81, 255, 0, 0.18); + +$role-color-cyan: rgba(0, 255, 234, 0.13); +$role-color-cyan-hover: rgba(0, 255, 234, 0.25); + +$role-color-red: rgba(255, 0, 0, 0.08); +$role-color-red-hover: rgba(255, 0, 0, 0.13); + +$role-color-yellow: rgba(255, 251, 0, 0.17); +$role-color-yellow-hover: rgba(255, 251, 0, 0.13); + +$role-color-orange: rgba(255, 174, 0, 0.2); +$role-color-orange-hover: rgba(255, 196, 0, 0.15); \ No newline at end of file diff --git a/Messenger.Client/src/components/chatInfo/ChatInfo.module.scss b/Messenger.Client/src/components/chatInfo/ChatInfo.module.scss index a99dab3..ce29594 100644 --- a/Messenger.Client/src/components/chatInfo/ChatInfo.module.scss +++ b/Messenger.Client/src/components/chatInfo/ChatInfo.module.scss @@ -135,7 +135,6 @@ overflow-y: scroll; width: 100%; height: 400px; - border-radius: 0 0 20px 0; &::-webkit-scrollbar { width: 2px; @@ -150,53 +149,6 @@ } } -.memberItem { - display: flex; - border-bottom: 2px solid $grey; - transition: 0.2s; - cursor: pointer; - - &:hover { - background-color: $light-grey - } -} - -.memberItemAvatar { - width: 40px; - height: 40px; - border-radius: 100%; - margin: 5px; - margin-left: 10px; - flex-shrink: 0; - object-fit: cover; -} - -.memberItemContainer { - position: relative; - width: 100%; - height: 100%; - margin-left: 5px; -} - -.memberItemDisplayName { - font-size: 15px; - margin-top: 5px; -} - -.memberItemBio { - font-size: 12px; - margin-top: 5px; - color: $light-grey-font-page; -} - -.memberItemRole { - position: absolute; - right: 5px; - top: 5px; - font-size: 12px; - color: $grey-font-page; -} - .okButton { width: 25px; height: 25px; diff --git a/Messenger.Client/src/components/chatInfo/ChatInfo.tsx b/Messenger.Client/src/components/chatInfo/ChatInfo.tsx index dc4b8b5..6e67393 100644 --- a/Messenger.Client/src/components/chatInfo/ChatInfo.tsx +++ b/Messenger.Client/src/components/chatInfo/ChatInfo.tsx @@ -9,11 +9,10 @@ import { currentChatState } from "../../state/CurrentChatState"; import { chatListWithMessagesState } from "../../state/ChatListWithMessagesState"; import { useNavigate } from "react-router-dom"; import { blackCoverState } from "../../state/BlackCoverState"; -import { authorizationState } from "../../state/AuthorizationState"; -import { currentProfileState } from "../../state/CurrentProfileState"; import { useDebouncedCallback } from "use-debounce"; import { motion } from "framer-motion"; import RouteConstants from "../../constants/RouteConstants"; +import MemberListItem from "../memberListItem/MemberListItem"; const ChatInfo = observer(() => { const [updateMode, setUpdateMode] = useState(false); @@ -106,20 +105,6 @@ const ChatInfo = observer(() => { blackCoverState.setImage(currentChatState.chat?.avatarLink ?? nonAvatar); }; - const showProfileByMessage = async (userId: string) => { - if (userId === authorizationState.data?.id) { - currentProfileState.setProfileNull(); - - return navigate(RouteConstants.Layout, { replace: true }); - } - - await currentProfileState - .getUserAsync(userId) - .catch((error: any) => { if (error.response.status !== 401) alert(error.response.data.message); }); - - return navigate(RouteConstants.Layout, { replace: true }); - }; - const onClickShowMemberListHandler = async () => { if (!currentChatStateChat) return; @@ -266,29 +251,7 @@ const ChatInfo = observer(() => { showMemberList && (
{currentChatState.chat?.members.map((i) => ( - showProfileByMessage(i.id)}> - -
-

{i.displayName}

-

{i.bio}

-

- { - currentChatState.chat?.usersWithRole.find( - (u) => u.userId === i.id - )?.roleTitle - } -

-
-
+ ))}
) diff --git a/Messenger.Client/src/components/memberListItem/MemberListItem.module.scss b/Messenger.Client/src/components/memberListItem/MemberListItem.module.scss new file mode 100644 index 0000000..992a249 --- /dev/null +++ b/Messenger.Client/src/components/memberListItem/MemberListItem.module.scss @@ -0,0 +1,120 @@ +@import "../../App.scss"; + +.memberItem { + position: relative; + display: flex; + border-bottom: 1px solid $grey; + cursor: pointer; + + &:hover { + background-color: $light-grey + } +} + +.memberItemGreenBackground { + position: relative; + display: flex; + border-bottom: 2px solid $grey; + cursor: pointer; + background-color: $role-color-green; + + &:hover { + background-color: $role-color-green-hover; + } +} + +.memberItemBlueBackground { + position: relative; + display: flex; + border-bottom: 2px solid $grey; + cursor: pointer; + background-color: $role-color-blue; + + &:hover { + background-color: $role-color-blue-hover; + } +} + +.memberItemCyanBackground { + position: relative; + display: flex; + border-bottom: 2px solid $grey; + cursor: pointer; + background-color: $role-color-cyan; + + &:hover { + background-color: $role-color-cyan-hover; + } +} + +.memberItemRedBackground { + position: relative; + display: flex; + border-bottom: 2px solid $grey; + cursor: pointer; + background-color: $role-color-red; + + &:hover { + background-color: $role-color-red-hover; + } +} + +.memberItemYellowBackground { + position: relative; + display: flex; + border-bottom: 2px solid $grey; + cursor: pointer; + background-color: $role-color-yellow; + + &:hover { + background-color: $role-color-yellow-hover; + } +} + +.memberItemOrangeBackground { + position: relative; + display: flex; + border-bottom: 2px solid $grey; + cursor: pointer; + background-color: $role-color-orange; + + &:hover { + background-color: $role-color-orange-hover; + } +} + +.memberItemAvatar { + width: 40px; + height: 40px; + border-radius: 100%; + margin: 5px; + margin-left: 10px; + flex-shrink: 0; + object-fit: cover; +} + +.memberItemContainer { + position: relative; + width: 100%; + height: 100%; + margin-left: 5px; +} + +.memberItemDisplayName { + font-size: 15px; + margin-top: 5px; +} + +.memberItemBio { + font-size: 12px; + margin-top: 5px; + color: $light-grey-font-page; +} + +.memberItemRole { + position: absolute; + right: 5px; + top: 5px; + font-size: 13px; + color: $grey-font-page; +} \ No newline at end of file diff --git a/Messenger.Client/src/components/memberListItem/MemberListItem.tsx b/Messenger.Client/src/components/memberListItem/MemberListItem.tsx new file mode 100644 index 0000000..6f5be1a --- /dev/null +++ b/Messenger.Client/src/components/memberListItem/MemberListItem.tsx @@ -0,0 +1,122 @@ +import React, { MouseEvent, useState } from "react"; +import { motion } from "framer-motion"; +import { observer } from "mobx-react-lite"; +import { useNavigate } from "react-router-dom"; +import RouteConstants from "../../constants/RouteConstants"; +import { RoleColor } from "../../models/enum/RoleColor"; +import IUserDto from "../../models/interfaces/IUserDto"; +import { authorizationState } from "../../state/AuthorizationState"; +import { currentChatState } from "../../state/CurrentChatState"; +import { currentProfileState } from "../../state/CurrentProfileState"; +import nonAvatar from "../../assets/images/non_avatar.jpg"; +import styles from "./MemberListItem.module.scss"; +import MemberListItemBurgerMenu from "../memberListItemBurgerMenu/MemberListItemBurgerMenu"; + +const MemberListItem = observer((props: IUserDto) => { + const [xMousePosition, setXMousePosition] = useState(0); + const [yMousePosition, setYMousePosition] = useState(0); + + const [showMenu, setShowMenu] = useState(false); + + var navigate = useNavigate(); + + const currentChatStateChat = currentChatState.chat; + const memberListItem = currentChatState.chat?.members.find(x => x.id === props.id); + const areYouOwner = currentChatState.chat?.isOwner ?? false; + const youRole = currentChatState.chat?.usersWithRole.find(x => x.userId === authorizationState.data?.id) ?? null; + + const isLastItem = (item: IUserDto | undefined) => { + var array = currentChatState.chat?.members; + + if (!array || array.length === 0) { + return false; + } + + return item === array[array.length - 1]; + } + + const MouseMoveHandler = (event: React.MouseEvent) => { + const targetCoords = event.currentTarget.getBoundingClientRect(); + const localX = event.clientX - targetCoords.left; + const localY = event.clientY - targetCoords.top; + + setXMousePosition(localX); + setYMousePosition(localY); + }; + + const showMenuHandler = (event: React.MouseEvent) => { + event.preventDefault(); + setShowMenu(true); + }; + + const getMemberItemById = (userId: string) => { + var role = currentChatStateChat?.usersWithRole.find(x => x.userId === userId); + + if (!role) return styles.memberItem; + + switch (role.roleColor) { + case RoleColor.Green: return styles.memberItemGreenBackground + case RoleColor.Blue: return styles.memberItemBlueBackground + case RoleColor.Cyan: return styles.memberItemCyanBackground + case RoleColor.Red: return styles.memberItemRedBackground + case RoleColor.Yellow: return styles.memberItemYellowBackground + case RoleColor.Orange: return styles.memberItemOrangeBackground + } + } + + const showProfile = async (userId: string) => { + if (userId === authorizationState.data?.id) { + currentProfileState.setProfileNull(); + + return navigate(RouteConstants.Layout, { replace: true }); + } + + await currentProfileState + .getUserAsync(userId) + .catch((error: any) => { if (error.response.status !== 401) alert(error.response.data.message); }); + + return navigate(RouteConstants.Layout, { replace: true }); + }; + + return showProfile(props.id)} + onMouseMove={MouseMoveHandler} + onContextMenu={showMenuHandler}> + + { + showMenu && ( + + ) + } +
+

{props.displayName}

+

{props.bio}

+

+ { + currentChatStateChat?.ownerId == props.id && "owner" || + currentChatState.chat?.usersWithRole.find( + (u) => u.userId === props.id + )?.roleTitle + } +

+
+
; +}); + +export default MemberListItem; \ No newline at end of file diff --git a/Messenger.Client/src/components/memberListItemBurgerMenu/MemberListItemBurgerMenu.module.scss b/Messenger.Client/src/components/memberListItemBurgerMenu/MemberListItemBurgerMenu.module.scss index e69de29..4d538f9 100644 --- a/Messenger.Client/src/components/memberListItemBurgerMenu/MemberListItemBurgerMenu.module.scss +++ b/Messenger.Client/src/components/memberListItemBurgerMenu/MemberListItemBurgerMenu.module.scss @@ -0,0 +1,53 @@ +@import "../../App.scss"; + +.memberListItemBurgerMenu { + display: flex; + width: 110px; + flex-direction: column; + position: absolute; + z-index: 2; + border-radius: 5px; + box-shadow: 0 0 5px $grey; + + svg { + margin-top: 6px; + } +} + +.deleteMessageButton { + display: flex; + border: none; + width: 100%; + height: 32px; + font-size: 15px; + background-color: white; + transition: .1s; + cursor: pointer; + + p { + margin: auto; + } + + &:hover { + background-color: $light-grey; + } +} + +.updateMessageButton { + display: flex; + border: none; + width: 100%; + height: 32px; + font-size: 15px; + background-color: white; + transition: .1s; + cursor: pointer; + + p { + margin: auto; + } + + &:hover { + background-color: $light-grey; + } +} \ No newline at end of file diff --git a/Messenger.Client/src/components/memberListItemBurgerMenu/MemberListItemBurgerMenu.tsx b/Messenger.Client/src/components/memberListItemBurgerMenu/MemberListItemBurgerMenu.tsx index d89a538..33bac49 100644 --- a/Messenger.Client/src/components/memberListItemBurgerMenu/MemberListItemBurgerMenu.tsx +++ b/Messenger.Client/src/components/memberListItemBurgerMenu/MemberListItemBurgerMenu.tsx @@ -1,11 +1,95 @@ -import React from "react"; +import React, { MouseEvent, useState } from "react"; import { observer } from "mobx-react-lite"; import styles from "./MemberListItemBurgerMenu.module.scss"; +import IRoleUserByChatDto from "../../models/interfaces/IRoleUserByChatDto"; +import { currentChatState } from "../../state/CurrentChatState"; -const MemberListItemBurgerMenu = observer(() => { - return ( -
+interface IMemberListItemBurgerMenuProps { + x: number; + y: number; + isMemberListItemMine: boolean; + areYouOwner: boolean; + yourRole: IRoleUserByChatDto | null; + isMemberListItemLast: boolean; + memberListUserId: string; + setShowMenu: React.Dispatch>; +} + +const MemberListItemBurgerMenu = observer(( + { x, + y, + isMemberListItemMine, + areYouOwner, + yourRole, + isMemberListItemLast, + memberListUserId, + setShowMenu }: IMemberListItemBurgerMenuProps) => { + const [xMousePosition] = useState(x - 7); + const [yMousePosition] = useState(y - 7); + + const banUser = async (event: MouseEvent) => { + event.stopPropagation(); + await currentChatState.banUserAsync(memberListUserId, 20); + } -
+ const kickUser = async (event: MouseEvent) => { + event.stopPropagation(); + await currentChatState.kickUserAsync(memberListUserId); + } + + return ( + <> + { + !isMemberListItemMine &&
setShowMenu(false)}> + { + areYouOwner && + <> + + + + + + } + { + !areYouOwner && yourRole && + <> + { + yourRole.canGivePermissionToUser && + + } + { + yourRole.canBanUser && + + } + { + yourRole.canAddAndRemoveUserToConversation && + + } + + } +
+ } + ); -}); \ No newline at end of file +}); + +export default MemberListItemBurgerMenu; \ No newline at end of file diff --git a/Messenger.Client/src/components/message/Message.module.scss b/Messenger.Client/src/components/message/Message.module.scss index 9e1776e..5037530 100644 --- a/Messenger.Client/src/components/message/Message.module.scss +++ b/Messenger.Client/src/components/message/Message.module.scss @@ -23,6 +23,7 @@ } .messageData { + position: relative; font-size: 17px; max-width: 400px; min-width: 120px; @@ -53,11 +54,20 @@ font-size: 14px; } +.nicknameAndRole { + display: flex; + justify-content: space-between; +} + .nickname { cursor: pointer; margin-bottom: 3px; } +.role { + font-size: 12px; +} + .text { font-size: 16px; word-wrap: break-word; @@ -208,6 +218,4 @@ .myMetaData { font-size: 13px; text-align: right; -} - -.clockSvg {} \ No newline at end of file +} \ No newline at end of file diff --git a/Messenger.Client/src/components/message/Message.tsx b/Messenger.Client/src/components/message/Message.tsx index 4e35876..754ac3e 100644 --- a/Messenger.Client/src/components/message/Message.tsx +++ b/Messenger.Client/src/components/message/Message.tsx @@ -82,8 +82,7 @@ const Message = observer((props: IMessageDto) => { transition={{ type: "Inertia", duration: .2 }} className={styles.myMessage} onContextMenu={showMenuHandler} - onMouseMove={MouseMoveHandler} - > + onMouseMove={MouseMoveHandler}> { showMenu && ( { text={props.text} isMyMessage={isMessageMine} isMessageLast={isMessageLast} - setShowMenu={setShowMenu} - /> + setShowMenu={setShowMenu} /> ) } {
{ !props.attachments[0] && -

- {currentChatState.chat?.type !== ChatType.Channel ? - props.ownerDisplayName - : currentChatState.chat.title}

+ +

+ {currentChatState.chat?.type !== ChatType.Channel ? + props.ownerDisplayName + : currentChatState.chat.title} +

+

{currentChatState.chat?.ownerId === props.ownerId ? + "owner" : + currentChatState.chat?.usersWithRole.find(x => x.userId === props.ownerId)?.roleTitle || ""} +

+
} { props.replyToMessageId && ( @@ -217,10 +222,10 @@ const Message = observer((props: IMessageDto) => { props.attachments[0] &&
{ props.attachments.map(x => - onClickOpenFullSizeAvatar(x.link)} src={x.link} - alt="" - key={x.id} />) + onClickOpenFullSizeAvatar(x.link)} src={x.link} + alt="" + key={x.id} />) }
} diff --git a/Messenger.Client/src/models/entities/CreateOrUpdateRoleUserEntity.ts b/Messenger.Client/src/models/entities/CreateOrUpdateRoleUserEntity.ts new file mode 100644 index 0000000..64db632 --- /dev/null +++ b/Messenger.Client/src/models/entities/CreateOrUpdateRoleUserEntity.ts @@ -0,0 +1,32 @@ +import { RoleColor } from "../enum/RoleColor"; + + +export default class CreateOrUpdateRoleUserEntity { + public chatId: string; + public userId: string; + public roleTitle: string; + public roleColor: RoleColor; + public canBanUser: boolean; + public canChangeChatData: boolean; + public canAddAndRemoveUserToConversation: boolean; + public canGivePermissionToUser: boolean; + + constructor( + chatId: string, + userId: string, + roleTitle: string, + roleColor: RoleColor, + canBanUser: boolean, + canChangeChatData: boolean, + canAddAndRemoveUserToConversation: boolean, + canGivePermissionToUser: boolean) { + this.chatId = chatId; + this.userId = userId; + this.roleTitle = roleTitle; + this.roleColor = roleColor; + this.canBanUser = canBanUser; + this.canChangeChatData = canChangeChatData; + this.canAddAndRemoveUserToConversation = canAddAndRemoveUserToConversation; + this.canGivePermissionToUser = canGivePermissionToUser; + } +} \ No newline at end of file diff --git a/Messenger.Client/src/models/entities/CreatePermissionsUserInConversationEntity.ts b/Messenger.Client/src/models/entities/CreatePermissionsUserInConversationEntity.ts new file mode 100644 index 0000000..a82d23c --- /dev/null +++ b/Messenger.Client/src/models/entities/CreatePermissionsUserInConversationEntity.ts @@ -0,0 +1,16 @@ + + + +export default class CreatePermissionsUserInConversationEntity { + public chatId: string; + public userId: string; + public canSendMedia: boolean; + public muteMinutes: number | null; + + constructor(chatId: string, userId: string, canSendMedia: boolean, muteMinutes: number | null) { + this.chatId = chatId; + this.userId = userId; + this.canSendMedia = canSendMedia; + this.muteMinutes = muteMinutes; + } +} \ No newline at end of file diff --git a/Messenger.Client/src/models/enum/RoleColor.ts b/Messenger.Client/src/models/enum/RoleColor.ts index a65eebf..538884f 100644 --- a/Messenger.Client/src/models/enum/RoleColor.ts +++ b/Messenger.Client/src/models/enum/RoleColor.ts @@ -1,11 +1,10 @@ export enum RoleColor { - Green = 0, + Green = 0, Blue = 1, Cyan = 2, Red = 3, Yellow = 4, - Black = 5, - Orange = 6 + Orange = 5 } \ No newline at end of file diff --git a/Messenger.Client/src/models/interfaces/IChatDto.ts b/Messenger.Client/src/models/interfaces/IChatDto.ts index 5abfa70..34554f4 100644 --- a/Messenger.Client/src/models/interfaces/IChatDto.ts +++ b/Messenger.Client/src/models/interfaces/IChatDto.ts @@ -15,6 +15,7 @@ export default interface IChatDto { lastMessageDateOfCreate: string | null; membersCount: number; canSendMedia: boolean; + ownerId: string; isOwner: boolean; isMember: boolean; muteDateOfExpire: string | null; diff --git a/Messenger.Client/src/models/interfaces/IPermissionDto.ts b/Messenger.Client/src/models/interfaces/IPermissionDto.ts new file mode 100644 index 0000000..a4230f6 --- /dev/null +++ b/Messenger.Client/src/models/interfaces/IPermissionDto.ts @@ -0,0 +1,8 @@ + + +export default interface IPermissionDto { + chatId: string; + userId: string; + canSendMedia: boolean; + muteDateOfExpire: string | null; +} \ No newline at end of file diff --git a/Messenger.Client/src/services/api/ConversationApi.ts b/Messenger.Client/src/services/api/ConversationApi.ts index 776ffc3..9fe7267 100644 --- a/Messenger.Client/src/services/api/ConversationApi.ts +++ b/Messenger.Client/src/services/api/ConversationApi.ts @@ -1,47 +1,73 @@ +import CreateOrUpdateRoleUserEntity from "../../models/entities/CreateOrUpdateRoleUserEntity"; +import CreatePermissionsUserInConversationEntity from "../../models/entities/CreatePermissionsUserInConversationEntity"; +import IPermissionDto from "../../models/interfaces/IPermissionDto"; +import IRoleUserByChatDto from "../../models/interfaces/IRoleUserByChatDto"; import IUserDto from "../../models/interfaces/IUserDto"; import api from "./baseAPI"; export default class ConversationApi { - public static async postConversationAddUserAsync(chatId: string, userId: string) { + public static async getUserPermissionAsync(chatId: string, userId: string) { + var params = { + chatId, + userId + } + return await api.get(`/Conversations/getUserPermission`, { params }); + } + + public static async postConversationCreateOrUpdateRoleUserAsync(entity: CreateOrUpdateRoleUserEntity) { + return await api.post(`/Conversations/createOrUpdateRoleUser`, entity, {}); + } + + public static async postConversationCreatePermissionsAsync(entity: CreatePermissionsUserInConversationEntity) { + return await api.post(`/Conversations/createPermissions`, entity, {}); + } + + public static async postConversationAddUserAsync(chatId: string, userId: string) { const params = { chatId, userId }; - - return await api.post(`/Conversations/addUser`, null, {params}); + + return await api.post(`/Conversations/addUser`, null, { params }); } public static async postConversationBanUserAsync(chatId: string, userId: string, banMinutes: number) { - const params = { chatId, userId, banMinutes }; - - return await api.post(`/Conversations/banUser`, null, {params}); + + return await api.post(`/Conversations/banUser`, null, { params }); } public static async postConversationUnbanUserAsync(chatId: string, userId: string) { - const params = { chatId, userId, }; - - return await api.post(`/Conversations/unbanUser`, null, {params}); + + return await api.post(`/Conversations/unbanUser`, null, { params }); } public static async postConversationRemoveUserAsync(chatId: string, userId: string) { + const params = { + chatId, + userId, + }; + return await api.post(`/Conversations/removeUser`, null, { params }); + } + + public static async delConversationRemoveRoleUser(chatId: string, userId: string) { const params = { chatId, userId, }; - - return await api.post(`/Conversations/removeUser`, null, {params}); + + return await api.delete(`/Conversations/removeRoleUser`, { params }); } } \ No newline at end of file diff --git a/Messenger.Client/src/state/CurrentChatState.ts b/Messenger.Client/src/state/CurrentChatState.ts index 917800d..73265aa 100644 --- a/Messenger.Client/src/state/CurrentChatState.ts +++ b/Messenger.Client/src/state/CurrentChatState.ts @@ -2,6 +2,7 @@ import { makeAutoObservable, runInAction } from "mobx"; import IChatDto from "../models/interfaces/IChatDto"; import IMessageDto from "../models/interfaces/IMessageDto"; import ChatApi from "../services/api/ChatApi"; +import ConversationApi from "../services/api/ConversationApi"; import UsersApi from "../services/api/UserApi"; class CurrentChatState { @@ -47,6 +48,32 @@ class CurrentChatState { }; //api + public banUserAsync = async (userId: string, banMinutes: number) => { + if (!this.chat) return; + + this.chat.members = this.chat.members.filter(x => x.id !== userId); + + const response = await ConversationApi.postConversationBanUserAsync( + this.chat.id, + userId, + banMinutes + ); + + return response; + }; + + public kickUserAsync = async (userId: string) => { + if (!this.chat) return; + + this.chat.members = this.chat.members.filter(x => x.id !== userId); + + const response = await ConversationApi.postConversationRemoveUserAsync( + this.chat.id, + userId); + + return response; + }; + public getUserListByChatAsync = async ( chatId: string, limit: number, diff --git a/Messenger.Domain/Enums/RoleColor.cs b/Messenger.Domain/Enums/RoleColor.cs index 13c357b..e5e13d9 100644 --- a/Messenger.Domain/Enums/RoleColor.cs +++ b/Messenger.Domain/Enums/RoleColor.cs @@ -7,6 +7,5 @@ public enum RoleColor Cyan, Red, Yellow, - Black, Orange } \ No newline at end of file diff --git a/Messenger.IntegrationTests/ApiCommands/BanUserInConversationCommandHandlerTests/BanUserInConversationTestSuccess.cs b/Messenger.IntegrationTests/ApiCommands/BanUserInConversationCommandHandlerTests/BanUserInConversationTestSuccess.cs index 1b43989..b248b87 100644 --- a/Messenger.IntegrationTests/ApiCommands/BanUserInConversationCommandHandlerTests/BanUserInConversationTestSuccess.cs +++ b/Messenger.IntegrationTests/ApiCommands/BanUserInConversationCommandHandlerTests/BanUserInConversationTestSuccess.cs @@ -40,7 +40,7 @@ public async Task Test() createConversationResult.Value.Id, bob.Value.Id, RoleTitle: "moderator", - RoleColor.Black, + RoleColor.Cyan, CanBanUser: true, CanChangeChatData: false, CanAddAndRemoveUserToConversation: false, diff --git a/Messenger.IntegrationTests/ApiQueries/GetChatListQueryHandlerTests/GetChatListTestSuccess.cs b/Messenger.IntegrationTests/ApiQueries/GetChatListQueryHandlerTests/GetChatListTestSuccess.cs index 3a5091d..03137a9 100644 --- a/Messenger.IntegrationTests/ApiQueries/GetChatListQueryHandlerTests/GetChatListTestSuccess.cs +++ b/Messenger.IntegrationTests/ApiQueries/GetChatListQueryHandlerTests/GetChatListTestSuccess.cs @@ -70,7 +70,7 @@ public async Task Test() thirdCreateConversationResult.Value.Id, alice.Value.Id, RoleTitle: "qwerty", - RoleColor.Black, + RoleColor.Cyan, CanBanUser: true, CanChangeChatData: false, CanAddAndRemoveUserToConversation: true, diff --git a/Messenger.IntegrationTests/ApiQueries/GetUserPermissionsQueryHandlerTests/GetUserPermissionsTestSuccess.cs b/Messenger.IntegrationTests/ApiQueries/GetUserPermissionsQueryHandlerTests/GetUserPermissionsTestSuccess.cs new file mode 100644 index 0000000..ad239a1 --- /dev/null +++ b/Messenger.IntegrationTests/ApiQueries/GetUserPermissionsQueryHandlerTests/GetUserPermissionsTestSuccess.cs @@ -0,0 +1,51 @@ +using FluentAssertions; +using Messenger.BusinessLogic.ApiCommands.Chats; +using Messenger.BusinessLogic.ApiCommands.Conversations; +using Messenger.BusinessLogic.ApiQueries.Conversations; +using Messenger.Domain.Enums; +using Messenger.IntegrationTests.Abstraction; +using Messenger.IntegrationTests.Helpers; +using Xunit; + +namespace Messenger.IntegrationTests.ApiQueries.GetUserPermissionsQueryHandlerTests; + +public class GetUserPermissionsTestSuccess : IntegrationTestBase, IIntegrationTest +{ + [Fact] + public async Task Test() + { + var user21Th = await MessengerModule.RequestAsync(CommandHelper.Registration21ThCommand(), CancellationToken.None); + var alice = await MessengerModule.RequestAsync(CommandHelper.RegistrationAliceCommand(), CancellationToken.None); + + var createConversationCommand = new CreateChatCommand( + user21Th.Value.Id, + Name: "qwerty", + Title: "qwerty", + ChatType.Conversation, + AvatarFile: null); + + var createConversationResult = await MessengerModule.RequestAsync(createConversationCommand, CancellationToken.None); + + var aliceJoinConversationCommand = new JoinToChatCommand(alice.Value.Id, createConversationResult.Value.Id); + await MessengerModule.RequestAsync(aliceJoinConversationCommand, CancellationToken.None); + + var createPermissionsUserInConversationCommand = + new CreatePermissionsUserInConversationCommand( + user21Th.Value.Id, + createConversationResult.Value.Id, + alice.Value.Id, + CanSendMedia: false, + MuteMinutes: 50); + + await MessengerModule.RequestAsync(createPermissionsUserInConversationCommand, CancellationToken.None); + + var getUserPermissionsQuery = new GetUserPermissionsQuery(createConversationResult.Value.Id, alice.Value.Id); + + var getUserPermissionsResult = await MessengerModule.RequestAsync(getUserPermissionsQuery, CancellationToken.None); + + getUserPermissionsResult.Value.UserId.Should().Be(alice.Value.Id); + getUserPermissionsResult.Value.ChatId.Should().Be(createConversationResult.Value.Id); + getUserPermissionsResult.Value.CanSendMedia.Should().Be(createPermissionsUserInConversationCommand.CanSendMedia); + getUserPermissionsResult.Value.MuteDateOfExpire.Should().NotBeNull(); + } +} \ No newline at end of file diff --git a/Messenger.WebApi/Controllers/ConversationsController.cs b/Messenger.WebApi/Controllers/ConversationsController.cs index b4ff7d4..3210be3 100644 --- a/Messenger.WebApi/Controllers/ConversationsController.cs +++ b/Messenger.WebApi/Controllers/ConversationsController.cs @@ -1,6 +1,8 @@ using MediatR; using Messenger.BusinessLogic.ApiCommands.Conversations; +using Messenger.BusinessLogic.ApiQueries.Conversations; using Messenger.BusinessLogic.Models; +using Messenger.BusinessLogic.Models.Requests; using Messenger.BusinessLogic.Responses.Abstractions; using Messenger.Domain.Constants; using Messenger.WebApi.Extensions; @@ -21,6 +23,69 @@ public ConversationsController(IMediator mediator) _mediator = mediator; } + [ProducesResponseType(typeof(Error), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(PermissionDto), StatusCodes.Status200OK)] + [HttpGet("getUserPermission")] + public async Task GetUserPermissions( + [FromQuery] Guid chatId, + [FromQuery] Guid userId, + CancellationToken cancellationToken) + { + var query = new GetUserPermissionsQuery(chatId, userId); + + var result = await _mediator.Send(query, cancellationToken); + + return result.ToActionResult(); + } + + [ProducesResponseType(typeof(Error), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(Error), StatusCodes.Status403Forbidden)] + [ProducesResponseType(typeof(RoleUserByChatDto), StatusCodes.Status200OK)] + [HttpPost("createOrUpdateRoleUser")] + public async Task CreateOrUpdateRoleUser( + [FromBody] CreateOrUpdateRoleUserRequest request, + CancellationToken cancellationToken) + { + var requesterId = new Guid(HttpContext.User.Claims.First(c => c.Type == ClaimConstants.Id).Value); + + var command = new CreateOrUpdateRoleUserInConversationCommand( + requesterId, + request.ChatId, + request.UserId, + request.RoleTitle, + request.RoleColor, + request.CanBanUser, + request.CanChangeChatData, + request.CanAddAndRemoveUserToConversation, + request.CanGivePermissionToUser); + + var result = await _mediator.Send(command, cancellationToken); + + return result.ToActionResult(); + } + + [ProducesResponseType(typeof(Error), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(Error), StatusCodes.Status403Forbidden)] + [ProducesResponseType(typeof(PermissionDto), StatusCodes.Status200OK)] + [HttpPost("createPermissions")] + public async Task CreatePermissionsUserInConversation( + [FromBody] CreatePermissionsUserInConversationRequest request, + CancellationToken cancellationToken) + { + var requesterId = new Guid(HttpContext.User.Claims.First(c => c.Type == ClaimConstants.Id).Value); + + var command = new CreatePermissionsUserInConversationCommand( + requesterId, + request.ChatId, + request.UserId, + request.CanSendMedia, + request.MuteMinutes); + + var result = await _mediator.Send(command, cancellationToken); + + return result.ToActionResult(); + } + [ProducesResponseType(typeof(Error), StatusCodes.Status409Conflict)] [ProducesResponseType(typeof(Error), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(Error), StatusCodes.Status403Forbidden)] @@ -98,4 +163,22 @@ public async Task RemoveUserFromConversation( return result.ToActionResult(); } + + [ProducesResponseType(typeof(Error), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(Error), StatusCodes.Status403Forbidden)] + [ProducesResponseType(typeof(RoleUserByChatDto), StatusCodes.Status200OK)] + [HttpDelete("removeRoleUser")] + public async Task RemoveRoleUserInConversation( + [FromQuery] Guid chatId, + [FromQuery] Guid userId, + CancellationToken cancellationToken) + { + var requesterId = new Guid(HttpContext.User.Claims.First(c => c.Type == ClaimConstants.Id).Value); + + var command = new RemoveRoleUserInConversationCommand(requesterId, chatId, userId); + + var result = await _mediator.Send(command, cancellationToken); + + return result.ToActionResult(); + } } \ No newline at end of file