Add chat bulk actions and avatar cache

This commit is contained in:
sevenhill
2026-07-05 17:49:08 +03:00
parent 81dc918a50
commit 4db643b028
16 changed files with 982 additions and 35 deletions
@@ -121,6 +121,86 @@ public sealed class ChatsController(
return NoContent();
}
[HttpPost("clear-history")]
public async Task<IActionResult> ClearHistories(ChatBulkActionRequest request, CancellationToken cancellationToken)
{
var chatIds = request.ChatIds.Distinct().ToArray();
if (chatIds.Length == 0)
{
return BadRequest("chatIds are required.");
}
var chats = await LoadChatsWithAttachmentsAsync(chatIds, cancellationToken);
if (chats.Count != chatIds.Length)
{
return NotFound();
}
foreach (var chat in chats)
{
var maxFailure = await TryApplyMaxChatActionAsync(
chat,
(externalChatId, chatUrl) => maxBridge.ClearChatHistoryAsync(externalChatId, chatUrl, cancellationToken),
"clear history",
cancellationToken);
if (maxFailure is not null)
{
return maxFailure;
}
}
var files = AttachmentFiles(chats).ToArray();
foreach (var chat in chats)
{
db.Messages.RemoveRange(chat.Messages);
chat.LastMessageAt = null;
chat.LastMessagePreview = null;
chat.UnreadCount = 0;
chat.UpdatedAt = DateTimeOffset.UtcNow;
}
await db.SaveChangesAsync(cancellationToken);
DeleteFiles(files);
await hubContext.Clients.All.SendAsync("ChatListInvalidated", cancellationToken);
return NoContent();
}
[HttpPost("delete")]
public async Task<IActionResult> DeleteChats(ChatBulkActionRequest request, CancellationToken cancellationToken)
{
var chatIds = request.ChatIds.Distinct().ToArray();
if (chatIds.Length == 0)
{
return BadRequest("chatIds are required.");
}
var chats = await LoadChatsWithAttachmentsAsync(chatIds, cancellationToken);
if (chats.Count != chatIds.Length)
{
return NotFound();
}
foreach (var chat in chats)
{
var maxFailure = await TryApplyMaxChatActionAsync(
chat,
(externalChatId, chatUrl) => maxBridge.DeleteChatAsync(externalChatId, chatUrl, cancellationToken),
"delete chat",
cancellationToken);
if (maxFailure is not null)
{
return maxFailure;
}
}
var files = AttachmentFiles(chats).ToArray();
db.Chats.RemoveRange(chats);
await db.SaveChangesAsync(cancellationToken);
DeleteFiles(files);
await hubContext.Clients.All.SendAsync("ChatListInvalidated", cancellationToken);
return NoContent();
}
[HttpGet("{chatId:guid}/search")]
public async Task<ActionResult<IReadOnlyList<MessageDto>>> SearchMessages(
Guid chatId,
@@ -656,6 +736,56 @@ public sealed class ChatsController(
statusCode: StatusCodes.Status409Conflict);
}
private async Task<ActionResult?> TryApplyMaxChatActionAsync(
Chat chat,
Func<string, string?, Task<MaxActionResult>> action,
string actionName,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(chat.ExternalId))
{
return null;
}
var result = await action(chat.ExternalId, chat.WebUrl);
if (result.Success)
{
return null;
}
return Problem(
result.Error ?? $"MAX {actionName} action failed for {chat.Title}.",
statusCode: StatusCodes.Status409Conflict);
}
private async Task<List<Chat>> LoadChatsWithAttachmentsAsync(IReadOnlyCollection<Guid> chatIds, CancellationToken cancellationToken)
{
return await db.Chats
.Include(x => x.Messages)
.ThenInclude(x => x.Attachments)
.Where(x => chatIds.Contains(x.Id))
.ToListAsync(cancellationToken);
}
private IEnumerable<string> AttachmentFiles(IEnumerable<Chat> chats)
{
return chats
.SelectMany(x => x.Messages)
.SelectMany(x => x.Attachments)
.Select(x => storage.GetPath(x.StorageFileName))
.Where(System.IO.File.Exists);
}
private static void DeleteFiles(IEnumerable<string> files)
{
foreach (var file in files)
{
System.IO.File.Delete(file);
}
}
private static string? NormalizeReaction(string? value)
{
var emoji = value?.Trim();