diff --git a/src/shared/Jordnaer.Shared/Database/GroupMembership.cs b/src/shared/Jordnaer.Shared/Database/GroupMembership.cs index 10cbc2de..63eeb2a1 100644 --- a/src/shared/Jordnaer.Shared/Database/GroupMembership.cs +++ b/src/shared/Jordnaer.Shared/Database/GroupMembership.cs @@ -2,20 +2,22 @@ namespace Jordnaer.Shared; public class GroupMembership { - public required Guid GroupId { get; set; } - public required string UserProfileId { get; set; } + public required Guid GroupId { get; set; } + public required string UserProfileId { get; set; } - public Group Group { get; set; } = null!; + public Group Group { get; set; } = null!; - /// - /// Whether the user requested to join the group or was invited. - /// - public bool UserInitiatedMembership { get; set; } + public UserProfile UserProfile { get; set; } = null!; - public DateTime CreatedUtc { get; set; } - public DateTime LastUpdatedUtc { get; set; } + /// + /// Whether the user requested to join the group or was invited. + /// + public bool UserInitiatedMembership { get; set; } - public MembershipStatus MembershipStatus { get; set; } - public PermissionLevel PermissionLevel { get; set; } = PermissionLevel.None; - public OwnershipLevel OwnershipLevel { get; set; } = OwnershipLevel.None; + public DateTime CreatedUtc { get; set; } + public DateTime LastUpdatedUtc { get; set; } + + public MembershipStatus MembershipStatus { get; set; } + public PermissionLevel PermissionLevel { get; set; } = PermissionLevel.None; + public OwnershipLevel OwnershipLevel { get; set; } = OwnershipLevel.None; } diff --git a/src/web/Jordnaer/Consumers/SendMessageConsumer.cs b/src/web/Jordnaer/Consumers/SendMessageConsumer.cs index fcda8c2e..6cef2a80 100644 --- a/src/web/Jordnaer/Consumers/SendMessageConsumer.cs +++ b/src/web/Jordnaer/Consumers/SendMessageConsumer.cs @@ -23,6 +23,8 @@ public SendMessageConsumer(JordnaerDbContext context, ILogger consumeContext) { + _logger.LogDebug("Consuming SendMessage message. ChatId: {ChatId}", consumeContext.Message.ChatId); + var chatMessage = consumeContext.Message; _context.ChatMessages.Add( diff --git a/src/web/Jordnaer/Consumers/StartChatConsumer.cs b/src/web/Jordnaer/Consumers/StartChatConsumer.cs index ac4ae6a0..f15af806 100644 --- a/src/web/Jordnaer/Consumers/StartChatConsumer.cs +++ b/src/web/Jordnaer/Consumers/StartChatConsumer.cs @@ -21,6 +21,8 @@ public StartChatConsumer(JordnaerDbContext context, ILogger l public async Task Consume(ConsumeContext consumeContext) { + _logger.LogInformation("Consuming StartChat message. ChatId: {ChatId}", consumeContext.Message.Id); + var chat = consumeContext.Message; _context.Chats.Add(new Chat diff --git a/src/web/Jordnaer/Features/GroupSearch/GroupCard.razor b/src/web/Jordnaer/Features/GroupSearch/GroupCard.razor index 7407b449..e867d7b2 100644 --- a/src/web/Jordnaer/Features/GroupSearch/GroupCard.razor +++ b/src/web/Jordnaer/Features/GroupSearch/GroupCard.razor @@ -1,10 +1,10 @@ - - - - + + + + @Group.ShortDescription diff --git a/src/web/Jordnaer/Features/Groups/GroupService.cs b/src/web/Jordnaer/Features/Groups/GroupService.cs index 737002ec..ac9fe36d 100644 --- a/src/web/Jordnaer/Features/Groups/GroupService.cs +++ b/src/web/Jordnaer/Features/Groups/GroupService.cs @@ -5,6 +5,8 @@ using OneOf; using OneOf.Types; using Serilog; +using System.Linq.Expressions; +using Jordnaer.Features.Authentication; using NotFound = OneOf.Types.NotFound; namespace Jordnaer.Features.Groups; @@ -17,28 +19,23 @@ public interface IGroupService Task, NotFound>> UpdateGroupAsync(string userId, Group group, CancellationToken cancellationToken = default); Task> DeleteGroupAsync(string userId, Guid id, CancellationToken cancellationToken = default); Task> GetSlimGroupsForUserAsync(string userId, CancellationToken cancellationToken = default); + + Task> GetGroupMembersByPredicateAsync(Expression> predicate, CancellationToken cancellationToken = default); + Task IsGroupMemberAsync(Guid groupId, CancellationToken cancellationToken = default); } -public class GroupService : IGroupService +public class GroupService( + IDbContextFactory contextFactory, + ILogger logger, + IDiagnosticContext diagnosticContext, + CurrentUser currentUser) + : IGroupService { - private readonly IDbContextFactory _contextFactory; - private readonly ILogger _logger; - private readonly IDiagnosticContext _diagnosticContext; - - public GroupService(IDbContextFactory contextFactory, - ILogger logger, - IDiagnosticContext diagnosticContext) - { - _contextFactory = contextFactory; - _logger = logger; - _diagnosticContext = diagnosticContext; - } - public async Task> GetGroupByIdAsync(Guid id, CancellationToken cancellationToken = default) { - _logger.LogFunctionBegan(); + logger.LogFunctionBegan(); - await using var context = await _contextFactory.CreateDbContextAsync(cancellationToken); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var group = await context.Groups .AsNoTracking() .FirstOrDefaultAsync(group => group.Id == id, cancellationToken: cancellationToken); @@ -50,9 +47,9 @@ public async Task> GetGroupByIdAsync(Guid id, Cancellatio public async Task> GetSlimGroupByNameAsync(string name, CancellationToken cancellationToken = default) { - _logger.LogFunctionBegan(); + logger.LogFunctionBegan(); - await using var context = await _contextFactory.CreateDbContextAsync(cancellationToken); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var group = await context.Groups .AsNoTracking() .Select(x => new GroupSlim @@ -74,9 +71,9 @@ public async Task> GetSlimGroupByNameAsync(string nam } public async Task> GetSlimGroupsForUserAsync(string userId, CancellationToken cancellationToken = default) { - _logger.LogFunctionBegan(); + logger.LogFunctionBegan(); - await using var context = await _contextFactory.CreateDbContextAsync(cancellationToken); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); var groups = await context.GroupMemberships .AsNoTracking() .Where(membership => membership.UserProfileId == userId && @@ -109,11 +106,52 @@ public async Task> GetSlimGroupsForUserAsync(string userId return groups; } + public async Task> GetGroupMembersByPredicateAsync(Expression> predicate, CancellationToken cancellationToken = default) + { + logger.LogFunctionBegan(); + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + var members = await context.GroupMemberships + .AsNoTracking() + .Where(predicate) + .OrderByDescending(x => x.OwnershipLevel) + .ThenByDescending(x => x.PermissionLevel) + .Select(x => new UserSlim + { + DisplayName = x.UserProfile.DisplayName, + Id = x.UserProfileId, + ProfilePictureUrl = x.UserProfile.ProfilePictureUrl, + UserName = x.UserProfile.UserName + }) + .ToListAsync(cancellationToken); + + return members; + } + + public async Task IsGroupMemberAsync(Guid groupId, CancellationToken cancellationToken = default) + { + logger.LogFunctionBegan(); + + if (currentUser.Id is null) + { + return false; + } + + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + var isGroupMember = await context.GroupMemberships + .AsNoTracking() + .AnyAsync(x => x.UserProfileId == currentUser.Id && + x.GroupId == groupId, + cancellationToken); + + return isGroupMember; + } + public async Task>> CreateGroupAsync(string userId, Group group, CancellationToken cancellationToken = default) { - _logger.LogFunctionBegan(); + logger.LogFunctionBegan(); - await using var context = await _contextFactory.CreateDbContextAsync(cancellationToken); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (await context.Groups.AsNoTracking().AnyAsync(x => x.Name == group.Name, cancellationToken)) { return new Error($"Gruppenavnet '{group.Name}' er allerede taget."); @@ -155,16 +193,16 @@ public async Task>> CreateGroupAsync(string userId, context.Groups.Add(group); await context.SaveChangesAsync(cancellationToken); - _logger.LogInformation("{UserId} created group '{groupName}'", userId, group.Name); + logger.LogInformation("{UserId} created group '{groupName}'", userId, group.Name); return new Success(); } public async Task, NotFound>> UpdateGroupAsync(string userId, Group group, CancellationToken cancellationToken = default) { - _logger.LogFunctionBegan(); + logger.LogFunctionBegan(); - await using var context = await _contextFactory.CreateDbContextAsync(cancellationToken); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); if (await context.Groups.AsNoTracking().AnyAsync(x => x.Name == group.Name, cancellationToken)) { return new Error($"Gruppenavnet '{group.Name}' er allerede taget."); @@ -198,19 +236,19 @@ public async Task, NotFound>> UpdateGroupAsync(stri public async Task> DeleteGroupAsync(string userId, Guid id, CancellationToken cancellationToken = default) { - _logger.LogFunctionBegan(); + logger.LogFunctionBegan(); - _diagnosticContext.Set("group_id", id); + diagnosticContext.Set("group_id", id); - await using var context = await _contextFactory.CreateDbContextAsync(cancellationToken); - var group = await context.Groups.FindAsync(id); + await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); + var group = await context.Groups.FindAsync([id], cancellationToken); if (group is null) { - _logger.LogInformation("Failed to find group by id."); + logger.LogInformation("Failed to find group by id."); return new NotFound(); } - _diagnosticContext.Set("group_name", group.Name); + diagnosticContext.Set("group_name", group.Name); var groupOwner = await context.GroupMemberships .SingleOrDefaultAsync(e => e.UserProfileId == userId && @@ -219,13 +257,13 @@ public async Task> DeleteGroupAsync(string userI if (groupOwner is null) { - _logger.LogError("Failed to delete group because it has no owner."); + logger.LogError("Failed to delete group because it has no owner."); return new Error(); } if (groupOwner.UserProfileId != userId) { - _logger.LogError("Failed to delete group because the request came from someone other than the owner. " + + logger.LogError("Failed to delete group because the request came from someone other than the owner. " + "The deletion was requested by the user: {@UserId}", userId); return new Error(); } @@ -233,7 +271,7 @@ public async Task> DeleteGroupAsync(string userI context.Groups.Remove(group); await context.SaveChangesAsync(cancellationToken); - _logger.LogInformation("Successfully deleted group"); + logger.LogInformation("Successfully deleted group"); return new Success(); } diff --git a/src/web/Jordnaer/Features/Groups/GroupSummaryCard.razor b/src/web/Jordnaer/Features/Groups/GroupSummaryCard.razor index 2272c83d..6630ca91 100644 --- a/src/web/Jordnaer/Features/Groups/GroupSummaryCard.razor +++ b/src/web/Jordnaer/Features/Groups/GroupSummaryCard.razor @@ -2,7 +2,7 @@ - @UserGroupAccess.Group.Name + @UserGroupAccess.Group.Name @if (UserGroupAccess.Group.ProfilePictureUrl is not null) { diff --git a/src/web/Jordnaer/Features/Profile/OpenChat.razor b/src/web/Jordnaer/Features/Profile/OpenChat.razor index 220622d4..65c280a0 100644 --- a/src/web/Jordnaer/Features/Profile/OpenChat.razor +++ b/src/web/Jordnaer/Features/Profile/OpenChat.razor @@ -1,6 +1,9 @@ @inject IChatService ChatService @inject NavigationManager NavigationManager @inject IDialogService DialogService +@inject CurrentUser CurrentUser + +@attribute [Authorize] Recipients { get; set; } + public required IEnumerable Recipients { get; set; } - /// - /// This is the current user's id - /// [Parameter] - public required string InitiatorId { get; set; } + public bool Disabled { get; set; } [Parameter] - public bool Disabled { get; set; } + public string? ChatName { get; set; } + + [Parameter] + public string? Title { get; set; } private bool _isMessageSent = false; private async Task OpenOrStartChat() { - var getChatResponse = await ChatService.GetChatByUserIdsAsync(InitiatorId, Recipients.Select(recipient => recipient.Id).ToArray()); + var getChatResponse = await ChatService.GetChatByUserIdsAsync(CurrentUser.Id!, Recipients.Select(recipient => recipient.Id).ToArray()); await getChatResponse.Match(chatId => { NavigationManager.NavigateTo($"/chat/{chatId}"); @@ -37,10 +40,11 @@ }, async notFound => { var parameters = new DialogParameters - { - { dialog => dialog.InitiatorId, InitiatorId }, - { dialog => dialog.Recipients, Recipients } - }; + { + { dialog => dialog.InitiatorId, CurrentUser.Id! }, + { dialog => dialog.Recipients, Recipients }, + { dialog => dialog.ChatName, ChatName } + }; var dialogReference = await DialogService.ShowAsync("Send besked", parameters); diff --git a/src/web/Jordnaer/Features/Profile/SendMessageDialog.razor b/src/web/Jordnaer/Features/Profile/SendMessageDialog.razor index bda9a076..1eecdd59 100644 --- a/src/web/Jordnaer/Features/Profile/SendMessageDialog.razor +++ b/src/web/Jordnaer/Features/Profile/SendMessageDialog.razor @@ -23,6 +23,7 @@ [CascadingParameter] MudDialogInstance MudDialog { get; set; } = null!; [Parameter] public required string InitiatorId { get; set; } [Parameter] public required List Recipients { get; set; } + [Parameter] public string? ChatName { get; set; } private bool _startingChat = false; private string _userText = string.Empty; @@ -50,19 +51,23 @@ var chatId = NewId.NextGuid(); await ChatService.StartChatAsync(new StartChat { + DisplayName = ChatName, LastMessageSentUtc = DateTime.UtcNow, StartedUtc = DateTime.UtcNow, Id = chatId, InitiatorId = InitiatorId, Recipients = Recipients, - Messages = new List {new() - { - ChatId = chatId, - Id = NewId.NextGuid(), - Text = _userText, - SentUtc = DateTime.UtcNow, - SenderId = InitiatorId - }} + Messages = + [ + new ChatMessageDto + { + ChatId = chatId, + Id = NewId.NextGuid(), + Text = _userText, + SentUtc = DateTime.UtcNow, + SenderId = InitiatorId + } + ] }); MudDialog.Close(DialogResult.Ok(true)); diff --git a/src/web/Jordnaer/Pages/GroupSearch/GroupDetails.razor b/src/web/Jordnaer/Pages/GroupSearch/GroupDetails.razor index d2587c60..e598441e 100644 --- a/src/web/Jordnaer/Pages/GroupSearch/GroupDetails.razor +++ b/src/web/Jordnaer/Pages/GroupSearch/GroupDetails.razor @@ -1,8 +1,9 @@ @page "/groups/{GroupName}" @inject IGroupService GroupService -@inject ISnackbar Snackbar @inject IJSRuntime JsRuntime +@inject IProfileCache ProfileCache + @@ -22,11 +23,24 @@ - @*// TODO: Request to join button*@ + + + + + + Anmod om Medlemskab + + + - + @if (_group.Categories.Length > 0) @@ -52,8 +66,12 @@ public string? GroupName { get; set; } private GroupSlim? _group; + private List _groupAdmins = []; private bool _isLoading = true; + private UserProfile? _currentUser; + private bool _isMemberOfGroup = true; + private IEnumerable _recipients = []; protected override async Task OnInitializedAsync() { @@ -63,8 +81,34 @@ response.Switch( groupSlim => _group = groupSlim, _ => { }); + + if (_group is null) + { + return; + } + + _currentUser = await ProfileCache.GetProfileAsync(); + if (_currentUser is null) + { + return; + } + + _isMemberOfGroup = await GroupService + .IsGroupMemberAsync(_group.Id); + + if (_isMemberOfGroup is false) + { + _groupAdmins = await GroupService + .GetGroupMembersByPredicateAsync(x => + x.GroupId == _group.Id && + x.PermissionLevel.HasFlag(PermissionLevel.Admin)); + + _recipients = _groupAdmins.Concat([_currentUser.ToUserSlim()]); + } + } _isLoading = false; + } } diff --git a/src/web/Jordnaer/Pages/Groups/MyGroups.razor b/src/web/Jordnaer/Pages/Groups/MyGroups.razor index d67aeb5c..01f15bf0 100644 --- a/src/web/Jordnaer/Pages/Groups/MyGroups.razor +++ b/src/web/Jordnaer/Pages/Groups/MyGroups.razor @@ -3,9 +3,11 @@ @attribute [Authorize] @inject IGroupService GroupService -@inject ISnackbar Snackbar @inject CurrentUser CurrentUser + + Opret gruppe @@ -26,7 +28,7 @@ @foreach (var group in _memberOf) { - + } diff --git a/src/web/Jordnaer/Pages/Profile/PublicProfile.razor b/src/web/Jordnaer/Pages/Profile/PublicProfile.razor index 13c54bb2..a283cd96 100644 --- a/src/web/Jordnaer/Pages/Profile/PublicProfile.razor +++ b/src/web/Jordnaer/Pages/Profile/PublicProfile.razor @@ -1,10 +1,11 @@ @page "/{userName}" -@inject ISnackbar Snackbar @inject IProfileService ProfileService @inject IJSRuntime JsRuntime @inject IProfileCache ProfileCache +@attribute [StreamRendering] + @if (_profile is null && _isLoading is false) {