Backfill preview-only incoming messages
This commit is contained in:
@@ -83,6 +83,11 @@ public sealed class ChatsController(
|
||||
}
|
||||
}
|
||||
|
||||
if (before is null)
|
||||
{
|
||||
await EnsureLastPreviewMessageAsync(chat, cancellationToken);
|
||||
}
|
||||
|
||||
if (before is null && chat.UnreadCount > 0)
|
||||
{
|
||||
chat.UnreadCount = 0;
|
||||
@@ -108,6 +113,95 @@ public sealed class ChatsController(
|
||||
return messages.OrderBy(x => x.SentAt).Select(projection.ToDto).ToArray();
|
||||
}
|
||||
|
||||
private async Task EnsureLastPreviewMessageAsync(Chat chat, CancellationToken cancellationToken)
|
||||
{
|
||||
var preview = MessageTextSanitizer.CleanChatPreview(chat.LastMessagePreview);
|
||||
if (string.IsNullOrWhiteSpace(preview) || chat.LastMessageAt is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var previewAt = chat.LastMessageAt.Value;
|
||||
if (chat.HistoryClearedAt is { } historyClearedAt && previewAt <= historyClearedAt)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var latestMessage = (await db.Messages
|
||||
.AsNoTracking()
|
||||
.Where(x => x.ChatId == chat.Id && x.DeletedAt == null)
|
||||
.ToArrayAsync(cancellationToken))
|
||||
.OrderByDescending(x => x.SentAt)
|
||||
.FirstOrDefault();
|
||||
if (latestMessage is not null && latestMessage.SentAt >= previewAt.AddSeconds(-1))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var externalId = PreviewExternalId(chat.Id, preview, previewAt);
|
||||
if (await HasPreviewPlaceholderAsync(chat.Id, externalId, preview, previewAt, cancellationToken))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
db.Messages.Add(new Message
|
||||
{
|
||||
ChatId = chat.Id,
|
||||
ExternalId = externalId,
|
||||
Direction = MessageDirection.Incoming,
|
||||
Text = preview,
|
||||
SentAt = previewAt,
|
||||
DeliveryState = MessageDeliveryState.Delivered,
|
||||
SenderExternalId = chat.ExternalId,
|
||||
SenderName = chat.Title
|
||||
});
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<bool> HasPreviewPlaceholderAsync(
|
||||
Guid chatId,
|
||||
string externalId,
|
||||
string preview,
|
||||
DateTimeOffset previewAt,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var previewMessages = await db.Messages
|
||||
.AsNoTracking()
|
||||
.Where(x =>
|
||||
x.ChatId == chatId &&
|
||||
x.DeletedAt == null &&
|
||||
x.Direction == MessageDirection.Incoming &&
|
||||
x.ExternalId != null &&
|
||||
(x.ExternalId == externalId || x.ExternalId.StartsWith("preview:")))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
|
||||
return previewMessages.Any(message =>
|
||||
string.Equals(message.ExternalId, externalId, StringComparison.Ordinal) ||
|
||||
string.Equals(
|
||||
MessageTextSanitizer.CleanMessageText(message.Text, false),
|
||||
preview,
|
||||
StringComparison.Ordinal) &&
|
||||
Math.Abs((message.SentAt.ToUniversalTime() - previewAt.ToUniversalTime()).TotalMinutes) <= 30);
|
||||
}
|
||||
|
||||
private static string PreviewExternalId(Guid chatId, string preview, DateTimeOffset previewAt)
|
||||
{
|
||||
var previewMinute = previewAt.ToUniversalTime().Ticks / TimeSpan.TicksPerMinute;
|
||||
return $"preview:{chatId:N}:{previewMinute}:{StableHash(preview.Trim().ToLowerInvariant())}";
|
||||
}
|
||||
|
||||
private static string StableHash(string value)
|
||||
{
|
||||
var hash = 2166136261u;
|
||||
foreach (var character in value)
|
||||
{
|
||||
hash ^= character;
|
||||
hash *= 16777619;
|
||||
}
|
||||
|
||||
return hash.ToString("x8");
|
||||
}
|
||||
|
||||
[HttpPost("{chatId:guid}/read")]
|
||||
public async Task<IActionResult> MarkRead(Guid chatId, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user