Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions extension/css/webmail.css
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,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;
}

Expand All @@ -208,16 +207,19 @@ body.cryptup_gmail div.action_menu_message_button {
text-transform: capitalize;
cursor: pointer;
padding: 4px;
font-size: 14px;
font-weight: 400;
color: #1f1f1f;
}

body.cryptup_gmail div.action_menu_message_button:hover {
background-color: #eee; /* mimic Gmail hover. uses exact color pallete from Gmail */
}

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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
};

Expand Down Expand Up @@ -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',
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand All @@ -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');
}
};

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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<Element>, replyOption: ReplyOption) => {
Expand All @@ -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
Expand Down Expand Up @@ -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);
};
}
12 changes: 0 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 18 additions & 18 deletions test/source/tests/gmail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@
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');
Expand All @@ -190,7 +190,7 @@
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();
})
);
Expand Down Expand Up @@ -405,10 +405,10 @@
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(
Expand All @@ -419,7 +419,7 @@
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
Expand All @@ -438,7 +438,7 @@
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
Expand Down Expand Up @@ -467,7 +467,7 @@
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();
Expand All @@ -485,17 +485,17 @@
})
);

test(
test.only(
'mail.google.com - plain reply with dot menu',
testWithBrowser(async (t, browser) => {
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="25"]', { 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(
Expand All @@ -518,7 +518,7 @@
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');
})
);
Expand All @@ -531,8 +531,8 @@
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(
Expand All @@ -556,7 +556,7 @@
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'));
Expand Down Expand Up @@ -589,7 +589,7 @@
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 });
Expand Down Expand Up @@ -621,7 +621,7 @@
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();
})
Expand All @@ -642,7 +642,7 @@
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();
})
);
Expand Down
Loading