Change editor rule
This commit is contained in:
@@ -6,46 +6,47 @@ using Musify.Application.Contracts;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Domain.Entities;
|
||||
|
||||
namespace Musify.Application.Users;
|
||||
|
||||
public record FollowUserCommand(long FollowerId, long FollowedId) : ICommand<ErrorOr<Success>>;
|
||||
|
||||
public class FollowUserCommandHandler(
|
||||
IDatabase database,
|
||||
ILogger<FollowUserCommandHandler> logger)
|
||||
: ICommandHandler<FollowUserCommand, ErrorOr<Success>>
|
||||
namespace Musify.Application.Users
|
||||
{
|
||||
public async ValueTask<ErrorOr<Success>> Handle(FollowUserCommand request, CancellationToken cancellationToken)
|
||||
public record FollowUserCommand(long FollowerId, long FollowedId) : ICommand<ErrorOr<Success>>;
|
||||
|
||||
public class FollowUserCommandHandler(
|
||||
IDatabase database,
|
||||
ILogger<FollowUserCommandHandler> logger)
|
||||
: ICommandHandler<FollowUserCommand, ErrorOr<Success>>
|
||||
{
|
||||
if (request.FollowerId == request.FollowedId)
|
||||
return Error.Validation(description: "A user cannot follow themselves.");
|
||||
|
||||
var followedExists = await database.Users
|
||||
.AsNoTracking()
|
||||
.AnyAsync(u => u.Id == request.FollowedId, cancellationToken);
|
||||
if (!followedExists)
|
||||
return AppErrors.NotFound("User", request.FollowedId);
|
||||
|
||||
var alreadyFollowing = await database.UserFollows
|
||||
.AsNoTracking()
|
||||
.AnyAsync(f => f.FollowerId == request.FollowerId && f.FollowedId == request.FollowedId, cancellationToken);
|
||||
if (alreadyFollowing)
|
||||
return Result.Success;
|
||||
|
||||
await database.UserFollows.AddAsync(new UserFollow
|
||||
public async ValueTask<ErrorOr<Success>> Handle(FollowUserCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
FollowerId = request.FollowerId,
|
||||
FollowedId = request.FollowedId
|
||||
}, cancellationToken);
|
||||
if (request.FollowerId == request.FollowedId)
|
||||
return Error.Validation(description: "A user cannot follow themselves.");
|
||||
|
||||
var followedExists = await database.Users
|
||||
.AsNoTracking()
|
||||
.AnyAsync(u => u.Id == request.FollowedId, cancellationToken);
|
||||
if (!followedExists)
|
||||
return AppErrors.NotFound("User", request.FollowedId);
|
||||
|
||||
var alreadyFollowing = await database.UserFollows
|
||||
.AsNoTracking()
|
||||
.AnyAsync(f => f.FollowerId == request.FollowerId && f.FollowedId == request.FollowedId, cancellationToken);
|
||||
if (alreadyFollowing)
|
||||
return Result.Success;
|
||||
|
||||
await database.UserFollows.AddAsync(new UserFollow
|
||||
{
|
||||
FollowerId = request.FollowerId,
|
||||
FollowedId = request.FollowedId
|
||||
}, cancellationToken);
|
||||
|
||||
// A concurrent double click can hit the unique index; already following is a successful no-op.
|
||||
var saved = await database.TrySaveChangesAsync(
|
||||
AppErrors.Conflict("UserFollow.Duplicate", "Already following this user."), cancellationToken);
|
||||
if (saved.IsError)
|
||||
return Result.Success;
|
||||
|
||||
logger.LogInformation("User {FollowerId} followed user {FollowedId}", request.FollowerId, request.FollowedId);
|
||||
|
||||
// A concurrent double click can hit the unique index; already following is a successful no-op.
|
||||
var saved = await database.TrySaveChangesAsync(
|
||||
AppErrors.Conflict("UserFollow.Duplicate", "Already following this user."), cancellationToken);
|
||||
if (saved.IsError)
|
||||
return Result.Success;
|
||||
|
||||
logger.LogInformation("User {FollowerId} followed user {FollowedId}", request.FollowerId, request.FollowedId);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
|
||||
namespace Musify.Application.Users;
|
||||
|
||||
public static class FollowVisibility
|
||||
namespace Musify.Application.Users
|
||||
{
|
||||
public static async Task<bool> CanViewFollowersAsync(
|
||||
IDatabase database,
|
||||
long? viewerId,
|
||||
long userId,
|
||||
CancellationToken cancellationToken)
|
||||
public static class FollowVisibility
|
||||
{
|
||||
if (viewerId is not { } viewer)
|
||||
return false;
|
||||
public static async Task<bool> CanViewFollowersAsync(
|
||||
IDatabase database,
|
||||
long? viewerId,
|
||||
long userId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (viewerId is not { } viewer)
|
||||
return false;
|
||||
|
||||
if (viewer == userId)
|
||||
return true;
|
||||
if (viewer == userId)
|
||||
return true;
|
||||
|
||||
var mutualRows = await database.UserFollows
|
||||
.AsNoTracking()
|
||||
.CountAsync(f =>
|
||||
(f.FollowerId == viewer && f.FollowedId == userId) ||
|
||||
(f.FollowerId == userId && f.FollowedId == viewer),
|
||||
cancellationToken);
|
||||
var mutualRows = await database.UserFollows
|
||||
.AsNoTracking()
|
||||
.CountAsync(f =>
|
||||
(f.FollowerId == viewer && f.FollowedId == userId) ||
|
||||
(f.FollowerId == userId && f.FollowedId == viewer),
|
||||
cancellationToken);
|
||||
|
||||
return mutualRows == 2;
|
||||
return mutualRows == 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,50 +3,51 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Users.Responses;
|
||||
|
||||
namespace Musify.Application.Users;
|
||||
|
||||
public record GetListeningStatsQuery(long UserId) : IQuery<ListeningStatsResponse>;
|
||||
|
||||
public class GetListeningStatsQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetListeningStatsQuery, ListeningStatsResponse>
|
||||
namespace Musify.Application.Users
|
||||
{
|
||||
public async ValueTask<ListeningStatsResponse> Handle(GetListeningStatsQuery request, CancellationToken cancellationToken)
|
||||
public record GetListeningStatsQuery(long UserId) : IQuery<ListeningStatsResponse>;
|
||||
|
||||
public class GetListeningStatsQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetListeningStatsQuery, ListeningStatsResponse>
|
||||
{
|
||||
var weekAgo = DateTime.UtcNow.AddDays(-7);
|
||||
|
||||
var thisWeekHistory = database.ListeningHistories
|
||||
.AsNoTracking()
|
||||
.Where(l => l.UserId == request.UserId && l.ListenedAt >= weekAgo);
|
||||
|
||||
var tracksThisWeek = await thisWeekHistory
|
||||
.Where(l => l.IsCounted)
|
||||
.Select(l => l.TrackId)
|
||||
.Distinct()
|
||||
.CountAsync(cancellationToken);
|
||||
|
||||
var secondsThisWeek = await thisWeekHistory
|
||||
.Join(database.Tracks.AsNoTracking(), l => l.TrackId, t => t.Id, (l, t) => l.PlayedSeconds ?? (l.IsCounted ? t.DurationSeconds : 0))
|
||||
.SumAsync(cancellationToken);
|
||||
|
||||
var listenedDates = await database.ListeningHistories
|
||||
.AsNoTracking()
|
||||
.Where(l => l.UserId == request.UserId && l.IsCounted)
|
||||
.Select(l => l.ListenedAt.Date)
|
||||
.Distinct()
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var listenedDateSet = listenedDates.ToHashSet();
|
||||
var streakDays = 0;
|
||||
var day = DateTime.UtcNow.Date;
|
||||
if (!listenedDateSet.Contains(day))
|
||||
day = day.AddDays(-1);
|
||||
|
||||
while (listenedDateSet.Contains(day))
|
||||
public async ValueTask<ListeningStatsResponse> Handle(GetListeningStatsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
streakDays++;
|
||||
day = day.AddDays(-1);
|
||||
}
|
||||
var weekAgo = DateTime.UtcNow.AddDays(-7);
|
||||
|
||||
return new ListeningStatsResponse(tracksThisWeek, (int)Math.Round(secondsThisWeek), streakDays);
|
||||
var thisWeekHistory = database.ListeningHistories
|
||||
.AsNoTracking()
|
||||
.Where(l => l.UserId == request.UserId && l.ListenedAt >= weekAgo);
|
||||
|
||||
var tracksThisWeek = await thisWeekHistory
|
||||
.Where(l => l.IsCounted)
|
||||
.Select(l => l.TrackId)
|
||||
.Distinct()
|
||||
.CountAsync(cancellationToken);
|
||||
|
||||
var secondsThisWeek = await thisWeekHistory
|
||||
.Join(database.Tracks.AsNoTracking(), l => l.TrackId, t => t.Id, (l, t) => l.PlayedSeconds ?? (l.IsCounted ? t.DurationSeconds : 0))
|
||||
.SumAsync(cancellationToken);
|
||||
|
||||
var listenedDates = await database.ListeningHistories
|
||||
.AsNoTracking()
|
||||
.Where(l => l.UserId == request.UserId && l.IsCounted)
|
||||
.Select(l => l.ListenedAt.Date)
|
||||
.Distinct()
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var listenedDateSet = listenedDates.ToHashSet();
|
||||
var streakDays = 0;
|
||||
var day = DateTime.UtcNow.Date;
|
||||
if (!listenedDateSet.Contains(day))
|
||||
day = day.AddDays(-1);
|
||||
|
||||
while (listenedDateSet.Contains(day))
|
||||
{
|
||||
streakDays++;
|
||||
day = day.AddDays(-1);
|
||||
}
|
||||
|
||||
return new ListeningStatsResponse(tracksThisWeek, (int)Math.Round(secondsThisWeek), streakDays);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,24 +5,25 @@ using Musify.Application.Contracts;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Application.Users.Responses;
|
||||
|
||||
namespace Musify.Application.Users;
|
||||
|
||||
public record GetUserByIdQuery(long Id)
|
||||
: IQuery<ErrorOr<UserApplicationResponse>>;
|
||||
|
||||
public class GetUserByIdQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetUserByIdQuery, ErrorOr<UserApplicationResponse>>
|
||||
namespace Musify.Application.Users
|
||||
{
|
||||
public async ValueTask<ErrorOr<UserApplicationResponse>> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await database.Users
|
||||
.AsNoTracking()
|
||||
.Where(u => u.Id == request.Id)
|
||||
.Select(u => UserApplicationResponse.FromEntity(u))
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
public record GetUserByIdQuery(long Id)
|
||||
: IQuery<ErrorOr<UserApplicationResponse>>;
|
||||
|
||||
return user == null
|
||||
? AppErrors.NotFound("User", request.Id)
|
||||
: user;
|
||||
public class GetUserByIdQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetUserByIdQuery, ErrorOr<UserApplicationResponse>>
|
||||
{
|
||||
public async ValueTask<ErrorOr<UserApplicationResponse>> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await database.Users
|
||||
.AsNoTracking()
|
||||
.Where(u => u.Id == request.Id)
|
||||
.Select(u => UserApplicationResponse.FromEntity(u))
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
return user == null
|
||||
? AppErrors.NotFound("User", request.Id)
|
||||
: user;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,36 +5,37 @@ using Musify.Application.Contracts;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Application.Users.Responses;
|
||||
|
||||
namespace Musify.Application.Users;
|
||||
|
||||
public record GetUserFollowersQuery(long? ViewerId, long UserId, int PageNumber, int PageSize)
|
||||
: IQuery<ErrorOr<PaginatedResponse<UserSummaryResponse>>>;
|
||||
|
||||
public class GetUserFollowersQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetUserFollowersQuery, ErrorOr<PaginatedResponse<UserSummaryResponse>>>
|
||||
namespace Musify.Application.Users
|
||||
{
|
||||
public async ValueTask<ErrorOr<PaginatedResponse<UserSummaryResponse>>> Handle(GetUserFollowersQuery request, CancellationToken cancellationToken)
|
||||
public record GetUserFollowersQuery(long? ViewerId, long UserId, int PageNumber, int PageSize)
|
||||
: IQuery<ErrorOr<PaginatedResponse<UserSummaryResponse>>>;
|
||||
|
||||
public class GetUserFollowersQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetUserFollowersQuery, ErrorOr<PaginatedResponse<UserSummaryResponse>>>
|
||||
{
|
||||
if (!await database.Users.AnyAsync(u => u.Id == request.UserId, cancellationToken))
|
||||
return AppErrors.NotFound("User", request.UserId);
|
||||
public async ValueTask<ErrorOr<PaginatedResponse<UserSummaryResponse>>> Handle(GetUserFollowersQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!await database.Users.AnyAsync(u => u.Id == request.UserId, cancellationToken))
|
||||
return AppErrors.NotFound("User", request.UserId);
|
||||
|
||||
if (!await FollowVisibility.CanViewFollowersAsync(database, request.ViewerId, request.UserId, cancellationToken))
|
||||
return AppErrors.Forbidden("User", request.UserId);
|
||||
if (!await FollowVisibility.CanViewFollowersAsync(database, request.ViewerId, request.UserId, cancellationToken))
|
||||
return AppErrors.Forbidden("User", request.UserId);
|
||||
|
||||
var viewerId = request.ViewerId;
|
||||
var viewerId = request.ViewerId;
|
||||
|
||||
return await database.UserFollows
|
||||
.AsNoTracking()
|
||||
.Where(f => f.FollowedId == request.UserId)
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.ThenBy(f => f.Id)
|
||||
.Select(f => new UserSummaryResponse(
|
||||
f.Follower.Id,
|
||||
f.Follower.Name,
|
||||
f.Follower.FirstName,
|
||||
f.Follower.SecondName,
|
||||
f.Follower.ProfilePictureUrl,
|
||||
viewerId != null && database.UserFollows.Any(v => v.FollowerId == viewerId && v.FollowedId == f.FollowerId)))
|
||||
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
|
||||
return await database.UserFollows
|
||||
.AsNoTracking()
|
||||
.Where(f => f.FollowedId == request.UserId)
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.ThenBy(f => f.Id)
|
||||
.Select(f => new UserSummaryResponse(
|
||||
f.Follower.Id,
|
||||
f.Follower.Name,
|
||||
f.Follower.FirstName,
|
||||
f.Follower.SecondName,
|
||||
f.Follower.ProfilePictureUrl,
|
||||
viewerId != null && database.UserFollows.Any(v => v.FollowerId == viewerId && v.FollowedId == f.FollowerId)))
|
||||
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,33 +5,34 @@ using Musify.Application.Contracts;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Application.Users.Responses;
|
||||
|
||||
namespace Musify.Application.Users;
|
||||
|
||||
public record GetUserFollowingQuery(long? ViewerId, long UserId, int PageNumber, int PageSize)
|
||||
: IQuery<ErrorOr<PaginatedResponse<UserSummaryResponse>>>;
|
||||
|
||||
public class GetUserFollowingQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetUserFollowingQuery, ErrorOr<PaginatedResponse<UserSummaryResponse>>>
|
||||
namespace Musify.Application.Users
|
||||
{
|
||||
public async ValueTask<ErrorOr<PaginatedResponse<UserSummaryResponse>>> Handle(GetUserFollowingQuery request, CancellationToken cancellationToken)
|
||||
public record GetUserFollowingQuery(long? ViewerId, long UserId, int PageNumber, int PageSize)
|
||||
: IQuery<ErrorOr<PaginatedResponse<UserSummaryResponse>>>;
|
||||
|
||||
public class GetUserFollowingQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetUserFollowingQuery, ErrorOr<PaginatedResponse<UserSummaryResponse>>>
|
||||
{
|
||||
if (!await database.Users.AnyAsync(u => u.Id == request.UserId, cancellationToken))
|
||||
return AppErrors.NotFound("User", request.UserId);
|
||||
public async ValueTask<ErrorOr<PaginatedResponse<UserSummaryResponse>>> Handle(GetUserFollowingQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!await database.Users.AnyAsync(u => u.Id == request.UserId, cancellationToken))
|
||||
return AppErrors.NotFound("User", request.UserId);
|
||||
|
||||
var viewerId = request.ViewerId;
|
||||
var viewerId = request.ViewerId;
|
||||
|
||||
return await database.UserFollows
|
||||
.AsNoTracking()
|
||||
.Where(f => f.FollowerId == request.UserId)
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.ThenBy(f => f.Id)
|
||||
.Select(f => new UserSummaryResponse(
|
||||
f.Followed.Id,
|
||||
f.Followed.Name,
|
||||
f.Followed.FirstName,
|
||||
f.Followed.SecondName,
|
||||
f.Followed.ProfilePictureUrl,
|
||||
viewerId != null && database.UserFollows.Any(v => v.FollowerId == viewerId && v.FollowedId == f.FollowedId)))
|
||||
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
|
||||
return await database.UserFollows
|
||||
.AsNoTracking()
|
||||
.Where(f => f.FollowerId == request.UserId)
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.ThenBy(f => f.Id)
|
||||
.Select(f => new UserSummaryResponse(
|
||||
f.Followed.Id,
|
||||
f.Followed.Name,
|
||||
f.Followed.FirstName,
|
||||
f.Followed.SecondName,
|
||||
f.Followed.ProfilePictureUrl,
|
||||
viewerId != null && database.UserFollows.Any(v => v.FollowerId == viewerId && v.FollowedId == f.FollowedId)))
|
||||
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,58 +5,59 @@ using Musify.Application.Contracts;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Application.Users.Responses;
|
||||
|
||||
namespace Musify.Application.Users;
|
||||
|
||||
public record GetUserProfileQuery(long UserId, long? ViewerId = null) : IQuery<ErrorOr<UserProfileResponse>>;
|
||||
|
||||
public class GetUserProfileQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetUserProfileQuery, ErrorOr<UserProfileResponse>>
|
||||
namespace Musify.Application.Users
|
||||
{
|
||||
public async ValueTask<ErrorOr<UserProfileResponse>> Handle(GetUserProfileQuery request, CancellationToken cancellationToken)
|
||||
public record GetUserProfileQuery(long UserId, long? ViewerId = null) : IQuery<ErrorOr<UserProfileResponse>>;
|
||||
|
||||
public class GetUserProfileQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetUserProfileQuery, ErrorOr<UserProfileResponse>>
|
||||
{
|
||||
var user = await database.Users
|
||||
.AsNoTracking()
|
||||
.Where(u => u.Id == request.UserId)
|
||||
.Select(u => new
|
||||
{
|
||||
u.Id,
|
||||
u.Name,
|
||||
u.FirstName,
|
||||
u.SecondName,
|
||||
u.ProfilePictureUrl,
|
||||
u.CreatedAt
|
||||
})
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (user == null)
|
||||
return AppErrors.NotFound("User", request.UserId);
|
||||
|
||||
var followersCount = await database.UserFollows
|
||||
.AsNoTracking()
|
||||
.CountAsync(f => f.FollowedId == request.UserId, cancellationToken);
|
||||
|
||||
var followingCount = await database.UserFollows
|
||||
.AsNoTracking()
|
||||
.CountAsync(f => f.FollowerId == request.UserId, cancellationToken);
|
||||
|
||||
var isFollowing = request.ViewerId is { } viewerId && viewerId != request.UserId &&
|
||||
await database.UserFollows
|
||||
public async ValueTask<ErrorOr<UserProfileResponse>> Handle(GetUserProfileQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = await database.Users
|
||||
.AsNoTracking()
|
||||
.AnyAsync(f => f.FollowerId == viewerId && f.FollowedId == request.UserId, cancellationToken);
|
||||
.Where(u => u.Id == request.UserId)
|
||||
.Select(u => new
|
||||
{
|
||||
u.Id,
|
||||
u.Name,
|
||||
u.FirstName,
|
||||
u.SecondName,
|
||||
u.ProfilePictureUrl,
|
||||
u.CreatedAt
|
||||
})
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
var canViewFollowers = await FollowVisibility.CanViewFollowersAsync(
|
||||
database, request.ViewerId, request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
return AppErrors.NotFound("User", request.UserId);
|
||||
|
||||
return new UserProfileResponse(
|
||||
user.Id,
|
||||
user.Name,
|
||||
user.FirstName,
|
||||
user.SecondName,
|
||||
user.ProfilePictureUrl,
|
||||
user.CreatedAt,
|
||||
followersCount,
|
||||
followingCount,
|
||||
isFollowing,
|
||||
canViewFollowers);
|
||||
var followersCount = await database.UserFollows
|
||||
.AsNoTracking()
|
||||
.CountAsync(f => f.FollowedId == request.UserId, cancellationToken);
|
||||
|
||||
var followingCount = await database.UserFollows
|
||||
.AsNoTracking()
|
||||
.CountAsync(f => f.FollowerId == request.UserId, cancellationToken);
|
||||
|
||||
var isFollowing = request.ViewerId is { } viewerId && viewerId != request.UserId &&
|
||||
await database.UserFollows
|
||||
.AsNoTracking()
|
||||
.AnyAsync(f => f.FollowerId == viewerId && f.FollowedId == request.UserId, cancellationToken);
|
||||
|
||||
var canViewFollowers = await FollowVisibility.CanViewFollowersAsync(
|
||||
database, request.ViewerId, request.UserId, cancellationToken);
|
||||
|
||||
return new UserProfileResponse(
|
||||
user.Id,
|
||||
user.Name,
|
||||
user.FirstName,
|
||||
user.SecondName,
|
||||
user.ProfilePictureUrl,
|
||||
user.CreatedAt,
|
||||
followersCount,
|
||||
followingCount,
|
||||
isFollowing,
|
||||
canViewFollowers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,37 +4,38 @@ using Musify.Application.Contracts;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Application.Users.Responses;
|
||||
|
||||
namespace Musify.Application.Users;
|
||||
|
||||
public record GetUsersQuery(int PageNumber, int PageSize, string? UsernameSearch, long? ViewerId = null)
|
||||
: IQuery<PaginatedResponse<UserSummaryResponse>>;
|
||||
|
||||
public class GetUsersQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetUsersQuery, PaginatedResponse<UserSummaryResponse>>
|
||||
namespace Musify.Application.Users
|
||||
{
|
||||
public async ValueTask<PaginatedResponse<UserSummaryResponse>> Handle(GetUsersQuery request, CancellationToken cancellationToken)
|
||||
public record GetUsersQuery(int PageNumber, int PageSize, string? UsernameSearch, long? ViewerId = null)
|
||||
: IQuery<PaginatedResponse<UserSummaryResponse>>;
|
||||
|
||||
public class GetUsersQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetUsersQuery, PaginatedResponse<UserSummaryResponse>>
|
||||
{
|
||||
var usersQuery = database.Users
|
||||
.AsNoTracking();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.UsernameSearch))
|
||||
public async ValueTask<PaginatedResponse<UserSummaryResponse>> Handle(GetUsersQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var normalizedUsername = TextNormalizer.Normalize(request.UsernameSearch);
|
||||
usersQuery = usersQuery.Where(user => user.NormalizedName.Contains(normalizedUsername));
|
||||
var usersQuery = database.Users
|
||||
.AsNoTracking();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.UsernameSearch))
|
||||
{
|
||||
var normalizedUsername = TextNormalizer.Normalize(request.UsernameSearch);
|
||||
usersQuery = usersQuery.Where(user => user.NormalizedName.Contains(normalizedUsername));
|
||||
}
|
||||
|
||||
var viewerId = request.ViewerId;
|
||||
|
||||
return await usersQuery
|
||||
.OrderBy(user => user.Name)
|
||||
.ThenBy(user => user.Id)
|
||||
.Select(user => new UserSummaryResponse(
|
||||
user.Id,
|
||||
user.Name,
|
||||
user.FirstName,
|
||||
user.SecondName,
|
||||
user.ProfilePictureUrl,
|
||||
viewerId != null && database.UserFollows.Any(f => f.FollowerId == viewerId && f.FollowedId == user.Id)))
|
||||
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
|
||||
}
|
||||
|
||||
var viewerId = request.ViewerId;
|
||||
|
||||
return await usersQuery
|
||||
.OrderBy(user => user.Name)
|
||||
.ThenBy(user => user.Id)
|
||||
.Select(user => new UserSummaryResponse(
|
||||
user.Id,
|
||||
user.Name,
|
||||
user.FirstName,
|
||||
user.SecondName,
|
||||
user.ProfilePictureUrl,
|
||||
viewerId != null && database.UserFollows.Any(f => f.FollowerId == viewerId && f.FollowedId == user.Id)))
|
||||
.ToPaginatedAsync(new PageRequest(request.PageNumber, request.PageSize), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,16 @@ using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
|
||||
namespace Musify.Application.Users;
|
||||
|
||||
public record IsFollowingUserQuery(long FollowerId, long FollowedId) : IQuery<bool>;
|
||||
|
||||
public class IsFollowingUserQueryHandler(IDatabase database)
|
||||
: IQueryHandler<IsFollowingUserQuery, bool>
|
||||
namespace Musify.Application.Users
|
||||
{
|
||||
public ValueTask<bool> Handle(IsFollowingUserQuery request, CancellationToken cancellationToken) =>
|
||||
new(database.UserFollows
|
||||
.AsNoTracking()
|
||||
.AnyAsync(f => f.FollowerId == request.FollowerId && f.FollowedId == request.FollowedId, cancellationToken));
|
||||
public record IsFollowingUserQuery(long FollowerId, long FollowedId) : IQuery<bool>;
|
||||
|
||||
public class IsFollowingUserQueryHandler(IDatabase database)
|
||||
: IQueryHandler<IsFollowingUserQuery, bool>
|
||||
{
|
||||
public ValueTask<bool> Handle(IsFollowingUserQuery request, CancellationToken cancellationToken) =>
|
||||
new(database.UserFollows
|
||||
.AsNoTracking()
|
||||
.AnyAsync(f => f.FollowerId == request.FollowerId && f.FollowedId == request.FollowedId, cancellationToken));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
namespace Musify.Application.Users.Responses;
|
||||
|
||||
public record ListeningStatsResponse(int TracksThisWeek, int SecondsThisWeek, int StreakDays);
|
||||
namespace Musify.Application.Users.Responses
|
||||
{
|
||||
public record ListeningStatsResponse(int TracksThisWeek, int SecondsThisWeek, int StreakDays);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
using Musify.Domain.Entities;
|
||||
|
||||
namespace Musify.Application.Users.Responses;
|
||||
|
||||
public record UserApplicationResponse(
|
||||
long Id,
|
||||
string Name,
|
||||
string? FirstName,
|
||||
string? SecondName,
|
||||
string? ProfilePictureUrl,
|
||||
DateTime CreatedAt,
|
||||
DateTime UpdatedAt)
|
||||
namespace Musify.Application.Users.Responses
|
||||
{
|
||||
public static UserApplicationResponse FromEntity(User user)
|
||||
public record UserApplicationResponse(
|
||||
long Id,
|
||||
string Name,
|
||||
string? FirstName,
|
||||
string? SecondName,
|
||||
string? ProfilePictureUrl,
|
||||
DateTime CreatedAt,
|
||||
DateTime UpdatedAt)
|
||||
{
|
||||
return new UserApplicationResponse(
|
||||
user.Id,
|
||||
user.Name,
|
||||
user.FirstName,
|
||||
user.SecondName,
|
||||
user.ProfilePictureUrl,
|
||||
user.CreatedAt,
|
||||
user.UpdatedAt);
|
||||
public static UserApplicationResponse FromEntity(User user)
|
||||
{
|
||||
return new UserApplicationResponse(
|
||||
user.Id,
|
||||
user.Name,
|
||||
user.FirstName,
|
||||
user.SecondName,
|
||||
user.ProfilePictureUrl,
|
||||
user.CreatedAt,
|
||||
user.UpdatedAt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
|
||||
namespace Musify.Application.Users.Responses;
|
||||
|
||||
public record UserProfileResponse(
|
||||
long Id,
|
||||
string Name,
|
||||
string? FirstName,
|
||||
string? SecondName,
|
||||
string? ProfilePictureUrl,
|
||||
DateTime CreatedAt,
|
||||
int FollowersCount,
|
||||
int FollowingCount,
|
||||
bool IsFollowing,
|
||||
bool CanViewFollowers);
|
||||
namespace Musify.Application.Users.Responses
|
||||
{
|
||||
public record UserProfileResponse(
|
||||
long Id,
|
||||
string Name,
|
||||
string? FirstName,
|
||||
string? SecondName,
|
||||
string? ProfilePictureUrl,
|
||||
DateTime CreatedAt,
|
||||
int FollowersCount,
|
||||
int FollowingCount,
|
||||
bool IsFollowing,
|
||||
bool CanViewFollowers);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
|
||||
namespace Musify.Application.Users.Responses;
|
||||
|
||||
public record UserSummaryResponse(
|
||||
long Id,
|
||||
string Name,
|
||||
string? FirstName,
|
||||
string? SecondName,
|
||||
string? ProfilePictureUrl,
|
||||
bool IsFollowedByViewer);
|
||||
namespace Musify.Application.Users.Responses
|
||||
{
|
||||
public record UserSummaryResponse(
|
||||
long Id,
|
||||
string Name,
|
||||
string? FirstName,
|
||||
string? SecondName,
|
||||
string? ProfilePictureUrl,
|
||||
bool IsFollowedByViewer);
|
||||
}
|
||||
|
||||
@@ -6,52 +6,53 @@ using Musify.Application.Contracts;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Domain.Entities;
|
||||
|
||||
namespace Musify.Application.Users;
|
||||
|
||||
public record SyncUserCommand(
|
||||
long Id,
|
||||
string Name,
|
||||
string? FirstName,
|
||||
string? SecondName,
|
||||
string? ProfilePictureUrl)
|
||||
: ICommand<ErrorOr<Success>>;
|
||||
|
||||
public class SyncUserCommandHandler(IDatabase database, ILogger<SyncUserCommandHandler> logger)
|
||||
: ICommandHandler<SyncUserCommand, ErrorOr<Success>>
|
||||
namespace Musify.Application.Users
|
||||
{
|
||||
public async ValueTask<ErrorOr<Success>> Handle(SyncUserCommand request, CancellationToken cancellationToken)
|
||||
public record SyncUserCommand(
|
||||
long Id,
|
||||
string Name,
|
||||
string? FirstName,
|
||||
string? SecondName,
|
||||
string? ProfilePictureUrl)
|
||||
: ICommand<ErrorOr<Success>>;
|
||||
|
||||
public class SyncUserCommandHandler(IDatabase database, ILogger<SyncUserCommandHandler> logger)
|
||||
: ICommandHandler<SyncUserCommand, ErrorOr<Success>>
|
||||
{
|
||||
var user = await database.Users
|
||||
.SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken);
|
||||
|
||||
if (user == null)
|
||||
public async ValueTask<ErrorOr<Success>> Handle(SyncUserCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
await database.Users.AddAsync(new User
|
||||
{
|
||||
Id = request.Id,
|
||||
Name = request.Name,
|
||||
NormalizedName = TextNormalizer.Normalize(request.Name),
|
||||
FirstName = request.FirstName,
|
||||
SecondName = request.SecondName,
|
||||
ProfilePictureUrl = request.ProfilePictureUrl
|
||||
}, cancellationToken);
|
||||
var user = await database.Users
|
||||
.SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken);
|
||||
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("Provisioned user {UserId}", request.Id);
|
||||
if (user == null)
|
||||
{
|
||||
await database.Users.AddAsync(new User
|
||||
{
|
||||
Id = request.Id,
|
||||
Name = request.Name,
|
||||
NormalizedName = TextNormalizer.Normalize(request.Name),
|
||||
FirstName = request.FirstName,
|
||||
SecondName = request.SecondName,
|
||||
ProfilePictureUrl = request.ProfilePictureUrl
|
||||
}, cancellationToken);
|
||||
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
logger.LogInformation("Provisioned user {UserId}", request.Id);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
user.Name = request.Name;
|
||||
user.NormalizedName = TextNormalizer.Normalize(request.Name);
|
||||
user.FirstName = request.FirstName;
|
||||
user.SecondName = request.SecondName;
|
||||
user.ProfilePictureUrl = request.ProfilePictureUrl;
|
||||
|
||||
var written = await database.SaveChangesAsync(cancellationToken);
|
||||
if (written > 0)
|
||||
logger.LogInformation("Synced profile for user {UserId}", request.Id);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
user.Name = request.Name;
|
||||
user.NormalizedName = TextNormalizer.Normalize(request.Name);
|
||||
user.FirstName = request.FirstName;
|
||||
user.SecondName = request.SecondName;
|
||||
user.ProfilePictureUrl = request.ProfilePictureUrl;
|
||||
|
||||
var written = await database.SaveChangesAsync(cancellationToken);
|
||||
if (written > 0)
|
||||
logger.LogInformation("Synced profile for user {UserId}", request.Id);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,28 +4,29 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Contracts;
|
||||
|
||||
namespace Musify.Application.Users;
|
||||
|
||||
public record UnfollowUserCommand(long FollowerId, long FollowedId) : ICommand<ErrorOr<Success>>;
|
||||
|
||||
public class UnfollowUserCommandHandler(
|
||||
IDatabase database,
|
||||
ILogger<UnfollowUserCommandHandler> logger)
|
||||
: ICommandHandler<UnfollowUserCommand, ErrorOr<Success>>
|
||||
namespace Musify.Application.Users
|
||||
{
|
||||
public async ValueTask<ErrorOr<Success>> Handle(UnfollowUserCommand request, CancellationToken cancellationToken)
|
||||
public record UnfollowUserCommand(long FollowerId, long FollowedId) : ICommand<ErrorOr<Success>>;
|
||||
|
||||
public class UnfollowUserCommandHandler(
|
||||
IDatabase database,
|
||||
ILogger<UnfollowUserCommandHandler> logger)
|
||||
: ICommandHandler<UnfollowUserCommand, ErrorOr<Success>>
|
||||
{
|
||||
var follow = await database.UserFollows
|
||||
.SingleOrDefaultAsync(f => f.FollowerId == request.FollowerId && f.FollowedId == request.FollowedId, cancellationToken);
|
||||
public async ValueTask<ErrorOr<Success>> Handle(UnfollowUserCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var follow = await database.UserFollows
|
||||
.SingleOrDefaultAsync(f => f.FollowerId == request.FollowerId && f.FollowedId == request.FollowedId, cancellationToken);
|
||||
|
||||
if (follow == null)
|
||||
return Result.Success;
|
||||
|
||||
database.UserFollows.Remove(follow);
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation("User {FollowerId} unfollowed user {FollowedId}", request.FollowerId, request.FollowedId);
|
||||
|
||||
if (follow == null)
|
||||
return Result.Success;
|
||||
|
||||
database.UserFollows.Remove(follow);
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation("User {FollowerId} unfollowed user {FollowedId}", request.FollowerId, request.FollowedId);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user