Hydrate generic media previews before notifying
This commit is contained in:
@@ -45,7 +45,12 @@ public sealed class MaxBridgeSyncService(
|
|||||||
var update = await maxBridgeClient.FetchChatHistoryAsync(externalChatId, chatUrl, cancellationToken);
|
var update = await maxBridgeClient.FetchChatHistoryAsync(externalChatId, chatUrl, cancellationToken);
|
||||||
return update is null
|
return update is null
|
||||||
? 0
|
? 0
|
||||||
: await ApplyUpdatesAsync([update], incrementUnread: false, sendPushNotifications: false, cancellationToken);
|
: await ApplyUpdatesAsync(
|
||||||
|
[update],
|
||||||
|
incrementUnread: false,
|
||||||
|
sendPushNotifications: false,
|
||||||
|
cancellationToken,
|
||||||
|
allowGenericMediaHistoryFetch: false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -58,7 +63,8 @@ public sealed class MaxBridgeSyncService(
|
|||||||
IReadOnlyList<MaxChatUpdate> updates,
|
IReadOnlyList<MaxChatUpdate> updates,
|
||||||
bool incrementUnread,
|
bool incrementUnread,
|
||||||
bool sendPushNotifications,
|
bool sendPushNotifications,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken,
|
||||||
|
bool allowGenericMediaHistoryFetch = true)
|
||||||
{
|
{
|
||||||
if (updates.Count == 0)
|
if (updates.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -74,6 +80,7 @@ public sealed class MaxBridgeSyncService(
|
|||||||
var createdMessages = new List<(Chat Chat, Message Message)>();
|
var createdMessages = new List<(Chat Chat, Message Message)>();
|
||||||
var updatedMessages = new List<(Chat Chat, Message Message)>();
|
var updatedMessages = new List<(Chat Chat, Message Message)>();
|
||||||
var previewNotificationMessages = 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);
|
var trackedChatsByExternalId = new Dictionary<string, Chat>(StringComparer.Ordinal);
|
||||||
|
|
||||||
foreach (var update in updates)
|
foreach (var update in updates)
|
||||||
@@ -196,6 +203,14 @@ public sealed class MaxBridgeSyncService(
|
|||||||
previewNotificationMessages.Add((chat, previewResult.NotificationMessage));
|
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)
|
if (previewResult.Changed)
|
||||||
{
|
{
|
||||||
changed++;
|
changed++;
|
||||||
@@ -387,6 +402,29 @@ public sealed class MaxBridgeSyncService(
|
|||||||
await hubContext.Clients.All.SendAsync("ChatListInvalidated", cancellationToken);
|
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;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,12 +438,12 @@ public sealed class MaxBridgeSyncService(
|
|||||||
var preview = MessageTextSanitizer.CleanChatPreview(update.LastMessagePreview);
|
var preview = MessageTextSanitizer.CleanChatPreview(update.LastMessagePreview);
|
||||||
if (string.IsNullOrWhiteSpace(preview))
|
if (string.IsNullOrWhiteSpace(preview))
|
||||||
{
|
{
|
||||||
return new ListPreviewApplyResult(false, null, null);
|
return new ListPreviewApplyResult(false, null, null, false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsPresencePreview(preview))
|
if (IsPresencePreview(preview))
|
||||||
{
|
{
|
||||||
return new ListPreviewApplyResult(false, null, null);
|
return new ListPreviewApplyResult(false, null, null, false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
var previousPreview = chat.LastMessagePreview;
|
var previousPreview = chat.LastMessagePreview;
|
||||||
@@ -413,15 +451,19 @@ public sealed class MaxBridgeSyncService(
|
|||||||
var previewAt = ResolveListPreviewAt(update, previousPreviewAt ?? chat.UpdatedAt);
|
var previewAt = ResolveListPreviewAt(update, previousPreviewAt ?? chat.UpdatedAt);
|
||||||
if (chat.HistoryClearedAt is { } historyClearedAt && previewAt <= historyClearedAt)
|
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 samePreview = string.Equals(previousPreview, preview, StringComparison.Ordinal);
|
||||||
var samePreviewAt = previousPreviewAt is not null &&
|
var samePreviewAt = previousPreviewAt is not null &&
|
||||||
Math.Abs((previousPreviewAt.Value - previewAt).TotalSeconds) < 1;
|
Math.Abs((previousPreviewAt.Value - previewAt).TotalSeconds) < 1;
|
||||||
if (samePreview && samePreviewAt)
|
if (samePreview && samePreviewAt)
|
||||||
{
|
{
|
||||||
return new ListPreviewApplyResult(false, null, null);
|
return new ListPreviewApplyResult(false, null, null, shouldFetchHistory, previewAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
chat.LastMessagePreview = preview;
|
chat.LastMessagePreview = preview;
|
||||||
@@ -438,18 +480,15 @@ public sealed class MaxBridgeSyncService(
|
|||||||
var shouldNotify = shouldNotifyCandidate &&
|
var shouldNotify = shouldNotifyCandidate &&
|
||||||
RememberPreviewNotification(chat.Id, preview, previewAt);
|
RememberPreviewNotification(chat.Id, preview, previewAt);
|
||||||
|
|
||||||
if (incrementUnread && shouldNotify)
|
if (incrementUnread && shouldNotify && !isGenericMediaPreview)
|
||||||
{
|
{
|
||||||
chat.UnreadCount++;
|
chat.UnreadCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
var message = shouldNotify && !MessageTextSanitizer.IsGenericMediaLabel(preview)
|
var message = shouldNotify && !isGenericMediaPreview
|
||||||
? CreatePreviewMessage(chat, preview, previewAt)
|
? CreatePreviewMessage(chat, preview, previewAt)
|
||||||
: null;
|
: null;
|
||||||
var notificationMessage = shouldNotify && message is null && MessageTextSanitizer.IsGenericMediaLabel(preview)
|
return new ListPreviewApplyResult(true, message, null, shouldFetchHistory, previewAt);
|
||||||
? CreatePreviewMessage(chat, preview, previewAt)
|
|
||||||
: null;
|
|
||||||
return new ListPreviewApplyResult(true, message, notificationMessage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsKnownOutgoingPreviewEcho(
|
private static bool IsKnownOutgoingPreviewEcho(
|
||||||
@@ -628,7 +667,12 @@ public sealed class MaxBridgeSyncService(
|
|||||||
text.Contains("file", StringComparison.Ordinal);
|
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);
|
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);
|
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)
|
private static string PreviewExternalId(Guid chatId, string preview, DateTimeOffset previewAt)
|
||||||
{
|
{
|
||||||
var previewMinute = previewAt.ToUniversalTime().Ticks / TimeSpan.TicksPerMinute;
|
var previewMinute = previewAt.ToUniversalTime().Ticks / TimeSpan.TicksPerMinute;
|
||||||
|
|||||||
Reference in New Issue
Block a user