diff --git a/Dockerfile.web b/Dockerfile.web index 1d7a5ec..cbf3686 100644 --- a/Dockerfile.web +++ b/Dockerfile.web @@ -3,24 +3,28 @@ FROM node:20-alpine AS build WORKDIR /app -# ROOT package.json -COPY package*.json ./ -COPY apps/web/package*.json ./apps/web/ +# Copy package files for apps/web +# We use the local package files to avoid workspace hoisting issues in Docker +COPY apps/web/package.json ./ +# If there is a lockfile in apps/web, use it, otherwise use root (but root is workspace, so better not) +# Let's try to generate a clean install +RUN npm install --legacy-peer-deps -RUN npm install +# Copy source code +COPY apps/web/ ./ -# Copy source -COPY . . +# Build args +ARG VITE_KLIPY_API_KEY +ENV VITE_KLIPY_API_KEY=$VITE_KLIPY_API_KEY # Build web frontend -WORKDIR /app/apps/web RUN npm run build # Stage 2: Serve with Nginx FROM nginx:alpine # Copy built assets -COPY --from=build /app/apps/web/dist /usr/share/nginx/html +COPY --from=build /app/dist /usr/share/nginx/html # Custom Nginx config to proxy /api and /socket.io to server:3001 COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf diff --git a/apps/server-net/src/Modules/Chats/Application/Chats/Create/CreateChatCommandHandler.cs b/apps/server-net/src/Modules/Chats/Application/Chats/Create/CreateChatCommandHandler.cs index 05c1325..ca6b75e 100644 --- a/apps/server-net/src/Modules/Chats/Application/Chats/Create/CreateChatCommandHandler.cs +++ b/apps/server-net/src/Modules/Chats/Application/Chats/Create/CreateChatCommandHandler.cs @@ -28,9 +28,11 @@ public sealed class CreateChatCommandHandler : ICommandHandler { public ChatType Type { get; private set; } public string? Name { get; private set; } + public string? Description { get; private set; } public string? Avatar { get; private set; } public DateTime CreatedAt { get; private set; } private readonly List _members = new(); public IReadOnlyCollection Members => _members.AsReadOnly(); - private Chat(Guid id, ChatType type, string? name, string? avatar) : base(id) + private Chat(Guid id, ChatType type, string? name, string? avatar, string? description = null) : base(id) { Type = type; Name = name; Avatar = avatar; + Description = description; CreatedAt = DateTime.UtcNow; } @@ -69,9 +71,9 @@ public sealed class Chat : AggregateRoot /// /// Фабричный метод для создания чата. /// - public static Chat Create(string? name, ChatType type, string? avatar = null) + public static Chat Create(string? name, ChatType type, string? avatar = null, string? description = null) { - var chat = new Chat(Guid.NewGuid(), type, name, avatar); + var chat = new Chat(Guid.NewGuid(), type, name, avatar, description); chat.RaiseDomainEvent(new ChatCreatedDomainEvent(chat)); return chat; } @@ -91,6 +93,8 @@ public sealed class Chat : AggregateRoot public void UpdateName(string name) => Name = name; + public void UpdateDescription(string? description) => Description = description; + public void UpdateAvatar(string? avatarUrl) => Avatar = avatarUrl; } diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Handlers/MessageSentDomainEventHandler.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Handlers/MessageSentDomainEventHandler.cs index 6efc89d..7194418 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Handlers/MessageSentDomainEventHandler.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Handlers/MessageSentDomainEventHandler.cs @@ -90,8 +90,10 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler(), - ReadBy = new List() + ReadBy = new List(), + message.StoryId, + message.StoryMediaUrl, + message.StoryMediaType }, cancellationToken); } } diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313204219_AddChatDescription.Designer.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313204219_AddChatDescription.Designer.cs new file mode 100644 index 0000000..873a46e --- /dev/null +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313204219_AddChatDescription.Designer.cs @@ -0,0 +1,263 @@ +// +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("20260313204219_AddChatDescription")] + partial class AddChatDescription + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Avatar") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Chats", "chats"); + }); + + modelBuilder.Entity("Vortex.Modules.Chats.Domain.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChatId") + .HasColumnType("uuid"); + + b.Property("Content") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.PrimitiveCollection("DeletedByUsers") + .IsRequired() + .HasColumnType("uuid[]") + .HasColumnName("DeletedByUsers"); + + b.Property("ForwardedFromId") + .HasColumnType("uuid"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("IsEdited") + .HasColumnType("boolean"); + + b.Property("Quote") + .HasColumnType("text"); + + b.Property("ReplyToId") + .HasColumnType("uuid"); + + b.Property("SenderId") + .HasColumnType("uuid"); + + b.Property("StoryId") + .HasColumnType("uuid"); + + b.Property("StoryMediaType") + .HasColumnType("text"); + + b.Property("StoryMediaUrl") + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Messages", "chats"); + }); + + modelBuilder.Entity("Vortex.Modules.Chats.Domain.Reaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Emoji") + .IsRequired() + .HasColumnType("text"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MessageId", "UserId", "Emoji") + .IsUnique(); + + b.ToTable("MessageReactions", "chats"); + }); + + modelBuilder.Entity("Vortex.Modules.Chats.Domain.ReadReceipt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b1.Property("ChatId") + .HasColumnType("uuid"); + + b1.Property("IsMuted") + .HasColumnType("boolean"); + + b1.Property("IsPinned") + .HasColumnType("boolean"); + + b1.Property("JoinedAt") + .HasColumnType("timestamp with time zone"); + + b1.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b1.Property("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("MessageId") + .HasColumnType("uuid"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b1.Property("Filename") + .HasColumnType("text"); + + b1.Property("Size") + .HasColumnType("bigint"); + + b1.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("MessageId", "Id"); + + b1.ToTable("MessageMedia", "chats"); + + b1.WithOwner() + .HasForeignKey("MessageId"); + }); + + b.Navigation("Media"); + }); + + modelBuilder.Entity("Vortex.Modules.Chats.Domain.Reaction", b => + { + b.HasOne("Vortex.Modules.Chats.Domain.Message", null) + .WithMany("Reactions") + .HasForeignKey("MessageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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("Reactions"); + + b.Navigation("ReadBy"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313204219_AddChatDescription.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313204219_AddChatDescription.cs new file mode 100644 index 0000000..2d4191b --- /dev/null +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/20260313204219_AddChatDescription.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Vortex.Modules.Chats.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddChatDescription : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Description", + schema: "chats", + table: "Chats", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Description", + schema: "chats", + table: "Chats"); + } + } +} diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/ChatsDbContextModelSnapshot.cs b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/ChatsDbContextModelSnapshot.cs index ec6064e..47a3877 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/ChatsDbContextModelSnapshot.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/Persistence/Migrations/ChatsDbContextModelSnapshot.cs @@ -35,6 +35,9 @@ namespace Vortex.Modules.Chats.Infrastructure.Persistence.Migrations b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); + b.Property("Description") + .HasColumnType("text"); + b.Property("Name") .HasColumnType("text"); diff --git a/apps/server-net/src/Modules/Chats/Infrastructure/SignalR/ChatHub.cs b/apps/server-net/src/Modules/Chats/Infrastructure/SignalR/ChatHub.cs index e8c2e7f..d722f14 100644 --- a/apps/server-net/src/Modules/Chats/Infrastructure/SignalR/ChatHub.cs +++ b/apps/server-net/src/Modules/Chats/Infrastructure/SignalR/ChatHub.cs @@ -383,8 +383,9 @@ public sealed class ChatHub : Hub } [HubMethodName("group_call_leave")] - public async Task GroupCallLeave(string chatId) + public async Task GroupCallLeave(GroupLeaveRequest request) { + var chatId = request.ChatId; var userId = _userContext.UserId.ToString(); if (_groupCallParticipants.TryGetValue(chatId, out var participants)) { @@ -457,6 +458,84 @@ public sealed class ChatHub : Hub }); } + [HubMethodName("group_call_status")] + public async Task GroupCallStatus(GroupCallStatusRequest request) + { + var chatId = request.ChatId; + var userId = _userContext.UserId.ToString(); + + if (_groupCallParticipants.TryGetValue(chatId, out var participants)) + { + if (participants.TryGetValue(userId, out var info)) + { + // Update local state if needed (e.g. muted status) + participants[userId] = info with + { + IsMuted = request.IsMuted, + IsVideoOff = request.IsVideoOff + }; + } + } + + await Clients.Group(request.ChatId).SendAsync("group_call_status_updated", new { + chatId = request.ChatId, + userId = Context.UserIdentifier, + isMuted = request.IsMuted, + isVideoOff = request.IsVideoOff + }); + } + + [HubMethodName("group_call_status_params")] + public async Task GroupCallStatusParams(string chatId, bool isMuted, bool isVideoOff) + { + var userId = _userContext.UserId.ToString(); + + if (_groupCallParticipants.TryGetValue(chatId, out var participants)) + { + if (participants.TryGetValue(userId, out var info)) + { + // Update local state if needed (e.g. muted status) + participants[userId] = info with + { + IsMuted = isMuted, + IsVideoOff = isVideoOff + }; + } + } + + await Clients.Group(chatId).SendAsync("group_call_status_updated", new { + chatId = chatId, + userId = Context.UserIdentifier, + isMuted = isMuted, + isVideoOff = isVideoOff + }); + } + + [HubMethodName("get_group_call_status")] + public async Task GetGroupCallStatus(GetGroupCallStatusRequest request) + { + var chatId = request.ChatId; + if (_groupCallParticipants.TryGetValue(chatId, out var participants)) + { + var others = participants.Values.ToList(); + _logger.LogInformation("Found {Count} participants for chat {ChatId}", others.Count, chatId); + await Clients.Caller.SendAsync("group_call_active", new + { + chatId = chatId, + participants = others.Select(p => p.Id).ToList() + }); + } + else + { + _logger.LogInformation("No active call for chat {ChatId}", chatId); + await Clients.Caller.SendAsync("group_call_active", new + { + chatId = chatId, + participants = new List() + }); + } + } + [HubMethodName("screen_share_started")] public async Task ScreenShareStarted(string chatId) { @@ -523,7 +602,10 @@ public sealed class ChatHub : Hub public record RemoveReactionRequest(Guid MessageId, Guid ChatId, string Emoji); public record DeleteMessagesHubRequest(Guid ChatId, List 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 ParticipantInfo(string Id, string Username, string DisplayName, string? Avatar, bool IsSharingScreen = false, bool IsMuted = false, bool IsVideoOff = false); + public record GroupLeaveRequest(string ChatId); + public record GetGroupCallStatusRequest(string ChatId); + public record GroupCallStatusRequest(string ChatId, bool IsMuted, bool IsVideoOff); 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); diff --git a/apps/server-net/src/Vortex.Host/Controllers/ChatsController.cs b/apps/server-net/src/Vortex.Host/Controllers/ChatsController.cs index 6af74c7..5b10b68 100644 --- a/apps/server-net/src/Vortex.Host/Controllers/ChatsController.cs +++ b/apps/server-net/src/Vortex.Host/Controllers/ChatsController.cs @@ -12,6 +12,8 @@ using Vortex.Shared.Kernel; using Vortex.Modules.Identity.Domain; using Vortex.Modules.Identity.Application.Abstractions; using Vortex.Modules.Chats.Infrastructure.Persistence; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; namespace Vortex.Host.Controllers; @@ -100,6 +102,9 @@ public sealed class ChatsController : ControllerBase m.Type, m.ReplyToId, m.Quote, + m.StoryId, + m.StoryMediaUrl, + m.StoryMediaType, m.IsEdited, m.IsDeleted, m.CreatedAt, @@ -119,7 +124,8 @@ public sealed class ChatsController : ControllerBase { id = c.Id, type = c.Type.ToString().ToLowerInvariant(), - name = c.Type == ChatType.Favorites ? "Избранное" : c.Name, + name = c.Type == ChatType.Favorites ? "Избранное" : (c.Type == ChatType.Personal ? null : c.Name), + description = c.Description, avatar = c.Avatar, createdAt = c.CreatedAt, members = members, @@ -160,10 +166,11 @@ public sealed class ChatsController : ControllerBase public async Task CreateGroup([FromBody] CreateGroupChatRequest request, CancellationToken ct) { var memberIds = request.MemberIds.ToList(); - if (!memberIds.Contains(_userContext.UserId)) + if (memberIds.Contains(_userContext.UserId)) { - memberIds.Add(_userContext.UserId); + memberIds.Remove(_userContext.UserId); } + memberIds.Insert(0, _userContext.UserId); var command = new CreateChatCommand(request.Name, ChatType.Group, memberIds); Result result = await _sender.Send(command, ct); @@ -191,6 +198,7 @@ public sealed class ChatsController : ControllerBase if (chat == null || !chat.Members.Any(m => m.UserId == _userContext.UserId)) return NotFound(); if (request.Name != null) chat.UpdateName(request.Name); + if (request.Description != null) chat.UpdateDescription(request.Description); chatRepository.Update(chat); await uow.SaveChangesAsync(ct); @@ -294,6 +302,47 @@ public sealed class ChatsController : ControllerBase return Ok(await MapChatAsync(id, ct)); } + [HttpPost("{id:guid}/avatar/crop")] + public async Task CropGroupAvatar(Guid id, [FromForm] IFormFile avatar, [FromForm] int x, [FromForm] int y, [FromForm] int width, [FromForm] int height, [FromServices] IChatRepository chatRepository, [FromServices] IChatsUnitOfWork uow, CancellationToken ct) + { + var chat = await chatRepository.GetByIdAsync(id, ct); + if (chat == null || !chat.Members.Any(m => m.UserId == _userContext.UserId)) return NotFound(); + if (avatar == null || avatar.Length == 0) return BadRequest("No file"); + + var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads", "avatars"); + if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath); + + var fileName = $"{Guid.NewGuid()}.jpg"; + var filePath = Path.Combine(uploadsPath, fileName); + + try + { + using (var inputStream = avatar.OpenReadStream()) + using (var image = await SixLabors.ImageSharp.Image.LoadAsync(inputStream)) + { + int startX = Math.Max(0, Math.Min(x, image.Width - 1)); + int startY = Math.Max(0, Math.Min(y, image.Height - 1)); + int rectWidth = Math.Max(1, Math.Min(width, image.Width - startX)); + int rectHeight = Math.Max(1, Math.Min(height, image.Height - startY)); + + image.Mutate(ctx => ctx.Crop(new SixLabors.ImageSharp.Rectangle(startX, startY, rectWidth, rectHeight))); + image.Mutate(ctx => ctx.Resize(400, 400)); + await image.SaveAsJpegAsync(filePath, ct); + } + } + catch (Exception ex) + { + return StatusCode(500, "Error processing image: " + ex.Message); + } + + var url = $"/uploads/avatars/{fileName}"; + chat.UpdateAvatar(url); + chatRepository.Update(chat); + await uow.SaveChangesAsync(ct); + + return Ok(await MapChatAsync(id, ct)); + } + [HttpDelete("{id:guid}/avatar")] public async Task RemoveGroupAvatar(Guid id, [FromServices] IChatRepository chatRepository, [FromServices] IChatsUnitOfWork uow, CancellationToken ct) { @@ -310,6 +359,7 @@ public sealed class ChatsController : ControllerBase private async Task MapChatAsync(Guid chatId, CancellationToken ct) { var chatRepository = HttpContext.RequestServices.GetRequiredService(); + var messageRepository = HttpContext.RequestServices.GetRequiredService(); var chat = await chatRepository.GetByIdAsync(chatId, ct); if (chat == null) return new { }; @@ -334,15 +384,48 @@ public sealed class ChatsController : ControllerBase }); } + var chatMessages = await messageRepository.GetChatMessagesAsync(chatId, 1, 0, ct); + var messagesList = new List(); + if (chatMessages.Any()) + { + var m = chatMessages.First(); + var senderObj = await _userRepository.GetByIdAsync(m.SenderId, ct); + messagesList.Add(new + { + m.Id, + m.ChatId, + m.SenderId, + m.Content, + m.Type, + m.ReplyToId, + m.Quote, + m.StoryId, + m.StoryMediaUrl, + m.StoryMediaType, + m.IsEdited, + m.IsDeleted, + m.CreatedAt, + Media = m.Media.ToList(), + Sender = senderObj != null ? new { + id = senderObj.Id, + username = senderObj.Username, + displayName = senderObj.DisplayName, + avatar = senderObj.Avatar + } : new { id = m.SenderId, username = "unknown", displayName = "Unknown", avatar = (string?)null }, + ReadBy = m.ReadBy.Select(r => new { userId = r.UserId }).ToList() + }); + } + return new { id = chat.Id, type = chat.Type.ToString().ToLowerInvariant(), - name = chat.Type == ChatType.Favorites ? "Избранное" : chat.Name, + name = chat.Type == ChatType.Favorites ? "Избранное" : (chat.Type == ChatType.Personal ? null : chat.Name), + description = chat.Description, avatar = chat.Avatar, createdAt = chat.CreatedAt, members = members, - messages = new List(), + messages = messagesList, unreadCount = 0 }; } @@ -351,6 +434,6 @@ public sealed class ChatsController : ControllerBase public sealed record CreateChatRequest(string Name, ChatType Type, List MemberIds); public sealed record CreatePersonalChatRequest(Guid UserId); public sealed record CreateGroupChatRequest(string Name, List MemberIds); -public sealed record UpdateChatRequest(string? Name); +public sealed record UpdateChatRequest(string? Name, string? Description); public sealed record AddMembersRequest(List UserIds); diff --git a/apps/server-net/src/Vortex.Host/Controllers/MessagesController.cs b/apps/server-net/src/Vortex.Host/Controllers/MessagesController.cs index 92c4757..3fd19e1 100644 --- a/apps/server-net/src/Vortex.Host/Controllers/MessagesController.cs +++ b/apps/server-net/src/Vortex.Host/Controllers/MessagesController.cs @@ -103,6 +103,8 @@ public sealed class MessagesController : ControllerBase avatar = fwd.Avatar } : null, storyId = m.StoryId, + storyMediaUrl = m.StoryMediaUrl, + storyMediaType = m.StoryMediaType, media = m.Media.Select(media => new { media.Id, media.Type, @@ -161,6 +163,9 @@ public sealed class MessagesController : ControllerBase displayName = fwd.DisplayName, avatar = fwd.Avatar } : null, + storyId = m.StoryId, + storyMediaUrl = m.StoryMediaUrl, + storyMediaType = m.StoryMediaType, media = m.Media.Select(media => new { media.Id, media.Type, @@ -217,7 +222,7 @@ public sealed class MessagesController : ControllerBase { var linkRegex = new Regex(@"https?://[^\s]+", RegexOptions.IgnoreCase); var contentLinks = !string.IsNullOrEmpty(m.Content) ? linkRegex.Matches(m.Content).Select(match => match.Value).ToList() : new List(); - var mediaLinks = m.Media.Where(media => media.Type?.ToLower() == "link").Select(media => media.Url).ToList(); + var mediaLinks = (m.Media ?? Enumerable.Empty()).Where(media => media.Type?.ToLower() == "link").Select(media => media.Url).ToList(); var allLinks = contentLinks.Concat(mediaLinks).Distinct().ToList(); if (allLinks.Any()) @@ -249,9 +254,12 @@ public sealed class MessagesController : ControllerBase var sender = await _userRepository.GetByIdAsync(m.SenderId, ct); result.Add(new { - m.Id, - m.ChatId, - m.SenderId, + m.ReplyToId, + m.Quote, + m.StoryId, + m.StoryMediaUrl, + m.StoryMediaType, + m.IsEdited, m.Content, m.Type, m.CreatedAt, diff --git a/apps/server-net/src/Vortex.Host/Controllers/UsersController.cs b/apps/server-net/src/Vortex.Host/Controllers/UsersController.cs index bf0cfd5..2f845cb 100644 --- a/apps/server-net/src/Vortex.Host/Controllers/UsersController.cs +++ b/apps/server-net/src/Vortex.Host/Controllers/UsersController.cs @@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Authorization; using Vortex.Modules.Identity.Domain; using Vortex.Shared.Kernel; using Vortex.Modules.Identity.Application.Abstractions; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; namespace Vortex.Host.Controllers; @@ -121,6 +123,58 @@ public sealed class UsersController : ControllerBase }); } + [HttpPost("avatar/crop")] + public async Task CropAvatar([FromForm] IFormFile avatar, [FromForm] int x, [FromForm] int y, [FromForm] int width, [FromForm] int height, CancellationToken ct) + { + var user = await _userRepository.GetByIdAsync(_userContext.UserId, ct); + if (user == null) return NotFound(); + + if (avatar == null || avatar.Length == 0) return BadRequest("No file"); + + var uploadsPath = Path.Combine(Directory.GetCurrentDirectory(), "uploads", "avatars"); + if (!Directory.Exists(uploadsPath)) Directory.CreateDirectory(uploadsPath); + + var ext = ".jpg"; + var fileName = $"{Guid.NewGuid()}{ext}"; + var filePath = Path.Combine(uploadsPath, fileName); + + try + { + using (var inputStream = avatar.OpenReadStream()) + using (var image = await SixLabors.ImageSharp.Image.LoadAsync(inputStream)) + { + // Clamp coordinates to image bounds + int startX = Math.Max(0, Math.Min(x, image.Width - 1)); + int startY = Math.Max(0, Math.Min(y, image.Height - 1)); + int rectWidth = Math.Max(1, Math.Min(width, image.Width - startX)); + int rectHeight = Math.Max(1, Math.Min(height, image.Height - startY)); + + image.Mutate(ctx => ctx.Crop(new SixLabors.ImageSharp.Rectangle(startX, startY, rectWidth, rectHeight))); + image.Mutate(ctx => ctx.Resize(400, 400)); + + await image.SaveAsJpegAsync(filePath, ct); + } + } + catch (Exception ex) + { + return StatusCode(500, "Error processing image: " + ex.Message); + } + + var avatarUrl = $"/uploads/avatars/{fileName}"; + user.UpdateAvatar(avatarUrl); + await _unitOfWork.SaveChangesAsync(ct); + + return Ok(new { + id = user.Id, + username = user.Username, + displayName = user.DisplayName, + avatar = user.Avatar, + bio = user.Bio, + birthday = user.Birthday, + createdAt = user.CreatedAt + }); + } + [HttpDelete("avatar")] public async Task DeleteAvatar(CancellationToken ct) { diff --git a/apps/server-net/src/Vortex.Host/Vortex.Host.csproj b/apps/server-net/src/Vortex.Host/Vortex.Host.csproj index 20544d4..7d84eed 100644 --- a/apps/server-net/src/Vortex.Host/Vortex.Host.csproj +++ b/apps/server-net/src/Vortex.Host/Vortex.Host.csproj @@ -18,6 +18,7 @@ all + diff --git a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/SixLabors.ImageSharp.dll b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/SixLabors.ImageSharp.dll new file mode 100644 index 0000000..7302493 Binary files /dev/null and b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/SixLabors.ImageSharp.dll differ diff --git a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Host.deps.json b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Host.deps.json index 37545e1..8bf410a 100644 --- a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Host.deps.json +++ b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Host.deps.json @@ -17,6 +17,7 @@ "Microsoft.EntityFrameworkCore.Design": "10.0.4", "Microsoft.IdentityModel.Tokens": "8.16.0", "Npgsql.EntityFrameworkCore.PostgreSQL": "10.0.0", + "SixLabors.ImageSharp": "3.1.12", "Swashbuckle.AspNetCore": "10.1.5", "System.IdentityModel.Tokens.Jwt": "8.16.0", "Vortex.Modules.Chats": "1.0.0", @@ -741,6 +742,14 @@ } } }, + "SixLabors.ImageSharp/3.1.12": { + "runtime": { + "lib/net6.0/SixLabors.ImageSharp.dll": { + "assemblyVersion": "3.0.0.0", + "fileVersion": "3.1.12.0" + } + } + }, "Swashbuckle.AspNetCore/10.1.5": { "dependencies": { "Swashbuckle.AspNetCore.Swagger": "10.1.5", @@ -1252,6 +1261,13 @@ "path": "npgsql.entityframeworkcore.postgresql/10.0.0", "hashPath": "npgsql.entityframeworkcore.postgresql.10.0.0.nupkg.sha512" }, + "SixLabors.ImageSharp/3.1.12": { + "type": "package", + "serviceable": true, + "sha512": "sha512-iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A==", + "path": "sixlabors.imagesharp/3.1.12", + "hashPath": "sixlabors.imagesharp.3.1.12.nupkg.sha512" + }, "Swashbuckle.AspNetCore/10.1.5": { "type": "package", "serviceable": true, diff --git a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Host.dll b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Host.dll index 3f0ecdb..fc35e8b 100644 Binary files a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Host.dll and b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Host.dll differ diff --git a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Host.exe b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Host.exe index ae9eded..d168df0 100644 Binary files a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Host.exe and b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Host.exe differ diff --git a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Host.pdb b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Host.pdb index 5ed2e10..af17839 100644 Binary files a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Host.pdb and b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Host.pdb differ diff --git a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Modules.Chats.dll b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Modules.Chats.dll index 1b4ab28..ec94f2f 100644 Binary files a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Modules.Chats.dll and b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Modules.Chats.dll differ diff --git a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Modules.Chats.pdb b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Modules.Chats.pdb index 660b954..dadf041 100644 Binary files a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Modules.Chats.pdb and b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Modules.Chats.pdb differ diff --git a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Modules.Identity.dll b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Modules.Identity.dll index e2b46a1..6674bf4 100644 Binary files a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Modules.Identity.dll and b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Modules.Identity.dll differ diff --git a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Modules.Identity.pdb b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Modules.Identity.pdb index 75e99cf..284ecf3 100644 Binary files a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Modules.Identity.pdb and b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Modules.Identity.pdb differ diff --git a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Shared.Infrastructure.dll b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Shared.Infrastructure.dll index 865990a..dbc9eb4 100644 Binary files a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Shared.Infrastructure.dll and b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Shared.Infrastructure.dll differ diff --git a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Shared.Infrastructure.pdb b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Shared.Infrastructure.pdb index d79bd5c..40cf9e8 100644 Binary files a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Shared.Infrastructure.pdb and b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Shared.Infrastructure.pdb differ diff --git a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Shared.Kernel.dll b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Shared.Kernel.dll index eb1c91e..3008838 100644 Binary files a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Shared.Kernel.dll and b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Shared.Kernel.dll differ diff --git a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Shared.Kernel.pdb b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Shared.Kernel.pdb index 2d5f470..7a471b1 100644 Binary files a/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Shared.Kernel.pdb and b/apps/server-net/src/Vortex.Host/bin/Debug/net10.0/Vortex.Shared.Kernel.pdb differ diff --git a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.AssemblyInfo.cs b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.AssemblyInfo.cs index 2b6c20d..954efd6 100644 --- a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.AssemblyInfo.cs +++ b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.AssemblyInfo.cs @@ -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+ca1d88191ca628dbe2371abab56fb3cdfe386d65")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+010b96d362a728b28bae56836e464103371899f4")] [assembly: System.Reflection.AssemblyProductAttribute("Vortex.Host")] [assembly: System.Reflection.AssemblyTitleAttribute("Vortex.Host")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.AssemblyInfoInputs.cache b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.AssemblyInfoInputs.cache index 40c03ba..cec7d8c 100644 --- a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.AssemblyInfoInputs.cache +++ b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.AssemblyInfoInputs.cache @@ -1 +1 @@ -f3d52bfd2b858d5d383380a9e4100efe419d613a4ecf13cf06c4ad546122d06b +42cfa915aa235da6705c7cf95cb1a14d6010145730d282552fab9d0c3500b1eb diff --git a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.assets.cache b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.assets.cache index 6f76058..8b32ae6 100644 Binary files a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.assets.cache and b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.assets.cache differ diff --git a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.csproj.AssemblyReference.cache b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.csproj.AssemblyReference.cache index 5934eb1..6989a5c 100644 Binary files a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.csproj.AssemblyReference.cache and b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.csproj.AssemblyReference.cache differ diff --git a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.csproj.CoreCompileInputs.cache b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.csproj.CoreCompileInputs.cache index 07e4967..2813904 100644 --- a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.csproj.CoreCompileInputs.cache +++ b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.csproj.CoreCompileInputs.cache @@ -1 +1 @@ -a0a268746b849e72488e8608dc7529251d735d933091a22e5f93d38662ef7534 +d31a690f913188ddcf07b749ff1ed53974efbd88f60dacca614c62e22165ddc7 diff --git a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.csproj.FileListAbsolute.txt b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.csproj.FileListAbsolute.txt index 7d2ebff..63b29ec 100644 --- a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.csproj.FileListAbsolute.txt +++ b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.csproj.FileListAbsolute.txt @@ -158,3 +158,4 @@ E:\GIT\forkmessager\apps\server-net\src\Vortex.Host\bin\Debug\net10.0\System.Ide E:\GIT\forkmessager\apps\server-net\src\Vortex.Host\bin\Debug\net10.0\Microsoft.AspNetCore.Authentication.JwtBearer.dll E:\GIT\forkmessager\apps\server-net\src\Vortex.Host\bin\Debug\net10.0\Microsoft.IdentityModel.Protocols.dll E:\GIT\forkmessager\apps\server-net\src\Vortex.Host\bin\Debug\net10.0\Microsoft.IdentityModel.Protocols.OpenIdConnect.dll +E:\GIT\forkmessager\apps\server-net\src\Vortex.Host\bin\Debug\net10.0\SixLabors.ImageSharp.dll diff --git a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.dll b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.dll index 3f0ecdb..fc35e8b 100644 Binary files a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.dll and b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.dll differ diff --git a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.pdb b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.pdb index 5ed2e10..af17839 100644 Binary files a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.pdb and b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/Vortex.Host.pdb differ diff --git a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/apphost.exe b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/apphost.exe index ae9eded..d168df0 100644 Binary files a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/apphost.exe and b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/apphost.exe differ diff --git a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/ref/Vortex.Host.dll b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/ref/Vortex.Host.dll index c826413..aa1a6fd 100644 Binary files a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/ref/Vortex.Host.dll and b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/ref/Vortex.Host.dll differ diff --git a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/refint/Vortex.Host.dll b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/refint/Vortex.Host.dll index c826413..aa1a6fd 100644 Binary files a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/refint/Vortex.Host.dll and b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/refint/Vortex.Host.dll differ diff --git a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/rjsmcshtml.dswa.cache.json b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/rjsmcshtml.dswa.cache.json index 7182b40..b2389e4 100644 --- a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/rjsmcshtml.dswa.cache.json +++ b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/rjsmcshtml.dswa.cache.json @@ -1 +1 @@ -{"GlobalPropertiesHash":"2XkPbXFkjVRjCUBTJbQq8CDd7pjsUOS1j3gPHS73xDA=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["uOUIBvk7jdHp7c93RIuECwNykpOmiZnVmQxETsngeR0=","ToV71GGCtcAptkaGqQYXeNtDQnAJEljvYAkAKnIRZ\u002Bs=","2fjU\u002BjCBiHEuKj2uQgoWti0oZXkpAdICFty/O9cJSyc=","aTLUi985DoL1wb7QmuhiHoO7J3hi30WEVAUbYVkXxss=","GsJcYsaPdTiZszOuaIQXvBoHa2G\u002B8z3ZDvEk9/eI47g="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file +{"GlobalPropertiesHash":"2XkPbXFkjVRjCUBTJbQq8CDd7pjsUOS1j3gPHS73xDA=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["uOUIBvk7jdHp7c93RIuECwNykpOmiZnVmQxETsngeR0=","ToV71GGCtcAptkaGqQYXeNtDQnAJEljvYAkAKnIRZ\u002Bs=","2fjU\u002BjCBiHEuKj2uQgoWti0oZXkpAdICFty/O9cJSyc=","aTLUi985DoL1wb7QmuhiHoO7J3hi30WEVAUbYVkXxss=","zR9GPKJ4P6VsVoHQ2DUOzlUgEbOZ6Jtpw3dF8z1T3G8="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/rjsmrazor.dswa.cache.json b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/rjsmrazor.dswa.cache.json index 66fb2e1..27a7454 100644 --- a/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/rjsmrazor.dswa.cache.json +++ b/apps/server-net/src/Vortex.Host/obj/Debug/net10.0/rjsmrazor.dswa.cache.json @@ -1 +1 @@ -{"GlobalPropertiesHash":"2L4HBRt/ChGO9rohi3wtSDqo95X47fme5m8OHgJIZL0=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["uOUIBvk7jdHp7c93RIuECwNykpOmiZnVmQxETsngeR0=","ToV71GGCtcAptkaGqQYXeNtDQnAJEljvYAkAKnIRZ\u002Bs=","2fjU\u002BjCBiHEuKj2uQgoWti0oZXkpAdICFty/O9cJSyc=","aTLUi985DoL1wb7QmuhiHoO7J3hi30WEVAUbYVkXxss=","GsJcYsaPdTiZszOuaIQXvBoHa2G\u002B8z3ZDvEk9/eI47g="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file +{"GlobalPropertiesHash":"2L4HBRt/ChGO9rohi3wtSDqo95X47fme5m8OHgJIZL0=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["uOUIBvk7jdHp7c93RIuECwNykpOmiZnVmQxETsngeR0=","ToV71GGCtcAptkaGqQYXeNtDQnAJEljvYAkAKnIRZ\u002Bs=","2fjU\u002BjCBiHEuKj2uQgoWti0oZXkpAdICFty/O9cJSyc=","aTLUi985DoL1wb7QmuhiHoO7J3hi30WEVAUbYVkXxss=","zR9GPKJ4P6VsVoHQ2DUOzlUgEbOZ6Jtpw3dF8z1T3G8="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/apps/server-net/src/Vortex.Host/obj/Vortex.Host.csproj.nuget.dgspec.json b/apps/server-net/src/Vortex.Host/obj/Vortex.Host.csproj.nuget.dgspec.json index 0dc8489..c380afa 100644 --- a/apps/server-net/src/Vortex.Host/obj/Vortex.Host.csproj.nuget.dgspec.json +++ b/apps/server-net/src/Vortex.Host/obj/Vortex.Host.csproj.nuget.dgspec.json @@ -1994,6 +1994,10 @@ "target": "Package", "version": "[10.0.0, )" }, + "SixLabors.ImageSharp": { + "target": "Package", + "version": "[3.1.12, )" + }, "Swashbuckle.AspNetCore": { "target": "Package", "version": "[10.1.5, )" diff --git a/apps/server-net/src/Vortex.Host/obj/Vortex.Host.csproj.nuget.g.props b/apps/server-net/src/Vortex.Host/obj/Vortex.Host.csproj.nuget.g.props index 536d413..2b2571f 100644 --- a/apps/server-net/src/Vortex.Host/obj/Vortex.Host.csproj.nuget.g.props +++ b/apps/server-net/src/Vortex.Host/obj/Vortex.Host.csproj.nuget.g.props @@ -17,6 +17,7 @@ + diff --git a/apps/server-net/src/Vortex.Host/obj/project.assets.json b/apps/server-net/src/Vortex.Host/obj/project.assets.json index c8e6fe3..c9cff77 100644 --- a/apps/server-net/src/Vortex.Host/obj/project.assets.json +++ b/apps/server-net/src/Vortex.Host/obj/project.assets.json @@ -1022,6 +1022,22 @@ } } }, + "SixLabors.ImageSharp/3.1.12": { + "type": "package", + "compile": { + "lib/net6.0/SixLabors.ImageSharp.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net6.0/SixLabors.ImageSharp.dll": { + "related": ".xml" + } + }, + "build": { + "build/SixLabors.ImageSharp.props": {} + } + }, "Swashbuckle.AspNetCore/10.1.5": { "type": "package", "dependencies": { @@ -3641,6 +3657,22 @@ "postgresql.png" ] }, + "SixLabors.ImageSharp/3.1.12": { + "sha512": "iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A==", + "type": "package", + "path": "sixlabors.imagesharp/3.1.12", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "LICENSE", + "build/SixLabors.ImageSharp.props", + "lib/net6.0/SixLabors.ImageSharp.dll", + "lib/net6.0/SixLabors.ImageSharp.xml", + "sixlabors.imagesharp.128.png", + "sixlabors.imagesharp.3.1.12.nupkg.sha512", + "sixlabors.imagesharp.nuspec" + ] + }, "Swashbuckle.AspNetCore/10.1.5": { "sha512": "/eNk9z/8quXhDX14o3XLbwAX/84uIWSbiUD7cI/UrQnoBMOiyAtzKxNEJUtf/TyxjFpcXxE9FAfLvtbNpxHBSg==", "type": "package", @@ -3962,6 +3994,7 @@ "Microsoft.EntityFrameworkCore.Design >= 10.0.4", "Microsoft.IdentityModel.Tokens >= 8.16.0", "Npgsql.EntityFrameworkCore.PostgreSQL >= 10.0.0", + "SixLabors.ImageSharp >= 3.1.12", "Swashbuckle.AspNetCore >= 10.1.5", "System.IdentityModel.Tokens.Jwt >= 8.16.0", "Vortex.Modules.Chats >= 1.0.0", @@ -4068,6 +4101,10 @@ "target": "Package", "version": "[10.0.0, )" }, + "SixLabors.ImageSharp": { + "target": "Package", + "version": "[3.1.12, )" + }, "Swashbuckle.AspNetCore": { "target": "Package", "version": "[10.1.5, )" diff --git a/apps/server-net/src/Vortex.Host/obj/project.nuget.cache b/apps/server-net/src/Vortex.Host/obj/project.nuget.cache index 52f085e..3deb3e9 100644 --- a/apps/server-net/src/Vortex.Host/obj/project.nuget.cache +++ b/apps/server-net/src/Vortex.Host/obj/project.nuget.cache @@ -1,6 +1,6 @@ { "version": 2, - "dgSpecHash": "Lr9L1t57HIQ=", + "dgSpecHash": "ghRzA2bAmsQ=", "success": true, "projectFilePath": "E:\\GIT\\forkmessager\\apps\\server-net\\src\\Vortex.Host\\Vortex.Host.csproj", "expectedPackageFiles": [ @@ -53,6 +53,7 @@ "C:\\Users\\HomePC\\.nuget\\packages\\newtonsoft.json\\13.0.3\\newtonsoft.json.13.0.3.nupkg.sha512", "C:\\Users\\HomePC\\.nuget\\packages\\npgsql\\10.0.0\\npgsql.10.0.0.nupkg.sha512", "C:\\Users\\HomePC\\.nuget\\packages\\npgsql.entityframeworkcore.postgresql\\10.0.0\\npgsql.entityframeworkcore.postgresql.10.0.0.nupkg.sha512", + "C:\\Users\\HomePC\\.nuget\\packages\\sixlabors.imagesharp\\3.1.12\\sixlabors.imagesharp.3.1.12.nupkg.sha512", "C:\\Users\\HomePC\\.nuget\\packages\\swashbuckle.aspnetcore\\10.1.5\\swashbuckle.aspnetcore.10.1.5.nupkg.sha512", "C:\\Users\\HomePC\\.nuget\\packages\\swashbuckle.aspnetcore.swagger\\10.1.5\\swashbuckle.aspnetcore.swagger.10.1.5.nupkg.sha512", "C:\\Users\\HomePC\\.nuget\\packages\\swashbuckle.aspnetcore.swaggergen\\10.1.5\\swashbuckle.aspnetcore.swaggergen.10.1.5.nupkg.sha512", diff --git a/apps/server-net/uploads/6f2ed78e-22ca-4a4b-90de-91880a0e6b3e.avif b/apps/server-net/uploads/6f2ed78e-22ca-4a4b-90de-91880a0e6b3e.avif new file mode 100644 index 0000000..99fc293 Binary files /dev/null and b/apps/server-net/uploads/6f2ed78e-22ca-4a4b-90de-91880a0e6b3e.avif differ diff --git a/apps/server-net/uploads/avatars/27e9aeb6-4340-482d-9168-deb5ee95cd40.jpg b/apps/server-net/uploads/avatars/27e9aeb6-4340-482d-9168-deb5ee95cd40.jpg new file mode 100644 index 0000000..3bda78f Binary files /dev/null and b/apps/server-net/uploads/avatars/27e9aeb6-4340-482d-9168-deb5ee95cd40.jpg differ diff --git a/apps/server-net/uploads/avatars/4a14a328-55e0-4b1e-a9e2-3425241a5cba.jpg b/apps/server-net/uploads/avatars/4a14a328-55e0-4b1e-a9e2-3425241a5cba.jpg new file mode 100644 index 0000000..3bda78f Binary files /dev/null and b/apps/server-net/uploads/avatars/4a14a328-55e0-4b1e-a9e2-3425241a5cba.jpg differ diff --git a/apps/server-net/uploads/avatars/5a62239f-65d0-4d65-9269-b66975226ca3.jpg b/apps/server-net/uploads/avatars/5a62239f-65d0-4d65-9269-b66975226ca3.jpg new file mode 100644 index 0000000..11667cb Binary files /dev/null and b/apps/server-net/uploads/avatars/5a62239f-65d0-4d65-9269-b66975226ca3.jpg differ diff --git a/apps/server-net/uploads/avatars/74c431e6-6ad4-427f-88ca-b0336c5e922c.jpg b/apps/server-net/uploads/avatars/74c431e6-6ad4-427f-88ca-b0336c5e922c.jpg new file mode 100644 index 0000000..f6f0e91 Binary files /dev/null and b/apps/server-net/uploads/avatars/74c431e6-6ad4-427f-88ca-b0336c5e922c.jpg differ diff --git a/apps/server-net/uploads/avatars/b15e4d92-a84d-4ef5-8a0f-cc897183001b.jpg b/apps/server-net/uploads/avatars/b15e4d92-a84d-4ef5-8a0f-cc897183001b.jpg new file mode 100644 index 0000000..9624542 Binary files /dev/null and b/apps/server-net/uploads/avatars/b15e4d92-a84d-4ef5-8a0f-cc897183001b.jpg differ diff --git a/apps/server-net/uploads/avatars/ca19e639-5432-4a6a-bd5d-58b2c2e3fb08.jpg b/apps/server-net/uploads/avatars/ca19e639-5432-4a6a-bd5d-58b2c2e3fb08.jpg new file mode 100644 index 0000000..4c53d63 Binary files /dev/null and b/apps/server-net/uploads/avatars/ca19e639-5432-4a6a-bd5d-58b2c2e3fb08.jpg differ diff --git a/apps/server-net/uploads/avatars/e270ccf1-c6e8-429d-ba8a-ddc9d4432abd.avif b/apps/server-net/uploads/avatars/e270ccf1-c6e8-429d-ba8a-ddc9d4432abd.avif new file mode 100644 index 0000000..99fc293 Binary files /dev/null and b/apps/server-net/uploads/avatars/e270ccf1-c6e8-429d-ba8a-ddc9d4432abd.avif differ diff --git a/apps/server-net/uploads/cedeca45-9040-4f91-81fd-1a4196b77f2c.png b/apps/server-net/uploads/cedeca45-9040-4f91-81fd-1a4196b77f2c.png new file mode 100644 index 0000000..1037754 Binary files /dev/null and b/apps/server-net/uploads/cedeca45-9040-4f91-81fd-1a4196b77f2c.png differ diff --git a/apps/web/package.json b/apps/web/package.json index b3c3f8b..cdd1997 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -11,6 +11,7 @@ "dependencies": { "@emoji-mart/data": "^1.2.1", "@emoji-mart/react": "^1.1.1", + "@microsoft/signalr": "^10.0.0", "@tanstack/react-query": "^5.62.0", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -19,9 +20,9 @@ "lucide-react": "^0.468.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-easy-crop": "^5.5.6", "socket.io-client": "^4.8.1", - "zustand": "^5.0.2", - "@microsoft/signalr": "^10.0.0" + "zustand": "^5.0.2" }, "devDependencies": { "@tailwindcss/vite": "^4.0.0", diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 7e512e7..83b3d89 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -123,7 +123,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal socket.on('group_call_active', handler); // Request current status when opening a group chat if (activeChat && chat?.type === 'group') { - socket.emit('group_call_status', { chatId: activeChat }); + socket.emit('get_group_call_status', { chatId: activeChat }); } return () => { socket.off('group_call_active', handler); }; }, [activeChat, user?.id, chat?.type]); diff --git a/apps/web/src/components/EmojiPicker.tsx b/apps/web/src/components/EmojiPicker.tsx index f907565..146ea31 100644 --- a/apps/web/src/components/EmojiPicker.tsx +++ b/apps/web/src/components/EmojiPicker.tsx @@ -4,14 +4,19 @@ import Picker from '@emoji-mart/react'; import data from '@emoji-mart/data'; import { Search, TrendingUp, Loader2 } from 'lucide-react'; import { useLang } from '../lib/i18n'; +import { useAuthStore } from '../stores/authStore'; -interface TenorGif { +interface KlipyGif { id: string; - media_formats?: { - gif?: { url: string }; - tinygif?: { url: string }; + images: { + original: { url: string }; + fixed_height_small: { url: string }; }; - content_description?: string; + file: { + sd?: { gif?: { url: string }; webp?: { url: string } }; + hd?: { gif?: { url: string }; webp?: { url: string } }; + }; + title?: string; } interface EmojiPickerProps { @@ -20,36 +25,65 @@ interface EmojiPickerProps { onClose: () => void; } -const getTenorKey = () => localStorage.getItem('vortex_tenor_key') || ''; +const getKlipyKey = () => import.meta.env.VITE_KLIPY_API_KEY || ''; +const getCustomerId = () => { + const user = useAuthStore.getState().user; + return user?.id || 'anonymous'; +}; export default function EmojiPicker({ onSelect, onSelectGif, onClose }: EmojiPickerProps) { const { lang, t } = useLang(); const [tab, setTab] = useState<'emoji' | 'gif'>('emoji'); const [gifQuery, setGifQuery] = useState(''); - const [gifs, setGifs] = useState([]); + const [gifs, setGifs] = useState([]); const [gifLoading, setGifLoading] = useState(false); - const [trendingGifs, setTrendingGifs] = useState([]); + const [trendingGifs, setTrendingGifs] = useState([]); const gifSearchRef = useRef(null); const debounceRef = useRef>(undefined); - // Load trending GIFs + // Helper to safely extract GIF array from various possible Klipy API responses + const extractGifs = (d: any): KlipyGif[] => { + if (!d) return []; + if (Array.isArray(d)) return d; + if (Array.isArray(d.data)) return d.data; + if (Array.isArray(d.result)) return d.result; + if (d.result && Array.isArray(d.result.data)) return d.result.data; + if (Array.isArray(d.gifs)) return d.gifs; + return []; + }; + + // Load trending GIFs (Klipy) useEffect(() => { - if (tab === 'gif' && getTenorKey() && trendingGifs.length === 0) { + if (tab === 'gif' && getKlipyKey() && trendingGifs.length === 0) { setGifLoading(true); - fetch(`https://tenor.googleapis.com/v2/featured?key=${getTenorKey()}&limit=30&media_filter=gif,tinygif`) + fetch(`https://api.klipy.com/api/v1/${getKlipyKey()}/gifs/trending?customer_id=${getCustomerId()}&per_page=30`) .then(r => r.json()) - .then(d => { setTrendingGifs(d.results || []); setGifLoading(false); }) - .catch(() => setGifLoading(false)); + .then(d => { + setTrendingGifs(extractGifs(d)); + setGifLoading(false); + }) + .catch((e) => { + console.error('Klipy trending error:', e); + setTrendingGifs([]); + setGifLoading(false); + }); } - }, [tab]); + }, [tab, trendingGifs.length]); const searchGifs = useCallback((q: string) => { - if (!getTenorKey() || !q.trim()) { setGifs([]); return; } + if (!getKlipyKey() || !q.trim()) { setGifs([]); return; } setGifLoading(true); - fetch(`https://tenor.googleapis.com/v2/search?key=${getTenorKey()}&q=${encodeURIComponent(q)}&limit=30&media_filter=gif,tinygif`) + fetch(`https://api.klipy.com/api/v1/${getKlipyKey()}/gifs/search?customer_id=${getCustomerId()}&q=${encodeURIComponent(q)}&per_page=30`) .then(r => r.json()) - .then(d => { setGifs(d.results || []); setGifLoading(false); }) - .catch(() => setGifLoading(false)); + .then(d => { + setGifs(extractGifs(d)); + setGifLoading(false); + }) + .catch((e) => { + console.error('Klipy search error:', e); + setGifs([]); + setGifLoading(false); + }); }, []); const handleGifSearch = (q: string) => { @@ -58,9 +92,9 @@ export default function EmojiPicker({ onSelect, onSelectGif, onClose }: EmojiPic debounceRef.current = setTimeout(() => searchGifs(q), 400); }; - const pickGif = (gif: TenorGif) => { - const url = gif.media_formats?.gif?.url || gif.media_formats?.tinygif?.url || ''; - const preview = gif.media_formats?.tinygif?.url || url; + const pickGif = (gif: KlipyGif) => { + const url = gif.file?.hd?.gif?.url || gif.file?.sd?.gif?.url || ''; + const preview = gif.file?.sd?.webp?.url || gif.file?.sd?.gif?.url || url; if (onSelectGif && url) { onSelectGif(url, preview); } @@ -112,7 +146,7 @@ export default function EmojiPicker({ onSelect, onSelectGif, onClose }: EmojiPic > EMOJI - {(getTenorKey() || onSelectGif) && ( + {(getKlipyKey() || onSelectGif) && ( - {chat.avatar && ( + {chat.avatar && !avatarUploading && ( )} )} - - - {/* Group name */} - {isEditingName ? ( -
- setGroupName(e.target.value)} - className="flex-1 text-lg font-bold text-center text-white bg-transparent border-b border-vortex-500 outline-none px-2 py-1" - autoFocus - onKeyDown={(e) => { - if (e.key === 'Enter') handleSaveName(); - if (e.key === 'Escape') { - setIsEditingName(false); - setGroupName(chat.name || ''); - } - }} - /> - - -
- ) : ( -
-

{chat.name}

- {isAdmin && ( +
+ {isEditingName ? ( +
+ setGroupName(e.target.value)} + className="bg-surface-tertiary border border-accent/30 rounded-xl px-4 py-2 text-lg font-bold text-white text-center focus:outline-none focus:border-accent" + autoFocus + /> + +
+ ) : ( +
isAdmin && setIsEditingName(true)} + > +

+ {chat.name || t('group')} +

+ {isAdmin && ( + + )} +
+ )} +

+ {chat.members.length} {t('members')} +

+
+ + {/* Description */} +
+ + {isEditingDesc ? ( +
+