Fix MAX attachment send and self push echoes
This commit is contained in:
@@ -130,23 +130,28 @@ public sealed class MaxBridgeSyncService(
|
||||
var previewHint = MessageTextSanitizer.CleanChatPreview(update.LastMessagePreview);
|
||||
var shouldLoadLastKnownMessage = !chatWasCreated &&
|
||||
!string.IsNullOrWhiteSpace(previewHint) &&
|
||||
!string.IsNullOrWhiteSpace(chat.LastMessagePreview) &&
|
||||
string.Equals(MessageTextSanitizer.CleanChatPreview(chat.LastMessagePreview), previewHint, StringComparison.Ordinal) &&
|
||||
update.LastMessageIsOutgoing != true &&
|
||||
update.Messages.Count == 0 &&
|
||||
HasListClockTime(update.LastMessageTimeText);
|
||||
List<LastKnownMessageSnapshot> lastKnownMessages = !shouldLoadLastKnownMessage
|
||||
var lastKnownOutgoingMessages = !shouldLoadLastKnownMessage
|
||||
? []
|
||||
: await db.Messages
|
||||
.AsNoTracking()
|
||||
.Include(x => x.Attachments)
|
||||
.Where(x =>
|
||||
x.ChatId == chat.Id &&
|
||||
x.DeletedAt == null &&
|
||||
x.Direction == MessageDirection.Outgoing &&
|
||||
x.Text == previewHint)
|
||||
.Select(x => new LastKnownMessageSnapshot(x.Direction, x.Text, x.SentAt))
|
||||
x.Direction == MessageDirection.Outgoing)
|
||||
.OrderByDescending(x => x.SentAt)
|
||||
.Take(5)
|
||||
.ToListAsync(cancellationToken);
|
||||
var lastKnownMessage = lastKnownMessages
|
||||
.OrderByDescending(x => x.SentAt)
|
||||
var lastKnownMessage = lastKnownOutgoingMessages
|
||||
.Select(x => new LastKnownMessageSnapshot(
|
||||
x.Direction,
|
||||
x.Text,
|
||||
LastMessagePreview(x.Text, x.Attachments),
|
||||
x.Attachments.Count > 0,
|
||||
x.SentAt))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (ApplyListPreview(
|
||||
@@ -349,14 +354,15 @@ public sealed class MaxBridgeSyncService(
|
||||
DateTimeOffset previewAt)
|
||||
{
|
||||
if (lastKnownMessage is null ||
|
||||
lastKnownMessage.Direction != MessageDirection.Outgoing ||
|
||||
string.IsNullOrWhiteSpace(lastKnownMessage.Text))
|
||||
lastKnownMessage.Direction != MessageDirection.Outgoing)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var lastText = MessageTextSanitizer.CleanChatPreview(lastKnownMessage.Text);
|
||||
if (!string.Equals(lastText, preview, StringComparison.Ordinal))
|
||||
var lastPreview = MessageTextSanitizer.CleanChatPreview(lastKnownMessage.Preview ?? lastKnownMessage.Text);
|
||||
var previewMatches = string.Equals(lastPreview, preview, StringComparison.Ordinal) ||
|
||||
lastKnownMessage.HasAttachments && MessageTextSanitizer.IsGenericMediaLabel(preview);
|
||||
if (!previewMatches)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -810,5 +816,7 @@ public sealed class MaxBridgeSyncService(
|
||||
private sealed record LastKnownMessageSnapshot(
|
||||
MessageDirection Direction,
|
||||
string? Text,
|
||||
string? Preview,
|
||||
bool HasAttachments,
|
||||
DateTimeOffset SentAt);
|
||||
}
|
||||
|
||||
@@ -666,6 +666,77 @@ public sealed class ApiSmokeTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SyncDoesNotPushListPreviewEchoOfOwnOutgoingAttachment()
|
||||
{
|
||||
var pushNotifications = new RecordingPushNotificationService();
|
||||
var bridge = new OutgoingPreviewEchoMaxBridgeClient(
|
||||
$"mock-self-echo-attachment-chat-{Guid.NewGuid():N}",
|
||||
"\u0424\u043e\u0442\u043e");
|
||||
using var factory = _factory.WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.ConfigureTestServices(services =>
|
||||
{
|
||||
services.RemoveAll<IHostedService>();
|
||||
services.RemoveAll<IMaxBridgeClient>();
|
||||
services.RemoveAll<IPushNotificationService>();
|
||||
services.AddSingleton<IMaxBridgeClient>(bridge);
|
||||
services.AddSingleton<IPushNotificationService>(pushNotifications);
|
||||
});
|
||||
});
|
||||
using var client = factory.CreateClient();
|
||||
await LoginAsync(client);
|
||||
|
||||
Guid chatId;
|
||||
var sentAt = DateTimeOffset.UtcNow.AddSeconds(-20);
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<QMaxDbContext>();
|
||||
var chat = new Chat
|
||||
{
|
||||
ExternalId = bridge.ChatExternalId,
|
||||
Title = "Self Attachment Echo",
|
||||
LastMessagePreview = "\u041c\u0435\u0434\u0438\u0430",
|
||||
LastMessageAt = sentAt,
|
||||
UnreadCount = 0
|
||||
};
|
||||
var message = new Message
|
||||
{
|
||||
Chat = chat,
|
||||
ExternalId = "self-echo-attachment-message",
|
||||
Direction = MessageDirection.Outgoing,
|
||||
SentAt = sentAt,
|
||||
DeliveryState = MessageDeliveryState.Sent
|
||||
};
|
||||
message.Attachments.Add(new MessageAttachment
|
||||
{
|
||||
Message = message,
|
||||
OriginalFileName = "sent-photo.jpg",
|
||||
StorageFileName = "sent-photo.jpg",
|
||||
ContentType = "image/jpeg",
|
||||
FileSizeBytes = 1024,
|
||||
Kind = AttachmentKind.Image
|
||||
});
|
||||
db.Chats.Add(chat);
|
||||
db.Messages.Add(message);
|
||||
await db.SaveChangesAsync();
|
||||
chatId = chat.Id;
|
||||
}
|
||||
|
||||
var sync = await client.PostAsync("/api/max/sync", null);
|
||||
await AssertStatusAsync(HttpStatusCode.OK, sync);
|
||||
|
||||
Assert.Empty(pushNotifications.SentMessages);
|
||||
using (var scope = factory.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<QMaxDbContext>();
|
||||
var chat = await db.Chats.FindAsync(chatId);
|
||||
Assert.NotNull(chat);
|
||||
Assert.Equal(0, chat!.UnreadCount);
|
||||
Assert.Equal("\u041c\u0435\u0434\u0438\u0430", chat.LastMessagePreview);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FirebasePushSkipsOutgoingMessages()
|
||||
{
|
||||
|
||||
+125
-18
@@ -2610,7 +2610,6 @@ async function clearSidebarSearch(p) {
|
||||
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
}
|
||||
}).catch(() => {});
|
||||
await p.keyboard.press("Escape").catch(() => {});
|
||||
await p.waitForTimeout(150);
|
||||
}
|
||||
|
||||
@@ -2830,7 +2829,7 @@ async function openDomChatByExternalId(p, externalChatId) {
|
||||
const rect = match.button.getBoundingClientRect();
|
||||
return {
|
||||
opened: false,
|
||||
x: Math.round(rect.left + Math.min(rect.width - 8, Math.max(8, rect.width * 0.35))),
|
||||
x: Math.round(rect.left + Math.min(rect.width - 8, Math.max(8, rect.width * 0.50))),
|
||||
y: Math.round(rect.top + rect.height / 2)
|
||||
};
|
||||
}, {
|
||||
@@ -3659,7 +3658,13 @@ async function uploadAttachmentFile(p, path, caption) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await waitForAttachmentPreview(p, 8000);
|
||||
await p.waitForTimeout(2000);
|
||||
const previewReady = await waitForAttachmentPreview(p, 10000);
|
||||
console.info(`[send/attachment] file selected; previewReady=${previewReady} url=${p.url()}`);
|
||||
if (!previewReady) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (caption.trim()) {
|
||||
await fillComposerText(p, caption.trim(), false);
|
||||
await p.waitForTimeout(300);
|
||||
@@ -3731,6 +3736,90 @@ async function attachFileViaUploadButton(p, filePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const fileChooserPromise = p.waitForEvent("filechooser", { timeout: 800 }).catch(() => null);
|
||||
await p.mouse.click(point.x, point.y);
|
||||
const fileChooser = await fileChooserPromise;
|
||||
if (fileChooser) {
|
||||
await fileChooser.setFiles(filePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
return await attachFileViaMenuItem(p, filePath);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function attachFileViaMenuItem(p, filePath) {
|
||||
const preferMedia = /\.(jpe?g|png|webp|gif|heic|mp4|mov|webm)$/i.test(filePath);
|
||||
const deadline = Date.now() + 3500;
|
||||
let point = null;
|
||||
while (Date.now() < deadline && !point) {
|
||||
point = await p.evaluate(({ preferMedia }) => {
|
||||
const isVisible = (element) => {
|
||||
if (!(element instanceof HTMLElement)) return false;
|
||||
const style = window.getComputedStyle(element);
|
||||
const rect = element.getBoundingClientRect();
|
||||
return style.visibility !== "hidden" &&
|
||||
style.display !== "none" &&
|
||||
rect.width > 0 &&
|
||||
rect.height > 0 &&
|
||||
rect.bottom >= 0 &&
|
||||
rect.right >= 0 &&
|
||||
rect.top <= window.innerHeight &&
|
||||
rect.left <= window.innerWidth;
|
||||
};
|
||||
const cleanText = (value) => String(value || "")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
const menuItems = Array.from(document.querySelectorAll("[role='menuitem'], button[class*='actionsMenuItem' i]"))
|
||||
.filter((element) => !element.disabled && isVisible(element))
|
||||
.filter((element) => !element.closest?.("[data-testid='composer'], [class*='composer' i]"))
|
||||
.map((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const text = cleanText([
|
||||
element.getAttribute("aria-label"),
|
||||
element.getAttribute("title"),
|
||||
element.getAttribute("data-testid"),
|
||||
element.className,
|
||||
element.innerText,
|
||||
element.textContent
|
||||
].join(" "));
|
||||
const media = /photo|video|media|\u0444\u043e\u0442\u043e|\u0432\u0438\u0434\u0435\u043e/.test(text);
|
||||
const file = /file|\u0444\u0430\u0439\u043b/.test(text);
|
||||
return { element, rect, text, media, file };
|
||||
})
|
||||
.filter((item) => item.media || item.file)
|
||||
.sort((a, b) => {
|
||||
const aPreferred = preferMedia ? a.media : a.file;
|
||||
const bPreferred = preferMedia ? b.media : b.file;
|
||||
return Number(bPreferred) - Number(aPreferred) ||
|
||||
a.rect.top - b.rect.top ||
|
||||
a.rect.left - b.rect.left;
|
||||
});
|
||||
const item = menuItems[0]?.element || null;
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rect = item.getBoundingClientRect();
|
||||
return {
|
||||
x: Math.round(rect.left + rect.width / 2),
|
||||
y: Math.round(rect.top + rect.height / 2)
|
||||
};
|
||||
}, { preferMedia }).catch(() => null);
|
||||
|
||||
if (!point) {
|
||||
await p.waitForTimeout(100);
|
||||
}
|
||||
}
|
||||
|
||||
if (!point) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const fileChooserPromise = p.waitForEvent("filechooser", { timeout: 5000 }).catch(() => null);
|
||||
await p.mouse.click(point.x, point.y);
|
||||
@@ -3767,6 +3856,31 @@ async function waitForAttachmentPreview(p, timeoutMs = 8000) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return rect.left > window.innerWidth * 0.30 && rect.top > window.innerHeight * 0.45;
|
||||
};
|
||||
const isPendingUploadSurface = (element) => {
|
||||
if (element.closest?.("aside, [class*='messageWrapper' i], [class*='bubble' i], [class*='message--' i], [role='listitem']")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const surface = element.closest?.([
|
||||
"[data-testid='composer']",
|
||||
"[class*='composer' i]",
|
||||
"[role='dialog']",
|
||||
"[class*='modal' i]",
|
||||
"[class*='preview' i]",
|
||||
"[class*='attach' i]",
|
||||
"[class*='upload' i]",
|
||||
"[class*='file' i]",
|
||||
"[class*='media' i]"
|
||||
].join(","));
|
||||
if (surface) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
return rect.left > window.innerWidth * 0.38 &&
|
||||
rect.top > window.innerHeight * 0.55 &&
|
||||
rect.bottom > window.innerHeight * 0.70;
|
||||
};
|
||||
const previewSelectors = [
|
||||
"[class*='preview' i]",
|
||||
"[class*='attach' i]",
|
||||
@@ -3778,24 +3892,17 @@ async function waitForAttachmentPreview(p, timeoutMs = 8000) {
|
||||
];
|
||||
const hasPreview = previewSelectors
|
||||
.flatMap((selector) => Array.from(document.querySelectorAll(selector)))
|
||||
.some((element) => isVisible(element) && bottomArea(element));
|
||||
const sendButton = Array.from(document.querySelectorAll("button, [role='button']"))
|
||||
.some((button) => {
|
||||
if (button.disabled || !isVisible(button) || !bottomArea(button)) {
|
||||
.some((element) => isVisible(element) && bottomArea(element) && isPendingUploadSurface(element));
|
||||
const hasFileName = Array.from(document.querySelectorAll("div, span, p"))
|
||||
.some((element) => {
|
||||
if (!isVisible(element) || !bottomArea(element) || !isPendingUploadSurface(element)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const text = [
|
||||
button.getAttribute("aria-label"),
|
||||
button.getAttribute("title"),
|
||||
button.getAttribute("data-testid"),
|
||||
button.className,
|
||||
button.innerText,
|
||||
button.textContent
|
||||
].join(" ").toLowerCase();
|
||||
return /send|submit|\u043e\u0442\u043f\u0440\u0430\u0432/.test(text);
|
||||
const text = String(element.innerText || element.textContent || "").trim();
|
||||
return /\.(jpe?g|png|webp|gif|heic|mp4|mov|webm|mp3|ogg|m4a|wav|pdf|docx?|xlsx?|zip|rar|7z)\b/i.test(text);
|
||||
});
|
||||
return hasPreview || sendButton;
|
||||
return hasPreview || hasFileName;
|
||||
}).catch(() => false);
|
||||
|
||||
if (ready) {
|
||||
@@ -3805,7 +3912,7 @@ async function waitForAttachmentPreview(p, timeoutMs = 8000) {
|
||||
await p.waitForTimeout(250);
|
||||
}
|
||||
|
||||
console.warn("[send/attachment] attachment preview was not detected before sending; continuing with send attempt");
|
||||
console.warn("[send/attachment] attachment preview was not detected before sending");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user