Bug 1538550 - [de-xbl] convert group binding to <richlistitem is='chat-group'>. r=mkmelin
authorKhushil Mistry <khushil324@gmail.com>
Wed, 17 Apr 2019 10:48:00 +0200
changeset 26373 42139c6ab12273f0a1ff95b03e74d23e50d7c484
parent 26372 1552305b49b0db4fd9e88586adf85202165a3963
child 26374 10c8c0ceb9fb77334333cc8342a8e53c75e8d7c0
push id15808
push usermozilla@jorgk.com
push dateWed, 17 Apr 2019 21:58:54 +0000
treeherdercomm-central@e92087648287 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmkmelin
bugs1538550
Bug 1538550 - [de-xbl] convert group binding to <richlistitem is='chat-group'>. r=mkmelin
mail/base/content/messenger.xul
mail/components/im/content/chat-group.js
mail/components/im/content/chat-messenger.inc.xul
mail/components/im/content/chat-messenger.js
mail/components/im/content/chat.css
mail/components/im/content/imgroup.xml
mail/components/im/jar.mn
mail/components/im/themes/chat.css
mail/themes/osx/mail/chat.css
--- a/mail/base/content/messenger.xul
+++ b/mail/base/content/messenger.xul
@@ -146,16 +146,17 @@
 <script type="application/javascript" src="chrome://messenger/content/msgViewPickerOverlay.js"/>
 <script type="application/javascript" src="chrome://global/content/viewZoomOverlay.js"/>
 <script type="application/javascript" src="chrome://communicator/content/utilityOverlay.js"/>
 <script type="application/javascript" src="chrome://messenger/content/quickFilterBar.js"/>
 <script type="application/javascript" src="chrome://messenger/content/newmailaccount/uriListener.js"/>
 <script type="application/javascript" src="chrome://messenger/content/chat/chat-conversation-info.js"/>
 <script type="application/javascript" src="chrome://gloda/content/autocomplete-richlistitem.js"/>
 <script type="application/javascript" src="chrome://messenger/content/chat/chat-contact.js"/>
+<script type="application/javascript" src="chrome://messenger/content/chat/chat-group.js"/>
 #ifdef XP_MACOSX
 <script type="application/javascript" src="chrome://messenger/content/macMessengerMenu.js"/>
 <script type="application/javascript" src="chrome://global/content/macWindowMenu.js"/>
 #endif
 
 <!-- move needed functions into a single js file -->
 <script type="application/javascript" src="chrome://messenger/content/threadPane.js"/>
 
rename from mail/components/im/content/imgroup.xml
rename to mail/components/im/content/chat-group.js
--- a/mail/components/im/content/imgroup.xml
+++ b/mail/components/im/content/chat-group.js
@@ -1,224 +1,225 @@
-<?xml version="1.0"?>
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+/* This Source Code Form is subject to the terms of the Mozilla Public
+  * License, v. 2.0. If a copy of the MPL was not distributed with this
+  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* global MozXULElement, MozElements */
 
-<!DOCTYPE bindings>
-
-<bindings id="groupBindings"
-          xmlns="http://www.mozilla.org/xbl"
-          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-          xmlns:xbl="http://www.mozilla.org/xbl">
+/**
+  * The MozChatGroup widget displays chat group name and behave as a
+  * expansion twisty for groups such as "Conversations",
+  * "Online Contacts" and "Offline Contacts".
+  *
+  * @extends {MozElements.MozRichlistitem}
+  */
+class MozChatGroup extends MozElements.MozRichlistitem {
+  static get inheritedAttributes() {
+    return {
+      "label": "value=name",
+    };
+  }
+  connectedCallback() {
+    if (this.delayConnectedCallback() || this.hasChildNodes()) {
+      return;
+    }
 
-  <binding id="group" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
-    <content persist="closed" collapsed="true">
-      <xul:image class="twisty"/>
-      <xul:label flex="1" crop="end" xbl:inherits="value=name"/>
-    </content>
-    <implementation>
-     <constructor>
-      <![CDATA[
-        if (this.hasAttribute("closed"))
-          this.setAttribute("aria-expanded", "false");
-        else
-          this.setAttribute("aria-expanded", "true");
-      ]]>
-     </constructor>
+    this.setAttribute("is", "chat-group");
+    this.setAttribute("collapsed", "true");
+
+    this._image = document.createElement("image");
+    this._image.classList.add("twisty");
+
+    this._label = document.createElement("label");
+    this._label.setAttribute("flex", "1");
+    this._label.setAttribute("crop", "end");
+
+    this.appendChild(this._image);
+    this.appendChild(this._label);
 
-     <!-- Takes as input two contact elements (imIContact type) and compares
-          - their nicknames alphabetically (case insensitive). This method
-          - behaves as a callback that Array.prototype.sort accepts as a
-          - parameter.
-          -->
-     <method name="sortComparator">
-      <parameter name="aContactA"/>
-      <parameter name="aContactB"/>
-      <body>
-      <![CDATA[
-        if (aContactA.statusType != aContactB.statusType)
-          return aContactB.statusType - aContactA.statusType;
-        let a = aContactA.displayName.toLowerCase();
-        let b = aContactB.displayName.toLowerCase();
-        return a.localeCompare(b);
-      ]]>
-      </body>
-     </method>
+    this.contacts = [];
+
+    this.contactsById = {};
+
+    this.displayName = "";
 
-     <field name="contacts">[]</field>
-     <field name="contactsById">({})</field>
+    this.addEventListener("click", (event) => {
+      // Check if there was 1 click on the image or 2 clicks on the label
+      if ((event.detail == 1 && event.originalTarget.localName == "image") ||
+          (event.detail == 2 && event.originalTarget.localName == "label")) {
+        this.toggleClosed();
+      } else if (event.originalTarget.localName == "button") {
+        this.hide();
+      }
+    });
 
-     <method name="addContact">
-      <parameter name="aContact"/>
-      <parameter name="aTagName"/>
-      <body>
-      <![CDATA[
-        if (this.contactsById.hasOwnProperty(aContact.id))
-          return null;
+    this.addEventListener("contextmenu", (event) => {
+      event.preventDefault();
+    });
 
-        let contactElt;
-        if (aTagName) {
-          contactElt = document.createElement(aTagName);
-        } else {
-          contactElt = document.createElement("richlistitem", { is: "chat-contact" });
-        }
-        if (this.hasAttribute("closed"))
-          contactElt.setAttribute("collapsed", "true");
+    if (this.classList.contains("closed")) {
+      this.setAttribute("aria-expanded", "true");
+    } else {
+      this.setAttribute("aria-expanded", "false");
+    }
+
+    this.initializeAttributeInheritance();
+  }
 
-        let end = this.contacts.length;
-        // Avoid the binary search loop if the contacts were already sorted.
-        if (end != 0 &&
-            this.sortComparator(aContact, this.contacts[end - 1].contact) < 0) {
-          let start = 0;
-          while (start < end) {
-            let middle = start + Math.floor((end - start) / 2);
-            if (this.sortComparator(aContact, this.contacts[middle].contact) < 0)
-              end = middle;
-            else
-              start = middle + 1;
-          }
-        }
-        let last = end == 0 ? this : this.contacts[end - 1];
-        this.parentNode.insertBefore(contactElt, last.nextSibling);
-        contactElt.build(aContact);
-        contactElt.group = this;
-        this.contacts.splice(end, 0, contactElt);
-        this.contactsById[aContact.id] = contactElt;
-        this.removeAttribute("collapsed");
-        this._updateGroupLabel();
-        return contactElt;
-      ]]>
-      </body>
-     </method>
+  /**
+   * Takes as input two contact elements (imIContact type) and compares
+   * their nicknames alphabetically (case insensitive). This method
+   * behaves as a callback that Array.prototype.sort accepts as a
+   * parameter.
+   */
+  sortComparator(contactA, contactB) {
+    if (contactA.statusType != contactB.statusType) {
+      return contactB.statusType - contactA.statusType;
+    }
+    let a = contactA.displayName.toLowerCase();
+    let b = contactB.displayName.toLowerCase();
+    return a.localeCompare(b);
+  }
+
+  addContact(contact, tagName) {
+    if (this.contactsById.hasOwnProperty(contact.id)) {
+      return null;
+    }
 
-     <method name="updateContactPosition">
-      <parameter name="aSubject"/>
-      <body>
-      <![CDATA[
-        let contactElt = this.contactsById[aSubject.id];
-        let index = this.contacts.indexOf(contactElt);
-        if (index == -1) {
-          // Sometimes we get a display-name-changed notification for
-          // an offline contact, if it's not in the list, just ignore it.
-          return;
+    let contactElt;
+    if (tagName) {
+      contactElt = document.createElement(tagName);
+    } else {
+      contactElt = document.createElement("richlistitem", { is: "chat-contact" });
+    }
+    if (this.classList.contains("closed")) {
+      contactElt.setAttribute("collapsed", "true");
+    }
+
+    let end = this.contacts.length;
+    // Avoid the binary search loop if the contacts were already sorted.
+    if (end != 0 &&
+        this.sortComparator(contact, this.contacts[end - 1].contact) < 0) {
+      let start = 0;
+      while (start < end) {
+        let middle = start + Math.floor((end - start) / 2);
+        if (this.sortComparator(contact, this.contacts[middle].contact) < 0) {
+          end = middle;
+        } else {
+          start = middle + 1;
         }
-        // See if the position of the contact should be changed.
-        if (index != 0 &&
-            this.sortComparator(contactElt.contact, this.contacts[index - 1].contact) < 0 ||
-            index != this.contacts.length - 1 &&
-            this.sortComparator(contactElt.contact, this.contacts[index + 1].contact) > 0) {
-          let oldItem = this.removeContact(aSubject);
-          let newItem = this.addContact(aSubject);
-          let list = this.parentNode;
-          if (list.selectedItem == oldItem)
-            list.selectedItem = newItem;
-        }
-      ]]>
-      </body>
-     </method>
-
-     <method name="removeContact">
-      <parameter name="aContact"/>
-      <body>
-      <![CDATA[
-        let contact = this.contactsById[aContact.id];
-        if (!contact)
-          throw new Error("Removing a contact that isn't here?");
+      }
+    }
+    let last = end == 0 ? this : this.contacts[end - 1];
+    this.parentNode.insertBefore(contactElt, last.nextSibling);
+    contactElt.build(contact);
+    contactElt.group = this;
+    this.contacts.splice(end, 0, contactElt);
+    this.contactsById[contact.id] = contactElt;
+    this.removeAttribute("collapsed");
+    this._updateGroupLabel();
+    return contactElt;
+  }
 
-        // create a new array to remove without breaking for each loops.
-        this.contacts = this.contacts.filter(c => c !== contact);
-        delete this.contactsById[contact.contact.id];
-
-        contact.destroy();
-
-        // Check if some contacts remain in the group, if empty hide it.
-        if (!this.contacts.length)
-          this.setAttribute("collapsed", "true");
-        else
-          this._updateGroupLabel();
+  updateContactPosition(subject) {
+    let contactElt = this.contactsById[subject.id];
+    let index = this.contacts.indexOf(contactElt);
+    if (index == -1) {
+      // Sometimes we get a display-name-changed notification for
+      // an offline contact, if it's not in the list, just ignore it.
+      return;
+    }
+    // See if the position of the contact should be changed.
+    if (index != 0 &&
+        this.sortComparator(contactElt.contact, this.contacts[index - 1].contact) < 0 ||
+        index != this.contacts.length - 1 &&
+        this.sortComparator(contactElt.contact, this.contacts[index + 1].contact) > 0) {
+      let oldItem = this.removeContact(subject);
+      let newItem = this.addContact(subject);
+      let list = this.parentNode;
+      if (list.selectedItem == oldItem) {
+        list.selectedItem = newItem;
+      }
+    }
+  }
 
-        return contact;
-      ]]>
-      </body>
-     </method>
+  removeContact(contactForID) {
+    let contact = this.contactsById[contactForID.id];
+    if (!contact) {
+      throw new Error("Can't remove contact for id=" + contactForID.id);
+    }
+
+    // create a new array to remove without breaking for each loops.
+    this.contacts = this.contacts.filter(c => c !== contact);
+    delete this.contactsById[contact.contact.id];
 
-     <method name="_updateClosedState">
-      <parameter name="aClosed"/>
-      <body>
-      <![CDATA[
-        for (let contact of this.contacts)
-          contact.collapsed = aClosed;
-      ]]>
-      </body>
-     </method>
+    contact.destroy();
+
+    // Check if some contacts remain in the group, if empty hide it.
+    if (!this.contacts.length) {
+      this.setAttribute("collapsed", "true");
+    } else {
+      this._updateGroupLabel();
+    }
+
+    return contact;
+  }
 
-     <method name="close">
-      <body>
-      <![CDATA[
-        if (this.hasAttribute("closed")) {
-          this.removeAttribute("closed");
-          this.setAttribute("aria-expanded", "true");
-          this._updateClosedState(false);
-        } else {
-          this.setAttribute("closed", "true");
-          this.setAttribute("aria-expanded", "false");
-          this._updateClosedState(true);
-        }
+  _updateClosedState(closed) {
+    for (let contact of this.contacts) {
+      contact.collapsed = closed;
+    }
+  }
 
-        this._updateGroupLabel();
-      ]]>
-      </body>
-     </method>
+  toggleClosed() {
+    if (this.classList.contains("closed")) {
+      this.classList.remove("closed");
+      this.setAttribute("aria-expanded", "true");
+      this._updateClosedState(false);
+    } else {
+      this.classList.add("closed");
+      this.setAttribute("aria-expanded", "false");
+      this._updateClosedState(true);
+    }
 
-     <field name="displayName"></field>
-     <method name="_updateGroupLabel">
-      <body>
-      <![CDATA[
-        if (!this.displayName)
-          this.displayName = this.getAttribute("name");
-        let name = this.displayName;
-        if (this.hasAttribute("closed"))
-          name += " (" + this.contacts.length + ")";
+    this._updateGroupLabel();
+  }
 
-        this.setAttribute("name", name);
-      ]]>
-      </body>
-     </method>
+  _updateGroupLabel() {
+    if (!this.displayName) {
+      this.displayName = this.getAttribute("name");
+    }
+    let name = this.displayName;
+    if (this.classList.contains("closed")) {
+      name += " (" + this.contacts.length + ")";
+    }
 
-     <method name="keyPress">
-      <parameter name="aEvent"/>
-      <body>
-      <![CDATA[
-        switch (aEvent.keyCode) {
-          case aEvent.DOM_VK_RETURN:
-            this.close();
-            break;
+    this.setAttribute("name", name);
+  }
 
-          case aEvent.DOM_VK_LEFT:
-            if (!this.hasAttribute("closed"))
-              this.close();
-            break;
+  keyPress(event) {
+    switch (event.keyCode) {
+      case event.DOM_VK_RETURN:
+        this.toggleClosed();
+        break;
 
-          case aEvent.DOM_VK_RIGHT:
-            if (this.hasAttribute("closed"))
-              this.close();
-            break;
+      case event.DOM_VK_LEFT:
+        if (!this.classList.contains("closed")) {
+          this.toggleClosed();
         }
-      ]]>
-      </body>
-     </method>
-    </implementation>
-    <handlers>
-     <handler event="click">
-     <![CDATA[
-        // Check if there was 1 click on the image or 2 clicks on the label
-        if ((event.detail == 1 && event.originalTarget.localName == "image") ||
-            (event.detail == 2 && event.originalTarget.localName == "label"))
-          this.close();
+        break;
 
-        if (event.originalTarget.localName == "button")
-          this.hide();
-     ]]>
-     </handler>
-    </handlers>
-  </binding>
-</bindings>
+      case event.DOM_VK_RIGHT:
+        if (this.classList.contains("closed")) {
+          this.toggleClosed();
+        }
+        break;
+    }
+  }
+}
+
+MozXULElement.implementCustomInterface(
+  MozChatGroup, [Ci.nsIDOMXULSelectControlItemElement]
+);
+
+customElements.define("chat-group", MozChatGroup, { "extends": "richlistitem" });
--- a/mail/components/im/content/chat-messenger.inc.xul
+++ b/mail/components/im/content/chat-messenger.inc.xul
@@ -94,20 +94,24 @@
           </toolbox>
 
           <notificationbox id="chatNotificationBox" flex="1" notificationside="top">
             <hbox id="chatPanel" flex="1">
               <vbox id="listPaneBox" minwidth="125" width="200" persist="width">
                 <richlistbox id="contactlistbox"
                              context="buddyListContextMenu"
                              tooltip="imTooltip" flex="1">
-                  <imgroup id="conversationsGroup" name="&conversationsHeader.label;"/>
+                  <richlistitem is="chat-group" id="conversationsGroup"
+                                name="&conversationsHeader.label;"/>
                   <imconv id="searchResultConv" displayname="&searchResultConversation.label;" hidden="true"/>
-                  <imgroup id="onlinecontactsGroup" name="&onlineContactsHeader.label;"/>
-                  <imgroup id="offlinecontactsGroup" name="&offlineContactsHeader.label;" closed="true"/>
+                  <richlistitem is="chat-group" id="onlinecontactsGroup"
+                                name="&onlineContactsHeader.label;"/>
+                  <richlistitem is="chat-group" id="offlinecontactsGroup"
+                                name="&offlineContactsHeader.label;"
+                                class="closed"/>
                 </richlistbox>
               </vbox>
               <splitter id="listSplitter" collapse="before"/>
               <deck id="conversationsDeck" flex="1">
                 <vbox flex="1" id="noConvScreen" class="im-placeholder-screen" align="center" pack="center">
                   <hbox id="noConvBox" class="im-placeholder-box" align="top">
                     <vbox id="noConvInnerBox" class="im-placeholder-innerbox" flex="1">
                       <label id="noConvTitle" class="im-placeholder-title">&chat.noConv.title;</label>
--- a/mail/components/im/content/chat-messenger.js
+++ b/mail/components/im/content/chat-messenger.js
@@ -603,17 +603,18 @@ var chatHandler = {
       return;
     let item = document.getElementById("contactlistbox").selectedItem;
     if (item.localName == "imconv" && item.convView)
       item.convView.focus();
   },
   onListItemSelected() {
     let contactlistbox = document.getElementById("contactlistbox");
     let item = contactlistbox.selectedItem;
-    if (!item || item.hidden || item.localName == "imgroup") {
+    if (!item || item.hidden ||
+        (item.localName == "richlistitem" && item.getAttribute("is") == "chat-group")) {
       this._hideContextPane(true);
       document.getElementById("conversationsDeck").selectedPanel =
         document.getElementById("noConvScreen");
       this.updateTitle();
       this.observedContact = null;
       return;
     }
 
@@ -867,18 +868,20 @@ var chatHandler = {
         return;
       }
       conv = conv.nextSibling;
     }
 
     // No unread messages, select the first conversation, but only if
     // the existing selection is uninteresting (a section header).
     if (firstConv) {
-      if (!selectedItem || selectedItem.localName == "imgroup")
+      if (!selectedItem ||
+          (selectedItem.localName == "richlistitem" && selectedItem.getAttribute("is") == "chat-group")) {
         list.selectedItem = firstConv;
+      }
       return;
     }
 
     // No conversation, if a visible item is selected, keep it.
     if (selectedItem && !selectedItem.collapsed)
       return;
 
     // Select the first visible group header.
--- a/mail/components/im/content/chat.css
+++ b/mail/components/im/content/chat.css
@@ -1,18 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 imconversation {
   -moz-binding: url("chrome://messenger/content/chat/imconversation.xml#conversation");
 }
 
-imgroup {
-  -moz-binding: url("chrome://messenger/content/chat/imgroup.xml#group");
+richlistitem[is="chat-group"] {
   -moz-box-align: center;
 }
 
 richlistitem[is="chat-contact"] {
   -moz-box-align: center;
 }
 
 .startChatBubble, .closeConversationButton {
--- a/mail/components/im/jar.mn
+++ b/mail/components/im/jar.mn
@@ -17,17 +17,17 @@ messenger.jar:
     content/messenger/chat/imAccounts.xul                (content/imAccounts.xul)
     content/messenger/chat/imAccountWizard.xul           (content/imAccountWizard.xul)
     content/messenger/chat/imAccountWizard.js            (content/imAccountWizard.js)
     content/messenger/chat/imContextMenu.js              (content/imContextMenu.js)
     content/messenger/chat/imStatusSelector.js           (content/imStatusSelector.js)
     content/messenger/chat/chat-contact.js               (content/chat-contact.js)
     content/messenger/chat/imconversation.xml            (content/imconversation.xml)
     content/messenger/chat/imconv.xml                    (content/imconv.xml)
-    content/messenger/chat/imgroup.xml                   (content/imgroup.xml)
+    content/messenger/chat/chat-group.js                 (content/chat-group.js)
     content/messenger/chat/chat-conversation-info.js     (content/chat-conversation-info.js)
 % skin messenger-messagestyles classic/1.0 %skin/classic/messenger/messages/
 	skin/classic/messenger/messages/mail/Bitmaps/minus-hover.png          (messages/mail/Bitmaps/minus-hover.png)
 	skin/classic/messenger/messages/mail/Bitmaps/minus.png                (messages/mail/Bitmaps/minus.png)
 	skin/classic/messenger/messages/mail/Bitmaps/plus-hover.png           (messages/mail/Bitmaps/plus-hover.png)
 	skin/classic/messenger/messages/mail/Bitmaps/plus.png                 (messages/mail/Bitmaps/plus.png)
 	skin/classic/messenger/messages/mail/Footer.html                      (messages/mail/Footer.html)
 	skin/classic/messenger/messages/mail/Incoming/buddy_icon.png          (messages/mail/Incoming/buddy_icon.png)
--- a/mail/components/im/themes/chat.css
+++ b/mail/components/im/themes/chat.css
@@ -91,46 +91,46 @@
   /* make it possible to let the children overwrite the end border.
      margin-inline-start because of the inverted direction */
   margin-inline-start: -1px;
   pointer-events: none;
 }
 
 /* move the scrollbar to the left */
 #contactlistbox:-moz-locale-dir(ltr),
-#contactlistbox:-moz-locale-dir(rtl) > :-moz-any(imconv, richlistitem[is="chat-contact"], imgroup) {
+#contactlistbox:-moz-locale-dir(rtl) > :-moz-any(imconv, richlistitem[is="chat-contact"], richlistitem[is="chat-group"]) {
   direction: rtl;
 }
 
 #contactlistbox:-moz-locale-dir(rtl),
-#contactlistbox:-moz-locale-dir(ltr) > :-moz-any(imconv, richlistitem[is="chat-contact"], imgroup) {
+#contactlistbox:-moz-locale-dir(ltr) > :-moz-any(imconv, richlistitem[is="chat-contact"], richlistitem[is="chat-group"]) {
   direction: ltr;
 }
 
-imgroup {
+richlistitem[is="chat-group"] {
   padding-inline-start: 4px;
   margin-inline-end: 1px;
 }
 
-imgroup > label {
+richlistitem[is="chat-group"] > label {
   margin-inline-start: 4px;
 }
 
-imgroup,
+richlistitem[is="chat-group"],
 imconv[unread] {
   font-weight: bold;
 }
 
 imconv[attention] {
   color: blue;
 }
 
-imgroup[selected] {
+richlistitem[is="chat-group"][selected] {
   background-color: var(--imgroup-selected-background-color);
-  color: var(--imbox-selected-text-color);
+  color: var(--imbox-selected-text-color) !important;
 }
 
 imconv,
 richlistitem[is="chat-contact"] {
   border-top: 1px solid transparent;
   border-bottom: 1px solid transparent;
   -moz-box-align: stretch;
 }
@@ -143,45 +143,45 @@ richlistitem[is="chat-contact"] > .box-l
   /* equalize the space, the .closeConversationButton uses */
   margin-inline-end: 22px;
 }
 
 .box-line[selected=true] {
   background-color: var(--tabline-color);
 }
 
-:-moz-any(imconv, richlistitem[is="chat-contact"], imgroup) {
+:-moz-any(imconv, richlistitem[is="chat-contact"], richlistitem[is="chat-group"]) {
   pointer-events: auto;
 }
 
-:-moz-any(imconv, richlistitem[is="chat-contact"], imgroup):not([selected=true]):hover {
+:-moz-any(imconv, richlistitem[is="chat-contact"], richlistitem[is="chat-group"]):not([selected=true]):hover {
   background-color: rgba(0,0,0,.1);
 }
 
-:root[lwt-tree] imgroup,
+:root[lwt-tree] richlistitem[is="chat-group"],
 :root[lwt-tree] imconv:not([selected]),
 :root[lwt-tree] richlistitem[is="chat-contact"]:not([selected]) {
   color: var(--sidebar-text-color);
 }
 
 imconv[selected=true],
 richlistitem[is="chat-contact"][selected=true] {
   background-color: var(--imbox-selected-background-color);
   border-color: var(--imbox-selected-border-color);
   color: var(--imbox-selected-text-color);
 }
 
-:root[lwt-tree] imgroup[selected],
-:root[lwt-tree] :-moz-any(imconv, richlistitem[is="chat-contact"], imgroup):not([selected=true]):hover {
+:root[lwt-tree] richlistitem[is="chat-group"][selected],
+:root[lwt-tree] :-moz-any(imconv, richlistitem[is="chat-contact"], richlistitem[is="chat-group"]):not([selected=true]):hover {
   background-color: var(--sidebar-highlight-background-color, hsla(0,0%,80%,.3));
   color: var(--sidebar-highlight-text-color, var(--sidebar-text-color));
 }
 
-:root[lwt-tree-brighttext] imgroup[selected],
-:root[lwt-tree-brighttext] :-moz-any(imconv, richlistitem[is="chat-contact"], imgroup):not([selected=true]):hover {
+:root[lwt-tree-brighttext] richlistitem[is="chat-group"][selected],
+:root[lwt-tree-brighttext] :-moz-any(imconv, richlistitem[is="chat-contact"], richlistitem[is="chat-group"]):not([selected=true]):hover {
   background-color: var(--sidebar-highlight-background-color, rgba(249,249,250,.1));
 }
 
 :root[lwt-tree] imconv[selected=true],
 :root[lwt-tree] richlistitem[is="chat-contact"][selected=true] {
   border-color: var(--sidebar-border-color, hsla(0,0%,60%,.4))
 }
 
@@ -734,36 +734,36 @@ richlistitem[inactive][selected] > label
 %endif
 }
 
 .conv-nicklist:focus > richlistitem[inactive][selected] > label {
   color: HighlightText !important;
 }
 
 /* from instantbird/themes/blist.css */
-imgroup .twisty {
+richlistitem[is="chat-group"] .twisty {
   padding-top: 1px;
   width: 8px; /* The image's width is 8 pixels */
   height: 8px;
 %ifndef XP_MACOSX
   margin-inline-end: 3px;
 %else
   margin-inline-end: 2px;
 %endif
   margin-inline-start: 5px;
   background: url("chrome://global/skin/icons/twisty-expanded.svg") no-repeat center;
   -moz-context-properties: fill;
   fill: currentColor;
 }
 
-imgroup[closed] .twisty {
+richlistitem[is="chat-group"].closed .twisty {
   background: url("chrome://global/skin/icons/twisty-collapsed.svg") no-repeat center;
 }
 
-imgroup[closed]:-moz-locale-dir(rtl) .twisty {
+richlistitem[is="chat-group"].closed:-moz-locale-dir(rtl) .twisty {
   background: url("chrome://global/skin/icons/twisty-collapsed-rtl.svg");
 }
 
 .prplBuddyIcon {
   margin: 2px 0;
 }
 
 #searchResultConv > .prplBuddyIcon > .protoIcon {
--- a/mail/themes/osx/mail/chat.css
+++ b/mail/themes/osx/mail/chat.css
@@ -19,17 +19,17 @@
   --imgroup-selected-background-color: ThreeDLightShadow;
 }
 
 #contactlistbox {
   background: transparent;
   -moz-appearance: none;
 }
 
-:root:not([lwt-tree]) imgroup[selected] {
+:root:not([lwt-tree]) richlistitem[is="chat-group"][selected] {
   -moz-appearance: -moz-mac-source-list-selection;
   -moz-font-smoothing-background-color: -moz-mac-source-list-selection;
 }
 
 .convUnreadCount,
 .contactDisplayName,
 .convDisplayName,
 .contactStatusText,