Stop repeated preview push notifications

This commit is contained in:
sevenhill
2026-07-04 13:23:23 +03:00
parent 459ac2bb82
commit b7241dd0df
@@ -5,6 +5,7 @@ using QMax.Api.Data.Entities;
using QMax.Api.Infrastructure.Hubs;
using QMax.Api.Infrastructure.Max;
using QMax.Api.Infrastructure.Storage;
using System.Collections.Concurrent;
using System.Text;
namespace QMax.Api.Services;
@@ -15,6 +16,8 @@ public sealed class MaxBridgeSyncService(
IHubContext<QMaxHub> hubContext,
ILogger<MaxBridgeSyncService> logger)
{
private static readonly ConcurrentDictionary<string, DateTimeOffset> PreviewNotifications = new();
public async Task<int> SyncOnceAsync(CancellationToken cancellationToken)
{
try
@@ -268,7 +271,7 @@ public sealed class MaxBridgeSyncService(
var previousPreview = chat.LastMessagePreview;
var previousPreviewAt = chat.LastMessageAt;
var previewAt = ResolveListPreviewAt(update);
var previewAt = ResolveListPreviewAt(update, previousPreviewAt ?? chat.UpdatedAt);
var samePreview = string.Equals(previousPreview, preview, StringComparison.Ordinal);
var samePreviewAt = previousPreviewAt is not null &&
Math.Abs((previousPreviewAt.Value - previewAt).TotalSeconds) < 1;
@@ -281,11 +284,13 @@ public sealed class MaxBridgeSyncService(
chat.LastMessageAt = previewAt;
chat.UpdatedAt = DateTimeOffset.UtcNow;
var shouldNotify = !chatWasCreated &&
var shouldNotifyCandidate = !chatWasCreated &&
!string.IsNullOrWhiteSpace(previousPreview) &&
update.LastMessageIsOutgoing != true &&
update.Messages.Count == 0 &&
IsFreshListPreview(update, previewAt);
var shouldNotify = shouldNotifyCandidate &&
RememberPreviewNotification(chat.Id, preview, previewAt);
if (incrementUnread && shouldNotify)
{
@@ -313,16 +318,36 @@ public sealed class MaxBridgeSyncService(
return true;
}
private static DateTimeOffset ResolveListPreviewAt(MaxChatUpdate update)
private static bool RememberPreviewNotification(Guid chatId, string preview, DateTimeOffset previewAt)
{
var now = DateTimeOffset.UtcNow;
if (PreviewNotifications.Count > 4096)
{
var cutoff = now.AddHours(-2);
foreach (var item in PreviewNotifications)
{
if (item.Value < cutoff)
{
PreviewNotifications.TryRemove(item.Key, out _);
}
}
}
var previewMinute = previewAt.ToUniversalTime().Ticks / TimeSpan.TicksPerMinute;
var key = $"{chatId:N}|{previewMinute}|{preview.Trim().ToLowerInvariant()}";
return PreviewNotifications.TryAdd(key, now);
}
private static DateTimeOffset ResolveListPreviewAt(MaxChatUpdate update, DateTimeOffset? fallback = null)
{
return TryParseMoscowListTime(update.LastMessageTimeText, out var parsed)
? parsed
: update.LastMessageAt ?? update.UpdatedAt;
: fallback ?? (update.Messages.Count > 0 ? update.LastMessageAt : null) ?? update.UpdatedAt;
}
private static bool IsFreshListPreview(MaxChatUpdate update, DateTimeOffset previewAt)
{
if (string.IsNullOrWhiteSpace(update.LastMessageTimeText))
if (!HasListClockTime(update.LastMessageTimeText))
{
return false;
}
@@ -331,6 +356,11 @@ public sealed class MaxBridgeSyncService(
return previewAt >= now.AddMinutes(-15) && previewAt <= now.AddMinutes(10);
}
private static bool HasListClockTime(string? value)
{
return System.Text.RegularExpressions.Regex.IsMatch(value?.Trim() ?? "", @"\b\d{1,2}:\d{2}\b");
}
private static bool TryParseMoscowListTime(string? value, out DateTimeOffset parsed)
{
parsed = default;