Skip to content

Commit

Permalink
implement sending of messages from group details
Browse files Browse the repository at this point in the history
  • Loading branch information
NielsPilgaard committed Mar 28, 2024
1 parent 9cfe004 commit 97719cd
Show file tree
Hide file tree
Showing 11 changed files with 176 additions and 76 deletions.
26 changes: 14 additions & 12 deletions src/shared/Jordnaer.Shared/Database/GroupMembership.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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!;

/// <summary>
/// Whether the user requested to join the group or was invited.
/// </summary>
public bool UserInitiatedMembership { get; set; }
public UserProfile UserProfile { get; set; } = null!;

public DateTime CreatedUtc { get; set; }
public DateTime LastUpdatedUtc { get; set; }
/// <summary>
/// Whether the user requested to join the group or was invited.
/// </summary>
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;
}
2 changes: 2 additions & 0 deletions src/web/Jordnaer/Consumers/SendMessageConsumer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public SendMessageConsumer(JordnaerDbContext context, ILogger<SendMessageConsume

public async Task Consume(ConsumeContext<SendMessage> consumeContext)
{
_logger.LogDebug("Consuming SendMessage message. ChatId: {ChatId}", consumeContext.Message.ChatId);

var chatMessage = consumeContext.Message;

_context.ChatMessages.Add(
Expand Down
2 changes: 2 additions & 0 deletions src/web/Jordnaer/Consumers/StartChatConsumer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public StartChatConsumer(JordnaerDbContext context, ILogger<StartChatConsumer> l

public async Task Consume(ConsumeContext<StartChat> consumeContext)
{
_logger.LogInformation("Consuming StartChat message. ChatId: {ChatId}", consumeContext.Message.Id);

var chat = consumeContext.Message;

_context.Chats.Add(new Chat
Expand Down
8 changes: 4 additions & 4 deletions src/web/Jordnaer/Features/GroupSearch/GroupCard.razor
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<MudCard Elevation="3">
<MudCardContent>
<MudImage ObjectPosition="ObjectPosition.Left" Style="border-radius: 50%" Src="@Group.ProfilePictureUrl" loading="lazy" />
<MudTextField Label="Group Name" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Group" ReadOnly T="string" Text="@Group.Name" />
<MudTextField Label="Location" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Place" ReadOnly T="string" Text="@Group.DisplayLocation()" />
<MudTextField Label="Short Description" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Description" ReadOnly T="string" Text="@Group.ShortDescription" />
<MudTextField Label="Member Count" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.People" ReadOnly T="int" Text="@Group.MemberCount.ToString()" />
<MudTextField Label="Navn" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.AlternateEmail" ReadOnly T="string" Text="@Group.Name" />
<MudTextField Label="Placering" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Place" ReadOnly T="string" Text="@Group.DisplayLocation()" />
<MudTextField Label="Kort beskrivelse" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Description" ReadOnly T="string" Text="@Group.ShortDescription" />
<MudTextField Label="Antal medlemmer" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.People" ReadOnly T="int" Text="@Group.MemberCount.ToString()" />
<MudText Class="mt-3" Typo="Typo.body1">@Group.ShortDescription</MudText>
</MudCardContent>
</MudCard>
Expand Down
106 changes: 72 additions & 34 deletions src/web/Jordnaer/Features/Groups/GroupService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,28 +19,23 @@ public interface IGroupService
Task<OneOf<Success, Error<string>, NotFound>> UpdateGroupAsync(string userId, Group group, CancellationToken cancellationToken = default);
Task<OneOf<Success, Error, NotFound>> DeleteGroupAsync(string userId, Guid id, CancellationToken cancellationToken = default);
Task<List<UserGroupAccess>> GetSlimGroupsForUserAsync(string userId, CancellationToken cancellationToken = default);

Task<List<UserSlim>> GetGroupMembersByPredicateAsync(Expression<Func<GroupMembership, bool>> predicate, CancellationToken cancellationToken = default);
Task<bool> IsGroupMemberAsync(Guid groupId, CancellationToken cancellationToken = default);
}

public class GroupService : IGroupService
public class GroupService(
IDbContextFactory<JordnaerDbContext> contextFactory,
ILogger<GroupService> logger,
IDiagnosticContext diagnosticContext,
CurrentUser currentUser)
: IGroupService
{
private readonly IDbContextFactory<JordnaerDbContext> _contextFactory;
private readonly ILogger<GroupService> _logger;
private readonly IDiagnosticContext _diagnosticContext;

public GroupService(IDbContextFactory<JordnaerDbContext> contextFactory,
ILogger<GroupService> logger,
IDiagnosticContext diagnosticContext)
{
_contextFactory = contextFactory;
_logger = logger;
_diagnosticContext = diagnosticContext;
}

public async Task<OneOf<Group, NotFound>> 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);
Expand All @@ -50,9 +47,9 @@ public async Task<OneOf<Group, NotFound>> GetGroupByIdAsync(Guid id, Cancellatio

public async Task<OneOf<GroupSlim, NotFound>> 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
Expand All @@ -74,9 +71,9 @@ public async Task<OneOf<GroupSlim, NotFound>> GetSlimGroupByNameAsync(string nam
}
public async Task<List<UserGroupAccess>> 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 &&
Expand Down Expand Up @@ -109,11 +106,52 @@ public async Task<List<UserGroupAccess>> GetSlimGroupsForUserAsync(string userId
return groups;
}

public async Task<List<UserSlim>> GetGroupMembersByPredicateAsync(Expression<Func<GroupMembership, bool>> 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<bool> 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<OneOf<Success, Error<string>>> 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<string>($"Gruppenavnet '{group.Name}' er allerede taget.");
Expand Down Expand Up @@ -155,16 +193,16 @@ public async Task<OneOf<Success, Error<string>>> 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<OneOf<Success, Error<string>, 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<string>($"Gruppenavnet '{group.Name}' er allerede taget.");
Expand Down Expand Up @@ -198,19 +236,19 @@ public async Task<OneOf<Success, Error<string>, NotFound>> UpdateGroupAsync(stri

public async Task<OneOf<Success, Error, NotFound>> 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 &&
Expand All @@ -219,21 +257,21 @@ public async Task<OneOf<Success, Error, NotFound>> 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();
}

context.Groups.Remove(group);
await context.SaveChangesAsync(cancellationToken);

_logger.LogInformation("Successfully deleted group");
logger.LogInformation("Successfully deleted group");

return new Success();
}
Expand Down
2 changes: 1 addition & 1 deletion src/web/Jordnaer/Features/Groups/GroupSummaryCard.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<MudCard Elevation="3" Style="position: relative">
<MudIcon Title="Du er ejer af denne gruppe" Icon="@Icons.Material.Filled.Star" Style="position: absolute;top: 16px; right: 16px; font-size: 24px" />
<MudCardContent>
<MudText Typo="Typo.h4" Align="Align.Left">@UserGroupAccess.Group.Name</MudText>
<MudText Typo="Typo.h5" Align="Align.Left">@UserGroupAccess.Group.Name</MudText>
@if (UserGroupAccess.Group.ProfilePictureUrl is not null)
{
<MudImage ObjectPosition="ObjectPosition.Left" Style="border-radius: 50%" Src="@UserGroupAccess.Group.ProfilePictureUrl" loading="lazy" />
Expand Down
26 changes: 15 additions & 11 deletions src/web/Jordnaer/Features/Profile/OpenChat.razor
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
@inject IChatService ChatService
@inject NavigationManager NavigationManager
@inject IDialogService DialogService
@inject CurrentUser CurrentUser

@attribute [Authorize]

<MudButton Disabled="Disabled"
EndIcon="@(_isMessageSent ? Icons.Material.Filled.Check : Icons.Material.Filled.Message)"
Expand All @@ -14,33 +17,34 @@
@code
{
[Parameter]
public required List<UserSlim> Recipients { get; set; }
public required IEnumerable<UserSlim> Recipients { get; set; }

/// <summary>
/// This is the current user's id
/// </summary>
[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<Task>(chatId =>
{
NavigationManager.NavigateTo($"/chat/{chatId}");
return Task.CompletedTask;
}, async notFound =>
{
var parameters = new DialogParameters<SendMessageDialog>
{
{ 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<SendMessageDialog>("Send besked", parameters);

Expand Down
21 changes: 13 additions & 8 deletions src/web/Jordnaer/Features/Profile/SendMessageDialog.razor
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
[CascadingParameter] MudDialogInstance MudDialog { get; set; } = null!;
[Parameter] public required string InitiatorId { get; set; }
[Parameter] public required List<UserSlim> Recipients { get; set; }
[Parameter] public string? ChatName { get; set; }

private bool _startingChat = false;
private string _userText = string.Empty;
Expand Down Expand Up @@ -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<ChatMessageDto> {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));
Expand Down
Loading

0 comments on commit 97719cd

Please sign in to comment.