Hydrate generic media previews before notifying

This commit is contained in:
sevenhill
2026-07-06 14:57:59 +03:00
parent df429d5c43
commit 4dfcff0301
@@ -45,7 +45,12 @@ public sealed class MaxBridgeSyncService(
var update = await maxBridgeClient.FetchChatHistoryAsync(externalChatId, chatUrl, cancellationToken);
return update is null
? 0
: await ApplyUpdatesAsync([update], incrementUnread: false, sendPushNotifications: false, cancellationToken);
: await ApplyUpdatesAsync(
[update],
incrementUnread: false,
sendPushNotifications: false,
cancellationToken,
allowGenericMediaHistoryFetch: false);
}
catch (Exception ex)
{
@@ -58,7 +63,8 @@ public sealed class MaxBridgeSyncService(
IReadOnlyList<MaxChatUpdate> updates,
bool incrementUnread,
bool sendPushNotifications,
CancellationToken cancellationToken)
CancellationToken cancellationToken,
bool allowGenericMediaHistoryFetch = true)
{
if (updates.Count == 0)
{
@@ -74,6 +80,7 @@ public sealed class MaxBridgeSyncService(
var createdMessages = new List<(Chat Chat, Message Message)>();
var updatedMessages = new List<(Chat Chat, Message Message)>();
var previewNotificationMessages = new List<(Chat Chat, Message Message)>();
var genericMediaHistoryRequests = new List<(string ExternalChatId, string? ChatUrl, DateTimeOffset PreviewAt)>();
var trackedChatsByExternalId = new Dictionary<string, Chat>(StringComparer.Ordinal);
foreach (var update in updates)
@@ -196,6 +203,14 @@ public sealed class MaxBridgeSyncService(
previewNotificationMessages.Add((chat, previewResult.NotificationMessage));
}
if (allowGenericMediaHistoryFetch &&
previewResult.ShouldFetchHistory &&
previewResult.PreviewAt is { } previewAt &&
!await HasMessageAtOrAfterAsync(db, chat.Id, previewAt, cancellationToken))
{
genericMediaHistoryRequests.Add((update.ExternalId, nextWebUrl ?? chat.WebUrl, previewAt));
}
if (previewResult.Changed)
{
changed++;
@@ -387,6 +402,29 @@ public sealed class MaxBridgeSyncService(
await hubContext.Clients.All.SendAsync("ChatListInvalidated", cancellationToken);
}
foreach (var request in genericMediaHistoryRequests
.GroupBy(x => x.ExternalChatId, StringComparer.Ordinal)
.Select(x => x.OrderByDescending(item => item.PreviewAt).First()))
{
try
{
var history = await maxBridgeClient.FetchChatHistoryAsync(request.ExternalChatId, request.ChatUrl, cancellationToken);
if (history is not null)
{
changed += await ApplyUpdatesAsync(
[history],
incrementUnread,
sendPushNotifications,
cancellationToken,
allowGenericMediaHistoryFetch: false);
}
}
catch (Exception ex)
{
logger.LogWarning(ex, "MAX media preview history fetch failed for {ExternalChatId}.", request.ExternalChatId);
}
}
return changed;
}
@@ -400,12 +438,12 @@ public sealed class MaxBridgeSyncService(
var preview = MessageTextSanitizer.CleanChatPreview(update.LastMessagePreview);
if (string.IsNullOrWhiteSpace(preview))
{
return new ListPreviewApplyResult(false, null, null);
return new ListPreviewApplyResult(false, null, null, false, null);
}
if (IsPresencePreview(preview))
{
return new ListPreviewApplyResult(false, null, null);
return new ListPreviewApplyResult(false, null, null, false, null);
}
var previousPreview = chat.LastMessagePreview;
@@ -413,15 +451,19 @@ public sealed class MaxBridgeSyncService(
var previewAt = ResolveListPreviewAt(update, previousPreviewAt ?? chat.UpdatedAt);
if (chat.HistoryClearedAt is { } historyClearedAt && previewAt <= historyClearedAt)
{
return new ListPreviewApplyResult(false, null, null);
return new ListPreviewApplyResult(false, null, null, false, null);
}
var isGenericMediaPreview = MessageTextSanitizer.IsGenericMediaLabel(preview);
var shouldFetchHistory = isGenericMediaPreview &&
update.Messages.Count == 0 &&
HasListClockTime(update.LastMessageTimeText);
var samePreview = string.Equals(previousPreview, preview, StringComparison.Ordinal);
var samePreviewAt = previousPreviewAt is not null &&
Math.Abs((previousPreviewAt.Value - previewAt).TotalSeconds) < 1;
if (samePreview && samePreviewAt)
{
return new ListPreviewApplyResult(false, null, null);
return new ListPreviewApplyResult(false, null, null, shouldFetchHistory, previewAt);
}
chat.LastMessagePreview = preview;
@@ -438,18 +480,15 @@ public sealed class MaxBridgeSyncService(
var shouldNotify = shouldNotifyCandidate &&
RememberPreviewNotification(chat.Id, preview, previewAt);
if (incrementUnread && shouldNotify)
if (incrementUnread && shouldNotify && !isGenericMediaPreview)
{
chat.UnreadCount++;
}
var message = shouldNotify && !MessageTextSanitizer.IsGenericMediaLabel(preview)
var message = shouldNotify && !isGenericMediaPreview
? CreatePreviewMessage(chat, preview, previewAt)
: null;
var notificationMessage = shouldNotify && message is null && MessageTextSanitizer.IsGenericMediaLabel(preview)
? CreatePreviewMessage(chat, preview, previewAt)
: null;
return new ListPreviewApplyResult(true, message, notificationMessage);
return new ListPreviewApplyResult(true, message, null, shouldFetchHistory, previewAt);
}
private static bool IsKnownOutgoingPreviewEcho(
@@ -628,7 +667,12 @@ public sealed class MaxBridgeSyncService(
text.Contains("file", StringComparison.Ordinal);
}
private sealed record ListPreviewApplyResult(bool Changed, Message? Message, Message? NotificationMessage);
private sealed record ListPreviewApplyResult(
bool Changed,
Message? Message,
Message? NotificationMessage,
bool ShouldFetchHistory,
DateTimeOffset? PreviewAt);
private sealed record MessageMergeResult(bool Changed, IReadOnlyList<MessageAttachment> AddedAttachments);
@@ -715,6 +759,22 @@ public sealed class MaxBridgeSyncService(
Math.Abs((candidate.SentAt.ToUniversalTime() - previewMessage.SentAt.ToUniversalTime()).TotalMinutes) <= 30);
}
private static async Task<bool> HasMessageAtOrAfterAsync(
QMaxDbContext db,
Guid chatId,
DateTimeOffset previewAt,
CancellationToken cancellationToken)
{
var cutoff = previewAt.ToUniversalTime().AddSeconds(-1);
var sentAtValues = await db.Messages
.AsNoTracking()
.Where(x => x.ChatId == chatId && x.DeletedAt == null)
.Select(x => x.SentAt)
.ToArrayAsync(cancellationToken);
return sentAtValues.Any(sentAt => sentAt.ToUniversalTime() >= cutoff);
}
private static string PreviewExternalId(Guid chatId, string preview, DateTimeOffset previewAt)
{
var previewMinute = previewAt.ToUniversalTime().Ticks / TimeSpan.TicksPerMinute;