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 ebb9b8f..1e739c8 100644 Binary files a/ui/public/img/others/no-image.png and b/ui/public/img/others/no-image.png differ 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 = () => { />