diff --git a/extension/css/webmail.css b/extension/css/webmail.css index eede1d13e4d..b07448fe347 100644 --- a/extension/css/webmail.css +++ b/extension/css/webmail.css @@ -29,6 +29,10 @@ outline: 0; } +.btDi4d { + position: initial !important; +} + .small #flowcrypt_secure_compose_button_icon { height: 40px; } @@ -190,8 +194,7 @@ body.cryptup_gmail .inserted div.reply_message_button { display: inline-flex; padding: 20px 12px 0 10px; opacity: 0.75; - margin-left: 18px; - margin-right: 8px; + margin: 0; height: auto; } @@ -207,7 +210,11 @@ body.cryptup_gmail div.action_menu_message_button { align-items: center; text-transform: capitalize; cursor: pointer; - padding: 4px; + padding: 8px 4px; + font-size: 14px; + font-weight: 400; + color: #1f1f1f; + line-height: 16px; } body.cryptup_gmail div.action_menu_message_button:hover { @@ -215,9 +222,9 @@ body.cryptup_gmail div.action_menu_message_button:hover { } body.cryptup_gmail div.action_menu_message_button > img { - height: 20px; - width: 20px; - padding: 0 12px; + height: auto; + width: 18px; + padding: 0 11px; object-fit: contain; } diff --git a/extension/js/content_scripts/webmail/gmail/gmail-element-replacer.ts b/extension/js/content_scripts/webmail/gmail/gmail-element-replacer.ts index 56b6979546f..36c01f418d5 100644 --- a/extension/js/content_scripts/webmail/gmail/gmail-element-replacer.ts +++ b/extension/js/content_scripts/webmail/gmail/gmail-element-replacer.ts @@ -56,8 +56,8 @@ export class GmailElementReplacer extends WebmailElementReplacer { msgInner: 'div.a3s:visible:not(.undefined), .message_inner_body:visible', msgInnerText: 'table.cf.An', msgInnerContainingPgp: "div.a3s:not(.undefined):contains('" + PgpArmor.headers('null').begin + "')", - msgActionsBtn: '.J-J5-Ji.aap', - msgActionsMenu: '.b7.J-M', + msgActionsBtn: '.Wsq5Cf', + msgActionsMenu: '.tB5Jxf-M-S5Cmsd, ul.aqdrmf-Kf[role="menu"]', attachmentsContainerOuter: 'div.hq.gt', attachmentsContainerInner: 'div.aQH', translatePrompt: '.adI, .wl4W9b', @@ -67,7 +67,7 @@ export class GmailElementReplacer extends WebmailElementReplacer { standardComposeRecipient: 'div.az9 span[email][data-hovercard-id]', numberOfAttachments: '.aVW', numberOfAttachmentsLabel: '.aVW span:first-child', - attachmentsButtons: '.aZi', + downloadAllAttachmentsButton: '.aZi, .pYTkkf-JX-ano', draftsList: '.ae4', }; @@ -297,8 +297,8 @@ export class GmailElementReplacer extends WebmailElementReplacer { return !!$('iframe.pgp_block').filter(':visible').length; }; - private addMenuButton = (replyOption: ReplyOption, gmailContextMenuBtn: string) => { - if ($(gmailContextMenuBtn).is(':visible') && !document.querySelector(`.action_${replyOption.replace('a_', '')}_message_button`)) { + private addMenuButton = (replyOption: ReplyOption, gmailContextMenuBtn: Element | null) => { + if (gmailContextMenuBtn && $(gmailContextMenuBtn).is(':visible') && !document.querySelector(`.action_${replyOption.replace('a_', '')}_message_button`)) { const button = $(this.factory.btnSecureMenuBtn(replyOption)).insertAfter(gmailContextMenuBtn); // xss-safe-factory button.on( 'click', @@ -323,7 +323,7 @@ export class GmailElementReplacer extends WebmailElementReplacer { // only replace the last one FlowCrypt reply button if does not have any buttons replaced yet, and only replace the last one for (const elem of convoReplyBtnsArr) { $(elem).addClass('inserted'); - const gmailReplyBtn = $(elem).find('.aaq.L3'); + const gmailReplyBtn = $(elem).find('.DILLkc'); const secureReplyBtn = $(this.factory.btnSecureReply()).insertAfter(gmailReplyBtn); // xss-safe-factory secureReplyBtn.addClass(gmailReplyBtn.attr('class') || ''); secureReplyBtn.off(); @@ -391,7 +391,7 @@ export class GmailElementReplacer extends WebmailElementReplacer { return; } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const messageContainer = secureReplyInvokedFromMenu ? $('.T-I-JO.T-I-Kq').closest('.h7') : $(btn.closest('.h7')!); + const messageContainer = secureReplyInvokedFromMenu ? $('.ii.gt').closest('div.h7') : $(btn.closest('.h7')!); if (messageContainer.is(':last-child')) { if (this.isEncrypted()) { await this.setReplyBoxEditable(replyOption); @@ -402,8 +402,7 @@ export class GmailElementReplacer extends WebmailElementReplacer { this.insertEncryptedReplyBox(messageContainer, replyOption); } if (secureReplyInvokedFromMenu) { - $(this.sel.msgActionsBtn).removeClass('T-I-JO T-I-Kq'); - $(this.sel.msgActionsMenu).hide(); + $(this.sel.msgActionsBtn).trigger('click'); } }; @@ -559,7 +558,7 @@ export class GmailElementReplacer extends WebmailElementReplacer { } if (nRenderedAttachments !== attachments.length) { // according to #4200, no point in showing "download all" button if at least one attachment is encrypted etc. - $(this.sel.attachmentsButtons).hide(); + $(this.sel.downloadAllAttachmentsButton).hide(); } if (nRenderedAttachments === 0) { attachmentsContainerInner.parents(this.sel.attachmentsContainerOuter).first().hide(); @@ -638,7 +637,23 @@ export class GmailElementReplacer extends WebmailElementReplacer { }; private getConvoRootEl = (anyInnerElement: HTMLElement) => { - return $(anyInnerElement).closest('div.if, div.aHU, td.Bu').first(); + const ancestorConvoRoot = $(anyInnerElement).closest('div.if, div.aHU, td.Bu'); + if (ancestorConvoRoot.length) { + return ancestorConvoRoot.first(); + } + + // New Gmail structure: conversation root might be a sibling + // Navigate to common parent container and find conversation root + const commonParent = $(anyInnerElement).closest('.nH.a98.iY, .if'); + if (commonParent.length) { + const siblingConvoRoot = commonParent.find('div.if, div.aHU, td.Bu').first(); + if (siblingConvoRoot.length) { + return siblingConvoRoot; + } + } + + // Fallback: return empty jQuery object to maintain consistent return type + return $(); }; private insertEncryptedReplyBox = (messageContainer: JQuery, replyOption: ReplyOption) => { @@ -655,7 +670,7 @@ export class GmailElementReplacer extends WebmailElementReplacer { private replaceStandardReplyBox = async (msgId?: string, force = false) => { const legacyDraftReplyRegex = new RegExp(/\[(flowcrypt|cryptup):link:draft_reply:([0-9a-fr\-]+)]/); - const newReplyBoxes = $('div.nr.tMHS5d, td.amr > div.nr, div.gA td.I5').not('.reply_message_evaluated').filter(':visible').get(); + const newReplyBoxes = $('div.nr.tMHS5d, div.gA td.I5, .amr > div.nr').not('.reply_message_evaluated').filter(':visible').get(); if (newReplyBoxes.length) { // removing this line will cause unexpected draft creation bug reappear // https://github.com/FlowCrypt/flowcrypt-browser/issues/5616#issuecomment-1972897692 @@ -946,27 +961,41 @@ export class GmailElementReplacer extends WebmailElementReplacer { * * Issue: https://github.com/FlowCrypt/flowcrypt-browser/issues/5933 */ - const messageContainer = $('.T-I-JO.T-I-Kq').closest('.h7'); + // Find the active message menu + const visibleMenu = Array.from(document.querySelectorAll(this.sel.msgActionsMenu)).find( + menu => window.getComputedStyle(menu).display !== 'none' && (menu as HTMLElement).offsetParent + ); + if (!visibleMenu) { + return; + } + + // Find the message container from the menu's position or context + const messageContainer = $('div.h7:visible').last(); // Get the last visible message container const msgIdElement = messageContainer.find('[data-legacy-message-id], [data-message-id]'); const msgId = msgIdElement.attr('data-legacy-message-id') || msgIdElement.attr('data-message-id'); - const replyAllMenuButton = document.querySelector('#r2'); + + // New Gmail uses data-action-type attributes + const replyButton = visibleMenu.querySelector('li[data-action-type="94"]'); + const forwardButton = visibleMenu.querySelector('li[data-action-type="25"]'); + const replyAllButton = visibleMenu.querySelector('li[data-action-type="24"]'); // Estimated based on pattern + // Cannot use jQuery $('#r2').is(':visible') because the element is considered invisible if its parent has display: none. - if (replyAllMenuButton && window.getComputedStyle(replyAllMenuButton).display !== 'none') { - this.addMenuButton('a_reply_all', '#r2'); + if (replyAllButton && window.getComputedStyle(replyAllButton).display !== 'none') { + this.addMenuButton('a_reply_all', replyAllButton); } else if (msgId) { try { const gmailMsg = await this.emailProvider.msgGet(msgId, 'metadata'); const replyMeta = GmailParser.determineReplyMeta(this.acctEmail, [], gmailMsg); if (replyMeta.to.length > 1) { - this.addMenuButton('a_reply_all', '#r'); + this.addMenuButton('a_reply_all', replyButton); } } catch (error) { console.error(`Failed to retrieve message metadata for ID ${msgId}:`, error); } } - this.addMenuButton('a_reply', '#r'); - this.addMenuButton('a_forward', '#r3'); + this.addMenuButton('a_reply', replyButton); + this.addMenuButton('a_forward', forwardButton); }; } diff --git a/package-lock.json b/package-lock.json index 21de99fd717..707fb74cfa9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1798,18 +1798,6 @@ "libqp": "2.1.1" } }, - "node_modules/@zone-eu/mailsplit": { - "version": "5.4.7", - "resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.7.tgz", - "integrity": "sha512-jApX86aDgolMz08pP20/J2zcns02NSK3zSiYouf01QQg4250L+GUAWSWicmS7eRvs+Z7wP7QfXrnkaTBGrIpwQ==", - "dev": true, - "license": "(MIT OR EUPL-1.1+)", - "dependencies": { - "libbase64": "1.3.0", - "libmime": "5.3.7", - "libqp": "2.1.1" - } - }, "node_modules/@zxcvbn-ts/core": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@zxcvbn-ts/core/-/core-3.0.4.tgz", diff --git a/test/source/tests/gmail.ts b/test/source/tests/gmail.ts index 070308c19a7..8c5f5bea769 100644 --- a/test/source/tests/gmail.ts +++ b/test/source/tests/gmail.ts @@ -180,7 +180,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test await gotoGmailPage(gmailPage, '/KtbxLvHkSWwbVHxgCbWNvXVKGjFgqMbGQq'); await Util.sleep(5); await gmailPage.waitAll('iframe'); - await gmailPage.waitAll(['.aZi'], { visible: true }); + await gmailPage.waitAll(['.pYTkkf-JX-ano'], { visible: true }); // attachments which contain emoji in filename are rendered correctly await gotoGmailPage(gmailPage, '/FMfcgzGtwqFGhMwWtLRjkPJlQlZHSlrW'); @@ -190,7 +190,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test const urls2 = await gmailPage.getFramesUrls(['/chrome/elements/attachment.htm']); expect(urls2.length).to.equal(2); expect(await gmailPage.waitForContent('.aVW span:first-child', '2')); - expect(await gmailPage.waitForContent('.aVW span.a2H', ' • Scanned by Gmail')); + expect(await gmailPage.waitForContent('.aVW span.a2H', '  •  Scanned by Gmail')); await gmailPage.close(); }) ); @@ -405,10 +405,10 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test testWithBrowser(async (t, browser) => { await BrowserRecipe.setUpCommonAcct(t, browser, 'ci.tests.gmail'); let gmailPage = await openGmailPage(t, browser, '/FMfcgzGkbDRNgcQxLmkhBCKVSFwkfdvV'); // plain convo - await gmailPage.waitAndClick('[data-tooltip="Reply"]', { delay: 1 }); + await gmailPage.waitAndClick('div.adn [aria-label="Reply"]', { delay: 1 }); await gmailPage.close(); gmailPage = await openGmailPage(t, browser, '/181d226b4e69f172'); // go to encrypted convo - await gmailPage.waitAndClick('[data-tooltip="Reply"]', { delay: 1 }); + await gmailPage.waitAndClick('div.adn [aria-label="Reply"]', { delay: 1 }); await gmailPage.waitTillGone('.reply_message'); await gmailPage.waitAll('[data-tooltip^="Send"]'); // The Send button from the Standard reply box await gmailPage.waitForContent( @@ -419,7 +419,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test await gmailPage.waitAll('.reply_message'); await pageHasSecureReplyContainer(t, browser, gmailPage, { isReplyPromptAccepted: true }); await gotoGmailPage(gmailPage, '/FMfcgzGkbDRNpjDdNvCrwzqvXspZZxvh'); // go to signed convo - await gmailPage.waitAndClick('[data-tooltip="Reply"]', { delay: 1 }); + await gmailPage.waitAndClick('div.adn [aria-label="Reply"]', { delay: 1 }); await gmailPage.waitTillGone('.reply_message'); await gmailPage.waitAll('[data-tooltip^="Send"]'); // The Send button from the Standard reply box await gmailPage.notPresent('.reply_message_evaluated .error_notification'); // should not show the warning about switching to encrypted reply @@ -438,7 +438,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test await gmailPage.waitAndClick('[role="listitem"] .adf.ads', { delay: 1 }); // click first message of thread await Util.sleep(3); const messages = await gmailPage.target.$$('[role="listitem"] .adn.ads'); - const plainReplyButton = await messages[0].$('[data-tooltip="Reply"]'); + const plainReplyButton = await messages[0].$('[aria-label="Reply"]'); await Util.sleep(1); await plainReplyButton!.click(); await gmailPage.waitAndClick('#switch_to_encrypted_reply'); // Switch to encrypted compose @@ -467,7 +467,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test const messages = await gmailPage.target.$$('[role="listitem"] .adn.ads'); expect(messages.length).to.equal(2); - const plainReplyButton = await messages[0].$('[data-tooltip="Reply"]'); + const plainReplyButton = await messages[0].$('[aria-label="Reply"]'); expect(plainReplyButton).to.be.ok; await Util.sleep(1); await plainReplyButton!.click(); @@ -491,11 +491,11 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test await BrowserRecipe.setUpCommonAcct(t, browser, 'ci.tests.gmail'); const gmailPage = await openGmailPage(t, browser); await gotoGmailPage(gmailPage, '/FMfcgzGkbDRNgcQxLmkhBCKVSFwkfdvV'); // plain convo - await gmailPage.waitAndClick('[data-tooltip="Reply"]', { delay: 1 }); + await gmailPage.waitAndClick('div.adn [aria-label="Reply"]', { delay: 1 }); await gotoGmailPage(gmailPage, '/FMfcgzGtwgfMhWTlgRwwKWzRhqNZzwXz'); // go to encrypted convo await Util.sleep(5); - await gmailPage.waitAndClick('.adn [data-tooltip="More"]', { delay: 1 }); - await gmailPage.waitAndClick('[act="24"]', { delay: 1 }); // click reply-all + await gmailPage.waitAndClick('.adn [aria-label="More message options"]', { delay: 1 }); + await gmailPage.waitAndClick('li[data-action-type="24"]', { delay: 1 }); // click reply-all await Util.sleep(3); await gmailPage.waitAll('[data-tooltip^="Send"]'); // The Send button from the Standard reply box await gmailPage.waitForContent( @@ -518,7 +518,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test await BrowserRecipe.setUpCommonAcct(t, browser, 'ci.tests.gmail'); const gmailPage = await openGmailPage(t, browser); await gotoGmailPage(gmailPage, '/FMfcgzQZTMHNLflWQjRcSvWlMsKbLhpr'); - await gmailPage.waitAndClick('.adn [data-tooltip="More"]', { delay: 1 }); + await gmailPage.waitAndClick('.adn [aria-label="More message options"]', { delay: 1 }); await gmailPage.waitAll('.action_reply_all_message_button'); }) ); @@ -531,8 +531,8 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test const gmailPage = await openGmailPage(t, browser); await gotoGmailPage(gmailPage, '/FMfcgzGtwgfMhWTlgRwwKWzRhqNZzwXz'); // go to encrypted convo await Util.sleep(5); - await gmailPage.waitAndClick('.adn [data-tooltip="More"]', { delay: 1 }); - await gmailPage.waitAndClick('[act="25"]', { delay: 1 }); // click forward + await gmailPage.waitAndClick('.adn [aria-label="More message options"]', { delay: 1 }); + await gmailPage.waitAndClick('li[data-action-type="25"]', { delay: 1 }); // click forward await Util.sleep(3); await gmailPage.waitAll('[data-tooltip^="Send"]'); // The Send button from the Standard reply box await gmailPage.waitForContent( @@ -556,7 +556,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test await BrowserRecipe.setUpCommonAcct(t, browser, 'ci.tests.gmail'); const gmailPage = await openGmailPage(t, browser); await gotoGmailPage(gmailPage, '/FMfcgzGtwgfMhWTlgRwwKWzRhqNZzwXz'); // go to encrypted convo - const gmailContextMenu = '.J-J5-Ji.aap'; + const gmailContextMenu = '.adn [aria-label="More message options"]'; await gmailPage.waitAndClick(gmailContextMenu); await Util.sleep(1); expect(await gmailPage.isElementPresent('@action-reply-message-button')); @@ -589,7 +589,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test const threadId = '181d226b4e69f172'; // 1st message -- thread id const gmailPage = await openGmailPage(t, browser, `/${threadId}`); await GmailPageRecipe.trimConvo(gmailPage, threadId); - await gmailPage.waitAndClick('[data-tooltip="Reply"]', { delay: 5 }); + await gmailPage.waitAndClick('div.adn [aria-label="Reply"]', { delay: 5 }); t.timeout(minutes(2)); // extend ava's timeout await Util.sleep(5); await gmailPage.waitTillFocusIsIn('div[aria-label="Message Body"]', { timeout: 10 }); @@ -621,7 +621,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test await Util.sleep(5); await gmailPage.waitAll('iframe'); expect(await gmailPage.isElementPresent('@container-attachments')).to.equal(false); - await gmailPage.waitAll(['.aZi'], { visible: false }); + await gmailPage.waitAll(['.pYTkkf-JX-ano'], { visible: false }); expect(await gmailPage.isElementVisible('.aQH')).to.equal(false); // original attachment container(s) should be hidden await gmailPage.close(); }) @@ -642,7 +642,7 @@ export const defineGmailTests = (testVariant: TestVariant, testWithBrowser: Test await Util.sleep(5); await gmailPage.waitAll('iframe'); expect(await gmailPage.isElementPresent('@container-attachments')).to.equal(false); - await gmailPage.waitAll(['.aZi'], { visible: false }); + await gmailPage.waitAll(['.pYTkkf-JX-ano'], { visible: false }); await gmailPage.close(); }) ); diff --git a/test/source/tests/setup.ts b/test/source/tests/setup.ts index f0b40120f9f..ddea1b21bbf 100644 --- a/test/source/tests/setup.ts +++ b/test/source/tests/setup.ts @@ -853,6 +853,7 @@ AN8G3r5Htj8olot+jm9mIa5XLXWzMNUZgg== await addKeyPopup.waitAndClick('.action_add_private_key', { delay: 1 }); await Util.sleep(1); await gmailPage.page.reload(); + await Util.sleep(3); await gmailPage.waitTillGone('@webmail-notification-notify_expiring_keys'); // remove added key and observe warning appears again await settingsPage.waitAndClick('@action-remove-key-1');