From af726c530a5658a7c74a251502ecf5970f33ee5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96zt=C3=BCrk?= Date: Fri, 12 Jun 2026 22:35:48 +0300 Subject: [PATCH] =?UTF-8?q?Messenger=20T=C3=BCm=20Mesajlar=20Logluyor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Messenger/MessengerDtos.cs | 44 ++ .../Messenger/MessengerAppService.cs | 381 ++++++++++++++++++ .../Messenger/MessengerHub.cs | 70 +--- .../Seeds/ListFormSeeder_Administration.cs | 2 +- .../Enums/TableNameEnum.cs | 2 + .../TableNameResolver.cs | 2 + .../Tenant/Messenger/MessengerConversation.cs | 29 ++ .../Messenger/MessengerConversationMessage.cs | 28 ++ .../EntityFrameworkCore/PlatformDbContext.cs | 37 ++ ....cs => 20260612183658_Initial.Designer.cs} | 176 +++++++- ...8_Initial.cs => 20260612183658_Initial.cs} | 83 ++++ .../PlatformDbContextModelSnapshot.cs | 174 ++++++++ ui/public/img/others/no-image.png | Bin 3923 -> 12576 bytes ui/public/version.json | 2 +- .../components/template/MessengerWidget.tsx | 188 +++++++-- ui/src/services/messenger.service.ts | 35 ++ ui/src/services/messenger.signalr.ts | 35 +- 17 files changed, 1196 insertions(+), 92 deletions(-) create mode 100644 api/src/Sozsoft.Platform.Domain/Entities/Tenant/Messenger/MessengerConversation.cs create mode 100644 api/src/Sozsoft.Platform.Domain/Entities/Tenant/Messenger/MessengerConversationMessage.cs rename api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/{20260610203148_Initial.Designer.cs => 20260612183658_Initial.Designer.cs} (98%) rename api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/{20260610203148_Initial.cs => 20260612183658_Initial.cs} (98%) diff --git a/api/src/Sozsoft.Platform.Application.Contracts/Messenger/MessengerDtos.cs b/api/src/Sozsoft.Platform.Application.Contracts/Messenger/MessengerDtos.cs index ab1a428..972ddeb 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/Messenger/MessengerDtos.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/Messenger/MessengerDtos.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using Volo.Abp.Application.Dtos; using Volo.Abp.Content; namespace Sozsoft.Platform.Messenger; @@ -36,6 +37,8 @@ public class MessengerUploadAttachmentInput public class MessengerSendMessageDto { + public Guid? ConversationId { get; set; } + [Required] public List RecipientIds { get; set; } = new(); @@ -49,6 +52,7 @@ public class MessengerMessageDto { public Guid Id { get; set; } public Guid? TenantId { get; set; } + public Guid ConversationId { get; set; } public Guid SenderId { get; set; } public string SenderUserName { get; set; } = string.Empty; public string SenderName { get; set; } = string.Empty; @@ -57,3 +61,43 @@ public class MessengerMessageDto public List Attachments { get; set; } = new(); public DateTime SentAt { get; set; } } + +public class MessengerMessageDeletedDto +{ + public Guid MessageId { get; set; } + public Guid ConversationId { get; set; } + public Guid SenderId { get; set; } + public List RecipientIds { get; set; } = new(); +} + +public class MessengerConversationDto : FullAuditedEntityDto +{ + public Guid? TenantId { get; set; } + public string? Title { get; set; } + public List ParticipantIds { get; set; } = new(); + public bool IsGroup { get; set; } + public Guid? LastSenderId { get; set; } + public string? LastMessagePreview { get; set; } + public DateTime? LastMessageTime { get; set; } + public int MessageCount { get; set; } +} + +public class MessengerConversationCreateUpdateDto +{ + [StringLength(256)] + public string? Title { get; set; } + + [Required] + public List ParticipantIds { get; set; } = new(); +} + +public class MessengerConversationListRequestDto : PagedAndSortedResultRequestDto +{ + public string? Filter { get; set; } +} + +public class MessengerGetMessagesInput : PagedAndSortedResultRequestDto +{ + public Guid? ConversationId { get; set; } + public List ParticipantIds { get; set; } = new(); +} diff --git a/api/src/Sozsoft.Platform.Application/Messenger/MessengerAppService.cs b/api/src/Sozsoft.Platform.Application/Messenger/MessengerAppService.cs index 8bab43a..73f4761 100644 --- a/api/src/Sozsoft.Platform.Application/Messenger/MessengerAppService.cs +++ b/api/src/Sozsoft.Platform.Application/Messenger/MessengerAppService.cs @@ -4,12 +4,16 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Linq.Dynamic.Core; +using System.Text.Json; using System.Threading.Tasks; using Sozsoft.Platform.BlobStoring; +using Sozsoft.Platform.Entities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Volo.Abp; +using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; using Volo.Abp.Identity; @@ -20,15 +24,21 @@ namespace Sozsoft.Platform.Messenger; public class MessengerAppService : ApplicationService { private readonly IRepository _userRepository; + private readonly IRepository _conversationRepository; + private readonly IRepository _messageRepository; private readonly BlobManager _blobManager; private readonly IConfiguration _configuration; public MessengerAppService( IRepository userRepository, + IRepository conversationRepository, + IRepository messageRepository, BlobManager blobManager, IConfiguration configuration) { _userRepository = userRepository; + _conversationRepository = conversationRepository; + _messageRepository = messageRepository; _blobManager = blobManager; _configuration = configuration; } @@ -78,6 +88,250 @@ public class MessengerAppService : ApplicationService }).ToList(); } + public async Task> GetConversationsAsync(MessengerConversationListRequestDto input) + { + var query = await _conversationRepository.GetQueryableAsync(); + var currentUserId = GetCurrentUserId(); + var currentUserKey = currentUserId.ToString("N"); + + query = query.Where(conversation => conversation.ParticipantKey.Contains(currentUserKey)); + + if (!input.Filter.IsNullOrWhiteSpace()) + { + var filter = input.Filter!.Trim().ToLower(); + query = query.Where(conversation => + (conversation.Title != null && conversation.Title.ToLower().Contains(filter)) || + (conversation.LastMessagePreview != null && conversation.LastMessagePreview.ToLower().Contains(filter))); + } + + var totalCount = await AsyncExecuter.CountAsync(query); + var sorting = input.Sorting.IsNullOrWhiteSpace() + ? $"{nameof(MessengerConversation.LastMessageTime)} desc" + : input.Sorting; + + var conversations = await AsyncExecuter.ToListAsync( + query.OrderBy(sorting).Skip(input.SkipCount).Take(input.MaxResultCount) + ); + + return new PagedResultDto( + totalCount, + conversations.Select(MapConversation).ToList() + ); + } + + public async Task GetConversationAsync(Guid id) + { + var conversation = await _conversationRepository.GetAsync(id); + EnsureConversationParticipant(conversation, GetCurrentUserId()); + + return MapConversation(conversation); + } + + public async Task CreateConversationAsync(MessengerConversationCreateUpdateDto input) + { + var currentUserId = GetCurrentUserId(); + var participantIds = await NormalizeAndValidateParticipantIdsAsync(input.ParticipantIds, currentUserId); + var participantKey = CreateParticipantKey(participantIds); + + var existing = await FindConversationByParticipantKeyAsync(participantKey); + if (existing is not null) + { + return MapConversation(existing); + } + + var conversation = new MessengerConversation(GuidGenerator.Create()) + { + TenantId = CurrentTenant.Id, + Title = input.Title?.Trim(), + ParticipantKey = participantKey, + ParticipantIdsJson = JsonSerializer.Serialize(participantIds), + IsGroup = participantIds.Count > 2 + }; + + await _conversationRepository.InsertAsync(conversation, autoSave: true); + + return MapConversation(conversation); + } + + public async Task UpdateConversationAsync(Guid id, MessengerConversationCreateUpdateDto input) + { + var conversation = await _conversationRepository.GetAsync(id); + var currentUserId = GetCurrentUserId(); + EnsureConversationParticipant(conversation, currentUserId); + + var participantIds = await NormalizeAndValidateParticipantIdsAsync(input.ParticipantIds, currentUserId); + var participantKey = CreateParticipantKey(participantIds); + + conversation.Title = input.Title?.Trim(); + conversation.ParticipantKey = participantKey; + conversation.ParticipantIdsJson = JsonSerializer.Serialize(participantIds); + conversation.IsGroup = participantIds.Count > 2; + + await _conversationRepository.UpdateAsync(conversation, autoSave: true); + + return MapConversation(conversation); + } + + public async Task DeleteConversationAsync(Guid id) + { + var conversation = await _conversationRepository.GetAsync(id); + EnsureConversationParticipant(conversation, GetCurrentUserId()); + + await _conversationRepository.DeleteAsync(conversation, autoSave: true); + } + + [HttpPost("api/app/messenger/messages")] + public async Task> GetMessagesAsync(MessengerGetMessagesInput input) + { + var currentUserId = GetCurrentUserId(); + var conversation = input.ConversationId.HasValue + ? await _conversationRepository.GetAsync(input.ConversationId.Value) + : await FindConversationByParticipantKeyAsync( + CreateParticipantKey(await NormalizeAndValidateParticipantIdsAsync(input.ParticipantIds, currentUserId)) + ); + + if (conversation is null) + { + return new List(); + } + + EnsureConversationParticipant(conversation, currentUserId); + + var query = await _messageRepository.GetQueryableAsync(); + query = query.Where(message => message.ConversationId == conversation.Id); + + var sorting = input.Sorting.IsNullOrWhiteSpace() + ? $"{nameof(MessengerConversationMessage.SentAt)} desc" + : input.Sorting; + + var messages = await AsyncExecuter.ToListAsync( + query.OrderBy(sorting).Skip(input.SkipCount).Take(input.MaxResultCount) + ); + + return messages + .OrderBy(message => message.SentAt) + .Select(MapMessage) + .ToList(); + } + + public async Task SendMessageAsync(MessengerSendMessageDto input) + { + var senderId = GetCurrentUserId(); + var recipientIds = input.RecipientIds + .Where(id => id != Guid.Empty && id != senderId) + .Distinct() + .ToList(); + + if (recipientIds.Count == 0) + { + throw new UserFriendlyException("En az bir alici secilmelidir."); + } + + if (input.Text.IsNullOrWhiteSpace() && input.Attachments.Count == 0) + { + throw new UserFriendlyException("Mesaj veya dosya gonderilmelidir."); + } + + var validRecipients = await GetValidUsersAsync(recipientIds); + if (validRecipients.Count != recipientIds.Count) + { + throw new UserFriendlyException("Bu tenant icinde gecersiz alici var."); + } + + MessengerConversation? conversation = null; + if (input.ConversationId.HasValue) + { + conversation = await _conversationRepository.GetAsync(input.ConversationId.Value); + EnsureConversationParticipant(conversation, senderId); + } + + var participantIds = recipientIds.Append(senderId).Distinct().ToList(); + var participantKey = CreateParticipantKey(participantIds); + conversation ??= await FindConversationByParticipantKeyAsync(participantKey); + + if (conversation is null) + { + conversation = new MessengerConversation(GuidGenerator.Create()) + { + TenantId = CurrentTenant.Id, + ParticipantKey = participantKey, + ParticipantIdsJson = JsonSerializer.Serialize(participantIds), + IsGroup = participantIds.Count > 2 + }; + + await _conversationRepository.InsertAsync(conversation, autoSave: true); + } + + var senderName = $"{CurrentUser.Name} {CurrentUser.SurName}".Trim() ?? CurrentUser.UserName ?? "Kullanici"; + var trimmedText = input.Text?.Trim(); + var message = new MessengerConversationMessage(GuidGenerator.Create()) + { + TenantId = CurrentTenant.Id, + ConversationId = conversation.Id, + SenderId = senderId, + SenderUserName = CurrentUser.UserName ?? string.Empty, + SenderName = senderName, + RecipientIdsJson = JsonSerializer.Serialize(recipientIds), + Text = trimmedText, + AttachmentsJson = JsonSerializer.Serialize(input.Attachments), + SentAt = Clock.Now + }; + + await _messageRepository.InsertAsync(message, autoSave: true); + + conversation.LastSenderId = senderId; + conversation.LastMessagePreview = GetMessagePreview(trimmedText, input.Attachments.Count); + conversation.LastMessageTime = message.SentAt; + conversation.MessageCount += 1; + await _conversationRepository.UpdateAsync(conversation, autoSave: true); + + return MapMessage(message); + } + + public async Task DeleteMessageAsync(Guid id) + { + var currentUserId = GetCurrentUserId(); + var message = await _messageRepository.GetAsync(id); + + if (message.SenderId != currentUserId) + { + throw new UserFriendlyException("Sadece kendi mesajlarinizi silebilirsiniz."); + } + + if (Clock.Now - message.SentAt > TimeSpan.FromMinutes(10)) + { + throw new UserFriendlyException("Mesajlar ilk 10 dakika icinde silinebilir."); + } + + var recipientIds = DeserializeGuidList(message.RecipientIdsJson); + var conversation = await _conversationRepository.GetAsync(message.ConversationId); + + await _messageRepository.DeleteAsync(message, autoSave: true); + + var query = await _messageRepository.GetQueryableAsync(); + var lastMessage = await AsyncExecuter.FirstOrDefaultAsync( + query + .Where(item => item.ConversationId == conversation.Id) + .OrderByDescending(item => item.SentAt) + ); + + conversation.MessageCount = Math.Max(0, conversation.MessageCount - 1); + conversation.LastSenderId = lastMessage?.SenderId; + conversation.LastMessagePreview = lastMessage is null + ? null + : GetMessagePreview(lastMessage.Text, DeserializeAttachments(lastMessage.AttachmentsJson).Count); + conversation.LastMessageTime = lastMessage?.SentAt; + await _conversationRepository.UpdateAsync(conversation, autoSave: true); + + return new MessengerMessageDeletedDto + { + MessageId = message.Id, + ConversationId = message.ConversationId, + SenderId = message.SenderId, + RecipientIds = recipientIds + }; + } + [HttpPost("api/app/messenger/upload-attachment")] public async Task UploadAttachmentAsync([FromForm] MessengerUploadAttachmentInput input) { @@ -111,4 +365,131 @@ public class MessengerAppService : ApplicationService Url = $"{baseUrl}/{tenantPart}/{BlobContainerNames.Messenger}/{savedFileName}" }; } + + private Guid GetCurrentUserId() + { + if (!CurrentUser.Id.HasValue) + { + throw new UserFriendlyException("Kullanici oturumu bulunamadi."); + } + + return CurrentUser.Id.Value; + } + + private async Task> NormalizeAndValidateParticipantIdsAsync(IEnumerable participantIds, Guid currentUserId) + { + var normalizedIds = participantIds + .Where(id => id != Guid.Empty) + .Append(currentUserId) + .Distinct() + .ToList(); + + if (normalizedIds.Count < 2) + { + throw new UserFriendlyException("En az iki katilimci secilmelidir."); + } + + var users = await GetValidUsersAsync(normalizedIds); + if (users.Count != normalizedIds.Count) + { + throw new UserFriendlyException("Bu tenant icinde gecersiz katilimci var."); + } + + return normalizedIds; + } + + private async Task> GetValidUsersAsync(List userIds) + { + return await _userRepository.GetListAsync(user => + userIds.Contains(user.Id) && + user.TenantId == CurrentTenant.Id && + user.IsActive); + } + + private async Task FindConversationByParticipantKeyAsync(string participantKey) + { + var query = await _conversationRepository.GetQueryableAsync(); + return await AsyncExecuter.FirstOrDefaultAsync(query.Where(conversation => conversation.ParticipantKey == participantKey)); + } + + private static string CreateParticipantKey(IEnumerable participantIds) + { + return string.Join(",", participantIds.Select(id => id.ToString("N")).OrderBy(id => id)); + } + + private static void EnsureConversationParticipant(MessengerConversation conversation, Guid currentUserId) + { + var participantIds = DeserializeGuidList(conversation.ParticipantIdsJson); + if (!participantIds.Contains(currentUserId)) + { + throw new UserFriendlyException("Bu gorusmeye erisim yetkiniz yok."); + } + } + + private static MessengerConversationDto MapConversation(MessengerConversation conversation) + { + return new MessengerConversationDto + { + Id = conversation.Id, + TenantId = conversation.TenantId, + Title = conversation.Title, + ParticipantIds = DeserializeGuidList(conversation.ParticipantIdsJson), + IsGroup = conversation.IsGroup, + LastSenderId = conversation.LastSenderId, + LastMessagePreview = conversation.LastMessagePreview, + LastMessageTime = conversation.LastMessageTime, + MessageCount = conversation.MessageCount, + CreationTime = conversation.CreationTime, + CreatorId = conversation.CreatorId, + LastModificationTime = conversation.LastModificationTime, + LastModifierId = conversation.LastModifierId + }; + } + + private static MessengerMessageDto MapMessage(MessengerConversationMessage message) + { + return new MessengerMessageDto + { + Id = message.Id, + TenantId = message.TenantId, + ConversationId = message.ConversationId, + SenderId = message.SenderId, + SenderUserName = message.SenderUserName, + SenderName = message.SenderName, + RecipientIds = DeserializeGuidList(message.RecipientIdsJson), + Text = message.Text, + Attachments = DeserializeAttachments(message.AttachmentsJson), + SentAt = message.SentAt + }; + } + + private static List DeserializeGuidList(string? json) + { + if (json.IsNullOrWhiteSpace()) + { + return new List(); + } + + return JsonSerializer.Deserialize>(json!) ?? new List(); + } + + private static List DeserializeAttachments(string? json) + { + if (json.IsNullOrWhiteSpace()) + { + return new List(); + } + + return JsonSerializer.Deserialize>(json!) ?? new List(); + } + + private static string GetMessagePreview(string? text, int attachmentCount) + { + if (!text.IsNullOrWhiteSpace()) + { + return text!.Length > 512 ? text[..512] : text; + } + + return attachmentCount > 0 ? $"{attachmentCount} dosya" : string.Empty; + } } diff --git a/api/src/Sozsoft.Platform.Application/Messenger/MessengerHub.cs b/api/src/Sozsoft.Platform.Application/Messenger/MessengerHub.cs index 402de24..caf9cd2 100644 --- a/api/src/Sozsoft.Platform.Application/Messenger/MessengerHub.cs +++ b/api/src/Sozsoft.Platform.Application/Messenger/MessengerHub.cs @@ -5,9 +5,6 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Guids; -using Volo.Abp.Identity; using Volo.Abp.MultiTenancy; using Volo.Abp.Users; @@ -18,19 +15,16 @@ public class MessengerHub : Hub { private readonly ICurrentTenant _currentTenant; private readonly ICurrentUser _currentUser; - private readonly IGuidGenerator _guidGenerator; - private readonly IRepository _userRepository; + private readonly MessengerAppService _messengerAppService; public MessengerHub( ICurrentTenant currentTenant, ICurrentUser currentUser, - IGuidGenerator guidGenerator, - IRepository userRepository) + MessengerAppService messengerAppService) { _currentTenant = currentTenant; _currentUser = currentUser; - _guidGenerator = guidGenerator; - _userRepository = userRepository; + _messengerAppService = messengerAppService; } public override async Task OnConnectedAsync() @@ -57,50 +51,7 @@ public class MessengerHub : Hub public async Task SendMessage(MessengerSendMessageDto input) { - if (!_currentUser.Id.HasValue) - { - throw new HubException("Kullanıcı oturumu bulunamadı."); - } - - var recipientIds = input.RecipientIds - .Where(id => id != Guid.Empty) - .Distinct() - .ToList(); - - if (recipientIds.Count == 0) - { - throw new HubException("En az bir alıcı seçilmelidir."); - } - - if (input.Text.IsNullOrWhiteSpace() && input.Attachments.Count == 0) - { - throw new HubException("Mesaj veya dosya gönderilmelidir."); - } - - var tenantId = _currentTenant.Id; - var validRecipients = await _userRepository.GetListAsync(user => - recipientIds.Contains(user.Id) && - user.TenantId == tenantId && - user.IsActive); - - if (validRecipients.Count == 0) - { - throw new HubException("Bu tenant içinde geçerli alıcı bulunamadı."); - } - - var senderName = _currentUser.Name ?? _currentUser.UserName ?? "Kullanıcı"; - var message = new MessengerMessageDto - { - Id = _guidGenerator.Create(), - TenantId = tenantId, - SenderId = _currentUser.Id.Value, - SenderUserName = _currentUser.UserName ?? string.Empty, - SenderName = senderName, - RecipientIds = validRecipients.Select(user => user.Id).ToList(), - Text = input.Text?.Trim(), - Attachments = input.Attachments, - SentAt = DateTime.UtcNow - }; + var message = await _messengerAppService.SendMessageAsync(input); var targetGroups = message.RecipientIds .Append(message.SenderId) @@ -111,6 +62,19 @@ public class MessengerHub : Hub await Clients.Groups(targetGroups).SendAsync("MessengerMessageReceived", message); } + public async Task DeleteMessage(Guid messageId) + { + var deletedMessage = await _messengerAppService.DeleteMessageAsync(messageId); + + var targetGroups = deletedMessage.RecipientIds + .Append(deletedMessage.SenderId) + .Distinct() + .Select(UserGroupName) + .ToList(); + + await Clients.Groups(targetGroups).SendAsync("MessengerMessageDeleted", deletedMessage); + } + private string TenantKey => _currentTenant.Id?.ToString("N") ?? "host"; private string TenantGroupName() => $"messenger:tenant:{TenantKey}"; diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs index 96e1e24..2e61d6a 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Administration.cs @@ -809,7 +809,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 710, true, true, true, true, false), EditingFormJson = JsonSerializer.Serialize(new List() { new () { Order=1,ColCount=1,ColSpan=1,ItemType="group",Items=[ - new EditingFormItemDto { Order=1, DataField="Avatar", ColSpan=1, EditorType2=EditorTypes.dxImageViewer, EditorOptions=EditorOptionValues.ImageUploadOptions(false) }, + new EditingFormItemDto { Order=1, DataField="Avatar", ColSpan=1, EditorType2=EditorTypes.dxImageUpload, EditorOptions=EditorOptionValues.ImageUploadOptions(false) }, new EditingFormItemDto { Order=2, DataField="Email", ColSpan=1, EditorType2=EditorTypes.dxTextBox }, new EditingFormItemDto { Order=3, DataField="Name", ColSpan=1, EditorType2=EditorTypes.dxTextBox }, new EditingFormItemDto { Order=4, DataField="Surname", ColSpan=1, EditorType2=EditorTypes.dxTextBox }, diff --git a/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs b/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs index e15e1ba..56c846b 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/Enums/TableNameEnum.cs @@ -85,6 +85,8 @@ public enum TableNameEnum Event, EventPhoto, EventComment, + MessengerConversation, + MessengerConversationMessage, Videoroom, VideoroomParticipant, VideoroomAttandance, diff --git a/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs b/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs index 9e4c8ed..965620b 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/TableNameResolver.cs @@ -104,6 +104,8 @@ public static class TableNameResolver { nameof(TableNameEnum.Event), (TablePrefix.TenantByName, MenuPrefix.Administration) }, { nameof(TableNameEnum.EventPhoto), (TablePrefix.TenantByName, MenuPrefix.Administration) }, { nameof(TableNameEnum.EventComment), (TablePrefix.TenantByName, MenuPrefix.Administration) }, + { nameof(TableNameEnum.MessengerConversation), (TablePrefix.TenantByName, MenuPrefix.Administration) }, + { nameof(TableNameEnum.MessengerConversationMessage), (TablePrefix.TenantByName, MenuPrefix.Administration) }, }; diff --git a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Messenger/MessengerConversation.cs b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Messenger/MessengerConversation.cs new file mode 100644 index 0000000..3e3a5ef --- /dev/null +++ b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Messenger/MessengerConversation.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace Sozsoft.Platform.Entities; + +public class MessengerConversation : FullAuditedEntity, IMultiTenant +{ + protected MessengerConversation() + { + } + + public MessengerConversation(Guid id) : base(id) + { + } + + public Guid? TenantId { get; set; } + public string? Title { get; set; } + public string ParticipantKey { get; set; } = string.Empty; + public string ParticipantIdsJson { get; set; } = "[]"; + public bool IsGroup { get; set; } + public Guid? LastSenderId { get; set; } + public string? LastMessagePreview { get; set; } + public DateTime? LastMessageTime { get; set; } + public int MessageCount { get; set; } + + public virtual ICollection Messages { get; set; } = new List(); +} diff --git a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Messenger/MessengerConversationMessage.cs b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Messenger/MessengerConversationMessage.cs new file mode 100644 index 0000000..5a9b6a4 --- /dev/null +++ b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Messenger/MessengerConversationMessage.cs @@ -0,0 +1,28 @@ +using System; +using Volo.Abp.Domain.Entities.Auditing; +using Volo.Abp.MultiTenancy; + +namespace Sozsoft.Platform.Entities; + +public class MessengerConversationMessage : FullAuditedEntity, IMultiTenant +{ + protected MessengerConversationMessage() + { + } + + public MessengerConversationMessage(Guid id) : base(id) + { + } + + public Guid? TenantId { get; set; } + public Guid ConversationId { get; set; } + public Guid SenderId { get; set; } + public string SenderUserName { get; set; } = string.Empty; + public string SenderName { get; set; } = string.Empty; + public string RecipientIdsJson { get; set; } = "[]"; + public string? Text { get; set; } + public string AttachmentsJson { get; set; } = "[]"; + public DateTime SentAt { get; set; } + + public virtual MessengerConversation Conversation { get; set; } = default!; +} diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs index aa7d82c..327d713 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs @@ -120,6 +120,9 @@ public class PlatformDbContext : public DbSet EventComments { get; set; } public DbSet EventLikes { get; set; } + public DbSet MessengerConversations { get; set; } + public DbSet MessengerConversationMessages { get; set; } + public DbSet Announcements { get; set; } public DbSet Surveys { get; set; } @@ -1370,6 +1373,40 @@ public class PlatformDbContext : .OnDelete(DeleteBehavior.Cascade); }); + builder.Entity(b => + { + b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.MessengerConversation)), Prefix.DbSchema); + b.ConfigureByConvention(); + + b.Property(x => x.Title).HasMaxLength(256); + b.Property(x => x.ParticipantKey).IsRequired().HasMaxLength(512); + b.Property(x => x.ParticipantIdsJson).IsRequired().HasColumnType("text"); + b.Property(x => x.LastMessagePreview).HasMaxLength(512); + b.Property(x => x.MessageCount).HasDefaultValue(0); + + b.HasIndex(x => new { x.TenantId, x.ParticipantKey }).IsUnique().HasFilter("[IsDeleted] = 0"); + + b.HasMany(x => x.Messages) + .WithOne(x => x.Conversation) + .HasForeignKey(x => x.ConversationId) + .OnDelete(DeleteBehavior.Cascade); + }); + + builder.Entity(b => + { + b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.MessengerConversationMessage)), Prefix.DbSchema); + b.ConfigureByConvention(); + + b.Property(x => x.SenderUserName).IsRequired().HasMaxLength(256); + b.Property(x => x.SenderName).IsRequired().HasMaxLength(256); + b.Property(x => x.RecipientIdsJson).IsRequired().HasColumnType("text"); + b.Property(x => x.Text).HasMaxLength(4096); + b.Property(x => x.AttachmentsJson).IsRequired().HasColumnType("text"); + b.Property(x => x.SentAt).IsRequired(); + + b.HasIndex(x => new { x.TenantId, x.ConversationId, x.SentAt }); + }); + //Videoroom builder.Entity(b => { diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260610203148_Initial.Designer.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260612183658_Initial.Designer.cs similarity index 98% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260610203148_Initial.Designer.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260612183658_Initial.Designer.cs index e0885ec..f938a22 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260610203148_Initial.Designer.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260612183658_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace Sozsoft.Platform.Migrations { [DbContext(typeof(PlatformDbContext))] - [Migration("20260610203148_Initial")] + [Migration("20260612183658_Initial")] partial class Initial { /// @@ -3610,6 +3610,164 @@ namespace Sozsoft.Platform.Migrations b.ToTable("Sas_H_MenuGroup", (string)null); }); + modelBuilder.Entity("Sozsoft.Platform.Entities.MessengerConversation", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsGroup") + .HasColumnType("bit"); + + b.Property("LastMessagePreview") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("LastMessageTime") + .HasColumnType("datetime2"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("LastSenderId") + .HasColumnType("uniqueidentifier"); + + b.Property("MessageCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("ParticipantIdsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParticipantKey") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ParticipantKey") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("Adm_T_MessengerConversation", (string)null); + }); + + modelBuilder.Entity("Sozsoft.Platform.Entities.MessengerConversationMessage", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("AttachmentsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConversationId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("RecipientIdsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderId") + .HasColumnType("uniqueidentifier"); + + b.Property("SenderName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("SenderUserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("SentAt") + .HasColumnType("datetime2"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("Text") + .HasMaxLength(4096) + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ConversationId"); + + b.HasIndex("TenantId", "ConversationId", "SentAt"); + + b.ToTable("Adm_T_MessengerConversationMessage", (string)null); + }); + modelBuilder.Entity("Sozsoft.Platform.Entities.Note", b => { b.Property("Id") @@ -8262,6 +8420,17 @@ namespace Sozsoft.Platform.Migrations .IsRequired(); }); + modelBuilder.Entity("Sozsoft.Platform.Entities.MessengerConversationMessage", b => + { + b.HasOne("Sozsoft.Platform.Entities.MessengerConversation", "Conversation") + .WithMany("Messages") + .HasForeignKey("ConversationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Conversation"); + }); + modelBuilder.Entity("Sozsoft.Platform.Entities.OrderItem", b => { b.HasOne("Sozsoft.Platform.Entities.Order", "Order") @@ -8692,6 +8861,11 @@ namespace Sozsoft.Platform.Migrations b.Navigation("Events"); }); + modelBuilder.Entity("Sozsoft.Platform.Entities.MessengerConversation", b => + { + b.Navigation("Messages"); + }); + modelBuilder.Entity("Sozsoft.Platform.Entities.Order", b => { b.Navigation("Items"); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260610203148_Initial.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260612183658_Initial.cs similarity index 98% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260610203148_Initial.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260612183658_Initial.cs index 8f6bec1..68ae347 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260610203148_Initial.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260612183658_Initial.cs @@ -594,6 +594,33 @@ namespace Sozsoft.Platform.Migrations table.PrimaryKey("PK_Adm_T_IpRestriction", x => x.Id); }); + migrationBuilder.CreateTable( + name: "Adm_T_MessengerConversation", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + TenantId = table.Column(type: "uniqueidentifier", nullable: true), + Title = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + ParticipantKey = table.Column(type: "nvarchar(512)", maxLength: 512, nullable: false), + ParticipantIdsJson = table.Column(type: "text", nullable: false), + IsGroup = table.Column(type: "bit", nullable: false), + LastSenderId = table.Column(type: "uniqueidentifier", nullable: true), + LastMessagePreview = table.Column(type: "nvarchar(512)", maxLength: 512, nullable: true), + LastMessageTime = table.Column(type: "datetime2", nullable: true), + MessageCount = table.Column(type: "int", nullable: false, defaultValue: 0), + CreationTime = table.Column(type: "datetime2", nullable: false), + CreatorId = table.Column(type: "uniqueidentifier", nullable: true), + LastModificationTime = table.Column(type: "datetime2", nullable: true), + LastModifierId = table.Column(type: "uniqueidentifier", nullable: true), + IsDeleted = table.Column(type: "bit", nullable: false, defaultValue: false), + DeleterId = table.Column(type: "uniqueidentifier", nullable: true), + DeletionTime = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Adm_T_MessengerConversation", x => x.Id); + }); + migrationBuilder.CreateTable( name: "Adm_T_Note", columns: table => new @@ -2118,6 +2145,39 @@ namespace Sozsoft.Platform.Migrations onDelete: ReferentialAction.Restrict); }); + migrationBuilder.CreateTable( + name: "Adm_T_MessengerConversationMessage", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + TenantId = table.Column(type: "uniqueidentifier", nullable: true), + ConversationId = table.Column(type: "uniqueidentifier", nullable: false), + SenderId = table.Column(type: "uniqueidentifier", nullable: false), + SenderUserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), + SenderName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), + RecipientIdsJson = table.Column(type: "text", nullable: false), + Text = table.Column(type: "nvarchar(max)", maxLength: 4096, nullable: true), + AttachmentsJson = table.Column(type: "text", nullable: false), + SentAt = table.Column(type: "datetime2", nullable: false), + CreationTime = table.Column(type: "datetime2", nullable: false), + CreatorId = table.Column(type: "uniqueidentifier", nullable: true), + LastModificationTime = table.Column(type: "datetime2", nullable: true), + LastModifierId = table.Column(type: "uniqueidentifier", nullable: true), + IsDeleted = table.Column(type: "bit", nullable: false, defaultValue: false), + DeleterId = table.Column(type: "uniqueidentifier", nullable: true), + DeletionTime = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Adm_T_MessengerConversationMessage", x => x.Id); + table.ForeignKey( + name: "FK_Adm_T_MessengerConversationMessage_Adm_T_MessengerConversation_ConversationId", + column: x => x.ConversationId, + principalTable: "Adm_T_MessengerConversation", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + migrationBuilder.CreateTable( name: "Adm_T_ReportTemplate", columns: table => new @@ -3570,6 +3630,23 @@ namespace Sozsoft.Platform.Migrations unique: true, filter: "[IsDeleted] = 0"); + migrationBuilder.CreateIndex( + name: "IX_Adm_T_MessengerConversation_TenantId_ParticipantKey", + table: "Adm_T_MessengerConversation", + columns: new[] { "TenantId", "ParticipantKey" }, + unique: true, + filter: "[IsDeleted] = 0"); + + migrationBuilder.CreateIndex( + name: "IX_Adm_T_MessengerConversationMessage_ConversationId", + table: "Adm_T_MessengerConversationMessage", + column: "ConversationId"); + + migrationBuilder.CreateIndex( + name: "IX_Adm_T_MessengerConversationMessage_TenantId_ConversationId_SentAt", + table: "Adm_T_MessengerConversationMessage", + columns: new[] { "TenantId", "ConversationId", "SentAt" }); + migrationBuilder.CreateIndex( name: "IX_Adm_T_ReportCategory_TenantId_Name", table: "Adm_T_ReportCategory", @@ -4148,6 +4225,9 @@ namespace Sozsoft.Platform.Migrations migrationBuilder.DropTable( name: "Adm_T_JobPosition"); + migrationBuilder.DropTable( + name: "Adm_T_MessengerConversationMessage"); + migrationBuilder.DropTable( name: "Adm_T_Note"); @@ -4334,6 +4414,9 @@ namespace Sozsoft.Platform.Migrations migrationBuilder.DropTable( name: "Adm_T_Department"); + migrationBuilder.DropTable( + name: "Adm_T_MessengerConversation"); + migrationBuilder.DropTable( name: "Adm_T_ReportCategory"); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs index aa0f849..d664154 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs @@ -3607,6 +3607,164 @@ namespace Sozsoft.Platform.Migrations b.ToTable("Sas_H_MenuGroup", (string)null); }); + modelBuilder.Entity("Sozsoft.Platform.Entities.MessengerConversation", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("IsGroup") + .HasColumnType("bit"); + + b.Property("LastMessagePreview") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("LastMessageTime") + .HasColumnType("datetime2"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("LastSenderId") + .HasColumnType("uniqueidentifier"); + + b.Property("MessageCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("ParticipantIdsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ParticipantKey") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ParticipantKey") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + + b.ToTable("Adm_T_MessengerConversation", (string)null); + }); + + modelBuilder.Entity("Sozsoft.Platform.Entities.MessengerConversationMessage", b => + { + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("AttachmentsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("ConversationId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreationTime") + .HasColumnType("datetime2") + .HasColumnName("CreationTime"); + + b.Property("CreatorId") + .HasColumnType("uniqueidentifier") + .HasColumnName("CreatorId"); + + b.Property("DeleterId") + .HasColumnType("uniqueidentifier") + .HasColumnName("DeleterId"); + + b.Property("DeletionTime") + .HasColumnType("datetime2") + .HasColumnName("DeletionTime"); + + b.Property("IsDeleted") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false) + .HasColumnName("IsDeleted"); + + b.Property("LastModificationTime") + .HasColumnType("datetime2") + .HasColumnName("LastModificationTime"); + + b.Property("LastModifierId") + .HasColumnType("uniqueidentifier") + .HasColumnName("LastModifierId"); + + b.Property("RecipientIdsJson") + .IsRequired() + .HasColumnType("text"); + + b.Property("SenderId") + .HasColumnType("uniqueidentifier"); + + b.Property("SenderName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("SenderUserName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("SentAt") + .HasColumnType("datetime2"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier") + .HasColumnName("TenantId"); + + b.Property("Text") + .HasMaxLength(4096) + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ConversationId"); + + b.HasIndex("TenantId", "ConversationId", "SentAt"); + + b.ToTable("Adm_T_MessengerConversationMessage", (string)null); + }); + modelBuilder.Entity("Sozsoft.Platform.Entities.Note", b => { b.Property("Id") @@ -8259,6 +8417,17 @@ namespace Sozsoft.Platform.Migrations .IsRequired(); }); + modelBuilder.Entity("Sozsoft.Platform.Entities.MessengerConversationMessage", b => + { + b.HasOne("Sozsoft.Platform.Entities.MessengerConversation", "Conversation") + .WithMany("Messages") + .HasForeignKey("ConversationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Conversation"); + }); + modelBuilder.Entity("Sozsoft.Platform.Entities.OrderItem", b => { b.HasOne("Sozsoft.Platform.Entities.Order", "Order") @@ -8689,6 +8858,11 @@ namespace Sozsoft.Platform.Migrations b.Navigation("Events"); }); + modelBuilder.Entity("Sozsoft.Platform.Entities.MessengerConversation", b => + { + b.Navigation("Messages"); + }); + modelBuilder.Entity("Sozsoft.Platform.Entities.Order", b => { b.Navigation("Items"); diff --git a/ui/public/img/others/no-image.png b/ui/public/img/others/no-image.png index ebb9b8f7f9ee149008f4f55f31380a23399d4bc7..1e739c804382451c69d0bccfd7dcff503773a726 100644 GIT binary patch literal 12576 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�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 Cx0T}n literal 3923 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 Yw1Msvqd;el>Hj$!>znG;={lkR4~%zIh5!Hn diff --git a/ui/public/version.json b/ui/public/version.json index 111c927..6348d9a 100644 --- a/ui/public/version.json +++ b/ui/public/version.json @@ -1,5 +1,5 @@ { - "commit": "6098758", + "commit": "7632c9e", "releases": [ { "version": "1.1.04", diff --git a/ui/src/components/template/MessengerWidget.tsx b/ui/src/components/template/MessengerWidget.tsx index ec12dfa..2323e61 100644 --- a/ui/src/components/template/MessengerWidget.tsx +++ b/ui/src/components/template/MessengerWidget.tsx @@ -7,16 +7,27 @@ import { toast } from '@/components/ui' import { AVATAR_URL } from '@/constants/app.constant' import { getMessengerContacts, + getMessengerMessages, MessengerAttachmentDto, MessengerContactDto, + MessengerMessageDto, uploadMessengerAttachment, } from '@/services/messenger.service' -import { messengerSignalR, MessengerMessageDto } from '@/services/messenger.signalr' +import { messengerSignalR } from '@/services/messenger.signalr' import { useStoreState } from '@/store' import dayjs from 'dayjs' import EmojiPicker, { EmojiClickData } from 'emoji-picker-react' import { useEffect, useMemo, useRef, useState } from 'react' -import { FaArrowLeft, FaPaperclip, FaRegSmile, FaSearch, FaTimes, FaTrash } from 'react-icons/fa' +import { + FaArrowLeft, + FaCompress, + FaExpand, + FaPaperclip, + FaRegSmile, + FaSearch, + FaTimes, + FaTrash, +} from 'react-icons/fa' import { IoCheckmarkDone, IoChatbubbleEllipsesOutline, IoSend } from 'react-icons/io5' const MAX_UPLOAD_SIZE = 10 * 1024 * 1024 @@ -26,10 +37,14 @@ const formatFileSize = (size: number) => { return `${(size / (1024 * 1024)).toFixed(1)} MB` } +const canDeleteMessage = (message: MessengerMessageDto, userId: string) => + message.senderId === userId && dayjs().diff(dayjs(message.sentAt), 'minute', true) < 10 + const MessengerWidget = () => { const auth = useStoreState((state) => state.auth) const tenantId = auth.user.tenantId || auth.tenant?.tenantId const fileInputRef = useRef(null) + const messageInputRef = useRef(null) const messagesEndRef = useRef(null) const [open, setOpen] = useState(false) @@ -43,25 +58,38 @@ const MessengerWidget = () => { const [uploading, setUploading] = useState(false) const [showEmoji, setShowEmoji] = useState(false) const [multiSelect, setMultiSelect] = useState(false) - const [unread, setUnread] = useState(0) + const [maximized, setMaximized] = useState(false) + const [unreadByContact, setUnreadByContact] = useState>({}) + + const unread = useMemo( + () => Object.values(unreadByContact).reduce((total, count) => total + count, 0), + [unreadByContact], + ) useEffect(() => { if (!auth.session.signedIn) return const unsubscribeMessage = messengerSignalR.onMessage((message) => { setMessages((prev) => (prev.some((item) => item.id === message.id) ? prev : [...prev, message])) - if (!open && message.senderId !== auth.user.id) { - setUnread((count) => count + 1) + if (message.senderId !== auth.user.id && (!open || !selectedIds.includes(message.senderId))) { + setUnreadByContact((current) => ({ + ...current, + [message.senderId]: (current[message.senderId] || 0) + 1, + })) } }) + const unsubscribeMessageDeleted = messengerSignalR.onMessageDeleted((message) => { + setMessages((prev) => prev.filter((item) => item.id !== message.messageId)) + }) const unsubscribeState = messengerSignalR.onStateChange(setConnected) messengerSignalR.start() return () => { unsubscribeMessage() + unsubscribeMessageDeleted() unsubscribeState() } - }, [auth.session.signedIn, auth.user.id, open]) + }, [auth.session.signedIn, auth.user.id, open, selectedIds]) useEffect(() => { if (!auth.session.signedIn) { @@ -71,7 +99,6 @@ const MessengerWidget = () => { useEffect(() => { if (!open) return - setUnread(0) getMessengerContacts(filter) .then((response) => setContacts(response.data)) .catch(() => setContacts([])) @@ -81,13 +108,35 @@ const MessengerWidget = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages, open]) + useEffect(() => { + if (!open || selectedIds.length === 0) return + + const timer = window.setTimeout(() => { + messageInputRef.current?.focus() + }, 0) + + return () => window.clearTimeout(timer) + }, [open, selectedIds]) + + useEffect(() => { + if (!open || selectedIds.length === 0) return + + getMessengerMessages({ + participantIds: selectedIds, + maxResultCount: 50, + sorting: 'sentAt desc', + }) + .then((response) => setMessages(response.data)) + .catch(() => setMessages([])) + }, [open, selectedIds]) + const selectedContacts = useMemo( () => contacts.filter((contact) => selectedIds.includes(contact.id)), [contacts, selectedIds], ) const visibleMessages = useMemo(() => { - if (selectedIds.length === 0) return messages.slice(-30) + if (selectedIds.length === 0) return [] return messages.filter((message) => { const isOwn = message.senderId === auth.user.id @@ -97,6 +146,14 @@ const MessengerWidget = () => { }, [auth.user.id, messages, selectedIds]) const toggleContact = (id: string) => { + setUnreadByContact((current) => { + if (!current[id]) return current + + const next = { ...current } + delete next[id] + return next + }) + if (!multiSelect) { setSelectedIds([id]) return @@ -114,6 +171,16 @@ const MessengerWidget = () => { }) } + const closeWidget = () => { + setOpen(false) + setMaximized(false) + setSelectedIds([]) + setMessages([]) + setText('') + setAttachments([]) + setShowEmoji(false) + } + const handleFiles = async (files: FileList | null) => { if (!files?.length) return @@ -153,21 +220,38 @@ const MessengerWidget = () => { setText('') setAttachments([]) setShowEmoji(false) + window.setTimeout(() => { + messageInputRef.current?.focus() + }, 0) } catch { toast.push(, { placement: 'top-end' }) } } + const deleteMessage = async (messageId: string) => { + if (!window.confirm('Mesaj silinsin mi?')) return + + try { + await messengerSignalR.deleteMessage(messageId) + } catch { + toast.push(, { placement: 'top-end' }) + } + } + const onEmojiClick = (emoji: EmojiClickData) => { setText((current) => `${current}${emoji.emoji}`) } if (!auth.session.signedIn) return null + const panelClassName = maximized + ? 'pointer-events-auto fixed inset-2 overflow-hidden rounded-lg border border-gray-200 bg-white shadow-2xl dark:border-gray-700 dark:bg-gray-800 sm:inset-4' + : 'pointer-events-auto fixed inset-x-2 bottom-20 top-3 overflow-hidden rounded-lg border border-gray-200 bg-white shadow-2xl dark:border-gray-700 dark:bg-gray-800 sm:absolute sm:inset-auto sm:bottom-16 sm:right-0 sm:h-[min(70vh,640px)] sm:w-[min(78vw,800px)]' + return (
{open && ( -
+
-
@@ -211,6 +305,7 @@ const MessengerWidget = () => {
{contacts.map((contact) => { const active = selectedIds.includes(contact.id) + const contactUnread = unreadByContact[contact.id] || 0 return ( ) })} @@ -277,6 +377,17 @@ const MessengerWidget = () => {
+
+ +
@@ -288,16 +399,31 @@ const MessengerWidget = () => { {visibleMessages.map((message) => { const own = message.senderId === auth.user.id + const deletable = canDeleteMessage(message, auth.user.id) return (
-
- {!own &&
{message.senderName}
} +
+
+
+ + + {own ? auth.user.name || auth.user.userName || 'Ben' : message.senderName} + +
{message.text &&
{message.text}
} {message.attachments.length > 0 && (
@@ -317,9 +443,24 @@ const MessengerWidget = () => { ))}
)} -
- {dayjs(message.sentAt).format('HH:mm')} +
+ {dayjs(message.sentAt).format('HH:mm')}
+
+ {own && deletable && ( + + )}
) @@ -391,6 +532,7 @@ const MessengerWidget = () => { />