Новый дизайн, удаление

This commit is contained in:
Халимов Рустам
2026-03-12 22:54:15 +03:00
parent 1ec6b438d2
commit 9daafae9c5
158 changed files with 1511 additions and 254 deletions

View File

@@ -0,0 +1,75 @@
using MediatR;
using Microsoft.AspNetCore.SignalR;
using Vortex.Modules.Chats.Domain;
using Vortex.Modules.Chats.Infrastructure.SignalR;
using Vortex.Shared.Kernel;
namespace Vortex.Modules.Chats.Application.Messages.Delete;
public sealed record DeleteMessagesCommand(
Guid ChatId,
Guid UserId,
List<Guid> MessageIds,
bool DeleteForAll) : global::Vortex.Shared.Kernel.ICommand;
public sealed class DeleteMessagesCommandHandler : global::Vortex.Shared.Kernel.ICommandHandler<DeleteMessagesCommand>
{
private readonly IMessageRepository _messageRepository;
private readonly Vortex.Modules.Chats.Application.Abstractions.IChatsUnitOfWork _unitOfWork;
private readonly IHubContext<ChatHub> _hubContext;
public DeleteMessagesCommandHandler(
IMessageRepository messageRepository,
Vortex.Modules.Chats.Application.Abstractions.IChatsUnitOfWork unitOfWork,
IHubContext<ChatHub> hubContext)
{
_messageRepository = messageRepository;
_unitOfWork = unitOfWork;
_hubContext = hubContext;
}
public async Task<global::Vortex.Shared.Kernel.Result> Handle(DeleteMessagesCommand request, CancellationToken cancellationToken)
{
foreach (var id in request.MessageIds)
{
var message = await _messageRepository.GetByIdAsync(id, cancellationToken);
if (message is null || message.ChatId != request.ChatId) continue;
if (request.DeleteForAll)
{
if (message.SenderId == request.UserId)
{
message.Delete();
}
}
else
{
message.DeleteForUser(request.UserId);
}
}
await _unitOfWork.SaveChangesAsync(cancellationToken);
if (request.DeleteForAll)
{
await _hubContext.Clients.Group(request.ChatId.ToString()).SendAsync("messages_deleted", new
{
chatId = request.ChatId,
messageIds = request.MessageIds,
deleteForAll = true
});
}
else
{
// Уведомляем только самого пользователя (все его текущие сессии)
await _hubContext.Clients.User(request.UserId.ToString()).SendAsync("messages_deleted", new
{
chatId = request.ChatId,
messageIds = request.MessageIds,
deleteForAll = false
});
}
return global::Vortex.Shared.Kernel.Result.Success();
}
}

View File

@@ -34,24 +34,31 @@ public sealed class AddReactionCommandHandler : ICommandHandler<AddReactionComma
public async Task<Result> Handle(AddReactionCommand request, CancellationToken cancellationToken)
{
var message = await _messageRepository.GetByIdAsync(request.MessageId, cancellationToken);
if (message is null) return Result.Failure(new Error("Messages.NotFound", "Message not found."));
message.AddReaction(request.UserId, request.Emoji);
await _unitOfWork.SaveChangesAsync(cancellationToken);
var username = await _displayNameProvider.GetDisplayNameAsync(request.UserId, cancellationToken);
await _hubContext.Clients.Group(request.ChatId.ToString()).SendAsync("reaction_added", new
try
{
messageId = request.MessageId,
chatId = request.ChatId,
userId = request.UserId,
username = username,
emoji = request.Emoji
});
var message = await _messageRepository.GetByIdAsync(request.MessageId, cancellationToken);
if (message is null) return Result.Failure(new Error("Messages.NotFound", "Message not found."));
return Result.Success();
message.AddReaction(request.UserId, request.Emoji);
await _unitOfWork.SaveChangesAsync(cancellationToken);
var username = await _displayNameProvider.GetDisplayNameAsync(request.UserId, cancellationToken);
await _hubContext.Clients.Group(request.ChatId.ToString()).SendAsync("reaction_added", new
{
messageId = request.MessageId,
chatId = request.ChatId,
userId = request.UserId,
username = username,
emoji = request.Emoji
});
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure(new Error("AddReaction.Error", ex.Message));
}
}
}

View File

@@ -31,21 +31,28 @@ public sealed class RemoveReactionCommandHandler : ICommandHandler<RemoveReactio
public async Task<Result> Handle(RemoveReactionCommand request, CancellationToken cancellationToken)
{
var message = await _messageRepository.GetByIdAsync(request.MessageId, cancellationToken);
if (message is null) return Result.Failure(new Error("Messages.NotFound", "Message not found."));
message.RemoveReaction(request.UserId, request.Emoji);
await _unitOfWork.SaveChangesAsync(cancellationToken);
await _hubContext.Clients.Group(request.ChatId.ToString()).SendAsync("reaction_removed", new
try
{
messageId = request.MessageId,
chatId = request.ChatId,
userId = request.UserId,
emoji = request.Emoji
});
var message = await _messageRepository.GetByIdAsync(request.MessageId, cancellationToken);
if (message is null) return Result.Failure(new Error("Messages.NotFound", "Message not found."));
return Result.Success();
message.RemoveReaction(request.UserId, request.Emoji);
await _unitOfWork.SaveChangesAsync(cancellationToken);
await _hubContext.Clients.Group(request.ChatId.ToString()).SendAsync("reaction_removed", new
{
messageId = request.MessageId,
chatId = request.ChatId,
userId = request.UserId,
emoji = request.Emoji
});
return Result.Success();
}
catch (Exception ex)
{
return Result.Failure(new Error("RemoveReaction.Error", ex.Message));
}
}
}

View File

@@ -2,6 +2,9 @@ using Vortex.Shared.Kernel;
namespace Vortex.Modules.Chats.Domain;
public sealed record ChatCreatedDomainEvent(Chat Chat) : IDomainEvent;
public sealed record ChatMemberAddedDomainEvent(Guid ChatId, Guid UserId) : IDomainEvent;
/// <summary>
/// Тип чата: личный или групповой.
/// </summary>
@@ -48,7 +51,9 @@ public sealed class Chat : AggregateRoot<Guid>
/// </summary>
public static Chat CreatePersonal()
{
return new Chat(Guid.NewGuid(), ChatType.Personal, null, null);
var chat = new Chat(Guid.NewGuid(), ChatType.Personal, null, null);
chat.RaiseDomainEvent(new ChatCreatedDomainEvent(chat));
return chat;
}
/// <summary>
@@ -56,7 +61,9 @@ public sealed class Chat : AggregateRoot<Guid>
/// </summary>
public static Chat CreateGroup(string name, string? avatar = null)
{
return new Chat(Guid.NewGuid(), ChatType.Group, name, avatar);
var chat = new Chat(Guid.NewGuid(), ChatType.Group, name, avatar);
chat.RaiseDomainEvent(new ChatCreatedDomainEvent(chat));
return chat;
}
/// <summary>
@@ -64,13 +71,16 @@ public sealed class Chat : AggregateRoot<Guid>
/// </summary>
public static Chat Create(string? name, ChatType type, string? avatar = null)
{
return new Chat(Guid.NewGuid(), type, name, avatar);
var chat = new Chat(Guid.NewGuid(), type, name, avatar);
chat.RaiseDomainEvent(new ChatCreatedDomainEvent(chat));
return chat;
}
public void AddMember(Guid userId, string role = "member")
{
if (_members.Any(m => m.UserId == userId)) return;
_members.Add(new ChatMember(Id, userId, role));
RaiseDomainEvent(new ChatMemberAddedDomainEvent(Id, userId));
}
public void RemoveMember(Guid userId)

View File

@@ -29,6 +29,9 @@ public sealed class Message : AggregateRoot<Guid>
private readonly List<ReadReceipt> _readBy = new();
public IReadOnlyCollection<ReadReceipt> ReadBy => _readBy.AsReadOnly();
private readonly List<Guid> _deletedByUsers = new();
public IReadOnlyCollection<Guid> DeletedByUsers => _deletedByUsers.AsReadOnly();
private Message(Guid id, Guid chatId, Guid senderId, string? content, string type, Guid? replyToId = null, string? quote = null, Guid? forwardedFromId = null) : base(id)
{
ChatId = chatId;
@@ -84,6 +87,16 @@ public sealed class Message : AggregateRoot<Guid>
{
Content = null;
IsDeleted = true;
_media.Clear();
_reactions.Clear();
}
public void DeleteForUser(Guid userId)
{
if (!_deletedByUsers.Contains(userId))
{
_deletedByUsers.Add(userId);
}
}
}
/// <summary>
@@ -101,6 +114,11 @@ public sealed class Reaction : Entity<Guid>
UserId = userId;
Emoji = emoji;
}
private Reaction() : base(Guid.Empty)
{
Emoji = string.Empty;
}
}
/// <summary>
@@ -122,4 +140,10 @@ public sealed class Media : Entity<Guid>
Filename = filename;
Size = size;
}
private Media() : base(Guid.Empty)
{
Type = string.Empty;
Url = string.Empty;
}
}

View File

@@ -0,0 +1,91 @@
using MediatR;
using Microsoft.AspNetCore.SignalR;
using Vortex.Modules.Chats.Domain;
using Vortex.Modules.Chats.Infrastructure.SignalR;
using Vortex.Shared.Kernel;
namespace Vortex.Modules.Chats.Infrastructure.Handlers;
/// <summary>
/// Обработчик доменного события создания чата.
/// </summary>
public sealed class ChatCreatedDomainEventHandler : INotificationHandler<ChatCreatedDomainEvent>, INotificationHandler<ChatMemberAddedDomainEvent>
{
private readonly IHubContext<ChatHub> _hubContext;
private readonly IChatRepository _chatRepository;
private readonly IUserDisplayNameProvider _displayNameProvider;
public ChatCreatedDomainEventHandler(
IHubContext<ChatHub> hubContext,
IChatRepository chatRepository,
IUserDisplayNameProvider displayNameProvider)
{
_hubContext = hubContext;
_chatRepository = chatRepository;
_displayNameProvider = displayNameProvider;
}
public async Task Handle(ChatCreatedDomainEvent notification, CancellationToken cancellationToken)
{
await NotifyNewChatAsync(notification.Chat, cancellationToken);
}
public async Task Handle(ChatMemberAddedDomainEvent notification, CancellationToken cancellationToken)
{
var chat = await _chatRepository.GetByIdAsync(notification.ChatId, cancellationToken);
if (chat != null)
{
// Уведомляем всех участников, что кто-то добавлен (или пользователь сам видит новый чат)
await NotifyNewChatAsync(chat, cancellationToken);
}
}
private async Task NotifyNewChatAsync(Chat chat, CancellationToken cancellationToken, string? targetUserId = null)
{
var members = new List<object>();
foreach (var member in chat.Members)
{
var userInfo = await _displayNameProvider.GetUserInfoAsync(member.UserId, cancellationToken);
members.Add(new
{
member.Id,
member.UserId,
member.Role,
member.JoinedAt,
User = userInfo != null
? new { userInfo.Id, userInfo.Username, userInfo.DisplayName, userInfo.Avatar, isOnline = false }
: new { Id = member.UserId, Username = "unknown", DisplayName = "Unknown", Avatar = (string?)null, isOnline = false }
});
}
var payload = new
{
chat.Id,
chat.Type,
chat.Name,
chat.Avatar,
chat.CreatedAt,
Members = members,
Messages = new List<object>(),
UnreadCount = 0
};
if (targetUserId != null)
{
// Отправляем конкретному пользователю (по всем его соединениям)
// Мы можем использовать метод Hub или просто отправить в группу UserId (если она есть)
// В нашем ChatHub мы добавляем пользователя в группы чатов при подключении.
// Но для нового чата он еще не в группе.
// Поэтому отправляем через User ID (SignalR поддерживает Clients.User(id))
await _hubContext.Clients.User(targetUserId).SendAsync("new_chat", payload, cancellationToken);
}
else
{
// Уведомляем всех текущих участников
foreach (var member in chat.Members)
{
await _hubContext.Clients.User(member.UserId.ToString()).SendAsync("new_chat", payload, cancellationToken);
}
}
}
}

View File

@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using MediatR;
using Vortex.Modules.Chats.Domain;
using Vortex.Shared.Kernel;
@@ -23,6 +24,12 @@ public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork
public DbSet<Chat> Chats => Set<Chat>();
public DbSet<Message> Messages => Set<Message>();
public DbSet<ReadReceipt> ReadReceipts => Set<ReadReceipt>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning));
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -40,7 +47,7 @@ public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork
mb.HasKey(m => m.Id);
mb.WithOwner().HasForeignKey(m => m.ChatId);
mb.HasIndex(m => new { m.ChatId, m.UserId }).IsUnique();
});
}).Navigation(c => c.Members).UsePropertyAccessMode(PropertyAccessMode.Field);
});
modelBuilder.Entity<Message>(builder =>
@@ -51,17 +58,26 @@ public sealed class ChatsDbContext : DbContext, IChatsUnitOfWork
builder.OwnsMany(m => m.Media, mb =>
{
mb.ToTable("MessageMedia");
mb.HasKey(x => x.Id);
mb.WithOwner().HasForeignKey(x => x.MessageId);
});
}).Navigation(m => m.Media).UsePropertyAccessMode(PropertyAccessMode.Field);
builder.OwnsMany(m => m.Reactions, mb =>
{
mb.ToTable("MessageReactions");
mb.HasKey(x => x.Id);
mb.WithOwner().HasForeignKey(x => x.MessageId);
mb.HasIndex(x => new { x.MessageId, x.UserId, x.Emoji }).IsUnique();
});
}).Navigation(m => m.Reactions).UsePropertyAccessMode(PropertyAccessMode.Field);
builder.PrimitiveCollection(m => m.DeletedByUsers)
.HasColumnName("DeletedByUsers")
.UsePropertyAccessMode(PropertyAccessMode.Field);
builder.HasMany(m => m.ReadBy)
.WithOne()
.HasForeignKey(r => r.MessageId)
.OnDelete(DeleteBehavior.Cascade);
builder.Navigation(m => m.ReadBy).UsePropertyAccessMode(PropertyAccessMode.Field);
});
modelBuilder.Entity<ReadReceipt>(builder =>

View File

@@ -0,0 +1,244 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Vortex.Modules.Chats.Infrastructure.Persistence;
#nullable disable
namespace Vortex.Modules.Chats.Infrastructure.Persistence.Migrations
{
[DbContext(typeof(ChatsDbContext))]
[Migration("20260312174910_UpdateChatsSchema")]
partial class UpdateChatsSchema
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("chats")
.HasAnnotation("ProductVersion", "10.0.4")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Vortex.Modules.Chats.Domain.Chat", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Avatar")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Chats", "chats");
});
modelBuilder.Entity("Vortex.Modules.Chats.Domain.Message", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("ChatId")
.HasColumnType("uuid");
b.Property<string>("Content")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.PrimitiveCollection<Guid[]>("DeletedByUsers")
.IsRequired()
.HasColumnType("uuid[]")
.HasColumnName("DeletedByUsers");
b.Property<Guid?>("ForwardedFromId")
.HasColumnType("uuid");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<bool>("IsEdited")
.HasColumnType("boolean");
b.Property<string>("Quote")
.HasColumnType("text");
b.Property<Guid?>("ReplyToId")
.HasColumnType("uuid");
b.Property<Guid>("SenderId")
.HasColumnType("uuid");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Messages", "chats");
});
modelBuilder.Entity("Vortex.Modules.Chats.Domain.ReadReceipt", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("MessageId")
.HasColumnType("uuid");
b.Property<DateTime>("ReadAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("MessageId", "UserId")
.IsUnique();
b.ToTable("ReadReceipts", "chats");
});
modelBuilder.Entity("Vortex.Modules.Chats.Domain.Chat", b =>
{
b.OwnsMany("Vortex.Modules.Chats.Domain.ChatMember", "Members", b1 =>
{
b1.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b1.Property<Guid>("ChatId")
.HasColumnType("uuid");
b1.Property<bool>("IsMuted")
.HasColumnType("boolean");
b1.Property<DateTime>("JoinedAt")
.HasColumnType("timestamp with time zone");
b1.Property<string>("Role")
.IsRequired()
.HasColumnType("text");
b1.Property<Guid>("UserId")
.HasColumnType("uuid");
b1.HasKey("Id");
b1.HasIndex("ChatId", "UserId")
.IsUnique();
b1.ToTable("ChatMembers", "chats");
b1.WithOwner()
.HasForeignKey("ChatId");
});
b.Navigation("Members");
});
modelBuilder.Entity("Vortex.Modules.Chats.Domain.Message", b =>
{
b.OwnsMany("Vortex.Modules.Chats.Domain.Media", "Media", b1 =>
{
b1.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b1.Property<string>("Filename")
.HasColumnType("text");
b1.Property<Guid>("MessageId")
.HasColumnType("uuid");
b1.Property<long?>("Size")
.HasColumnType("bigint");
b1.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b1.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
b1.HasKey("Id");
b1.HasIndex("MessageId");
b1.ToTable("MessageMedia", "chats");
b1.WithOwner()
.HasForeignKey("MessageId");
});
b.OwnsMany("Vortex.Modules.Chats.Domain.Reaction", "Reactions", b1 =>
{
b1.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b1.Property<string>("Emoji")
.IsRequired()
.HasColumnType("text");
b1.Property<Guid>("MessageId")
.HasColumnType("uuid");
b1.Property<Guid>("UserId")
.HasColumnType("uuid");
b1.HasKey("Id");
b1.HasIndex("MessageId", "UserId", "Emoji")
.IsUnique();
b1.ToTable("MessageReactions", "chats");
b1.WithOwner()
.HasForeignKey("MessageId");
});
b.Navigation("Media");
b.Navigation("Reactions");
});
modelBuilder.Entity("Vortex.Modules.Chats.Domain.ReadReceipt", b =>
{
b.HasOne("Vortex.Modules.Chats.Domain.Message", null)
.WithMany("ReadBy")
.HasForeignKey("MessageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Vortex.Modules.Chats.Domain.Message", b =>
{
b.Navigation("ReadBy");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,32 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Vortex.Modules.Chats.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class UpdateChatsSchema : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid[]>(
name: "DeletedByUsers",
schema: "chats",
table: "Messages",
type: "uuid[]",
nullable: false,
defaultValue: new Guid[0]);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DeletedByUsers",
schema: "chats",
table: "Messages");
}
}
}

View File

@@ -62,6 +62,11 @@ namespace Vortex.Modules.Chats.Infrastructure.Persistence.Migrations
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.PrimitiveCollection<Guid[]>("DeletedByUsers")
.IsRequired()
.HasColumnType("uuid[]")
.HasColumnName("DeletedByUsers");
b.Property<Guid?>("ForwardedFromId")
.HasColumnType("uuid");

View File

@@ -17,6 +17,8 @@ public sealed class ChatHub : Hub
{
// Маппинг userId → список connectionId
private static readonly ConcurrentDictionary<string, HashSet<string>> _userConnections = new();
// chatId → (userId → ParticipantInfo)
private static readonly ConcurrentDictionary<string, ConcurrentDictionary<string, ParticipantInfo>> _groupCallParticipants = new();
private readonly ISender _sender;
private readonly IUserContext _userContext;
@@ -118,6 +120,22 @@ public sealed class ChatHub : Hub
});
}
[HubMethodName("delete_messages")]
public async Task DeleteMessages(DeleteMessagesHubRequest request)
{
var parsedIds = request.MessageIds
.Select(id => Guid.TryParse(id, out var parsed) ? parsed : Guid.Empty)
.Where(id => id != Guid.Empty)
.ToList();
if (parsedIds.Any())
{
var command = new Vortex.Modules.Chats.Application.Messages.Delete.DeleteMessagesCommand(
request.ChatId, _userContext.UserId, parsedIds, request.DeleteForAll);
await _sender.Send(command);
}
}
[HubMethodName("typing_start")]
public async Task TypingStart(string chatId)
{
@@ -130,12 +148,30 @@ public sealed class ChatHub : Hub
await Clients.Group(chatId).SendAsync("user_stopped_typing", new { ChatId = chatId, UserId = _userContext.UserId });
}
[HubMethodName("join_chat")]
public async Task JoinChat(string chatId)
{
// Simple security check: check if user is member of chat (optional but recommended)
if (Guid.TryParse(chatId, out var chatGuid))
{
var userChats = await _chatRepository.GetUserChatsAsync(_userContext.UserId, Context.ConnectionAborted);
if (userChats.Any(c => c.Id == chatGuid))
{
await Groups.AddToGroupAsync(Context.ConnectionId, chatId);
}
}
}
[HubMethodName("add_reaction")]
public async Task AddReaction(AddReactionRequest request)
{
var command = new Vortex.Modules.Chats.Application.Messages.React.AddReactionCommand(
request.MessageId, _userContext.UserId, request.Emoji, request.ChatId);
await _sender.Send(command);
var result = await _sender.Send(command);
if (result.IsFailure)
{
throw new HubException(result.Error.Description);
}
}
[HubMethodName("remove_reaction")]
@@ -143,7 +179,11 @@ public sealed class ChatHub : Hub
{
var command = new Vortex.Modules.Chats.Application.Messages.React.RemoveReactionCommand(
request.MessageId, _userContext.UserId, request.Emoji, request.ChatId);
await _sender.Send(command);
var result = await _sender.Send(command);
if (result.IsFailure)
{
throw new HubException(result.Error.Description);
}
}
// ────────────────────────────────────────────────────────────────
@@ -255,6 +295,150 @@ public sealed class ChatHub : Hub
});
}
// ────────────────────────────────────────────────────────────────
// Group Call signals
// ────────────────────────────────────────────────────────────────
[HubMethodName("group_call_join")]
public async Task GroupCallJoin(GroupCallJoinRequest request)
{
var chatId = request.ChatId;
var userId = _userContext.UserId.ToString();
var displayName = Context.User?.FindFirstValue("name") ?? Context.User?.FindFirstValue(ClaimTypes.Name) ?? "User";
var avatar = Context.User?.FindFirstValue("avatar");
var username = Context.User?.FindFirstValue("unique_name") ?? Context.User?.Identity?.Name ?? "user";
var userInfo = new ParticipantInfo(userId, username, displayName, avatar);
var participants = _groupCallParticipants.GetOrAdd(chatId, _ => new ConcurrentDictionary<string, ParticipantInfo>());
participants.TryAdd(userId, userInfo);
// Notify others
await Clients.Group(chatId).SendAsync("group_call_user_joined", new {
chatId = chatId,
userId = userId,
userInfo = userInfo
});
// Send current participants to joiner (excluding self)
var others = participants.Values.Where(p => p.Id != userId).ToList();
await Clients.Caller.SendAsync("group_call_participants", new {
chatId = chatId,
participants = others
});
}
[HubMethodName("group_call_leave")]
public async Task GroupCallLeave(string chatId)
{
var userId = _userContext.UserId.ToString();
if (_groupCallParticipants.TryGetValue(chatId, out var participants))
{
participants.TryRemove(userId, out _);
if (participants.IsEmpty)
{
_groupCallParticipants.TryRemove(chatId, out _);
}
}
await Clients.Group(chatId).SendAsync("group_call_user_left", new {
chatId = chatId,
userId = userId
});
}
[HubMethodName("group_call_offer")]
public async Task GroupCallOffer(GroupCallOfferRequest request)
{
await SendToUserAsync(request.TargetUserId, "group_call_offer", new
{
chatId = request.ChatId,
from = _userContext.UserId.ToString(),
offer = request.Offer
});
}
[HubMethodName("group_call_answer")]
public async Task GroupCallAnswer(GroupCallAnswerRequest request)
{
await SendToUserAsync(request.TargetUserId, "group_call_answer", new
{
chatId = request.ChatId,
from = _userContext.UserId.ToString(),
answer = request.Answer
});
}
[HubMethodName("group_ice_candidate")]
public async Task GroupIceCandidate(GroupIceCandidateRequest request)
{
await SendToUserAsync(request.TargetUserId, "group_ice_candidate", new
{
chatId = request.ChatId,
from = _userContext.UserId.ToString(),
candidate = request.Candidate
});
}
[HubMethodName("group_call_renegotiate")]
public async Task GroupRenegotiate(GroupRenegotiateRequest request)
{
await SendToUserAsync(request.TargetUserId, "group_call_renegotiate", new
{
chatId = request.ChatId,
from = _userContext.UserId.ToString(),
offer = request.Offer
});
}
[HubMethodName("group_call_renegotiate_answer")]
public async Task GroupRenegotiateAnswer(GroupRenegotiateAnswerRequest request)
{
await SendToUserAsync(request.TargetUserId, "group_call_renegotiate_answer", new
{
chatId = request.ChatId,
from = _userContext.UserId.ToString(),
answer = request.Answer
});
}
[HubMethodName("screen_share_started")]
public async Task ScreenShareStarted(string chatId)
{
var userId = _userContext.UserId.ToString();
if (_groupCallParticipants.TryGetValue(chatId, out var participants))
{
if (participants.TryGetValue(userId, out var info))
{
participants[userId] = info with { IsSharingScreen = true };
}
}
await Clients.Group(chatId).SendAsync("screen_share_started", new {
chatId = chatId,
userId = userId
});
}
[HubMethodName("screen_share_stopped")]
public async Task ScreenShareStopped(string chatId)
{
var userId = _userContext.UserId.ToString();
if (_groupCallParticipants.TryGetValue(chatId, out var participants))
{
if (participants.TryGetValue(userId, out var info))
{
participants[userId] = info with { IsSharingScreen = false };
}
}
await Clients.Group(chatId).SendAsync("screen_share_stopped", new {
chatId = chatId,
userId = userId
});
}
// ────────────────────────────────────────────────────────────────
// Records
// ────────────────────────────────────────────────────────────────
@@ -278,4 +462,12 @@ public sealed class ChatHub : Hub
public record CallTypeChangedRequest(string TargetUserId, string CallType);
public record AddReactionRequest(Guid MessageId, Guid ChatId, string Emoji);
public record RemoveReactionRequest(Guid MessageId, Guid ChatId, string Emoji);
public record DeleteMessagesHubRequest(Guid ChatId, List<string> MessageIds, bool DeleteForAll);
public record GroupCallJoinRequest(string ChatId, string CallType);
public record ParticipantInfo(string Id, string Username, string DisplayName, string? Avatar, bool IsSharingScreen = false);
public record GroupCallOfferRequest(string ChatId, string TargetUserId, object Offer);
public record GroupCallAnswerRequest(string ChatId, string TargetUserId, object Answer);
public record GroupIceCandidateRequest(string ChatId, string TargetUserId, object Candidate);
public record GroupRenegotiateRequest(string ChatId, string TargetUserId, object Offer);
public record GroupRenegotiateAnswerRequest(string ChatId, string TargetUserId, object Answer);
}

View File

@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Vortex.Modules.Chats")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+d025c98e61cc08a53e6ebc62544d35be73a039e7")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+1ec6b438d29413482e1393c8ee5717f1211bd2c1")]
[assembly: System.Reflection.AssemblyProductAttribute("Vortex.Modules.Chats")]
[assembly: System.Reflection.AssemblyTitleAttribute("Vortex.Modules.Chats")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@@ -1 +1 @@
3f81474107c936fb7023dd1f1d41b8c6e04cc78e956d41fb0e98d29b21aae2c3
f9ffb401f31277f8b221e98682e16484fff43d34c6ed6bf8ad0e087ed2774d78

View File

@@ -10,7 +10,7 @@ build_property.PlatformNeutralAssembly =
build_property.EnforceExtendedAnalyzerRules =
build_property._SupportedPlatformList = Linux,macOS,Windows
build_property.RootNamespace = Vortex.Modules.Chats
build_property.ProjectDir = e:\GIT\forkmessager\apps\server-net\src\Modules\Chats\
build_property.ProjectDir = E:\GIT\forkmessager\apps\server-net\src\Modules\Chats\
build_property.EnableComHosting =
build_property.EnableGeneratedComInterfaceComImportInterop =
build_property.EffectiveAnalysisLevelStyle = 10.0

View File

@@ -1 +1 @@
361b9f18466c016bfc9d27e7847a8f24463479b03b82ec7866a1124dede24edb
b343bc938c4e02003d18f33f285c39da0e0ac9c0a7417a27b8b695b2e991a45e

View File

@@ -1,17 +1,17 @@
{
"format": 1,
"restore": {
"e:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Chats\\Vortex.Modules.Chats.csproj": {}
"E:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Chats\\Vortex.Modules.Chats.csproj": {}
},
"projects": {
"e:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Chats\\Vortex.Modules.Chats.csproj": {
"E:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Chats\\Vortex.Modules.Chats.csproj": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Chats\\Vortex.Modules.Chats.csproj",
"projectUniqueName": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Chats\\Vortex.Modules.Chats.csproj",
"projectName": "Vortex.Modules.Chats",
"projectPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Chats\\Vortex.Modules.Chats.csproj",
"projectPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Chats\\Vortex.Modules.Chats.csproj",
"packagesPath": "C:\\Users\\HomePC\\.nuget\\packages\\",
"outputPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Chats\\obj\\",
"outputPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Chats\\obj\\",
"projectStyle": "PackageReference",
"fallbackFolders": [
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"
@@ -33,8 +33,8 @@
"net10.0": {
"targetAlias": "net10.0",
"projectReferences": {
"e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj": {
"projectPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj"
"E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj": {
"projectPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj"
}
}
}
@@ -518,14 +518,14 @@
}
}
},
"e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj": {
"E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj",
"projectUniqueName": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj",
"projectName": "Vortex.Shared.Kernel",
"projectPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj",
"projectPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj",
"packagesPath": "C:\\Users\\HomePC\\.nuget\\packages\\",
"outputPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\obj\\",
"outputPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\obj\\",
"projectStyle": "PackageReference",
"fallbackFolders": [
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"

View File

@@ -785,11 +785,11 @@
"project": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Chats\\Vortex.Modules.Chats.csproj",
"projectUniqueName": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Chats\\Vortex.Modules.Chats.csproj",
"projectName": "Vortex.Modules.Chats",
"projectPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Chats\\Vortex.Modules.Chats.csproj",
"projectPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Chats\\Vortex.Modules.Chats.csproj",
"packagesPath": "C:\\Users\\HomePC\\.nuget\\packages\\",
"outputPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Chats\\obj\\",
"outputPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Chats\\obj\\",
"projectStyle": "PackageReference",
"fallbackFolders": [
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"
@@ -811,8 +811,8 @@
"net10.0": {
"targetAlias": "net10.0",
"projectReferences": {
"e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj": {
"projectPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj"
"E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj": {
"projectPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj"
}
}
}

View File

@@ -1,8 +1,8 @@
{
"version": 2,
"dgSpecHash": "TeczXo+31S0=",
"dgSpecHash": "rWL1CDgJNu8=",
"success": true,
"projectFilePath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Chats\\Vortex.Modules.Chats.csproj",
"projectFilePath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Chats\\Vortex.Modules.Chats.csproj",
"expectedPackageFiles": [
"C:\\Users\\HomePC\\.nuget\\packages\\mediatr\\12.0.1\\mediatr.12.0.1.nupkg.sha512",
"C:\\Users\\HomePC\\.nuget\\packages\\mediatr.contracts\\2.0.1\\mediatr.contracts.2.0.1.nupkg.sha512",

View File

@@ -12,6 +12,9 @@ public sealed class Story : Entity<Guid>
public DateTime CreatedAt { get; private set; }
public DateTime ExpiresAt { get; private set; }
private readonly List<StoryViewer> _viewers = new();
public IReadOnlyCollection<StoryViewer> Viewers => _viewers.AsReadOnly();
private Story(Guid id, Guid userId, string type, string? mediaUrl, string? content, string? bgColor) : base(id)
{
UserId = userId;
@@ -27,4 +30,10 @@ public sealed class Story : Entity<Guid>
{
return new Story(Guid.NewGuid(), userId, type, mediaUrl, content, bgColor);
}
public void AddViewer(Guid userId)
{
if (_viewers.Any(v => v.UserId == userId)) return;
_viewers.Add(new StoryViewer(Id, userId));
}
}

View File

@@ -0,0 +1,17 @@
using Vortex.Shared.Kernel;
namespace Vortex.Modules.Identity.Domain;
public sealed class StoryViewer : Entity<Guid>
{
public Guid StoryId { get; private set; }
public Guid UserId { get; private set; }
public DateTime ViewedAt { get; private set; }
internal StoryViewer(Guid storyId, Guid userId) : base(Guid.NewGuid())
{
StoryId = storyId;
UserId = userId;
ViewedAt = DateTime.UtcNow;
}
}

View File

@@ -1,5 +1,6 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Vortex.Shared.Kernel;
using Vortex.Modules.Identity.Domain;
@@ -22,7 +23,14 @@ public sealed class IdentityDbContext : DbContext, IIdentityUnitOfWork
public DbSet<User> Users => Set<User>();
public DbSet<Story> Stories => Set<Story>();
public DbSet<StoryViewer> StoryViewers => Set<StoryViewer>();
public DbSet<Friendship> Friendships => Set<Friendship>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning));
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -40,6 +48,17 @@ public sealed class IdentityDbContext : DbContext, IIdentityUnitOfWork
builder.ToTable("Stories");
builder.HasKey(s => s.Id);
builder.Property(s => s.Type).IsRequired();
builder.HasMany(s => s.Viewers)
.WithOne()
.HasForeignKey(v => v.StoryId)
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<StoryViewer>(builder =>
{
builder.ToTable("StoryViewers");
builder.HasKey(v => v.Id);
builder.HasIndex(v => new { v.StoryId, v.UserId }).IsUnique();
});
modelBuilder.Entity<User>(builder =>

View File

@@ -0,0 +1,178 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Vortex.Modules.Identity.Infrastructure.Persistence;
#nullable disable
namespace Vortex.Modules.Identity.Infrastructure.Persistence.Migrations
{
[DbContext(typeof(IdentityDbContext))]
[Migration("20260312174453_AddHideStoryViewsAndFixStoryViewer")]
partial class AddHideStoryViewsAndFixStoryViewer
{
/// <inheritdoc />
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("Vortex.Modules.Identity.Domain.Friendship", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("FriendId")
.HasColumnType("uuid");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("UserId", "FriendId")
.IsUnique();
b.ToTable("Friendships", "identity");
});
modelBuilder.Entity("Vortex.Modules.Identity.Domain.Story", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("BgColor")
.HasColumnType("text");
b.Property<string>("Content")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("MediaUrl")
.HasColumnType("text");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("text");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.ToTable("Stories", "identity");
});
modelBuilder.Entity("Vortex.Modules.Identity.Domain.StoryViewer", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("StoryId")
.HasColumnType("uuid");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<DateTime>("ViewedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("StoryId", "UserId")
.IsUnique();
b.ToTable("StoryViewers", "identity");
});
modelBuilder.Entity("Vortex.Modules.Identity.Domain.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("Avatar")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<string>("Bio")
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.Property<DateTime?>("Birthday")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("Email")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<bool>("HideStoryViews")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users", "identity");
});
modelBuilder.Entity("Vortex.Modules.Identity.Domain.StoryViewer", b =>
{
b.HasOne("Vortex.Modules.Identity.Domain.Story", null)
.WithMany("Viewers")
.HasForeignKey("StoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Vortex.Modules.Identity.Domain.Story", b =>
{
b.Navigation("Viewers");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,52 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Vortex.Modules.Identity.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddHideStoryViewsAndFixStoryViewer : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "StoryViewers",
schema: "identity",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
StoryId = table.Column<Guid>(type: "uuid", nullable: false),
UserId = table.Column<Guid>(type: "uuid", nullable: false),
ViewedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_StoryViewers", x => x.Id);
table.ForeignKey(
name: "FK_StoryViewers_Stories_StoryId",
column: x => x.StoryId,
principalSchema: "identity",
principalTable: "Stories",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_StoryViewers_StoryId_UserId",
schema: "identity",
table: "StoryViewers",
columns: new[] { "StoryId", "UserId" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "StoryViewers",
schema: "identity");
}
}
}

View File

@@ -82,6 +82,29 @@ namespace Vortex.Modules.Identity.Infrastructure.Persistence.Migrations
b.ToTable("Stories", "identity");
});
modelBuilder.Entity("Vortex.Modules.Identity.Domain.StoryViewer", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("StoryId")
.HasColumnType("uuid");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.Property<DateTime>("ViewedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("StoryId", "UserId")
.IsUnique();
b.ToTable("StoryViewers", "identity");
});
modelBuilder.Entity("Vortex.Modules.Identity.Domain.User", b =>
{
b.Property<Guid>("Id")
@@ -132,6 +155,20 @@ namespace Vortex.Modules.Identity.Infrastructure.Persistence.Migrations
b.ToTable("Users", "identity");
});
modelBuilder.Entity("Vortex.Modules.Identity.Domain.StoryViewer", b =>
{
b.HasOne("Vortex.Modules.Identity.Domain.Story", null)
.WithMany("Viewers")
.HasForeignKey("StoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Vortex.Modules.Identity.Domain.Story", b =>
{
b.Navigation("Viewers");
});
#pragma warning restore 612, 618
}
}

View File

@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Vortex.Modules.Identity")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+d025c98e61cc08a53e6ebc62544d35be73a039e7")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+1ec6b438d29413482e1393c8ee5717f1211bd2c1")]
[assembly: System.Reflection.AssemblyProductAttribute("Vortex.Modules.Identity")]
[assembly: System.Reflection.AssemblyTitleAttribute("Vortex.Modules.Identity")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@@ -1 +1 @@
c3cadb6f052217ce8055d22cc45365823011381722fc02323060802003a463fa
9d9ffd258fd1015aa25ef1b7c83a7e79e77e5f90a31d4742aa8a559cc7698555

View File

@@ -10,7 +10,7 @@ build_property.PlatformNeutralAssembly =
build_property.EnforceExtendedAnalyzerRules =
build_property._SupportedPlatformList = Linux,macOS,Windows
build_property.RootNamespace = Vortex.Modules.Identity
build_property.ProjectDir = e:\GIT\forkmessager\apps\server-net\src\Modules\Identity\
build_property.ProjectDir = E:\GIT\forkmessager\apps\server-net\src\Modules\Identity\
build_property.EnableComHosting =
build_property.EnableGeneratedComInterfaceComImportInterop =
build_property.EffectiveAnalysisLevelStyle = 10.0

View File

@@ -1 +1 @@
9c4a98722858a19d09e1cea0910af6e8695a81fc69a145e71388b513aa5d75c8
f5710cf9cae3bcef5bc636925e0d10679b38fdce0a7e8d6824b7b47ab526589d

View File

@@ -1,17 +1,17 @@
{
"format": 1,
"restore": {
"e:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Identity\\Vortex.Modules.Identity.csproj": {}
"E:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Identity\\Vortex.Modules.Identity.csproj": {}
},
"projects": {
"e:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Identity\\Vortex.Modules.Identity.csproj": {
"E:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Identity\\Vortex.Modules.Identity.csproj": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Identity\\Vortex.Modules.Identity.csproj",
"projectUniqueName": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Identity\\Vortex.Modules.Identity.csproj",
"projectName": "Vortex.Modules.Identity",
"projectPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Identity\\Vortex.Modules.Identity.csproj",
"projectPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Identity\\Vortex.Modules.Identity.csproj",
"packagesPath": "C:\\Users\\HomePC\\.nuget\\packages\\",
"outputPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Identity\\obj\\",
"outputPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Identity\\obj\\",
"projectStyle": "PackageReference",
"fallbackFolders": [
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"
@@ -33,8 +33,8 @@
"net10.0": {
"targetAlias": "net10.0",
"projectReferences": {
"e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj": {
"projectPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj"
"E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj": {
"projectPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj"
}
}
}
@@ -534,14 +534,14 @@
}
}
},
"e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj": {
"E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj",
"projectUniqueName": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj",
"projectName": "Vortex.Shared.Kernel",
"projectPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj",
"projectPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj",
"packagesPath": "C:\\Users\\HomePC\\.nuget\\packages\\",
"outputPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\obj\\",
"outputPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\obj\\",
"projectStyle": "PackageReference",
"fallbackFolders": [
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"

View File

@@ -1201,11 +1201,11 @@
"project": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Identity\\Vortex.Modules.Identity.csproj",
"projectUniqueName": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Identity\\Vortex.Modules.Identity.csproj",
"projectName": "Vortex.Modules.Identity",
"projectPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Identity\\Vortex.Modules.Identity.csproj",
"projectPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Identity\\Vortex.Modules.Identity.csproj",
"packagesPath": "C:\\Users\\HomePC\\.nuget\\packages\\",
"outputPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Identity\\obj\\",
"outputPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Identity\\obj\\",
"projectStyle": "PackageReference",
"fallbackFolders": [
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"
@@ -1227,8 +1227,8 @@
"net10.0": {
"targetAlias": "net10.0",
"projectReferences": {
"e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj": {
"projectPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj"
"E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj": {
"projectPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj"
}
}
}

View File

@@ -1,8 +1,8 @@
{
"version": 2,
"dgSpecHash": "LFvHqUy/0DQ=",
"dgSpecHash": "zJhKnu5UrIw=",
"success": true,
"projectFilePath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Identity\\Vortex.Modules.Identity.csproj",
"projectFilePath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Modules\\Identity\\Vortex.Modules.Identity.csproj",
"expectedPackageFiles": [
"C:\\Users\\HomePC\\.nuget\\packages\\bcrypt.net-next\\4.1.0\\bcrypt.net-next.4.1.0.nupkg.sha512",
"C:\\Users\\HomePC\\.nuget\\packages\\mediatr\\12.0.1\\mediatr.12.0.1.nupkg.sha512",

View File

@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Vortex.Shared.Infrastructure")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+d025c98e61cc08a53e6ebc62544d35be73a039e7")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+1ec6b438d29413482e1393c8ee5717f1211bd2c1")]
[assembly: System.Reflection.AssemblyProductAttribute("Vortex.Shared.Infrastructure")]
[assembly: System.Reflection.AssemblyTitleAttribute("Vortex.Shared.Infrastructure")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@@ -1 +1 @@
adb7ba56083ad67b90d8f8374db47603dfc37300aa26f470695f98b925535a7f
e0bbb37b072e496f3944a8671d38c2aa23b6d62d5e51acb6547971f52a1ef575

View File

@@ -10,7 +10,7 @@ build_property.PlatformNeutralAssembly =
build_property.EnforceExtendedAnalyzerRules =
build_property._SupportedPlatformList = Linux,macOS,Windows
build_property.RootNamespace = Vortex.Shared.Infrastructure
build_property.ProjectDir = e:\GIT\forkmessager\apps\server-net\src\Shared\Vortex.Shared.Infrastructure\
build_property.ProjectDir = E:\GIT\forkmessager\apps\server-net\src\Shared\Vortex.Shared.Infrastructure\
build_property.EnableComHosting =
build_property.EnableGeneratedComInterfaceComImportInterop =
build_property.EffectiveAnalysisLevelStyle = 10.0

View File

@@ -1,17 +1,17 @@
{
"format": 1,
"restore": {
"e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Infrastructure\\Vortex.Shared.Infrastructure.csproj": {}
"E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Infrastructure\\Vortex.Shared.Infrastructure.csproj": {}
},
"projects": {
"e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Infrastructure\\Vortex.Shared.Infrastructure.csproj": {
"E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Infrastructure\\Vortex.Shared.Infrastructure.csproj": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Infrastructure\\Vortex.Shared.Infrastructure.csproj",
"projectUniqueName": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Infrastructure\\Vortex.Shared.Infrastructure.csproj",
"projectName": "Vortex.Shared.Infrastructure",
"projectPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Infrastructure\\Vortex.Shared.Infrastructure.csproj",
"projectPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Infrastructure\\Vortex.Shared.Infrastructure.csproj",
"packagesPath": "C:\\Users\\HomePC\\.nuget\\packages\\",
"outputPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Infrastructure\\obj\\",
"outputPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Infrastructure\\obj\\",
"projectStyle": "PackageReference",
"fallbackFolders": [
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"
@@ -33,8 +33,8 @@
"net10.0": {
"targetAlias": "net10.0",
"projectReferences": {
"e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj": {
"projectPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj"
"E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj": {
"projectPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj"
}
}
}
@@ -510,14 +510,14 @@
}
}
},
"e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj": {
"E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj",
"projectUniqueName": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj",
"projectName": "Vortex.Shared.Kernel",
"projectPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj",
"projectPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj",
"packagesPath": "C:\\Users\\HomePC\\.nuget\\packages\\",
"outputPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\obj\\",
"outputPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\obj\\",
"projectStyle": "PackageReference",
"fallbackFolders": [
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"

View File

@@ -992,11 +992,11 @@
"project": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Infrastructure\\Vortex.Shared.Infrastructure.csproj",
"projectUniqueName": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Infrastructure\\Vortex.Shared.Infrastructure.csproj",
"projectName": "Vortex.Shared.Infrastructure",
"projectPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Infrastructure\\Vortex.Shared.Infrastructure.csproj",
"projectPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Infrastructure\\Vortex.Shared.Infrastructure.csproj",
"packagesPath": "C:\\Users\\HomePC\\.nuget\\packages\\",
"outputPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Infrastructure\\obj\\",
"outputPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Infrastructure\\obj\\",
"projectStyle": "PackageReference",
"fallbackFolders": [
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"
@@ -1018,8 +1018,8 @@
"net10.0": {
"targetAlias": "net10.0",
"projectReferences": {
"e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj": {
"projectPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj"
"E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj": {
"projectPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj"
}
}
}

View File

@@ -1,8 +1,8 @@
{
"version": 2,
"dgSpecHash": "mujERCxuDP8=",
"dgSpecHash": "+h84t9fYbLg=",
"success": true,
"projectFilePath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Infrastructure\\Vortex.Shared.Infrastructure.csproj",
"projectFilePath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Infrastructure\\Vortex.Shared.Infrastructure.csproj",
"expectedPackageFiles": [
"C:\\Users\\HomePC\\.nuget\\packages\\mediatr\\12.0.1\\mediatr.12.0.1.nupkg.sha512",
"C:\\Users\\HomePC\\.nuget\\packages\\mediatr.contracts\\2.0.1\\mediatr.contracts.2.0.1.nupkg.sha512",

View File

@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Vortex.Shared.Kernel")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+d025c98e61cc08a53e6ebc62544d35be73a039e7")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+1ec6b438d29413482e1393c8ee5717f1211bd2c1")]
[assembly: System.Reflection.AssemblyProductAttribute("Vortex.Shared.Kernel")]
[assembly: System.Reflection.AssemblyTitleAttribute("Vortex.Shared.Kernel")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@@ -1 +1 @@
2ab218fedb325bf279a8607f3ddd12468bb20cfa3895570bcfa7cdb1e276425f
b12266e8303643e0701f21b3366bc23363791cc5e63972b801eea2f6405398dc

View File

@@ -10,7 +10,7 @@ build_property.PlatformNeutralAssembly =
build_property.EnforceExtendedAnalyzerRules =
build_property._SupportedPlatformList = Linux,macOS,Windows
build_property.RootNamespace = Vortex.Shared.Kernel
build_property.ProjectDir = e:\GIT\forkmessager\apps\server-net\src\Shared\Vortex.Shared.Kernel\
build_property.ProjectDir = E:\GIT\forkmessager\apps\server-net\src\Shared\Vortex.Shared.Kernel\
build_property.EnableComHosting =
build_property.EnableGeneratedComInterfaceComImportInterop =
build_property.EffectiveAnalysisLevelStyle = 10.0

View File

@@ -1,17 +1,17 @@
{
"format": 1,
"restore": {
"e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj": {}
"E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj": {}
},
"projects": {
"e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj": {
"E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj": {
"version": "1.0.0",
"restore": {
"projectUniqueName": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj",
"projectUniqueName": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj",
"projectName": "Vortex.Shared.Kernel",
"projectPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj",
"projectPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj",
"packagesPath": "C:\\Users\\HomePC\\.nuget\\packages\\",
"outputPath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\obj\\",
"outputPath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\obj\\",
"projectStyle": "PackageReference",
"fallbackFolders": [
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages"

View File

@@ -1,8 +1,8 @@
{
"version": 2,
"dgSpecHash": "SdDYXwY4FZU=",
"dgSpecHash": "qfmzINr+8lA=",
"success": true,
"projectFilePath": "e:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj",
"projectFilePath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Shared\\Vortex.Shared.Kernel\\Vortex.Shared.Kernel.csproj",
"expectedPackageFiles": [
"C:\\Users\\HomePC\\.nuget\\packages\\mediatr\\12.0.1\\mediatr.12.0.1.nupkg.sha512",
"C:\\Users\\HomePC\\.nuget\\packages\\mediatr.contracts\\2.0.1\\mediatr.contracts.2.0.1.nupkg.sha512",

View File

@@ -74,6 +74,22 @@ public sealed class ChatsController : ControllerBase
{
var m = chatMessages.First();
var senderObj = await _userRepository.GetByIdAsync(m.SenderId, ct);
var reactionsWithUser = new List<object>();
foreach (var r in m.Reactions)
{
var rUser = await _userRepository.GetByIdAsync(r.UserId, ct);
reactionsWithUser.Add(new
{
id = r.Id,
emoji = r.Emoji,
userId = r.UserId,
user = rUser != null
? new { id = rUser.Id, username = rUser.Username, displayName = rUser.DisplayName }
: new { id = r.UserId, username = "unknown", displayName = "Unknown" }
});
}
messagesList.Add(new
{
m.Id,
@@ -93,7 +109,7 @@ public sealed class ChatsController : ControllerBase
displayName = senderObj.DisplayName,
avatar = senderObj.Avatar
} : new { id = m.SenderId, username = "unknown", displayName = "Unknown", avatar = (string?)null },
Reactions = new List<object>(),
reactions = reactionsWithUser,
ReadBy = m.ReadBy.Select(r => new { userId = r.UserId }).ToList()
});
}
@@ -131,7 +147,7 @@ public sealed class ChatsController : ControllerBase
[HttpPost("personal")]
public async Task<IActionResult> CreatePersonal([FromBody] CreatePersonalChatRequest request, CancellationToken ct)
{
var command = new CreateChatCommand((string?)null, ChatType.Personal, new List<Guid> { _userContext.UserId, request.UserId });
var command = new CreateChatCommand(string.Empty, ChatType.Personal, new List<Guid> { _userContext.UserId, request.UserId });
Result<Guid> result = await _sender.Send(command, ct);
if (result.IsFailure) return BadRequest(result.Error);
@@ -142,7 +158,13 @@ public sealed class ChatsController : ControllerBase
[HttpPost("group")]
public async Task<IActionResult> CreateGroup([FromBody] CreateGroupChatRequest request, CancellationToken ct)
{
var command = new CreateChatCommand(request.Name, ChatType.Group, request.MemberIds);
var memberIds = request.MemberIds.ToList();
if (!memberIds.Contains(_userContext.UserId))
{
memberIds.Add(_userContext.UserId);
}
var command = new CreateChatCommand(request.Name, ChatType.Group, memberIds);
Result<Guid> result = await _sender.Send(command, ct);
if (result.IsFailure) return BadRequest(result.Error);

View File

@@ -30,6 +30,9 @@ public sealed class MessagesController : ControllerBase
{
var messages = await messageRepository.GetChatMessagesAsync(chatId, 50, 0, ct);
// Filter out messages deleted for this user
messages = messages.Where(m => !m.DeletedByUsers.Contains(_userContext.UserId)).ToList();
// Collect all user IDs needed (senders, forwarded from)
var userIds = messages.Select(m => m.SenderId).ToList();
userIds.AddRange(messages.Where(m => m.ForwardedFromId.HasValue).Select(m => m.ForwardedFromId!.Value));
@@ -61,6 +64,21 @@ public sealed class MessagesController : ControllerBase
}
}
var reactionsWithUser = new List<object>();
foreach (var r in m.Reactions)
{
var rUser = await _userRepository.GetByIdAsync(r.UserId, ct);
reactionsWithUser.Add(new
{
id = r.Id,
emoji = r.Emoji,
userId = r.UserId,
user = rUser != null
? new { id = rUser.Id, username = rUser.Username, displayName = rUser.DisplayName }
: new { id = r.UserId, username = "unknown", displayName = "Unknown" }
});
}
result.Add(new
{
m.Id,
@@ -94,7 +112,7 @@ public sealed class MessagesController : ControllerBase
displayName = s.DisplayName,
avatar = s.Avatar
} : new { id = m.SenderId, username = "unknown", displayName = "Unknown", avatar = (string?)null },
reactions = m.Reactions.Select(r => new { r.UserId, r.Emoji }).ToList(),
reactions = reactionsWithUser,
readBy = m.ReadBy.Select(r => new { userId = r.UserId }).ToList()
});
}
@@ -107,6 +125,9 @@ public sealed class MessagesController : ControllerBase
{
var messages = await messageRepository.SearchMessagesAsync(q, chatId, ct);
// Filter out messages deleted for this user
messages = messages.Where(m => !m.DeletedByUsers.Contains(_userContext.UserId)).ToList();
var userIds = messages.Select(m => m.SenderId).ToList();
userIds.AddRange(messages.Where(m => m.ForwardedFromId.HasValue).Select(m => m.ForwardedFromId!.Value));

View File

@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using Vortex.Modules.Identity.Domain;
using Vortex.Modules.Identity.Infrastructure.Persistence;
@@ -16,12 +17,18 @@ public sealed class StoriesController : ControllerBase
private readonly IdentityDbContext _context;
private readonly IUserContext _userContext;
private readonly IUserRepository _userRepository;
private readonly Microsoft.AspNetCore.SignalR.IHubContext<Vortex.Modules.Chats.Infrastructure.SignalR.ChatHub> _hubContext;
public StoriesController(IdentityDbContext context, IUserContext userContext, IUserRepository userRepository)
public StoriesController(
IdentityDbContext context,
IUserContext userContext,
IUserRepository userRepository,
Microsoft.AspNetCore.SignalR.IHubContext<Vortex.Modules.Chats.Infrastructure.SignalR.ChatHub> hubContext)
{
_context = context;
_userContext = userContext;
_userRepository = userRepository;
_hubContext = hubContext;
}
[HttpGet]
@@ -37,17 +44,25 @@ public sealed class StoriesController : ControllerBase
friendIds.Add(currentUserId);
var stories = await _context.Stories
.Include(s => s.Viewers)
.Where(s => s.ExpiresAt > DateTime.UtcNow && friendIds.Contains(s.UserId))
.OrderByDescending(s => s.CreatedAt)
.ToListAsync(ct);
var groups = stories.GroupBy(s => s.UserId);
var groups = stories.GroupBy(s => s.UserId).ToList();
var result = new List<object>();
var userIds = groups.Select(g => g.Key).ToList();
var userMap = new Dictionary<Guid, dynamic>();
foreach (var uid in userIds)
{
var u = await _userRepository.GetByIdAsync(uid, ct);
if (u != null) userMap[uid] = u;
}
foreach (var group in groups)
{
var user = await _userRepository.GetByIdAsync(group.Key, ct);
if (user == null) continue;
if (!userMap.TryGetValue(group.Key, out var user)) continue;
result.Add(new
{
@@ -67,10 +82,10 @@ public sealed class StoriesController : ControllerBase
bgColor = s.BgColor,
createdAt = s.CreatedAt,
expiresAt = s.ExpiresAt,
viewCount = 0,
viewed = false
}).ToList(),
hasUnviewed = true
viewCount = s.Viewers.Count,
viewed = s.Viewers.Any(v => v.UserId == currentUserId)
}).OrderBy(s => s.createdAt).ToList(),
hasUnviewed = group.Any(s => !s.Viewers.Any(v => v.UserId == currentUserId))
});
}
@@ -99,6 +114,84 @@ public sealed class StoriesController : ControllerBase
return Ok(new { id = story.Id });
}
[HttpPost("{id}/view")]
public async Task<IActionResult> ViewStory(Guid id, CancellationToken ct)
{
var story = await _context.Stories
.Include(s => s.Viewers)
.FirstOrDefaultAsync(s => s.Id == id, ct);
if (story == null) return NotFound();
if (story.UserId == _userContext.UserId) return Ok(new { message = "Owner view" });
if (!story.Viewers.Any(v => v.UserId == _userContext.UserId))
{
story.AddViewer(_userContext.UserId);
await _context.SaveChangesAsync(ct);
// Notify owner via SignalR
var viewer = await _userRepository.GetByIdAsync(_userContext.UserId, ct);
await _hubContext.Clients.User(story.UserId.ToString()).SendAsync("story_viewed", new
{
storyId = story.Id,
userId = _userContext.UserId,
username = viewer?.Username,
displayName = viewer?.DisplayName,
avatar = viewer?.Avatar,
viewedAt = DateTime.UtcNow,
viewCount = story.Viewers.Count
}, ct);
}
return Ok(new { message = "Story viewed" });
}
[HttpGet("{id}/viewers")]
public async Task<IActionResult> GetStoryViewers(Guid id, CancellationToken ct)
{
var story = await _context.Stories
.Include(s => s.Viewers)
.FirstOrDefaultAsync(s => s.Id == id, ct);
if (story == null) return NotFound();
if (story.UserId != _userContext.UserId) return Forbid();
var viewerIds = story.Viewers.Select(v => v.UserId).ToList();
var viewers = new List<object>();
foreach (var viewerId in viewerIds)
{
var user = await _userRepository.GetByIdAsync(viewerId, ct);
if (user == null) continue;
var viewerRecord = story.Viewers.First(v => v.UserId == viewerId);
viewers.Add(new
{
userId = user.Id,
username = user.Username,
displayName = user.DisplayName,
avatar = user.Avatar,
viewedAt = viewerRecord.ViewedAt
});
}
return Ok(viewers);
}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteStory(Guid id, CancellationToken ct)
{
var story = await _context.Stories.FindAsync(new object[] { id }, ct);
if (story == null) return NotFound();
if (story.UserId != _userContext.UserId) return Forbid();
_context.Stories.Remove(story);
await _context.SaveChangesAsync(ct);
return Ok(new { message = "Story deleted" });
}
}
public sealed record CreateStoryRequest(string Type, string? MediaUrl, string? Content, string? BgColor);

View File

@@ -13,7 +13,7 @@ using System.Reflection;
[assembly: System.Reflection.AssemblyCompanyAttribute("Vortex.Host")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+d025c98e61cc08a53e6ebc62544d35be73a039e7")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+1ec6b438d29413482e1393c8ee5717f1211bd2c1")]
[assembly: System.Reflection.AssemblyProductAttribute("Vortex.Host")]
[assembly: System.Reflection.AssemblyTitleAttribute("Vortex.Host")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

View File

@@ -1 +1 @@
7eb1f40e06ff22545aecfb70096f70b5f91da82514bc2bf01247c568eaabc058
c8308646f5647671185661b28c5c3eabe4dd11328d5c7402bbee505991c08d6d

View File

@@ -1 +1 @@
11d26d1b87f3679d8be8eeb05168fa71c45a3d83b12aa28eb232183b34639103
a0a268746b849e72488e8608dc7529251d735d933091a22e5f93d38662ef7534

Some files were not shown because too many files have changed in this diff Show More