Does the New Gmail mean an end of using InboxSDK?

259 views
Skip to first unread message

Lou Wynn

unread,
Aug 4, 2018, 12:33:43 AM8/4/18
to InboxSDK
My use case of the InboxSDK in a short sentence is performing end-to-end encryption, which requires decrypting and displaying a message at the extension.

One of the biggest problems I have so far for the InboxSDK in the New Gmail is multiple firing of the ThreadView handler. After composing a new message, if I click on the "View message" link in the "Message sent" notification, the thread view handler fires at least twice, which triggers the decrypting process every time.

What makes it worse is unpredictable re-display of the message, which I described in this issue: https://groups.google.com/forum/#!topic/inboxsdk/A7kcj7wYRM8. When a message is re-displayed, the original ciphertext is used. I use a mutation observer to listen to the node change, so I can decrypt the ciphertext and display the plaintext back again.

What makes it worst is the inconsistent behavior of the thread view reached through different links. There are at least the following ways to get a thread view, which I'll call context below:

1. After composing a new message and sending it out, clicking on the "View message" link in the "Message sent" notification to read the message just sent.
2. Clicking a thread in the Inbox list.
3. Clicking a thread in the Sent list.

I have to observe different nodes in different contexts when using the mutation observer. In the first context, I have to observe

 <div class="nH" style role="main">

otherwise, the multiple display issue will override decrypted plaintext. However, this freezes the page in the second context.

I need to observe

<div class="nH" role="list" style> (or "nH bh")

in order not to freeze the page in the second context, but it does not work for the first context because the first context at least re-displays the message three times and my mutation observer doesn't respond to the last display, which reveals that a child tree is cut off between the two above divs. I attached a screenshot to see where the two divs are and the InboxSDK insertion of thread id.


I'm stuck now on the New Gmail with InboxSDK. The Classic Gmail doesn't have these issues.

Any advice for me if I'd like to continue to use InboxSDK for my project?

Chris Cowan

unread,
Aug 6, 2018, 8:58:38 PM8/6/18
to InboxSDK
We're continually updating the InboxSDK to stay compatible with Gmail. We fully intend to stay compatible with Gmail's redesign.

Thanks for reporting the issue of the thread view handler being called twice for an individual thread! We'll address that issue.

> However, this freezes the page in the second context.

Does the following describe your extension?: In your message view handler, you are modifying the message elements, and then using a MutationObserver to track when Gmail changes the element so you can re-apply some changes. If that's what you're doing, then I think you might have a bug that your own modifications are triggering your MutationObserver, causing you to make new modifications, which trigger your MutationObserver again. You may want to set a special class name in elements that you modify which your mutation handler detects and does nothing if it finds the class name. If that's not what you're doing, then you should try using dev tools to pause js execution while the page is frozen, and see if your code is involved.

You may also want to disconnect your MutationObserver from the thread view when the thread view emits its destroy event. This prevents your mutation callback from firing after the user browses away from the thread, which it could if Gmail re-uses the DOM element. It will also stop your original mutation callback from firing after the second thread view handler is called. (When the thread view handler is being called twice, what's actually happening is that we detect an element on the page as a thread, we create a ThreadView object, we call the registered thread view handlers for it, and then some time later, Gmail makes some change to the page which causes the InboxSDK to temporarily fail to recognize it as still being a thread, so the existing ThreadView object emits a destroy event just as if the user browsed away from the thread, and then we re-detect the thread element, create a new ThreadView object, and then call the registered thread view handlers for it as if the user just browsed to the thread.)

sdk.Conversations.registerThreadViewHandler(threadView => {
const mutationObserver = new MutationObserver(...);
mutationObserver.observe(...);
threadView.on('destroy', () => {
mutationObserver.disconnect();
});
});


Lou Wynn

unread,
Aug 9, 2018, 7:37:38 PM8/9/18
to InboxSDK
Hi Chris,

Your description of my extension is correct. I did use a guard to check if I've changed some element in the mutation observer.

I started with a minimum piece of code that has InboxSDK and the mutation observer and added more code by pieces. It turned out that the freezing was not caused by InboxSDK, nor my mutation observer. I don't quite understand the reason. Here's the working code of my observer:

function isMessageNode(msgEle:HTMLElement): boolean {
 if (! msgEle.innerText) return false;

  if (isPrimaryMessage(msgEle)
   || msgEle.className === "gmail_quote" // quoted message
 ) {
     return true;
 }
 return false;
}

// Clapsed message: className="adf ads" or div class "iA g6"
// Expanded message: "adn ads", its sibling is reply div
function isPrimaryMessage(msgEle:HTMLElement) {
 return msgEle.parentElement // "a3s aXjCH"
         && msgEle.parentElement.className.startsWith("ii gt")
         && msgEle.parentElement.style.display != "none"
   || msgEle.className === "h7 ie nH oy8Mbf" // Display after reply
   || msgEle.firstChild && (msgEle.firstChild as HTMLElement).className === "adn ads" // Expanded message display div
   || msgEle.className === "ii gt" && msgEle.style.display != "none" // Message view body element
   ;
}

class MessageObserver {
 observer: MutationObserver;
 observingStarted: boolean;
 observedThreadView: any;
 previousObservedThreadView: any;
 myEmailAddress: string;
 observerConfig: {childList: boolean; subtree: boolean};

  constructor(myEmail: string) {
   this.observer = new MutationObserver(this.watchMessage());
   this.observingStarted = false;
   this.myEmailAddress = myEmail;
   this.observerConfig = {childList: true, subtree: true};
 }

  watchMessage() {
   let that = this;
   return (mutationList) => {
     mutationList.forEach(mutation => {
       if (mutation.type === 'childList') {
         [].slice.call(mutation.addedNodes)
         .filter(isMessageNode).map(ele => decryptMessageNode(that.myEmailAddress, ele));
       }
     });
   }
 }

  startObserving() {
   if (!this.observedThreadView || this.observedThreadView === this.previousObservedThreadView)
     return;
   // Observe the thread view element
   let $threadElement = $('div.nH[role="main"][style]');
    let threadEle = $threadElement.get(0);
   if (!threadEle) return;

    this.previousObservedThreadView = this.observedThreadView;
   if (this.observingStarted) {
     this.observer.disconnect();
   }
   this.observingStarted = true;
   this.observer.observe(threadEle, this.observerConfig);
 }

  stopObserving() {
   if (this.observingStarted) {
     this.observer.disconnect();
     this.observedThreadView = this.previousObservedThreadView = undefined;
     this.observingStarted = false;
   }
 }

  setObservedThreadView(tv) {
   this.observedThreadView = tv;
 }
}

sdk.Conversations.registerThreadViewHandler(threadView => {
   messageObserver.setObservedThreadView(threadView);
   messageObserver.startObserving();

    threadView.on("destroy", event => {
     messageObserver.stopObserving();
   });
});


What is related to my problem is these lines in the watchMessage closure:
        if (mutation.type === 'childList') {
         [].slice.call(mutation.addedNodes)
         .filter(isMessageNode).map(ele => decryptMessageNode(that.myEmailAddress, ele));
       }

When the Gmail page froze, my code only used the map function that looked like the following:
        if (mutation.type === 'childList') {
         [].slice.call(mutation.addedNodes).map(processNode);
       }

where,

function processNode(node) {
 
if (!isMessageNode(node)) return;
  decryptMessageNode
(myEmailAddress, node);
}

and I used JQuery to select nodes in the isMessageNode() function. After some time, the Chrome debugger stops at the first line of the JQuery file.

Without using the JQuery and splitting the map function into the filter and map function, I don't get frozen Gmail page anymore. I'm not exactly sure what happened with them, but at least for now things appear to work.

I'm sorry to report an issue that is not caused by the InboxSDK, but I'm very grateful for your help.

Lou Wynn

unread,
Aug 9, 2018, 7:52:23 PM8/9/18
to InboxSDK
For a strange reason, Groups didn't display my entire message. I'm having to type it again. :(

  stopObserving() {
   if (this.observingStarted) {
     this.observer.disconnect();
     this.observedThreadView = this.previousObservedThreadView = undefined;
     this.observingStarted = false;
   }
 }

  setObservedThreadView(tv) {
   this.observedThreadView = tv;
 }
}

What's related part is the following in the watchMessage() function:

        if (mutation.type === 'childList') {
         [].slice.call(mutation.addedNodes)
         .filter(isMessageNode).map(ele => decryptMessageNode(that.myEmailAddress, ele));
       }

When the freezing happened, I had something that looked like this:

        if (mutation.type === 'childList') {
         [].slice.call(mutation.addedNodes).map(processNode);
       }

where

function processNode(node) {
 
if (!isMessageNode(node)) return;
  decryptMessageNode(that.myEmailAddress, node);
}

and I used JQuery in the isMessageNode() function to select nodes. After the Gmail page froze for some time, the Chrome debugger stopped at the first line of JQuery file.

I'm not sure what happened. By using the filter function first and then map and removing JQuery in the selection process, things start to work.

I'm sorry to report an issue that was not of InboxSDK, but I'm grateful for your feedback and help.

   if<span

Chris Cowan

unread,
Aug 9, 2018, 9:00:15 PM8/9/18
to InboxSDK
If your decryptMessageNode function modifies a message element, then it seems likely that it could trigger the mutation observer (which will then call decryptMessageNode again and so on). I don't see explicit checks to prevent this. You could check if this is the case by temporarily modifying your code to have a global counter variable (`let counter = 0;` at the top) and then add the line `if (counter++ > 100) throw new Error('hit counter limit');` to the top of your MutationObserver callback. With that code in place, do the actions that trigger a page freeze, and I expect that instead of a page freeze, you'll see that error fire.

You could fix the issue by making decryptMessageNode add a class name (or other attribute) like "myextension_modified" to all elements that it adds or modifies in the page, and then have it avoid making any changes to elements that already have that class name.

Some other things about the code that could be improved but are probably unrelated to the freezing issue:
  • The code has an issue that if there are ever multiple thread views at once, then when the first thread view emits a destroy event, the messageObserver singleton will stop watching all open threads. There may be cases in the InboxSDK that there are temporarily multiple active thread view instances such as when a user goes directly from one thread to another. Also, it seems like there's a lot of unnecessary complexity arising from MessageObserver's statefulness. You could instead make a new instance of it for every thread, which would simplify the code (no need to track previousObservedThread) and entirely avoid issues about multiple threads.
  • The code is doing exact equality checks and .startsWith() on the class name property of elements. If another extension, the InboxSDK, a Gmail update, or some obscure Gmail option causes the element to have an additional class name or to merely have the class names in a different order, then your code may not identify the element. Instead of doing `if (el.className === "foo bar")` to check if an element has two class names set, you should do `if (el.classList.contains("foo") && el.classList.contains("bar"))`.

Lou Wynn

unread,
Aug 10, 2018, 2:10:00 AM8/10/18
to InboxSDK
Hi Chris,

Thanks very much for these techniques, which greatly improved the quality of my code after I implemented your advice.

I tried using the counter and not using it, but in my case, there is no difference. It might be related to how I change nodes after decrypting. I simply set the innerHTML value of the containing div with decrypted plaintext. My observer configuration is `{childList: boolean; subtree: boolean}`. Changing the value of the property doesn't seem to add an entry in the addedNodes list of the childList in the mutation list.

Thank you again for the valuable advice on dealing with Gmail and InboxSDK specifics and the mutation observer in general. It's much simpler now:

class MessageObserver {
 observer: MutationObserver;
 observingStarted: boolean;
 myEmailAddress: string;
 observerConfig: {childList: boolean; subtree: boolean};

  constructor(myEmail: string) {
   this.observer = new MutationObserver(this.watchMessage());
   this.observingStarted = false;
   this.myEmailAddress = myEmail;
   this.observerConfig = {childList: true, subtree: true};
 }

  watchMessage() {
   let myEmail = this.myEmailAddress;
   return (mutationList) => {
     mutationList.forEach(mutation => {
       if (mutation.type === 'childList') {
         [].slice.call(mutation.addedNodes)
         .filter(isRawMessageNode).map(ele => decryptMessageNode(myEmail, ele));
       }
     });
   }
 }

  startObserving() {
   // Observe the thread view element
   // let $threadElement = $('div.nH[role="list"]') || $('div.nH.bh[role="list"]');
   let threadEle = $('div.nH[role="main"][style]').get(0);
   if (!threadEle) return;
   this.observer.observe(threadEle, this.observerConfig);
 }

  stopObserving() {
   this.observer.disconnect();
 }
}

  sdk.Conversations.registerThreadViewHandler(threadView => {
   
// Decryption and verifying
    let messageObserver
= new MessageObserver(sdk.User.getEmailAddress());

    messageObserver
.startObserving();

    threadView
.on("destroy", event => {
      messageObserver
.stopObserving();
   
});
 
});

function isRawMessageNode(msgEle:HTMLElement): boolean {
 if (! msgEle.innerText
   // || msgEle.getAttribute(decryptedAttribute) == decryptedAttributeValue
 ) {
   return false;
 }

  if (isPrimaryMessage(msgEle)
   || msgEle.classList.contains("gmail_quote") // quoted message
 ) {
   return true;
 }
 return false;
}

// Clapsed message: className="adf ads" or div class "iA g6"
// Expanded message: "adn ads", its sibling is reply div
function isPrimaryMessage(msgEle:HTMLElement) {
 return msgEle.parentElement // "a3s aXjCH"
         && hasAllClasses(msgEle.parentElement, ["ii", "gt"])
         && msgEle.parentElement.style.display != "none"
   || hasAllClasses(msgEle, ["h7", "ie", "nH", "oy8Mbf"]) // Display after reply
   || msgEle.firstChild && hasAllClasses(<HTMLElement>msgEle.firstChild, ["adn", "ads"]) // Expanded message display div
   || hasAllClasses(msgEle, ["ii", "gt"]) && msgEle.style.display != "none" // Message view body element
   ;
}

function hasAllClasses(el: HTMLElement, classes: string[]): boolean {
 if (! el.classList) return false;

  for(let i = 0; i < classes.length; ++i) {
   if (! el.classList.contains(classes[i])) {
     return false;
   }
 }
 return true;
}


Reply all
Reply to author
Forward
0 new messages