diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 1fe5d5102e5..43714b8d68c 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -61,7 +61,7 @@ blocks: run: when: branch = 'master' OR branch =~ 'live-test' OR branch =~ 'gmail-test' execution_time_limit: - minutes: 45 + minutes: 60 task: secrets: - name: flowcrypt-browser-ci-secrets diff --git a/extension/img/svgs/reply-all-icon.svg b/extension/img/svgs/reply-all-icon.svg new file mode 100644 index 00000000000..30828ebd030 --- /dev/null +++ b/extension/img/svgs/reply-all-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extension/js/common/xss-safe-factory.ts b/extension/js/common/xss-safe-factory.ts index 26a97152388..c5da95a3685 100644 --- a/extension/js/common/xss-safe-factory.ts +++ b/extension/js/common/xss-safe-factory.ts @@ -269,9 +269,15 @@ export class XssSafeFactory { `; }; - public actionsMenuBtn = (action: 'reply' | 'forward') => { - return `
- secure ${action} + public btnSecureMenuBtn = (replyOption: ReplyOption) => { + const actionText = replyOption.replace('a_', '').replace('_', ' '); + const action = { + underscore: actionText.replace(' ', '_'), + hyphen: actionText.replace(' ', '-'), + }; + // * The action_${action.underscore}_message_button is used as an identifier in GmailElementReplacer.actionActivateSecureReplyHandler() + return `
+ secure ${actionText}
`; }; diff --git a/extension/js/content_scripts/webmail/generic/webmail-element-replacer.ts b/extension/js/content_scripts/webmail/generic/webmail-element-replacer.ts index 044e101f120..db51b273ac9 100644 --- a/extension/js/content_scripts/webmail/generic/webmail-element-replacer.ts +++ b/extension/js/content_scripts/webmail/generic/webmail-element-replacer.ts @@ -14,7 +14,6 @@ export abstract class WebmailElementReplacer { public abstract reinsertReplyBox: (replyMsgId: string) => void; public abstract scrollToReplyBox: (replyMsgId: string) => void; public abstract scrollToCursorInReplyBox: (replyMsgId: string, cursorOffsetTop: number) => void; - public abstract addSecureActionsToMessageMenu: () => void; public runIntervalFunctionsPeriodically = () => { const intervalFunctions = this.getIntervalFunctions(); 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 2d2a512f7ce..379a17104d2 100644 --- a/extension/js/content_scripts/webmail/gmail/gmail-element-replacer.ts +++ b/extension/js/content_scripts/webmail/gmail/gmail-element-replacer.ts @@ -79,6 +79,7 @@ export class GmailElementReplacer extends WebmailElementReplacer { super(); this.webmailCommon = new WebmailCommon(acctEmail, injector); this.pubLookup = new PubLookup(clientConfiguration); + this.setupSecureActionsOnGmailMenu(); } public getIntervalFunctions = (): IntervalFunction[] => { @@ -148,14 +149,14 @@ export class GmailElementReplacer extends WebmailElementReplacer { } }; - public addSecureActionsToMessageMenu = () => { - $(document).on('click', 'div.aHU.hx', event => { - const $actionsBtn = $(event.currentTarget).find(this.sel.msgActionsBtn); - if ($actionsBtn.length && !$('.action_menu_message_button').length) { - this.addMenuButton('reply', '#r'); - this.addMenuButton('forward', '#r3'); + public setupSecureActionsOnGmailMenu = () => { + const observer = new MutationObserver(() => { + const gmailActionsMenu = document.querySelector(this.sel.msgActionsMenu); + if (gmailActionsMenu && (gmailActionsMenu as HTMLElement).offsetParent !== undefined) { + this.addSecureActionsToMessageMenu(); } }); + observer.observe(document.body, { childList: true, subtree: true }); }; private everything = () => { @@ -291,13 +292,14 @@ export class GmailElementReplacer extends WebmailElementReplacer { return !!$('iframe.pgp_block').filter(':visible').length; }; - private addMenuButton = (action: 'reply' | 'forward', selector: string) => { - const gmailActionsMenuContainer = $(this.sel.msgActionsMenu).find(selector); - const button = $(this.factory.actionsMenuBtn(action)).insertAfter(gmailActionsMenuContainer); // xss-safe-factory - button.on( - 'click', - Ui.event.handle((el, ev: JQuery.Event) => this.actionActivateSecureReplyHandler(el, ev)) - ); + private addMenuButton = (replyOption: ReplyOption, gmailContextMenuBtn: string) => { + if ($(gmailContextMenuBtn).is(':visible')) { + const button = $(this.factory.btnSecureMenuBtn(replyOption)).insertAfter(gmailContextMenuBtn); // xss-safe-factory + button.on( + 'click', + Ui.event.handle((el, ev: JQuery.Event) => this.actionActivateSecureReplyHandler(el, ev)) + ); + } }; private replaceConvoBtns = (force = false) => { @@ -371,7 +373,14 @@ export class GmailElementReplacer extends WebmailElementReplacer { private actionActivateSecureReplyHandler = async (btn: HTMLElement, event: JQuery.Event) => { event.stopImmediatePropagation(); const secureReplyInvokedFromMenu = btn.className.includes('action_menu_message_button'); - const replyOption: ReplyOption = btn.className.includes('reply') ? 'a_reply' : 'a_forward'; + let replyOption: ReplyOption; + if (btn.className.includes('reply-all')) { + replyOption = 'a_reply_all'; + } else if (btn.className.includes('forward')) { + replyOption = 'a_forward'; + } else { + replyOption = 'a_reply'; + } if ($('#switch_to_encrypted_reply').length) { $('#switch_to_encrypted_reply').trigger('click'); return; @@ -922,4 +931,13 @@ export class GmailElementReplacer extends WebmailElementReplacer { } } }; + + private addSecureActionsToMessageMenu = () => { + if ($('.action_menu_message_button').length) { + return; + } + this.addMenuButton('a_reply', '#r'); + this.addMenuButton('a_reply_all', '#r2'); + this.addMenuButton('a_forward', '#r3'); + }; } diff --git a/extension/js/content_scripts/webmail/gmail/gmail-webmail-startup.ts b/extension/js/content_scripts/webmail/gmail/gmail-webmail-startup.ts index a6992cba791..583af380062 100644 --- a/extension/js/content_scripts/webmail/gmail/gmail-webmail-startup.ts +++ b/extension/js/content_scripts/webmail/gmail/gmail-webmail-startup.ts @@ -45,7 +45,6 @@ export class GmailWebmailStartup { const messageRenderer = await MessageRenderer.newInstance(acctEmail, new Gmail(acctEmail), relayManager, factory); this.replacer = new GmailElementReplacer(factory, clientConfiguration, acctEmail, messageRenderer, injector, notifications, relayManager); await notifications.showInitial(acctEmail); - this.replacer.addSecureActionsToMessageMenu(); this.replacer.runIntervalFunctionsPeriodically(); }; diff --git a/extension/manifest.json b/extension/manifest.json index 8c57be8e8c8..176223d2b23 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -78,6 +78,7 @@ "resources": [ "/css/webmail.css", "/img/svgs/reply-icon.svg", + "/img/svgs/reply-all-icon.svg", "/img/svgs/forward-icon.svg", "/img/svgs/spinner-white-small.svg", "/img/svgs/spinner-green-small.svg", diff --git a/test/source/tests/gmail.ts b/test/source/tests/gmail.ts index cdce0c650b8..e046509cac6 100644 --- a/test/source/tests/gmail.ts +++ b/test/source/tests/gmail.ts @@ -512,22 +512,25 @@ 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 - await Util.sleep(5); - const actionsMenuSelector = '.J-J5-Ji.aap'; - await gmailPage.waitAndClick(actionsMenuSelector); - await Util.sleep(3); + const gmailContextMenu = '.J-J5-Ji.aap'; + await gmailPage.waitAndClick(gmailContextMenu); + await Util.sleep(1); expect(await gmailPage.isElementPresent('@action-reply-message-button')); await gmailPage.waitAndClick('@action-reply-message-button'); const replyBox = await gmailPage.getFrame(['/chrome/elements/compose.htm'], { sleep: 5 }); - await Util.sleep(3); await replyBox.waitForContent('@input-body', ''); - await gmailPage.waitAndClick(actionsMenuSelector); - await Util.sleep(3); + await gmailPage.waitAndClick(gmailContextMenu); + await Util.sleep(1); + expect(await gmailPage.isElementPresent('@action-reply-all-message-button')); + await gmailPage.waitAndClick('@action-reply-all-message-button'); + const replyBox2 = await gmailPage.getFrame(['/chrome/elements/compose.htm'], { sleep: 5 }); + await replyBox2.waitForContent('@input-body', ''); + await gmailPage.waitAndClick(gmailContextMenu); + await Util.sleep(1); expect(await gmailPage.isElementPresent('@action-forward-message-button')); await gmailPage.waitAndClick('@action-forward-message-button'); - const replyBox2 = await gmailPage.getFrame(['/chrome/elements/compose.htm'], { sleep: 5 }); - await Util.sleep(3); - await replyBox2.waitForContent('@input-body', '---------- Forwarded message ---------'); + const replyBox3 = await gmailPage.getFrame(['/chrome/elements/compose.htm'], { sleep: 5 }); + await replyBox3.waitForContent('@input-body', '---------- Forwarded message ---------'); }) );