diff --git a/backend/src/Modules/Admin/Presentation/Endpoints/AdminEndpoints.cs b/backend/src/Modules/Admin/Presentation/Endpoints/AdminEndpoints.cs index 61b655b..68fdd89 100644 --- a/backend/src/Modules/Admin/Presentation/Endpoints/AdminEndpoints.cs +++ b/backend/src/Modules/Admin/Presentation/Endpoints/AdminEndpoints.cs @@ -79,13 +79,13 @@ public sealed class AdminEndpoints : ICarterModule return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound("User not found"); }); - group.MapGet("clean/dry-run", async (IFileStorageService fileStorage, AuthDbContext identityDb, ISender sender, CancellationToken ct) => + group.MapGet("clean/dry-run", async ([FromServices] IFileStorageService fileStorage, [FromServices] AuthDbContext identityDb, ISender sender, CancellationToken ct) => { var result = await sender.Send(new CleanDryRunQuery(fileStorage, identityDb), ct); return Results.Ok(result.Value); }); - group.MapPost("clean/run", async (IFileStorageService fileStorage, AuthDbContext identityDb, ISender sender, CancellationToken ct) => + group.MapPost("clean/run", async ([FromServices] IFileStorageService fileStorage, [FromServices] AuthDbContext identityDb, ISender sender, CancellationToken ct) => { var result = await sender.Send(new CleanRunCommand(fileStorage, identityDb), ct); return Results.Ok(result.Value); diff --git a/backend/src/Modules/Auth/Migrations/20270327140400_AddUserExtendedFields.Designer.cs b/backend/src/Modules/Auth/Migrations/20270327140400_AddUserExtendedFields.Designer.cs new file mode 100644 index 0000000..7db5898 --- /dev/null +++ b/backend/src/Modules/Auth/Migrations/20270327140400_AddUserExtendedFields.Designer.cs @@ -0,0 +1,92 @@ +// +using System; +using Knot.Modules.Auth.Infrastructure.Persistence; +using Knot.Modules.Auth.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Knot.Modules.Auth.Migrations +{ + [DbContext(typeof(AuthDbContext))] + [Migration("20270327140400_AddUserExtendedFields")] + partial class AddUserExtendedFields + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("identity") + .HasAnnotation("ProductVersion", "10.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Knot.Modules.Auth.Domain.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("Bio") + .HasColumnType("text"); + + b.Property("Birthday") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Domain") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("HideStatus") + .HasColumnType("boolean"); + + b.Property("HideStoryViews") + .HasColumnType("boolean"); + + b.Property("IsExternal") + .HasColumnType("boolean"); + + b.Property("IsOnline") + .HasColumnType("boolean"); + + b.Property("LastSeen") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users", "identity"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Modules/Auth/Migrations/20270327140400_AddUserExtendedFields.cs b/backend/src/Modules/Auth/Migrations/20270327140400_AddUserExtendedFields.cs new file mode 100644 index 0000000..60ec936 --- /dev/null +++ b/backend/src/Modules/Auth/Migrations/20270327140400_AddUserExtendedFields.cs @@ -0,0 +1,82 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Knot.Modules.Auth.Migrations +{ + /// + public partial class AddUserExtendedFields : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsExternal", + schema: "identity", + table: "Users", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "Domain", + schema: "identity", + table: "Users", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "IsOnline", + schema: "identity", + table: "Users", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "LastSeen", + schema: "identity", + table: "Users", + type: "timestamp with time zone", + nullable: true); + + migrationBuilder.AddColumn( + name: "HideStatus", + schema: "identity", + table: "Users", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsExternal", + schema: "identity", + table: "Users"); + + migrationBuilder.DropColumn( + name: "Domain", + schema: "identity", + table: "Users"); + + migrationBuilder.DropColumn( + name: "IsOnline", + schema: "identity", + table: "Users"); + + migrationBuilder.DropColumn( + name: "LastSeen", + schema: "identity", + table: "Users"); + + migrationBuilder.DropColumn( + name: "HideStatus", + schema: "identity", + table: "Users"); + } + } +} diff --git a/backend/src/Modules/Auth/Migrations/AuthDbContextModelSnapshot.cs b/backend/src/Modules/Auth/Migrations/AuthDbContextModelSnapshot.cs index 6bdbe0f..c68015a 100644 --- a/backend/src/Modules/Auth/Migrations/AuthDbContextModelSnapshot.cs +++ b/backend/src/Modules/Auth/Migrations/AuthDbContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using Knot.Modules.Auth.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; @@ -45,12 +45,27 @@ namespace Knot.Modules.Auth.Migrations .IsRequired() .HasColumnType("text"); + b.Property("Domain") + .HasColumnType("text"); + b.Property("Email") .HasColumnType("text"); + b.Property("HideStatus") + .HasColumnType("boolean"); + b.Property("HideStoryViews") .HasColumnType("boolean"); + b.Property("IsExternal") + .HasColumnType("boolean"); + + b.Property("IsOnline") + .HasColumnType("boolean"); + + b.Property("LastSeen") + .HasColumnType("timestamp with time zone"); + b.Property("PasswordHash") .IsRequired() .HasColumnType("text"); diff --git a/backend/src/Modules/Federation/Presentation/Endpoints/FederationEndpoints.cs b/backend/src/Modules/Federation/Presentation/Endpoints/FederationEndpoints.cs index a753458..d730dba 100644 --- a/backend/src/Modules/Federation/Presentation/Endpoints/FederationEndpoints.cs +++ b/backend/src/Modules/Federation/Presentation/Endpoints/FederationEndpoints.cs @@ -41,7 +41,7 @@ public sealed class FederationEndpoints : ICarterModule return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound(new { error = result.Error.Description }); }); - group.MapGet("/proxy/{id}", async (string id, [FromHeader(Name = "X-Knot-Signature")] string signature, [FromHeader(Name = "X-Knot-Domain")] string senderDomain, IFileStorageService storage, ISettingsService settingsService, CancellationToken ct) => + group.MapGet("/proxy/{id}", async (string id, [FromHeader(Name = "X-Knot-Signature")] string signature, [FromHeader(Name = "X-Knot-Domain")] string senderDomain, [FromServices] IFileStorageService storage, [FromServices] ISettingsService settingsService, CancellationToken ct) => { // 1. Валидация подписи (упрощенно) var settings = await settingsService.GetSettingsAsync(ct); diff --git a/backend/src/Modules/Profiles/Application/Abstractions/IAvatarStorageService.cs b/backend/src/Modules/Profiles/Application/Abstractions/IAvatarStorageService.cs deleted file mode 100644 index 45c880c..0000000 --- a/backend/src/Modules/Profiles/Application/Abstractions/IAvatarStorageService.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace Knot.Modules.Profiles.Application.Abstractions; - -/// -/// Интерфейс для хранения аватаров в S3 (MinIO). -/// Отдельный от IFileStorageService модуля Storage, -/// чтобы не создавать прямой зависимости на Storage модуль. -/// -public interface IAvatarStorageService -{ - /// - /// Загружает файл в S3-бакет аватаров и возвращает fileId (ключ объекта). - /// - Task UploadAsync(System.IO.Stream stream, string fileName, string contentType, CancellationToken ct = default); - - /// - /// Удаляет файл аватара из S3 по fileId. - /// - Task DeleteAsync(string fileId, CancellationToken ct = default); -} diff --git a/backend/src/Modules/Profiles/Application/Abstractions/IProfilesDbContext.cs b/backend/src/Modules/Profiles/Application/Abstractions/IProfilesDbContext.cs deleted file mode 100644 index 21b9fc1..0000000 --- a/backend/src/Modules/Profiles/Application/Abstractions/IProfilesDbContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Knot.Modules.Profiles.Domain; -using Microsoft.EntityFrameworkCore; -using System.Threading; -using System.Threading.Tasks; - -using Knot.Modules.Profiles.Application.Abstractions; -namespace Knot.Modules.Profiles.Application.Abstractions; - -public interface IProfilesDbContext -{ - DbSet Profiles { get; } - - Task SaveChangesAsync(CancellationToken cancellationToken = default); -} diff --git a/backend/src/Modules/Profiles/Application/Profiles/Avatar/CropAvatar.cs b/backend/src/Modules/Profiles/Application/Profiles/Avatar/CropAvatar.cs index fac3893..4c15b41 100644 --- a/backend/src/Modules/Profiles/Application/Profiles/Avatar/CropAvatar.cs +++ b/backend/src/Modules/Profiles/Application/Profiles/Avatar/CropAvatar.cs @@ -1,5 +1,5 @@ -using Knot.Shared.Kernel; -using Knot.Modules.Profiles.Application.Abstractions; +using Knot.Shared.Kernel; +using Knot.Modules.Profiles.Domain; using Knot.Modules.Profiles.Application.Profiles.DTOs; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; @@ -34,10 +34,10 @@ internal sealed class CropAvatarCommandHandler : ICommandHandler(ProfilesErrors.ProfileNotFound); - // Обрезать и ресайзнуть изображение до 400×400 + // Обрезать Рё ресайзнуть изображение РґРѕ 400Г—400 using var ms = await CropAndResizeAsync(request, cancellationToken); - // Удалить старый аватар из S3 + // Удалить старый аватар РёР· S3 if (!string.IsNullOrEmpty(profile.AvatarUrl)) await _avatarStorage.DeleteAsync(profile.AvatarUrl, cancellationToken); diff --git a/backend/src/Modules/Profiles/Application/Profiles/Avatar/UploadAvatar.cs b/backend/src/Modules/Profiles/Application/Profiles/Avatar/UploadAvatar.cs index 6f60e8c..1012802 100644 --- a/backend/src/Modules/Profiles/Application/Profiles/Avatar/UploadAvatar.cs +++ b/backend/src/Modules/Profiles/Application/Profiles/Avatar/UploadAvatar.cs @@ -1,5 +1,5 @@ -using Knot.Shared.Kernel; -using Knot.Modules.Profiles.Application.Abstractions; +using Knot.Shared.Kernel; +using Knot.Modules.Profiles.Domain; using Knot.Modules.Profiles.Application.Profiles.DTOs; using System; using System.IO; @@ -8,7 +8,7 @@ using System.Threading.Tasks; namespace Knot.Modules.Profiles.Application.Profiles.Avatar; -// ─── Upload ──────────────────────────────────────────────────────────────── +// в”Ђв”Ђв”Ђ Upload в”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђ public sealed record UploadAvatarCommand( Guid UserId, @@ -33,7 +33,7 @@ internal sealed class UploadAvatarCommandHandler : ICommandHandler(ProfilesErrors.ProfileNotFound); - // Удалить старый аватар из S3, если был + // Удалить старый аватар РёР· S3, если был if (!string.IsNullOrEmpty(profile.AvatarUrl)) await _avatarStorage.DeleteAsync(profile.AvatarUrl, cancellationToken); @@ -47,7 +47,7 @@ internal sealed class UploadAvatarCommandHandler : ICommandHandler; diff --git a/backend/src/Modules/Profiles/Application/Profiles/DTOs/ProfilesRequests.cs b/backend/src/Modules/Profiles/Application/Profiles/DTOs/ProfilesRequests.cs index ebf67a7..64e11a9 100644 --- a/backend/src/Modules/Profiles/Application/Profiles/DTOs/ProfilesRequests.cs +++ b/backend/src/Modules/Profiles/Application/Profiles/DTOs/ProfilesRequests.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Knot.Modules.Profiles.Application.Profiles.DTOs; diff --git a/backend/src/Modules/Profiles/Application/Profiles/DTOs/UserProfileDto.cs b/backend/src/Modules/Profiles/Application/Profiles/DTOs/UserProfileDto.cs index 2bdfe81..6d13fcd 100644 --- a/backend/src/Modules/Profiles/Application/Profiles/DTOs/UserProfileDto.cs +++ b/backend/src/Modules/Profiles/Application/Profiles/DTOs/UserProfileDto.cs @@ -1,4 +1,4 @@ -using System; +using System; using Knot.Modules.Profiles.Domain; namespace Knot.Modules.Profiles.Application.Profiles.DTOs; diff --git a/backend/src/Modules/Profiles/Application/Profiles/GetUser/GetUser.cs b/backend/src/Modules/Profiles/Application/Profiles/GetUser/GetUser.cs index e39eadf..399d0a5 100644 --- a/backend/src/Modules/Profiles/Application/Profiles/GetUser/GetUser.cs +++ b/backend/src/Modules/Profiles/Application/Profiles/GetUser/GetUser.cs @@ -1,5 +1,5 @@ -using Knot.Shared.Kernel; -using Knot.Modules.Profiles.Application.Abstractions; +using Knot.Shared.Kernel; +using Knot.Modules.Profiles.Domain; using Knot.Modules.Profiles.Application.Profiles.DTOs; using System; using System.Threading; diff --git a/backend/src/Modules/Profiles/Application/Profiles/Integration/UserRegisteredDomainEventHandler.cs b/backend/src/Modules/Profiles/Application/Profiles/Integration/UserRegisteredDomainEventHandler.cs index 5f13c39..a2b1857 100644 --- a/backend/src/Modules/Profiles/Application/Profiles/Integration/UserRegisteredDomainEventHandler.cs +++ b/backend/src/Modules/Profiles/Application/Profiles/Integration/UserRegisteredDomainEventHandler.cs @@ -1,8 +1,14 @@ +using MediatR; +using System.Threading; +using System.Threading.Tasks; +using Knot.Modules.Profiles.Domain; +using Knot.Shared.Kernel.Events; + namespace Knot.Modules.Profiles.Application.Profiles.Integration; /// -/// Обработчик события из модуля Auth. -/// Создаёт пустой профиль при регистрации пользователя. +/// Обработчик события РёР· модуля Auth. +/// Создаёт пустой профиль РїСЂРё регистрации пользователя. /// public class UserRegisteredDomainEventHandler : INotificationHandler { diff --git a/backend/src/Modules/Profiles/Application/Profiles/Search/SearchUsers.cs b/backend/src/Modules/Profiles/Application/Profiles/Search/SearchUsers.cs index 162b063..ec79843 100644 --- a/backend/src/Modules/Profiles/Application/Profiles/Search/SearchUsers.cs +++ b/backend/src/Modules/Profiles/Application/Profiles/Search/SearchUsers.cs @@ -1,5 +1,5 @@ -using Knot.Shared.Kernel; -using Knot.Modules.Profiles.Application.Abstractions; +using Knot.Shared.Kernel; +using Knot.Modules.Profiles.Domain; using Knot.Modules.Profiles.Application.Profiles.DTOs; using System.Collections.Generic; using System.Linq; diff --git a/backend/src/Modules/Profiles/Application/Profiles/UpdateProfile/UpdateProfile.cs b/backend/src/Modules/Profiles/Application/Profiles/UpdateProfile/UpdateProfile.cs index df2d880..8b90b67 100644 --- a/backend/src/Modules/Profiles/Application/Profiles/UpdateProfile/UpdateProfile.cs +++ b/backend/src/Modules/Profiles/Application/Profiles/UpdateProfile/UpdateProfile.cs @@ -1,5 +1,5 @@ -using Knot.Shared.Kernel; -using Knot.Modules.Profiles.Application.Abstractions; +using Knot.Shared.Kernel; +using Knot.Modules.Profiles.Domain; using Knot.Modules.Profiles.Application.Profiles.DTOs; using System; using System.Threading; diff --git a/backend/src/Modules/Profiles/Application/Profiles/UpdateSettings/UpdateSettings.cs b/backend/src/Modules/Profiles/Application/Profiles/UpdateSettings/UpdateSettings.cs index 7b2a702..6fd9688 100644 --- a/backend/src/Modules/Profiles/Application/Profiles/UpdateSettings/UpdateSettings.cs +++ b/backend/src/Modules/Profiles/Application/Profiles/UpdateSettings/UpdateSettings.cs @@ -1,5 +1,5 @@ -using Knot.Shared.Kernel; -using Knot.Modules.Profiles.Application.Abstractions; +using Knot.Shared.Kernel; +using Knot.Modules.Profiles.Domain; using Knot.Modules.Profiles.Application.Profiles.DTOs; using System; using System.Threading; diff --git a/backend/src/Modules/Profiles/DependencyInjection.cs b/backend/src/Modules/Profiles/DependencyInjection.cs index 5ea6e58..c061ce0 100644 --- a/backend/src/Modules/Profiles/DependencyInjection.cs +++ b/backend/src/Modules/Profiles/DependencyInjection.cs @@ -1,3 +1,9 @@ +using Knot.Modules.Profiles.Domain; +using Knot.Modules.Profiles.Infrastructure.Database; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Driver; + namespace Knot.Modules.Profiles; public static class DependencyInjection @@ -11,10 +17,11 @@ public static class DependencyInjection // MongoDB Registration var mongoConnection = configuration.GetConnectionString("MongoConnection") ?? configuration["MONGO_URL"] - ?? "mongodb://localhost:27017"; + ?? "mongodb://mongo:27017"; var mongoClient = new MongoClient(mongoConnection); - var database = mongoClient.GetDatabase("knot_messager"); + var databaseName = new MongoUrl(mongoConnection).DatabaseName ?? "knot_messager"; + var database = mongoClient.GetDatabase(databaseName); services.AddSingleton(database); services.AddMediatR(config => diff --git a/backend/src/Modules/Profiles/Domain/Events/ProfileDomainEvents.cs b/backend/src/Modules/Profiles/Domain/Events/ProfileDomainEvents.cs index 2e49542..7f30d7e 100644 --- a/backend/src/Modules/Profiles/Domain/Events/ProfileDomainEvents.cs +++ b/backend/src/Modules/Profiles/Domain/Events/ProfileDomainEvents.cs @@ -1,11 +1,11 @@ -namespace Knot.Modules.Profiles.Domain.Events; +namespace Knot.Modules.Profiles.Domain.Events; /// -/// Профиль пользователя создан. +/// Профиль пользователя создан. /// public record ProfileCreatedDomainEvent(Guid ProfileId) : IDomainEvent; /// -/// Профиль пользователя обновлен. +/// Профиль пользователя обновлен. /// public record ProfileUpdatedDomainEvent(Guid ProfileId) : IDomainEvent; diff --git a/backend/src/Modules/Profiles/Domain/IAvatarStorageService.cs b/backend/src/Modules/Profiles/Domain/IAvatarStorageService.cs new file mode 100644 index 0000000..96f5192 --- /dev/null +++ b/backend/src/Modules/Profiles/Domain/IAvatarStorageService.cs @@ -0,0 +1,11 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Knot.Modules.Profiles.Domain; + +public interface IAvatarStorageService +{ + Task UploadAsync(Stream content, string fileName, string contentType, CancellationToken ct = default); + Task DeleteAsync(string fileId, CancellationToken ct = default); +} diff --git a/backend/src/Modules/Profiles/Application/Abstractions/IProfileRepository.cs b/backend/src/Modules/Profiles/Domain/IProfileRepository.cs similarity index 62% rename from backend/src/Modules/Profiles/Application/Abstractions/IProfileRepository.cs rename to backend/src/Modules/Profiles/Domain/IProfileRepository.cs index 2f8e22d..74f365a 100644 --- a/backend/src/Modules/Profiles/Application/Abstractions/IProfileRepository.cs +++ b/backend/src/Modules/Profiles/Domain/IProfileRepository.cs @@ -1,8 +1,13 @@ -namespace Knot.Modules.Profiles.Application.Abstractions; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Knot.Modules.Profiles.Domain; public interface IProfileRepository { - Task GetByIdAsync(Guid userId, CancellationToken ct = default); + Task GetByIdAsync(Guid id, CancellationToken ct = default); Task GetByUsernameAsync(string username, CancellationToken ct = default); Task AddAsync(ProfileDocument profile, CancellationToken ct = default); Task UpdateAsync(ProfileDocument profile, CancellationToken ct = default); diff --git a/backend/src/Modules/Profiles/Application/Abstractions/IProfilesUnitOfWork.cs b/backend/src/Modules/Profiles/Domain/IProfilesUnitOfWork.cs similarity index 61% rename from backend/src/Modules/Profiles/Application/Abstractions/IProfilesUnitOfWork.cs rename to backend/src/Modules/Profiles/Domain/IProfilesUnitOfWork.cs index eba54c7..b18ed41 100644 --- a/backend/src/Modules/Profiles/Application/Abstractions/IProfilesUnitOfWork.cs +++ b/backend/src/Modules/Profiles/Domain/IProfilesUnitOfWork.cs @@ -1,7 +1,7 @@ -using System.Threading; +using System.Threading; using System.Threading.Tasks; -namespace Knot.Modules.Profiles.Application.Abstractions; +namespace Knot.Modules.Profiles.Domain; public interface IProfilesUnitOfWork { diff --git a/backend/src/Modules/Profiles/Domain/Profile.cs b/backend/src/Modules/Profiles/Domain/Profile.cs index 3eaaf22..d368d12 100644 --- a/backend/src/Modules/Profiles/Domain/Profile.cs +++ b/backend/src/Modules/Profiles/Domain/Profile.cs @@ -1,4 +1,4 @@ -namespace Knot.Modules.Profiles.Domain; +namespace Knot.Modules.Profiles.Domain; public sealed class Profile : AggregateRoot { diff --git a/backend/src/Modules/Profiles/Domain/ProfileDocument.cs b/backend/src/Modules/Profiles/Domain/ProfileDocument.cs index b593e36..2deffad 100644 --- a/backend/src/Modules/Profiles/Domain/ProfileDocument.cs +++ b/backend/src/Modules/Profiles/Domain/ProfileDocument.cs @@ -1,8 +1,11 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + namespace Knot.Modules.Profiles.Domain; /// -/// MongoDB-документ профиля пользователя. -/// Id совпадает с UserId из модуля Auth (Postgres). +/// MongoDB-документ профиля пользователя. +/// Id совпадает СЃ UserId РёР· модуля Auth (Postgres). /// public sealed class ProfileDocument { diff --git a/backend/src/Modules/Profiles/Domain/ProfilesErrors.cs b/backend/src/Modules/Profiles/Domain/ProfilesErrors.cs index 43bac43..23bc1be 100644 --- a/backend/src/Modules/Profiles/Domain/ProfilesErrors.cs +++ b/backend/src/Modules/Profiles/Domain/ProfilesErrors.cs @@ -1,4 +1,4 @@ -namespace Knot.Modules.Profiles.Domain; +namespace Knot.Modules.Profiles.Domain; using Knot.Shared.Kernel; public static class ProfilesErrors { diff --git a/backend/src/Modules/Profiles/GlobalUsings.cs b/backend/src/Modules/Profiles/GlobalUsings.cs index 687f9d5..ff58cfb 100644 --- a/backend/src/Modules/Profiles/GlobalUsings.cs +++ b/backend/src/Modules/Profiles/GlobalUsings.cs @@ -1,7 +1,6 @@ global using Knot.Shared.Kernel; global using Knot.Shared.Kernel.Events; global using Knot.Shared.Kernel.Storage; -global using Knot.Modules.Profiles.Application.Abstractions; global using Knot.Modules.Profiles.Domain; global using Knot.Modules.Profiles.Domain.Events; global using Knot.Modules.Profiles.Infrastructure.Database; diff --git a/backend/src/Modules/Profiles/Infrastructure/Database/AvatarStorageService.cs b/backend/src/Modules/Profiles/Infrastructure/Database/AvatarStorageService.cs index 06b369b..31a1641 100644 --- a/backend/src/Modules/Profiles/Infrastructure/Database/AvatarStorageService.cs +++ b/backend/src/Modules/Profiles/Infrastructure/Database/AvatarStorageService.cs @@ -1,3 +1,9 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Knot.Modules.Profiles.Domain; +using Knot.Shared.Kernel.Storage; + namespace Knot.Modules.Profiles.Infrastructure.Database; public class AvatarStorageService : IAvatarStorageService @@ -11,13 +17,13 @@ public class AvatarStorageService : IAvatarStorageService public async Task UploadAsync(Stream content, string fileName, string contentType, CancellationToken ct = default) { - // IFileStorageService не принимает CancellationToken в UploadFileAsync + // IFileStorageService РЅРµ принимает CancellationToken РІ UploadFileAsync return await _fileStorage.UploadFileAsync(content, fileName, contentType); } public async Task DeleteAsync(string fileId, CancellationToken ct = default) { - // IFileStorageService не принимает CancellationToken в DeleteFileAsync + // IFileStorageService РЅРµ принимает CancellationToken РІ DeleteFileAsync await _fileStorage.DeleteFileAsync(fileId); } } diff --git a/backend/src/Modules/Profiles/Infrastructure/Database/ProfileRepository.cs b/backend/src/Modules/Profiles/Infrastructure/Database/ProfileRepository.cs index daea3d5..90bb8e1 100644 --- a/backend/src/Modules/Profiles/Infrastructure/Database/ProfileRepository.cs +++ b/backend/src/Modules/Profiles/Infrastructure/Database/ProfileRepository.cs @@ -1,3 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Knot.Modules.Profiles.Domain; +using MongoDB.Bson; +using MongoDB.Driver; + namespace Knot.Modules.Profiles.Infrastructure.Database; public class ProfileRepository : IProfileRepository diff --git a/backend/src/Modules/Profiles/Infrastructure/Database/ProfilesUnitOfWork.cs b/backend/src/Modules/Profiles/Infrastructure/Database/ProfilesUnitOfWork.cs index feaff52..88fc017 100644 --- a/backend/src/Modules/Profiles/Infrastructure/Database/ProfilesUnitOfWork.cs +++ b/backend/src/Modules/Profiles/Infrastructure/Database/ProfilesUnitOfWork.cs @@ -1,8 +1,10 @@ +using System.Threading; +using System.Threading.Tasks; +using Knot.Modules.Profiles.Domain; + namespace Knot.Modules.Profiles.Infrastructure.Database; public class ProfilesUnitOfWork : IProfilesUnitOfWork { - // MongoDB не поддерживает транзакции без репликации в простом виде, - // поэтому UnitOfWork здесь формальный для соответствия интерфейсу. public Task SaveChangesAsync(CancellationToken ct = default) => Task.CompletedTask; } diff --git a/backend/src/Modules/Profiles/Presentation/Endpoints/ProfilesEndpoints.cs b/backend/src/Modules/Profiles/Presentation/Endpoints/ProfilesEndpoints.cs index bb06d79..009bcd0 100644 --- a/backend/src/Modules/Profiles/Presentation/Endpoints/ProfilesEndpoints.cs +++ b/backend/src/Modules/Profiles/Presentation/Endpoints/ProfilesEndpoints.cs @@ -1,4 +1,4 @@ -using Carter; +using Carter; using Knot.Shared.Kernel; using Knot.Modules.Profiles.Application.Profiles.Avatar; using Knot.Modules.Profiles.Application.Profiles.GetProfile; diff --git a/backend/src/Modules/Storage/Presentation/Endpoints/FilesEndpoints.cs b/backend/src/Modules/Storage/Presentation/Endpoints/FilesEndpoints.cs index 5ab0469..13cd44c 100644 --- a/backend/src/Modules/Storage/Presentation/Endpoints/FilesEndpoints.cs +++ b/backend/src/Modules/Storage/Presentation/Endpoints/FilesEndpoints.cs @@ -19,7 +19,7 @@ public sealed class FilesEndpoints : ICarterModule { var group = app.MapGroup("api/files"); - group.MapGet("{id}", async (string id, [FromQuery] bool download, IFileStorageService fileStorage, IMemoryCache cache, CancellationToken ct) => + group.MapGet("{id}", async (string id, [FromQuery] bool download, [FromServices] IFileStorageService fileStorage, [FromServices] IMemoryCache cache, CancellationToken ct) => { // (Текущая логика локальных файлов остается без изменений) try diff --git a/client-web/src/core/domain/types.ts b/client-web/src/core/domain/types.ts index 21918eb..8708666 100644 --- a/client-web/src/core/domain/types.ts +++ b/client-web/src/core/domain/types.ts @@ -95,7 +95,14 @@ export interface Message { isDeleted?: boolean; quote?: string | null; media?: MediaItem[]; - sender: { id: string; username: string; displayName: string }; + sender: { + id: string; + username: string; + userName?: string; + displayName: string; + avatar?: string | null; + avatarUrl?: string | null; + }; } | null; forwardedFrom?: UserBasic | null; media: MediaItem[]; diff --git a/client-web/src/core/presentation/layouts/SideMenu.tsx b/client-web/src/core/presentation/layouts/SideMenu.tsx index c6651f0..d7cde90 100644 --- a/client-web/src/core/presentation/layouts/SideMenu.tsx +++ b/client-web/src/core/presentation/layouts/SideMenu.tsx @@ -135,7 +135,7 @@ export default function SideMenu({ isOpen, onClose, onOpenProfile }: SideMenuPro onClose(); }; - const initials = (user?.displayName || user?.username || '??') + const initials = (user?.displayName || user?.userName || user?.username || '??') .split(' ') .map((w: string) => w[0]) .join('') @@ -193,11 +193,11 @@ export default function SideMenu({ isOpen, onClose, onOpenProfile }: SideMenuPro {/* Name & username */}

- {user?.displayName || user?.username} + {user?.displayName || user?.userName || user?.username || ''}

- {user?.username} + {user?.userName || user?.username || ''}
{/* Bottom fade line */} @@ -526,8 +526,8 @@ export default function SideMenu({ isOpen, onClose, onOpenProfile }: SideMenuPro )}
-

{u.displayName || u.username}

-

@{u.username}

+

{u.displayName || u.userName || u.username || ''}

+

@{u.userName || u.username || ''}

); @@ -218,7 +218,7 @@ export default function Sidebar() { {showNewChat && setShowNewChat(false)} />} - {showProfile && setShowProfile(false)} isSelf />} + {showProfile && user && setShowProfile(false)} isSelf />} } -
- {users.length === 0 && !isSearching && ( -
- {t.noUsersFound} -
- )} - {users.map(u => ( -
fetchUserDetails(u.id)}> - {u.avatar ? ( - avatar - ) : ( -
- -
+
+ {users.length === 0 && !isSearching && ( +
+ {t.noUsersFound} +
)} -
-

{u.displayName}

-
@{u.username}
-
+ {users.map(u => ( +
fetchUserDetails(u.id)}> + {u.avatarUrl || u.avatar ? ( + avatar + ) : ( +
+ +
+ )} +
+

{u.displayName || u.userName || u.username || ''}

+
@{u.userName || u.username || ''}
+
+
+ ))}
- ))} -
- - ) : ( - - - -
-
- {selectedUser.avatar ? ( - Avatar - ) : ( -
- -
- )} - -

{selectedUser.displayName}

-

@{selectedUser.username}

+ + ) : ( + + + +
+
+ {selectedUser.avatarUrl || selectedUser.avatar ? ( + Avatar + ) : ( +
+ +
+ )} + +

{selectedUser.displayName || selectedUser.userName || selectedUser.username || ''}

+

@{selectedUser.userName || selectedUser.username || ''}

diff --git a/client-web/src/modules/calls/presentation/components/CallModal.tsx b/client-web/src/modules/calls/presentation/components/CallModal.tsx index 953401d..ae92964 100644 --- a/client-web/src/modules/calls/presentation/components/CallModal.tsx +++ b/client-web/src/modules/calls/presentation/components/CallModal.tsx @@ -11,7 +11,7 @@ type CallState = 'idle' | 'calling' | 'incoming' | 'connected' | 'ended'; interface CallModalProps { isOpen: boolean; onClose: () => void; - targetUser: { id: string; displayName?: string; username: string; avatar?: string | null } | null; + targetUser: { id: string; displayName?: string; username?: string; avatar?: string | null } | null; callType: 'voice' | 'video'; incoming?: { from: string; diff --git a/client-web/src/modules/chats/presentation/ChatPage.tsx b/client-web/src/modules/chats/presentation/ChatPage.tsx index eed8346..e54c6d2 100644 --- a/client-web/src/modules/chats/presentation/ChatPage.tsx +++ b/client-web/src/modules/chats/presentation/ChatPage.tsx @@ -6,7 +6,7 @@ import { getSocket, disconnectSocket } from '../../../core/infrastructure/socket import { ChatApi } from '../infrastructure/chatApi'; import { playNotificationSound, isChatMuted, playCallRingtone, stopCallRingtone } from '../../../core/utils/sounds'; import { useLang } from '../../../core/infrastructure/i18n'; -import type { Message, UserBasic, CallInfo } from '../../../core/domain/types'; +import type { Message, UserBasic, CallInfo, ChatMember } from '../../../core/domain/types'; import { Send, Check, Phone, PhoneOff } from 'lucide-react'; import Sidebar from '../../../core/presentation/layouts/Sidebar'; import ChatView from './components/ChatView'; diff --git a/client-web/src/modules/chats/presentation/components/ChatListItem.tsx b/client-web/src/modules/chats/presentation/components/ChatListItem.tsx index cd28974..94ae243 100644 --- a/client-web/src/modules/chats/presentation/components/ChatListItem.tsx +++ b/client-web/src/modules/chats/presentation/components/ChatListItem.tsx @@ -32,19 +32,19 @@ function ChatListItem({ chat, isActive }: ChatListItemProps) { const otherMember = chat.members.find((m) => m.user.id !== user?.id); const isFavorites = chat.type === 'favorites'; - const chatName = isFavorites + const chatName = (isFavorites ? t('favorites') : chat.type === 'personal' ? otherMember?.user.displayName || otherMember?.user.userName || otherMember?.user.username || t('chat') - : chat.name || t('group'); + : chat.name || t('group')) || '??'; - const chatAvatar = isFavorites + const chatAvatar: string | null = isFavorites ? null : chat.type === 'personal' - ? otherMember?.user.avatarUrl || otherMember?.user.avatar - : chat.avatarUrl || chat.avatar; + ? otherMember?.user.avatarUrl || otherMember?.user.avatar || null + : chat.avatarUrl || chat.avatar || null; - const isOnline = chat.type === 'personal' && otherMember?.user.isOnline; + const isOnline = chat.type === 'personal' && !!otherMember?.user.isOnline; // Check if someone is typing in this chat const typingInChat = typingUsers.filter((t) => t.chatId === chat.id && t.userId !== user?.id); @@ -128,8 +128,9 @@ function ChatListItem({ chat, isActive }: ChatListItemProps) { } catch (e) { console.error(e); } }; - const initials = chatName + const initials = (chatName || '??') .split(' ') + .filter(Boolean) .map((w: string) => w[0]) .join('') .slice(0, 2) @@ -151,7 +152,7 @@ function ChatListItem({ chat, isActive }: ChatListItemProps) {
) : ( - + )}
diff --git a/client-web/src/modules/chats/presentation/components/ChatView.tsx b/client-web/src/modules/chats/presentation/components/ChatView.tsx index 03d9ac3..2258a04 100644 --- a/client-web/src/modules/chats/presentation/components/ChatView.tsx +++ b/client-web/src/modules/chats/presentation/components/ChatView.tsx @@ -93,13 +93,13 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal const chatName = isFavorites ? t('favorites') : chat?.type === 'personal' - ? otherMember?.user.displayName || otherMember?.user.username || t('chat') + ? otherMember?.user.displayName || otherMember?.user.userName || otherMember?.user.username || t('chat') : chat?.name || t('group'); const chatAvatar = isFavorites ? null : chat?.type === 'personal' - ? otherMember?.user.avatar - : chat?.avatar; + ? otherMember?.user.avatarUrl || otherMember?.user.avatar || null + : chat?.avatarUrl || chat?.avatar || null; const isOnline = chat?.type === 'personal' && otherMember?.user.isOnline; const typingInChat = typingUsers.filter((t) => t.chatId === activeChat && t.userId !== user?.id); @@ -422,7 +422,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal ); } - const initials = chatName + const initials = (chatName || '??') .split(' ') .map((w: string) => w[0]) .join('') diff --git a/client-web/src/modules/chats/presentation/components/MessageBubble.tsx b/client-web/src/modules/chats/presentation/components/MessageBubble.tsx index 9e4a7bd..93d680a 100644 --- a/client-web/src/modules/chats/presentation/components/MessageBubble.tsx +++ b/client-web/src/modules/chats/presentation/components/MessageBubble.tsx @@ -151,10 +151,9 @@ function MessageBubble({ }; const chatForDelete = chats.find(c => c.id === message.chatId); + const otherMember = chatForDelete?.members.find(m => m.user.id !== user?.id); const otherMemberName = chatForDelete?.type === 'personal' - ? chatForDelete.members.find(m => m.user.id !== user?.id)?.user.displayName - || chatForDelete.members.find(m => m.user.id !== user?.id)?.user.username - || '' + ? otherMember?.user.displayName || otherMember?.user.userName || otherMember?.user.username || '' : ''; const isPinned = pinnedMessages[message.chatId]?.id === message.id; @@ -306,7 +305,7 @@ function MessageBubble({ reactionGroups[r.emoji] = { count: 0, users: [], isMine: false, avatars: [] }; } reactionGroups[r.emoji].count++; - const displayName = r.user?.displayName || r.user?.username || '?'; + const displayName = r.user?.displayName || r.user?.userName || r.user?.username || '?'; reactionGroups[r.emoji].users.push(displayName); if (reactionGroups[r.emoji].avatars.length < 3) { reactionGroups[r.emoji].avatars.push({ @@ -318,8 +317,8 @@ function MessageBubble({ if (r.userId === user?.id) reactionGroups[r.emoji].isMine = true; }); - const senderName = message.sender?.displayName || message.sender?.username || ''; - const senderAvatar = message.sender?.avatar; + const senderName = message.sender?.displayName || message.sender?.userName || message.sender?.username || ''; + const senderAvatar = message.sender?.avatarUrl || message.sender?.avatar; const firstUrlMatch = message.content?.match(/https?:\/\/[^\s]+/); const firstUrl = firstUrlMatch ? firstUrlMatch[0] : null; @@ -453,7 +452,7 @@ function MessageBubble({ }} >

- {message.replyTo.sender?.displayName || message.replyTo.sender?.username} + {message.replyTo.sender?.displayName || message.replyTo.sender?.userName || message.replyTo.sender?.username || ''}

{message.replyTo.isDeleted ? ( @@ -536,7 +535,7 @@ function MessageBubble({ onClick={() => onViewProfile?.(message.forwardedFromId!)} >
- {(t('forwardedFrom' as any) === 'forwardedFrom' ? 'Переслано от' : t('forwardedFrom' as any))} {message.forwardedFrom.displayName || message.forwardedFrom.username} + {(t('forwardedFrom' as any) === 'forwardedFrom' ? 'Переслано от' : t('forwardedFrom' as any))} {message.forwardedFrom.displayName || message.forwardedFrom.userName || message.forwardedFrom.username || ''}
)} diff --git a/client-web/src/modules/chats/presentation/components/MessageInput.tsx b/client-web/src/modules/chats/presentation/components/MessageInput.tsx index a09c341..9c78a8a 100644 --- a/client-web/src/modules/chats/presentation/components/MessageInput.tsx +++ b/client-web/src/modules/chats/presentation/components/MessageInput.tsx @@ -22,7 +22,7 @@ import { useAuthStore } from '../../../auth/application/authStore'; import { ChatApi } from '../../infrastructure/chatApi'; import { getSocket } from '../../../../core/infrastructure/socket'; import { useLang } from '../../../../core/infrastructure/i18n'; -import { AUDIO_EXTENSIONS, MAX_FILE_SIZE } from '../../../../core/domain/types'; +import { AUDIO_EXTENSIONS, MAX_FILE_SIZE, type ChatMember } from '../../../../core/domain/types'; import { useNotificationStore } from '../../../../core/application/stores/notificationStore'; import EmojiPicker from './EmojiPicker'; @@ -70,27 +70,31 @@ export default function MessageInput({ chatId }: MessageInputProps) { const filteredMembers = mentionQuery !== null && isGroup ? chatMembers.filter((m) => { const q = mentionQuery.toLowerCase(); - return m.user.displayName.toLowerCase().includes(q) || m.user.username.toLowerCase().includes(q); + return (m.user.displayName || '').toLowerCase().includes(q) + || (m.user.userName || '').toLowerCase().includes(q) + || (m.user.username || '').toLowerCase().includes(q); }).slice(0, 6) : []; - const insertMention = (member: { user: { username: string } }) => { + const insertMention = (member: ChatMember) => { const el = inputRef.current; if (!el) return; + const username = member.user.userName || member.user.username; + if (!username) return; const cursorPos = el.selectionStart; const before = text.substring(0, cursorPos); const after = text.substring(cursorPos); // Find the @ that started this mention const atIdx = before.lastIndexOf('@'); if (atIdx === -1) return; - const newText = before.substring(0, atIdx) + `@${member.user.username} ` + after; + const newText = before.substring(0, atIdx) + `@${username} ` + after; setText(newText); setDraft(chatId, newText); setMentionQuery(null); setMentionIndex(0); setTimeout(() => { el.focus(); - const newPos = atIdx + member.user.username.length + 2; + const newPos = atIdx + username.length + 2; el.setSelectionRange(newPos, newPos); }, 0); }; @@ -617,7 +621,7 @@ export default function MessageInput({ chatId }: MessageInputProps) {

{editingMessage ? t('editing') - : `${t('replyTo')} ${replyTo?.sender?.displayName || replyTo?.sender?.username || ''}`} + : `${t('replyTo')} ${replyTo?.sender?.displayName || replyTo?.sender?.userName || replyTo?.sender?.username || ''}`}

{replyTo?.quote ? `«${replyTo.quote}»` : (editingMessage || replyTo)?.content || t('media') || 'Медиа'} @@ -813,16 +817,16 @@ export default function MessageInput({ chatId }: MessageInputProps) { i === mentionIndex ? 'bg-accent/20 text-white' : 'text-zinc-300 hover:bg-white/5' }`} > - {m.user.avatar ? ( - + {m.user.avatarUrl || m.user.avatar ? ( + ) : (
- {(m.user.displayName || m.user.username)[0]?.toUpperCase()} + {(m.user.displayName || m.user.userName || m.user.username || '?')[0]?.toUpperCase()}
)}
-

{m.user.displayName || m.user.username}

-

@{m.user.username}

+

{m.user.displayName || m.user.userName || m.user.username || '??'}

+

@{m.user.userName || m.user.username || '??'}

))} diff --git a/client-web/src/modules/chats/presentation/components/NewChatModal.tsx b/client-web/src/modules/chats/presentation/components/NewChatModal.tsx index f448eb1..72a7df8 100644 --- a/client-web/src/modules/chats/presentation/components/NewChatModal.tsx +++ b/client-web/src/modules/chats/presentation/components/NewChatModal.tsx @@ -171,10 +171,10 @@ export default function NewChatModal({ onClose }: NewChatModalProps) { ) : (
- {(u.displayName || u.username)?.[0]?.toUpperCase()} + {(u.displayName || u.userName || u.username || '?')[0]?.toUpperCase()}
)} - {u.displayName || u.username} + {u.displayName || u.userName || u.username || ''} ))} @@ -276,7 +276,7 @@ export default function NewChatModal({ onClose }: NewChatModalProps) { ) : (
- {(u.displayName || u.username)?.[0]?.toUpperCase() || '?'} + {(u.displayName || u.userName || u.username || '?')[0]?.toUpperCase() || '?'}
)} {u.isOnline && ( @@ -285,9 +285,9 @@ export default function NewChatModal({ onClose }: NewChatModalProps) {

- {u.displayName || u.username} + {u.displayName || u.userName || u.username || ''}

-

@{u.username}

+

@{u.userName || u.username || ''}

{mode === 'group-select' && (
) : (
- {(u.displayName || u.username)?.[0]?.toUpperCase() || '?'} + {(u.displayName || u.userName || u.username || '?')[0]?.toUpperCase() || '?'}
)} {u.isOnline && ( @@ -335,9 +335,9 @@ export default function NewChatModal({ onClose }: NewChatModalProps) {

- {u.displayName || u.username} + {u.displayName || u.userName || u.username || ''}

-

@{u.username}

+

@{u.userName || u.username || ''}

{mode === 'group-select' && (
>([]); + const [viewers, setViewers] = useState>([]); const [viewersLoading, setViewersLoading] = useState(false); const [showReplyInput, setShowReplyInput] = useState(false); @@ -207,7 +207,7 @@ export default function StoryViewer({ stories, initialUserIndex, initialStoryInd const socket = getSocket(); - const handleStoryViewed = (data: { storyId: string; userId: string; username: string; displayName: string; avatar: string | null; viewedAt: string; viewCount: number; ownerId: string }) => { + const handleStoryViewed = (data: { storyId: string; userId: string; username: string; userName?: string; displayName: string; avatar: string | null; viewedAt: string; viewCount: number; ownerId: string }) => { // console.log('[StoryViewer] story_viewed received:', data); if (!currentStory || data.storyId !== currentStory.id) return; // Only process if this user is the owner @@ -227,6 +227,7 @@ export default function StoryViewer({ stories, initialUserIndex, initialStoryInd return [...prev, { userId: data.userId, username: data.username, + userName: data.userName, displayName: data.displayName, avatar: data.avatar, viewedAt: data.viewedAt @@ -235,14 +236,14 @@ export default function StoryViewer({ stories, initialUserIndex, initialStoryInd } }; - const handleStoryReply = (data: { storyId: string; userId: string; username: string; displayName: string; avatar: string | null; content: string; createdAt: string; ownerId: string }) => { + const handleStoryReply = (data: { storyId: string; userId: string; username: string; userName?: string; displayName: string; avatar: string | null; content: string; createdAt: string; ownerId: string }) => { // console.log('[StoryViewer] story_reply received:', data); // Only process if this user is the owner if (data.ownerId !== user?.id) return; // Could show notification or update UI }; - const handleStoryReaction = (data: { storyId: string; userId: string; username: string; displayName: string; avatar: string | null; emoji: string; createdAt: string; ownerId: string }) => { + const handleStoryReaction = (data: { storyId: string; userId: string; username: string; userName?: string; displayName: string; avatar: string | null; emoji: string; createdAt: string; ownerId: string }) => { // console.log('[StoryViewer] story_reaction received:', data); // Only process if this user is the owner if (data.ownerId !== user?.id) return; @@ -408,13 +409,13 @@ export default function StoryViewer({ stories, initialUserIndex, initialStoryInd

- {currentUser.user.id === user?.id ? t('myStory') : currentUser.user.displayName || currentUser.user.username} + {currentUser.user.id === user?.id ? t('myStory') : currentUser.user.displayName || currentUser.user.userName || currentUser.user.username || ''}

{timeAgo(currentStory.createdAt)}

@@ -600,12 +601,12 @@ export default function StoryViewer({ stories, initialUserIndex, initialStoryInd
-

{v.displayName || v.username}

+

{v.displayName || v.username || ''}

@{v.username}

{timeAgo(v.viewedAt)} diff --git a/client-web/src/modules/users/presentation/components/UserProfile.tsx b/client-web/src/modules/users/presentation/components/UserProfile.tsx index 815c451..a7ab47d 100644 --- a/client-web/src/modules/users/presentation/components/UserProfile.tsx +++ b/client-web/src/modules/users/presentation/components/UserProfile.tsx @@ -501,14 +501,14 @@ export default function UserProfile({ userId, chatId, onClose, onGoToMessage, is
) : (

- {profile.displayName || profile.userName || profile.username} + {profile.displayName || profile.userName || profile.username || ''}

)} {/* Username (неизменяемый) */}
- {profile.userName || profile.username} + {profile.userName || profile.username || ''}
{/* Онлайн статус */} @@ -985,10 +985,10 @@ export default function UserProfile({ userId, chatId, onClose, onGoToMessage, is user: { id: profile.id, userName: profile.userName || profile.username || '', - username: profile.username, - displayName: profile.displayName, - avatar: profile.avatar, - avatarUrl: profile.avatarUrl || profile.avatar + username: profile.username || '', + displayName: profile.displayName || '', + avatar: profile.avatar ?? null, + avatarUrl: (profile.avatarUrl || profile.avatar) ?? null }, stories: sortedStories, hasUnviewed: false