Bug 663695 - Implement 'Reorder Attachments' functionality, UI, and keyboard shortcuts; fix focus/selection issues. r=aceman, ui-r=bwinton,paenglab
authorThomas Duellmann <bugzilla2007@duellmann24.net>
Tue, 10 Oct 2017 13:08:45 +0200
changeset 29360 5b6e551a23fb5004ca8ff16dcb5913abde78f33d
parent 29359 6c8dea328d6d4b6cac2785eb6065078f5b9a86c4
child 29361 01bc9dc1e659d125ee31b95ec588befeccbff70e
push id2068
push userclokep@gmail.com
push dateMon, 13 Nov 2017 19:02:14 +0000
treeherdercomm-beta@9c7e7ce8672b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaceman, bwinton, paenglab
bugs663695
Bug 663695 - Implement 'Reorder Attachments' functionality, UI, and keyboard shortcuts; fix focus/selection issues. r=aceman, ui-r=bwinton,paenglab
mail/components/compose/content/MsgComposeCommands.js
mail/components/compose/content/messengercompose.xul
mail/locales/en-US/chrome/messenger/messengercompose/messengercompose.dtd
mail/themes/linux/mail/compose/messengercompose.css
mail/themes/linux/mail/messenger.css
mail/themes/osx/mail/compose/messengercompose.css
mail/themes/osx/mail/messenger.css
mail/themes/shared/jar.inc.mn
mail/themes/shared/mail/icons/move-bottom.svg
mail/themes/shared/mail/icons/move-down.svg
mail/themes/shared/mail/icons/move-together.svg
mail/themes/shared/mail/icons/move-top.svg
mail/themes/shared/mail/icons/move-up.svg
mail/themes/shared/mail/icons/sort.svg
mail/themes/shared/mail/messenger.css
mail/themes/windows/mail/compose/messengercompose.css
mail/themes/windows/mail/messenger.css
--- a/mail/components/compose/content/MsgComposeCommands.js
+++ b/mail/components/compose/content/MsgComposeCommands.js
@@ -18,16 +18,18 @@ Components.utils.import("resource:///mod
 Components.utils.import("resource:///modules/MailUtils.js");
 Components.utils.import("resource://gre/modules/InlineSpellChecker.jsm");
 Components.utils.import("resource://gre/modules/PluralForm.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/AppConstants.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
+                                  "resource://gre/modules/ShortcutUtils.jsm");
 
 /**
  * interfaces
  */
 var nsIMsgCompDeliverMode = Components.interfaces.nsIMsgCompDeliverMode;
 var nsIMsgCompSendFormat = Components.interfaces.nsIMsgCompSendFormat;
 var nsIMsgCompConvertible = Components.interfaces.nsIMsgCompConvertible;
 var nsIMsgCompType = Components.interfaces.nsIMsgCompType;
@@ -157,16 +159,26 @@ function ReleaseGlobalVariables()
   gMessenger = null;
   gDisableAttachmentReminder = false;
   _gComposeBundle = null;
   MailServices.mailSession.RemoveMsgWindow(msgWindow);
   msgWindow = null;
 }
 
 /**
+ * Get a pretty, human-readable shortcut key string from a given <key> id.
+ *
+ * @param aKeyId   the ID of a <key> element
+ * @return string  pretty, human-readable shortcut key string from the <key>
+ */
+function getPrettyKey(aKeyId) {
+  return ShortcutUtils.prettifyShortcut(document.getElementById(aKeyId));
+}
+
+/**
  * Disables or enables editable elements in the window.
  * The elements to operate on are marked with the "disableonsend" attribute.
  * This includes elements like the address list, attachment list, subject
  * and message body.
  *
  * @param aDisable  true = disable items. false = enable items.
  */
 function updateEditableFields(aDisable)
@@ -934,16 +946,146 @@ var attachmentBucketController = {
       isEnabled: function() {
         return MessageGetNumSelectedAttachments() == 1;
       },
       doCommand: function() {
         RenameSelectedAttachment();
       }
     },
 
+    cmd_reorderAttachments: {
+      isEnabled: function() {
+        if (attachmentsCount() == 0) {
+          let reorderAttachmentsPanel =
+            document.getElementById("reorderAttachmentsPanel");
+          if (reorderAttachmentsPanel.state == "open") {
+            // When the panel is open and all attachments get deleted,
+            // we get notified here and want to close the panel.
+            reorderAttachmentsPanel.hidePopup();
+          }
+        }
+        return (attachmentsCount() > 1);
+      },
+      doCommand: function() {
+        showReorderAttachmentsPanel();
+      }
+    },
+
+    cmd_moveAttachmentUp: {
+      isEnabled: function() {
+        let attachmentsSelectedCount = MessageGetNumSelectedAttachments();
+        return attachmentsSelectedCount > 0 &&
+               attachmentsSelectedCount != attachmentsCount() &&
+               !attachmentsSelectionIsBlock("top");
+      },
+      doCommand: function() {
+        moveSelectedAttachments("up");
+      }
+    },
+
+    cmd_moveAttachmentDown: {
+      isEnabled: function() {
+        let attachmentsSelectedCount = MessageGetNumSelectedAttachments();
+        return attachmentsSelectedCount > 0 &&
+               attachmentsSelectedCount != attachmentsCount() &&
+               !attachmentsSelectionIsBlock("bottom");
+      },
+      doCommand: function() {
+        moveSelectedAttachments("down");
+      }
+    },
+
+    cmd_moveAttachmentBundleUp: {
+      isEnabled: function() {
+        let attachmentsSelectedCount = MessageGetNumSelectedAttachments();
+        return attachmentsSelectedCount > 1 &&
+               !attachmentsSelectionIsBlock();
+      },
+      doCommand: function() {
+        moveSelectedAttachments("bundleUp");
+      }
+    },
+
+    cmd_moveAttachmentBundleDown: {
+      isEnabled: function() {
+        let attachmentsSelectedCount = MessageGetNumSelectedAttachments();
+        return attachmentsSelectedCount > 1 &&
+               !attachmentsSelectionIsBlock();
+      },
+      doCommand: function() {
+        moveSelectedAttachments("bundleDown");
+      }
+    },
+
+    cmd_moveAttachmentTop: {
+      isEnabled: function() {
+        let attachmentsSelectedCount = MessageGetNumSelectedAttachments();
+        return attachmentsSelectedCount > 0 &&
+               attachmentsSelectedCount != attachmentsCount() &&
+               !attachmentsSelectionIsBlock("top");
+      },
+      doCommand: function() {
+        moveSelectedAttachments("top");
+      }
+    },
+
+    cmd_moveAttachmentBottom: {
+      isEnabled: function() {
+        let attachmentsSelectedCount = MessageGetNumSelectedAttachments();
+        return attachmentsSelectedCount > 0 &&
+               attachmentsSelectedCount != attachmentsCount() &&
+               !attachmentsSelectionIsBlock("bottom");
+      },
+      doCommand: function() {
+        moveSelectedAttachments("bottom");
+      }
+    },
+
+    cmd_sortAttachmentsToggle: {
+      isEnabled: function() {
+        let attachmentsSelectedCount = MessageGetNumSelectedAttachments();
+        let currSortOrder = attachmentsSelectionGetSortOrder();
+        let isBlock = attachmentsSelectionIsBlock();
+        // If current sorting is ascending AND it's a block; OR
+        // if current sorting is descending AND it's NOT a block yet:
+        // Offer toggle button face to sort descending.
+        // In all other cases, offer toggle button face to sort ascending.
+        let btnAscending = !((currSortOrder == "ascending") && isBlock ||
+                             (currSortOrder == "descending") && !isBlock);
+
+        let toggleCmd = document.getElementById("cmd_sortAttachmentsToggle");
+        let toggleBtn = document.getElementById("btn_sortAttachmentsToggle");
+        let sortDirection;
+        let btnLabelAttr;
+        let btnAccKeyAttr;
+        // Set sortDirection for toggleCmd, and respective button face.
+        if (btnAscending) {
+          sortDirection = "ascending";
+          btnLabelAttr = "data-labelAZ";
+          btnAccKeyAttr = "data-accesskeyAZ";
+        } else {
+          sortDirection = "descending";
+          btnLabelAttr = "data-labelZA";
+          btnAccKeyAttr = "data-accesskeyZA";
+        }
+        // Set the sort direction for toggleCmd.
+        toggleCmd.setAttribute("data-sortdirection", sortDirection);
+        // The button's icon is set dynamically via CSS involving the button's
+        // data-sortdirection attribute, which is forwarded by the command.
+        toggleBtn.setAttribute("label", toggleBtn.getAttribute(btnLabelAttr));
+        toggleBtn.setAttribute("accesskey", toggleBtn.getAttribute(btnAccKeyAttr));
+
+        return attachmentsSelectedCount > 1 &&
+               !(currSortOrder == "equivalent" && isBlock);
+      },
+      doCommand: function() {
+        moveSelectedAttachments("toggleSort");
+      }
+    },
+
     cmd_convertCloud: {
       isEnabled: function() {
         // Hide the command entirely if Filelink is disabled, or if there are
         // no cloud accounts.
         let cmd = document.getElementById("cmd_convertCloud");
 
         cmd.hidden = (!Services.prefs.getBoolPref("mail.cloud_files.enabled") ||
                       cloudFileAccounts.accounts.length == 0) ||
@@ -1246,36 +1388,49 @@ function openEditorContextMenu(popup)
 
 function updateEditItems()
 {
   goUpdateCommand("cmd_paste");
   goUpdateCommand("cmd_pasteNoFormatting");
   goUpdateCommand("cmd_pasteQuote");
   goUpdateCommand("cmd_delete");
   goUpdateCommand("cmd_renameAttachment");
+  goUpdateCommand("cmd_reorderAttachments");
   goUpdateCommand("cmd_selectAll");
   goUpdateCommand("cmd_openAttachment");
   goUpdateCommand("cmd_findReplace");
   goUpdateCommand("cmd_find");
   goUpdateCommand("cmd_findNext");
   goUpdateCommand("cmd_findPrev");
 }
 
 function updateAttachmentItems()
 {
   goUpdateCommand("cmd_attachCloud");
   goUpdateCommand("cmd_convertCloud");
   goUpdateCommand("cmd_convertAttachment");
   goUpdateCommand("cmd_cancelUpload");
   goUpdateCommand("cmd_delete");
   goUpdateCommand("cmd_renameAttachment");
+  updateReorderAttachmentsItems();
   goUpdateCommand("cmd_selectAll");
   goUpdateCommand("cmd_openAttachment");
 }
 
+function updateReorderAttachmentsItems() {
+  goUpdateCommand("cmd_reorderAttachments");
+  goUpdateCommand("cmd_moveAttachmentUp");
+  goUpdateCommand("cmd_moveAttachmentDown");
+  goUpdateCommand("cmd_moveAttachmentBundleUp");
+  goUpdateCommand("cmd_moveAttachmentBundleDown");
+  goUpdateCommand("cmd_moveAttachmentTop");
+  goUpdateCommand("cmd_moveAttachmentBottom");
+  goUpdateCommand("cmd_sortAttachmentsToggle");
+}
+
 /**
  * Update all the commands for sending a message to reflect their current state.
  */
 function updateSendCommands(aHaveController)
 {
   updateSendLock();
   if (aHaveController) {
     goUpdateCommand("cmd_sendButton");
@@ -2175,16 +2330,17 @@ attachmentWorker.onmessage = function(ev
     manageAttachmentNotification(true);
 };
 
 /**
  * Called when number of attachments changes.
  */
 function AttachmentsChanged() {
   manageAttachmentNotification(true);
+  updateAttachmentItems();
 }
 
 /**
  * This functions returns a valid spellcheck language. It checks that a
  * dictionary exists for the language passed in, if any. It also retrieves the
  * corresponding preference and ensures that a dictionary exists. If not, it
  * adjusts the preference accordingly.
  * When the nominated dictionary does not exist, the effects are very confusing
@@ -4277,22 +4433,108 @@ function AddAttachments(aAttachments, aC
     UpdateAttachmentBucket(true);
     dispatchAttachmentBucketEvent("attachments-added", addedAttachments);
     AttachmentsChanged();
   }
 
   return items;
 }
 
+/**
+ * Get the number of all attachments of the message.
+ *
+ * @return the number of all attachment items in attachmentBucket;
+ *         0 if attachmentBucket not found or no attachments in the list.
+ */
+function attachmentsCount()
+{
+  let bucketList = document.getElementById("attachmentBucket");
+  return (bucketList) ? bucketList.itemCount : 0;
+}
+
 function MessageGetNumSelectedAttachments()
 {
-  var bucketList = document.getElementById("attachmentBucket");
+  let bucketList = document.getElementById("attachmentBucket");
   return (bucketList) ? bucketList.selectedCount : 0;
 }
 
+/**
+ * Returns a sorted-by-index, "non-live" array of selected attachment list items.
+ *
+ * @param aAscending  true (default): sort return array ascending
+ *                    false         : sort return array descending
+ * @return {array}    an array of selected listitem elements in attachmentBucket
+ *                    listbox, "non-live" and sorted by their index in the list;
+ *                    [] if no attachments selected
+ */
+function attachmentsSelectedItemsGetSortedArray(aAscending = true)
+{
+  if (!MessageGetNumSelectedAttachments())
+    return [];
+
+  let bucketList = document.getElementById("attachmentBucket");
+  // bucketList.selectedItems is a "live" and "unordered" node list (items get
+  // added in the order they were added to the selection). But we want a stable
+  // ("non-live") array of selected items, sorted by their index in the list.
+  let selItems = [...bucketList.selectedItems];
+  if (aAscending) {
+    selItems.sort(
+      (a, b) => bucketList.getIndexOfItem(a) - bucketList.getIndexOfItem(b));
+  } else { // descending
+    selItems.sort(
+      (a, b) => bucketList.getIndexOfItem(b) - bucketList.getIndexOfItem(a));
+  }
+  return selItems;
+}
+
+/**
+ * Return true if the selected attachment items are a coherent block in the list,
+ * otherwise false.
+ *
+ * @param aListPosition (optional)  "top"   : Return true only if the block is
+ *                                            at the top of the list.
+ *                                  "bottom": Return true only if the block is
+ *                                            at the bottom of the list.
+ * @return {boolean} true : The selected attachment items are a coherent block
+ *                          (at the list edge if/as specified by 'aListPosition'),
+ *                          or only 1 item selected.
+ *                   false: The selected attachment items are NOT a coherent block
+ *                          (at the list edge if/as specified by 'aListPosition'),
+ *                          or no attachments selected, or no attachments,
+ *                          or no attachmentBucket.
+ */
+function attachmentsSelectionIsBlock(aListPosition)
+{
+  let selectedCount = MessageGetNumSelectedAttachments();
+  if (selectedCount < 1)
+    // No attachments selected, no attachments, or no attachmentBucket.
+    return false;
+
+  let bucketList = document.getElementById("attachmentBucket");
+  let selItems = attachmentsSelectedItemsGetSortedArray();
+  let indexFirstSelAttachment =
+    bucketList.getIndexOfItem(selItems[0]);
+  let indexLastSelAttachment =
+    bucketList.getIndexOfItem(selItems[selectedCount - 1]);
+  let isBlock = ((indexFirstSelAttachment) ==
+                 (indexLastSelAttachment + 1 - selectedCount));
+
+  switch (aListPosition) {
+  case "top":
+    // True if selection is a coherent block at the top of the list.
+    return (indexFirstSelAttachment == 0) && isBlock;
+  case "bottom":
+    // True if selection is a coherent block at the bottom of the list.
+    return (indexLastSelAttachment == (attachmentsCount() - 1)) && isBlock;
+  default:
+    // True if selection is a coherent block.
+    return isBlock;
+  }
+}
+
 function AttachPage()
 {
   let result = {value:"http://"};
   if (Services.prompt.prompt(window,
                       getComposeBundle().getString("attachPageDlogTitle"),
                       getComposeBundle().getString("attachPageDlogMessage"),
                       result,
                       null,
@@ -4358,16 +4600,21 @@ function RemoveAllAttachments()
 
     removedAttachments.appendElement(child.attachment);
     // Let's release the attachment object hold by the node else it won't go
     // away until the window is destroyed
     child.attachment = null;
   }
 
   if (removedAttachments.length > 0) {
+    // Bug workaround: Force update of selectedCount and selectedItem.
+    bucket.clearSelection();
+
+    gContentChanged = true;
+
     dispatchAttachmentBucketEvent("attachments-removed", removedAttachments);
     UpdateAttachmentBucket(false);
     AttachmentsChanged();
   }
 }
 
 /**
  * Display/hide and update the content of the attachment bucket (specifically
@@ -4391,16 +4638,19 @@ function UpdateAttachmentBucket(aShowBuc
   document.getElementById("attachments-box").collapsed = !aShowBucket;
   document.getElementById("attachmentbucket-sizer").collapsed = !aShowBucket;
 }
 
 function RemoveSelectedAttachment()
 {
   let bucket = document.getElementById("attachmentBucket");
   if (bucket.selectedItems.length > 0) {
+    // Remember the current focus index so we can try to restore it when done.
+    let focusIndex = bucket.currentIndex;
+
     let fileHandler = Services.io.getProtocolHandler("file")
                               .QueryInterface(Components.interfaces.nsIFileProtocolHandler);
     let removedAttachments = Components.classes["@mozilla.org/array;1"]
                                        .createInstance(Components.interfaces.nsIMutableArray);
 
     for (let i = bucket.selectedCount - 1; i >= 0; i--) {
       let item = bucket.removeItemAt(bucket.getIndexOfItem(bucket.getSelectedItem(i)));
       if (item.attachment.size != -1) {
@@ -4421,17 +4671,29 @@ function RemoveSelectedAttachment()
       }
 
       removedAttachments.appendElement(item.attachment);
       // Let's release the attachment object held by the node else it won't go
       // away until the window is destroyed
       item.attachment = null;
     }
 
+    // Bug workaround: Force update of selectedCount and selectedItem, both wrong
+    // after item removal, to avoid confusion for listening command controllers.
+    bucket.clearSelection();
+
+    // Try to restore original focus or somewhere close by.
+    bucket.currentIndex = (focusIndex < bucket.itemCount) ?   // If possible,
+                          focusIndex   // restore focus at original position;
+                        : ( (bucket.itemCount > 0) ? // else: if attachments exist,
+                            (bucket.itemCount - 1)   // focus last item;
+                          : -1)                      // else: nothing to focus.
+
     gContentChanged = true;
+
     dispatchAttachmentBucketEvent("attachments-removed", removedAttachments);
     AttachmentsChanged();
   }
 }
 
 function RenameSelectedAttachment()
 {
   let bucket = document.getElementById("attachmentBucket");
@@ -4456,16 +4718,335 @@ function RenameSelectedAttachment()
     item.setAttribute("name", attachmentName.value);
 
     gContentChanged = true;
 
     let event = document.createEvent("CustomEvent");
     event.initCustomEvent("attachment-renamed", true, true, originalName);
     item.dispatchEvent(event);
   }
+
+  let reorderAttachmentsPanel = document.getElementById("reorderAttachmentsPanel");
+  let attachmentBucket = document.getElementById("attachmentBucket");
+  if (reorderAttachmentsPanel.state == "open") {
+    // Hack to ensure that reorderAttachmentsPanel does not get closed as we exit.
+    attachmentBucket.setAttribute("data-ignorenextblur", "true");
+  }
+}
+
+/**
+ * Move selected attachment(s) within the attachment list.
+ *
+ * @param aDirection  "up"        : Move attachments up in the list.
+ *                    "down"      : Move attachments down in the list.
+ *                    "top"       : Move attachments to the top of the list.
+ *                    "bottom"    : Move attachments to the bottom of the list.
+ *                    "bundleUp"  : Move attachments together (upwards).
+ *                    "bundleDown": Move attachments together (downwards).
+ *                    "toggleSort": Sort attachments alphabetically (toggle).
+ */
+function moveSelectedAttachments(aDirection)
+{
+  // Command controllers will bail out if no or all attachments are selected,
+  // or if block selections can't be moved, or if other direction-specific
+  // adverse circumstances prevent the intended movement.
+
+  if (!aDirection)
+    return;
+
+  let bucket = document.getElementById("attachmentBucket");
+
+  // Ensure focus on bucket when we're coming from 'Reorder Attachments' panel.
+  bucket.focus();
+
+  // Get a sorted and "non-live" array of bucket.selectedItems.
+  let selItems = attachmentsSelectedItemsGetSortedArray();
+
+  let visibleIndex = bucket.currentIndex; // In case of misspelled aDirection.
+  // Keep track of the item we had focused originally. Deselect it though,
+  // since listbox gets confused if you move its focused item around.
+  let focusItem = bucket.currentItem;
+  bucket.currentItem = null;
+  let upwards;
+  let targetItem;
+
+  switch (aDirection) {
+    case "up":
+    case "down":
+      // Move selected attachments upwards/downwards.
+      upwards = (aDirection == "up") ? true : false;
+      let blockItems = [];
+
+      for (let item of selItems) {
+        // Handle adjacent selected items en block, via blockItems array.
+        blockItems.push(item); // Add current selItem to blockItems.
+        let nextItem = item.nextSibling;
+        if (!nextItem || !nextItem.selected) {
+          // If current selItem is the last blockItem, check out its adjacent
+          // item in the intended direction to see if there's room for moving.
+          // Note that the block might contain one or more items.
+          let checkItem = upwards ?
+                          blockItems[0].previousSibling
+                        : nextItem;
+          // If block-adjacent checkItem exists (and is not selected because
+          // then it would be part of the block), we can move the block to the
+          // right position.
+          if (checkItem) {
+            targetItem = upwards ?
+                         // Upwards: Insert block items before checkItem,
+                         // i.e. before previousSibling of block.
+                         checkItem
+                         // Downwards: Insert block items *after* checkItem,
+                         // i.e. *before* nextSibling.nextSibling of block,
+                         // which works according to spec even if that's null.
+                       : checkItem.nextSibling;
+            // Move current blockItems.
+            for (let blockItem of blockItems) {
+              bucket.insertBefore(blockItem, targetItem);
+            }
+          }
+          // Else if checkItem doesn't exist, the block is already at the edge
+          // of the list, so we can't move it in the intended direction.
+          blockItems.length = 0; // Either way, we're done with the current block.
+        }
+        // Else if current selItem is NOT the end of the current block, proceed:
+        // Add next selItem to the block and see if that's the end of the block.
+      } // Next selItem.
+
+      // Ensure helpful visibility of moved items (scroll into view if needed):
+      // If first item of selection is now at the top, first list item.
+      // Else if last item of selection is now at the bottom, last list item.
+      // Otherwise, let's see where we are going by ensuring visibility of the
+      // nearest unselected sibling of selection according to direction of move.
+      visibleIndex = (bucket.getIndexOfItem(selItems[0]) == 0) ? 0
+                   : ((bucket.getIndexOfItem(selItems[selItems.length - 1]) ==
+                       (bucket.itemCount - 1)) ?
+                       (bucket.itemCount - 1)
+                     : (upwards ? bucket.getIndexOfItem(selItems[0].previousSibling)
+                       : bucket.getIndexOfItem(selItems[selItems.length - 1].nextSibling)
+                       )
+                     );
+      break;
+
+    case "top":
+    case "bottom":
+    case "bundleUp":
+    case "bundleDown":
+      // Bundle selected attachments to top/bottom of the list or upwards/downwards.
+
+      upwards = (["top", "bundleUp"].includes(aDirection)) ? true : false;
+      // Downwards: Reverse order of selItems so we can use the same algorithm.
+      if (!upwards)
+        selItems.reverse();
+
+      if (["top", "bottom"].includes(aDirection)) {
+        let listEdgeItem = bucket.getItemAtIndex(upwards ? 0 : bucket.itemCount - 1);
+        let selEdgeItem = selItems[0];
+        if (selEdgeItem != listEdgeItem) {
+          // Top/Bottom: Move the first/last selected item to the edge of the list
+          // so that we always have an initial anchor target block in the right
+          // place, so we can use the same algorithm for top/bottom and
+          // inner bundling.
+          targetItem = upwards ?
+                       // Upwards: Insert before first list item.
+                       listEdgeItem
+                       // Downwards: Insert after last list item, i.e.
+                       // *before* non-existing listEdgeItem.nextSibling,
+                       // which is null. It works because it's a feature.
+                     : null;
+          bucket.insertBefore(selEdgeItem, targetItem);
+        }
+      }
+      // We now have a selected block (at least one item) at the target position.
+      // Let's find the end (inner edge) of that block and move only the
+      // remaining selected items to avoid unnecessary moves.
+      targetItem = null;
+      for (let item of selItems) {
+        if (targetItem) {
+          // We know where to move it, so move it!
+          bucket.insertBefore(item, targetItem);
+          if (!upwards) {
+          // Downwards: As selItems are reversed, and there's no insertAfter()
+          // method to insert *after* a stable target, we need to insert
+          // *before* the first item of the target block at target position,
+          // which is the current selItem which we've just moved onto the block.
+          targetItem = item;
+          }
+        } else {
+          // If there's no targetItem yet, find the inner edge of the target block.
+          let nextItem = upwards ? item.nextSibling : item.previousSibling;
+          if (!nextItem.selected) {
+            // If nextItem is not selected, current selItem is the inner edge of
+            // the initial anchor target block, so we can set targetItem.
+            targetItem = upwards ?
+                         // Upwards: set stable targetItem.
+                         nextItem
+                         // Downwards: set initial targetItem.
+                       : item;
+          }
+          // Else if nextItem is selected, it is still part of initial anchor
+          // target block, so just proceed to look for the edge of that block.
+        }
+      } // next selItem
+
+      // Ensure visibility of first/last selected item after the move.
+      visibleIndex = bucket.getIndexOfItem(selItems[0]);
+      break;
+
+    case "toggleSort":
+      // Sort the selected attachments alphabetically after moving them together.
+      // The command updater of cmd_sortAttachmentsToggle toggles the sorting
+      // direction based on the current sorting and block status of the selection.
+
+      let toggleCmd = document.getElementById("cmd_sortAttachmentsToggle");
+      let sortDirection = toggleCmd.getAttribute("data-sortdirection") || "ascending";
+      // Move selected attachments together before sorting as a block.
+      goDoCommand("cmd_moveAttachmentBundleUp");
+
+      // Find the end of the selected block to find our targetItem.
+      for (let item of selItems) {
+        let nextItem = item.nextSibling;
+        if (!nextItem || !nextItem.selected) {
+          // If there's no nextItem (block at list bottom), or nextItem is
+          // not selected, we've reached the end of the block.
+          // Set the block's nextSibling as targetItem and exit loop.
+          // Works by definition even if nextSibling aka nextItem is null.
+          targetItem = nextItem;
+          break;
+        }
+        // else if (nextItem && nextItem.selected), nextItem is still part of
+        // the block, so proceed with checking its nextSibling.
+      } // next selItem
+
+      // Now let's sort our selItems according to sortDirection.
+      if (sortDirection == "ascending") {
+        selItems.sort(
+          (a, b) => a.attachment.name.localeCompare(b.attachment.name));
+      } else { // "descending"
+        selItems.sort(
+          (a, b) => b.attachment.name.localeCompare(a.attachment.name));
+      }
+
+      // Insert selItems in new order before the nextSibling of the block.
+      for (let item of selItems) {
+        bucket.insertBefore(item, targetItem);
+      }
+
+      // Ensure visibility of first block item after sorting.
+      visibleIndex = bucket.getIndexOfItem(selItems[0]);
+      break;
+  } // end switch (aDirection)
+
+  // Restore original focus.
+  bucket.currentItem = focusItem;
+  // Ensure smart visibility of a relevant item according to direction.
+  bucket.ensureIndexIsVisible(visibleIndex);
+
+  // Moving selected items around does not trigger auto-updating of our command
+  // handlers, so we must do it now as the position of selected items has changed.
+  updateReorderAttachmentsItems();
+}
+
+function showReorderAttachmentsPanel() {
+    document.getElementById("reorderAttachmentsPanel")
+            .openPopup(document.getElementById("attachmentBucket"),
+                       "after_start", 15, 0, true);
+    // Focus attachmentBucket so that keyboard operation for
+    // selecting and moving attachment items works;
+    // the panel helpfully presents the keyboard shortcuts
+    // for moving things around.
+    document.getElementById("attachmentBucket").focus();
+}
+
+/**
+ * Returns a string representing the current sort order of selected attachment
+ * items by their names. We don't check if selected items form a coherent block
+ * or not; use attachmentsSelectionIsBlock() to check on that.
+ *
+ * @return {string} "ascending" : Sort order is ascending.
+ *                  "descending": Sort order is descending.
+ *                  "equivalent": The names of all selected items are equivalent.
+ *                  ""          : There's no sort order, or only 1 item selected,
+ *                                or no items selected, or no attachments,
+ *                                or no attachmentBucket.
+ */
+function attachmentsSelectionGetSortOrder()
+{
+  if (MessageGetNumSelectedAttachments() <= 1)
+    return "";
+
+  let selItems = attachmentsSelectedItemsGetSortedArray();
+  // We're comparing each item to the next item, so exclude the last item.
+  let selItems1 = selItems.slice(0, -1);
+  let someAscending;
+  let someDescending;
+
+  // Check if some adjacent items are sorted ascending.
+  someAscending = selItems1.some((item, index) =>
+    item.attachment.name.localeCompare(selItems[index + 1].attachment.name) < 0);
+
+  // Check if some adjacent items are sorted descending.
+  someDescending = selItems1.some((item, index) =>
+    item.attachment.name.localeCompare(selItems[index + 1].attachment.name) > 0);
+
+  // Unsorted (but not all equivalent in sort order)
+  if (someAscending && someDescending)
+    return "";
+
+  if (someAscending && !someDescending)
+    return "ascending";
+
+  if (someDescending && !someAscending)
+    return "descending";
+
+  // No ascending pairs, no descending pairs, so all equivalent in sort order.
+  // if (!someAscending && !someDescending)
+  return "equivalent";
+}
+
+function reorderAttachmentsPanelOnPopupShowing() {
+  let panel = document.getElementById("reorderAttachmentsPanel");
+  let buttonsNodeList = panel.querySelectorAll(".panelButton");
+  let buttons = [...buttonsNodeList]; // convert NodeList to Array
+  // Let's add some pretty keyboard shortcuts to the buttons.
+  buttons.forEach(btn => {
+    if (btn.hasAttribute("key")) {
+      btn.setAttribute("prettykey", getPrettyKey(btn.getAttribute("key")));
+    }
+  })
+  // This depends on the fact that the command handlers of cmd_moveAttachment*
+  // and cmd_sortAttachmentsToggle do not require focus in attachmentBucket as
+  // they just check for selected attachments. Otherwise updating these commands
+  // would need to happen *after* the panel is shown.
+  updateReorderAttachmentsItems();
+}
+
+function attachmentBucketOnBlur() {
+  // Ensure that reorderAttachmentsPanel remains open while we're focused
+  // on attachmentBucket or the panel, otherwise hide it.
+  let attachmentBucket = document.getElementById("attachmentBucket");
+  if (attachmentBucket.getAttribute("data-ignorenextblur") == "true") {
+    // Hack to prevent the panel from hiding after RenameSelectedAttachment()
+    attachmentBucket.setAttribute("data-ignorenextblur", "false");
+    return;
+  }
+  let reorderAttachmentsPanel = document.getElementById("reorderAttachmentsPanel");
+  if (document.activeElement.id != "attachmentBucket" ||
+      document.activeElement.id != "reorderAttachmentsPanel")
+    reorderAttachmentsPanel.hidePopup();
+}
+
+function attachmentBucketOnKeyUp(aEvent) {
+  // When ESC is pressed, close reorderAttachmentsPanel.
+  if (aEvent.key == "Escape") {
+    let reorderAttachmentsPanel = document.getElementById("reorderAttachmentsPanel");
+    if (reorderAttachmentsPanel.state == "open") {
+      reorderAttachmentsPanel.hidePopup();
+    }
+  }
 }
 
 function AttachmentElementHasItems()
 {
   var element = document.getElementById("attachmentBucket");
   return element ? (element.getRowCount() > 0) : false;
 }
 
@@ -4836,17 +5417,17 @@ function fromKeyPress(event)
 
 function subjectKeyPress(event)
 {
   gSubjectChanged = true;
   if (event.keyCode == KeyEvent.DOM_VK_RETURN)
     SetMsgBodyFrameFocus();
 }
 
-function AttachmentBucketClicked(event)
+function AttachmentBucketDoubleClicked(event)
 {
   let boundTarget = document.getBindingParent(event.originalTarget);
   if (event.button == 0 && boundTarget && boundTarget.localName == "scrollbox")
     goDoCommand('cmd_attachFile');
 }
 
 // we can drag and drop addresses, files, messages and urls into the compose envelope
 var envelopeDragObserver = {
--- a/mail/components/compose/content/messengercompose.xul
+++ b/mail/components/compose/content/messengercompose.xul
@@ -124,24 +124,53 @@
   <!--command id="cmd_find"/-->
   <!--command id="cmd_findNext"/-->
   <command id="cmd_undo" oncommand="goDoCommand('cmd_undo')" disabled="true"/>
   <command id="cmd_redo" oncommand="goDoCommand('cmd_redo')" disabled="true"/>
   <command id="cmd_cut" oncommand="goDoCommand('cmd_cut')" disabled="true"/>
   <command id="cmd_copy" oncommand="goDoCommand('cmd_copy')" disabled="true"/>
   <command id="cmd_paste" oncommand="goDoCommand('cmd_paste')" disabled="true"/>
   <command id="cmd_rewrap" oncommand="goDoCommand('cmd_rewrap')"/>
-  <command id="cmd_delete" oncommand="goDoCommand('cmd_delete')" valueDefault="&deleteCmd.label;" valueDefaultAccessKey="&deleteCmd.accesskey;" valueRemoveAttachmentAccessKey="&removeAttachment.accesskey;" disabled="true"/>
-  <command id="cmd_selectAll" oncommand="goDoCommand('cmd_selectAll')" disabled="true"/>
-  <command id="cmd_openAttachment" oncommand="goDoCommand('cmd_openAttachment')" disabled="true"/>
-  <command id="cmd_renameAttachment" oncommand="goDoCommand('cmd_renameAttachment')" disabled="true"/>
-  <command id="cmd_account" oncommand="goDoCommand('cmd_account')"/>
+  <command id="cmd_delete"
+           oncommand="goDoCommand('cmd_delete')"
+           valueDefault="&deleteCmd.label;"
+           valueDefaultAccessKey="&deleteCmd.accesskey;"
+           valueRemoveAttachmentAccessKey="&removeAttachment.accesskey;"
+           disabled="true"/>
+  <command id="cmd_selectAll"
+           oncommand="goDoCommand('cmd_selectAll')" disabled="true"/>
+  <command id="cmd_openAttachment"
+           oncommand="goDoCommand('cmd_openAttachment')" disabled="true"/>
+  <command id="cmd_renameAttachment"
+           oncommand="goDoCommand('cmd_renameAttachment')" disabled="true"/>
+  <command id="cmd_reorderAttachments"
+           oncommand="goDoCommand('cmd_reorderAttachments')" disabled="true"/>
+  <command id="cmd_account"
+           oncommand="goDoCommand('cmd_account')"/>
+
+  <!-- Reorder Attachments Panel -->
+  <command id="cmd_moveAttachmentUp"
+           oncommand="goDoCommand('cmd_moveAttachmentUp')" disabled="true"/>
+  <command id="cmd_moveAttachmentDown"
+           oncommand="goDoCommand('cmd_moveAttachmentDown')" disabled="true"/>
+  <command id="cmd_moveAttachmentBundleUp"
+           oncommand="goDoCommand('cmd_moveAttachmentBundleUp')" disabled="true"/>
+  <command id="cmd_moveAttachmentBundleDown"
+           oncommand="goDoCommand('cmd_moveAttachmentBundleDown')" disabled="true"/>
+  <command id="cmd_moveAttachmentTop"
+           oncommand="goDoCommand('cmd_moveAttachmentTop')" disabled="true"/>
+  <command id="cmd_moveAttachmentBottom"
+           oncommand="goDoCommand('cmd_moveAttachmentBottom')" disabled="true"/>
+  <command id="cmd_sortAttachmentsToggle"
+           data-sortdirection="ascending"
+           oncommand="goDoCommand('cmd_sortAttachmentsToggle')" disabled="true"/>
 
   <!-- View Menu -->
-  <command id="cmd_showFormatToolbar" oncommand="goDoCommand('cmd_showFormatToolbar')"/>
+  <command id="cmd_showFormatToolbar"
+           oncommand="goDoCommand('cmd_showFormatToolbar')"/>
 
   <commandset id="viewZoomCommands"
               commandupdater="false"
               events="create-menu-view"
               oncommandupdate="goUpdateMailMenuItems(this);">
     <command id="cmd_fullZoomReduce"
              oncommand="goDoCommand('cmd_fullZoomReduce');"/>
     <command id="cmd_fullZoomEnlarge"
@@ -219,30 +248,66 @@
   <key id="pastequotationkb" key="&pasteAsQuotationCmd.key;"
        observes="cmd_pasteQuote" modifiers="accel, shift"/>
   <key id="pastenoformattingkb" key="&pasteNoFormattingCmd.key;"
        modifiers="accel, shift" observes="cmd_pasteNoFormatting"/>
   <key id="key_rewrap" key="&editRewrapCmd.key;" command="cmd_rewrap" modifiers="accel"/>
 #ifdef XP_MACOSX
   <key id="key_delete" keycode="VK_BACK" command="cmd_delete"/>
   <key id="key_delete2" keycode="VK_DELETE" command="cmd_delete"/>
+  <key id="key_reorderAttachments"
+       key="&reorderAttachmentsCmd.key;" modifiers="control"
+       command="cmd_reorderAttachments"/>
 #else
   <key id="key_delete" keycode="VK_DELETE" command="cmd_delete"/>
-  <key id="key_renameAttachment" keycode="VK_F2" oncommand="goDoCommand('cmd_renameAttachment');"/>
+  <key id="key_renameAttachment" keycode="VK_F2"
+       command="cmd_renameAttachment"/>
+  <key id="key_reorderAttachments"
+       key="&reorderAttachmentsCmd.key;" modifiers="alt"
+       command="cmd_reorderAttachments"/>
 #endif
   <key id="key_selectAll" key="&selectAllCmd.key;" modifiers="accel"/>
   <key id="key_find" key="&findBarCmd.key;" command="cmd_find" modifiers="accel"/>
 #ifndef XP_MACOSX
   <key id="key_findReplace" key="&findReplaceCmd.key;" command="cmd_findReplace" modifiers="accel"/>
 #endif
   <key id="key_findNext" key="&findAgainCmd.key;" command="cmd_findNext" modifiers="accel"/>
   <key id="key_findPrev" key="&findPrevCmd.key;" command="cmd_findPrev" modifiers="accel, shift"/>
   <key keycode="&findAgainCmd.key2;" command="cmd_findNext"/>
   <key keycode="&findPrevCmd.key2;"  command="cmd_findPrev" modifiers="shift"/>
 
+  <!-- Reorder Attachments Panel -->
+#ifdef XP_MACOSX
+  <key id="key_moveAttachmentUp" keycode="VK_UP" modifiers="accel"
+       command="cmd_moveAttachmentUp"/>
+  <key id="key_moveAttachmentDown" keycode="VK_DOWN" modifiers="accel"
+       command="cmd_moveAttachmentDown"/>
+  <key id="key_moveAttachmentBundleUp" keycode="VK_LEFT" modifiers="accel alt"
+       command="cmd_moveAttachmentBundleUp"/>
+  <key id="key_moveAttachmentBundleDown" keycode="VK_RIGHT" modifiers="accel alt"
+       command="cmd_moveAttachmentBundleDown"/>
+  <key id="key_moveAttachmentTop" keycode="VK_UP" modifiers="accel alt"
+       command="cmd_moveAttachmentTop"/>
+  <key id="key_moveAttachmentBottom" keycode="VK_DOWN" modifiers="accel alt"
+       command="cmd_moveAttachmentBottom"/>
+#else
+  <key id="key_moveAttachmentUp" keycode="VK_UP" modifiers="alt"
+       command="cmd_moveAttachmentUp"/>
+  <key id="key_moveAttachmentDown" keycode="VK_DOWN" modifiers="alt"
+       command="cmd_moveAttachmentDown"/>
+  <key id="key_moveAttachmentBundleUp" keycode="VK_LEFT" modifiers="alt"
+       command="cmd_moveAttachmentBundleUp"/>
+  <key id="key_moveAttachmentBundleDown" keycode="VK_RIGHT" modifiers="alt"
+       command="cmd_moveAttachmentBundleDown"/>
+  <key id="key_moveAttachmentTop" keycode="VK_Home" modifiers="alt"
+       command="cmd_moveAttachmentTop"/>
+  <key id="key_moveAttachmentBottom" keycode="VK_End" modifiers="alt"
+       command="cmd_moveAttachmentBottom"/>
+#endif
+
   <!-- View Menu -->
   <key id="key_addressSidebar" keycode="VK_F9" oncommand="toggleAddressPicker();"/>
 
   <keyset id="viewZoomKeys">
     <key id="key_fullZoomReduce"  key="&fullZoomReduceCmd.commandkey;"
          command="cmd_fullZoomReduce"  modifiers="accel"/>
     <key                          key="&fullZoomReduceCmd.commandkey2;"
          command="cmd_fullZoomReduce"  modifiers="accel"/>
@@ -287,16 +352,67 @@
 
   <key keycode="VK_ESCAPE" oncommand="handleEsc();"/>
 </keyset>
 
 <keyset id="baseMenuKeyset"/>
 
 <keyset id="editorKeys"/>
 
+<!-- Reorder Attachments Panel -->
+<panel id="reorderAttachmentsPanel"
+       backdrag="true"
+       orient="vertical"
+       type="arrow"
+       flip="slide"
+       onpopupshowing="reorderAttachmentsPanelOnPopupShowing();"
+       consumeoutsideclicks="false"
+       noautohide="true">
+  <description class="panelTitle">&reorderAttachmentsPanel.label;</description>
+  <toolbarbutton id="btn_moveAttachmentTop"
+                 class="panelButton"
+                 label="&moveAttachmentTopPanelBtn.label;"
+                 accesskey="&moveAttachmentTopPanelBtn.accesskey;"
+                 key="key_moveAttachmentTop"
+                 command="cmd_moveAttachmentTop"/>
+  <toolbarbutton id="btn_moveAttachmentUp"
+                 class="panelButton"
+                 label="&moveAttachmentUpPanelBtn.label;"
+                 accesskey="&moveAttachmentUpPanelBtn.accesskey;"
+                 key="key_moveAttachmentUp"
+                 command="cmd_moveAttachmentUp"/>
+  <toolbarbutton id="btn_moveAttachmentBundleUp"
+                 class="panelButton"
+                 label="&moveAttachmentBundleUpPanelBtn.label;"
+                 accesskey="&moveAttachmentBundleUpPanelBtn.accesskey;"
+                 key="key_moveAttachmentBundleUp"
+                 command="cmd_moveAttachmentBundleUp"/>
+  <toolbarbutton id="btn_moveAttachmentDown"
+                 class="panelButton"
+                 label="&moveAttachmentDownPanelBtn.label;"
+                 accesskey="&moveAttachmentDownPanelBtn.accesskey;"
+                 key="key_moveAttachmentDown"
+                 command="cmd_moveAttachmentDown"/>
+  <toolbarbutton id="btn_moveAttachmentBottom"
+                 class="panelButton"
+                 label="&moveAttachmentBottomPanelBtn.label;"
+                 accesskey="&moveAttachmentBottomPanelBtn.accesskey;"
+                 key="key_moveAttachmentBottom"
+                 command="cmd_moveAttachmentBottom"/>
+  <toolbarbutton id="btn_sortAttachmentsToggle"
+                 class="panelButton"
+                 label="&sortAttachmentsTogglePanelBtn.AZ.label;"
+                 accesskey="&sortAttachmentsTogglePanelBtn.AZ.accesskey;"
+                 data-labelAZ="&sortAttachmentsTogglePanelBtn.AZ.label;"
+                 data-accesskeyAZ="&sortAttachmentsTogglePanelBtn.AZ.accesskey;"
+                 data-labelZA="&sortAttachmentsTogglePanelBtn.ZA.label;"
+                 data-accesskeyZA="&sortAttachmentsTogglePanelBtn.ZA.accesskey;"
+                 command="cmd_sortAttachmentsToggle"/>
+</panel>
+
 <menupopup id="msgComposeContext"
            onpopupshowing="if (event.target != this) return true; openEditorContextMenu(this);">
 
   <!-- Spellchecking menu items -->
   <menuitem id="spellCheckNoSuggestions" label="&spellNoSuggestions.label;" disabled="true"/>
   <menuseparator id="spellCheckAddSep" />
   <menuitem id="spellCheckAddToDictionary" label="&spellAddToDictionary.label;" accesskey="&spellAddToDictionary.accesskey;"
             oncommand="gSpellChecker.addToDictionary();"/>
@@ -340,24 +456,29 @@
 </menupopup>
 
 <menupopup id="msgComposeAttachmentItemContext"
            onpopupshowing="updateAttachmentItems();">
   <menuitem id="composeAttachmentContext_openItem"
             label="&openAttachment.label;"
             accesskey="&openAttachment.accesskey;"
             command="cmd_openAttachment"/>
+  <menuitem id="composeAttachmentContext_renameItem"
+            label="&renameAttachment.label;"
+            accesskey="&renameAttachment.accesskey;"
+            command="cmd_renameAttachment"/>
+  <menuitem id="composeAttachmentContext_reorderItem"
+            label="&reorderAttachments.label;"
+            accesskey="&reorderAttachments.accesskey;"
+            command="cmd_reorderAttachments"/>
+  <menuseparator id="composeAttachmentContext_beforeRemoveSeparator"/>
   <menuitem id="composeAttachmentContext_deleteItem"
             label="&removeAttachment.label;"
             accesskey="&removeAttachment.accesskey;"
             command="cmd_delete"/>
-  <menuitem id="composeAttachmentContext_renameItem"
-            label="&renameAttachment.label;"
-            accesskey="&renameAttachment.accesskey;"
-            command="cmd_renameAttachment"/>
   <menu id="composeAttachmentContext_convertCloudMenu"
         label="&convertCloud.label;"
         accesskey="&convertCloud.accesskey;"
         command="cmd_convertCloud">
     <menupopup id="convertCloudMenuItems_popup"
                onpopupshowing="addConvertCloudMenuItems(this, 'convertCloudSeparator', 'context_convertCloud');">
       <menuitem id="convertCloudMenuItems_popup_convertAttachment"
                 type="radio" name="context_convertCloud"
@@ -400,16 +521,20 @@
             accesskey="&attachPage.accesskey;"
             command="cmd_attachPage"/>
   <menuseparator id="attachmentListContext_remindLaterSeparator"/>
   <menuitem id="attachmentListContext_remindLaterItem"
             type="checkbox"
             label="&remindLater.label;"
             accesskey="&remindLater.accesskey;"
             command="cmd_remindLater"/>
+  <menuitem id="attachmentListContext_reorderItem"
+            label="&reorderAttachments.label;"
+            accesskey="&reorderAttachments.accesskey;"
+            command="cmd_reorderAttachments"/>
 </menupopup>
 
 <menupopup id="toolbar-context-menu"
            onpopupshowing="onViewToolbarsPopupShowing(event, 'compose-toolbox');">
   <menuseparator/>
   <menuitem id="CustomizeComposeToolbar"
             command="cmd_CustomizeComposeToolbar"
             label="&customizeToolbar.label;"
@@ -536,25 +661,43 @@
           <menuitem id="menu_redo" label="&redoCmd.label;" key="key_redo" accesskey="&redoCmd.accesskey;" command="cmd_redo"/>
           <menuseparator/>
           <menuitem id="menu_cut" label="&cutCmd.label;" key="key_cut" accesskey="&cutCmd.accesskey;" command="cmd_cut"/>
           <menuitem id="menu_copy" label="&copyCmd.label;" key="key_copy" accesskey="&copyCmd.accesskey;" command="cmd_copy"/>
           <menuitem id="menu_paste" label="&pasteCmd.label;" key="key_paste" accesskey="&pasteCmd.accesskey;" command="cmd_paste"/>
           <menuitem id="menu_pasteNoFormatting" command="cmd_pasteNoFormatting"
                     key="pastenoformattingkb"/>
           <menuitem id="menu_pasteQuote"/>
-          <menuitem id="menu_delete" label="&deleteCmd.label;" key="key_delete" accesskey="&deleteCmd.accesskey;" command="cmd_delete"/>
+          <menuitem id="menu_delete"
+                    label="&deleteCmd.label;"
+                    accesskey="&deleteCmd.accesskey;"
+                    key="key_delete"
+                    command="cmd_delete"/>
           <menuseparator/>
-          <menuitem id="menu_rewrap" label="&editRewrapCmd.label;" key="key_rewrap" accesskey="&editRewrapCmd.accesskey;" command="cmd_rewrap"/>
+          <menuitem id="menu_rewrap"
+                    label="&editRewrapCmd.label;"
+                    accesskey="&editRewrapCmd.accesskey;"
+                    key="key_rewrap"
+                    command="cmd_rewrap"/>
           <menuitem id="menu_RenameAttachment"
                     label="&renameAttachmentCmd.label;"
                     accesskey="&renameAttachmentCmd.accesskey;"
-                    key="key_renameAttachment" command="cmd_renameAttachment"/>
+                    key="key_renameAttachment"
+                    command="cmd_renameAttachment"/>
+          <menuitem id="menu_reorderAttachments"
+                    label="&reorderAttachmentsCmd.label;"
+                    accesskey="&reorderAttachmentsCmd.accesskey;"
+                    key="key_reorderAttachments"
+                    command="cmd_reorderAttachments"/>
           <menuseparator/>
-          <menuitem id="menu_selectAll" label="&selectAllCmd.label;" key="key_selectAll" accesskey="&selectAllCmd.accesskey;" command="cmd_selectAll"/>
+          <menuitem id="menu_selectAll"
+                    label="&selectAllCmd.label;"
+                    accesskey="&selectAllCmd.accesskey;"
+                    key="key_selectAll"
+                    command="cmd_selectAll"/>
           <menuseparator/>
           <menuitem id="menu_findBar"
                     label="&findBarCmd.label;"
                     accesskey="&findBarCmd.accesskey;"
                     key="key_find"
                     command="cmd_find"/>
 #ifndef XP_MACOSX
           <menuitem id="menu_findReplace"
@@ -994,19 +1137,21 @@
                    crop="right" accesskey="&attachments.accesskey;" flex="1"/>
             <label id="attachmentBucketSize"/>
           </hbox>
           <attachmentlist orient="vertical" id="attachmentBucket"
                           disableonsend="true"
                           seltype="multiple" flex="1" height="0"
                           context="msgComposeAttachmentListContext"
                           itemcontext="msgComposeAttachmentItemContext"
-                          onclick="AttachmentBucketClicked(event);"
+                          ondblclick="AttachmentBucketDoubleClicked(event);"
+                          onkeyup="attachmentBucketOnKeyUp(event);"
                           onselect="updateAttachmentItems();"
-                          ondragstart="nsDragAndDrop.startDrag(event, attachmentBucketDNDObserver);"/>
+                          ondragstart="nsDragAndDrop.startDrag(event, attachmentBucketDNDObserver);"
+                          onblur="attachmentBucketOnBlur();"/>
         </vbox>
       </hbox>
     </toolbar>
 
     <!-- These toolbar items get filled out from the editorOverlay -->
     <toolbox id="FormatToolbox" mode="icons">
       <toolbar class="chromeclass-toolbar" id="FormatToolbar" persist="collapsed"
                customizable="true" nowindowdrag="true">
--- a/mail/locales/en-US/chrome/messenger/messengercompose/messengercompose.dtd
+++ b/mail/locales/en-US/chrome/messenger/messengercompose/messengercompose.dtd
@@ -80,16 +80,19 @@
 <!ENTITY pasteAsQuotationCmd.key "o">
 <!ENTITY editRewrapCmd.accesskey "w">
 <!ENTITY deleteCmd.label "Delete">
 <!ENTITY deleteCmd.accesskey "d">
 <!ENTITY editRewrapCmd.label "Rewrap">
 <!ENTITY editRewrapCmd.key "R">
 <!ENTITY renameAttachmentCmd.label "Rename Attachment…">
 <!ENTITY renameAttachmentCmd.accesskey "e">
+<!ENTITY reorderAttachmentsCmd.label "Reorder Attachments…">
+<!ENTITY reorderAttachmentsCmd.accesskey "s">
+<!ENTITY reorderAttachmentsCmd.key "x">
 <!ENTITY selectAllCmd.label "Select All">
 <!ENTITY selectAllCmd.key "A">
 <!ENTITY selectAllCmd.accesskey "a">
 <!ENTITY findBarCmd.label "Find…">
 <!ENTITY findBarCmd.accesskey "F">
 <!ENTITY findBarCmd.key "F">
 <!ENTITY findReplaceCmd.label "Find and Replace…">
 <!ENTITY findReplaceCmd.accesskey "l">
@@ -98,16 +101,40 @@
 <!ENTITY findAgainCmd.accesskey "g">
 <!ENTITY findAgainCmd.key "G">
 <!ENTITY findAgainCmd.key2 "VK_F3">
 <!ENTITY findPrevCmd.label "Find Previous">
 <!ENTITY findPrevCmd.accesskey "v">
 <!ENTITY findPrevCmd.key "G">
 <!ENTITY findPrevCmd.key2 "VK_F3">
 
+<!-- Reorder Attachment Panel -->
+<!ENTITY reorderAttachmentsPanel.label "Reorder Attachments">
+<!ENTITY moveAttachmentTopPanelBtn.label "Move to Top">
+<!ENTITY moveAttachmentTopPanelBtn.accesskey "T">
+<!ENTITY moveAttachmentUpPanelBtn.label "Move Up">
+<!ENTITY moveAttachmentUpPanelBtn.accesskey "U">
+<!ENTITY moveAttachmentBundleUpPanelBtn.label "Move together">
+<!ENTITY moveAttachmentBundleUpPanelBtn.accesskey "v">
+<!ENTITY moveAttachmentDownPanelBtn.label "Move Down">
+<!ENTITY moveAttachmentDownPanelBtn.accesskey "D">
+<!ENTITY moveAttachmentBottomPanelBtn.label "Move to Bottom">
+<!ENTITY moveAttachmentBottomPanelBtn.accesskey "B">
+<!-- LOCALIZATION NOTE (sortAttachmentsTogglePanelBtn.AZ.label):
+     Please ensure that this translation matches
+     sortAttachmentsTogglePanelBtn.ZA.label, except for the sort direction. -->
+<!ENTITY sortAttachmentsTogglePanelBtn.AZ.label "Sort Selection: A - Z">
+<!-- LOCALIZATION NOTE (sortAttachmentsTogglePanelBtn.AZ.accesskey):
+     This accesskey should be the same like
+     sortAttachmentsTogglePanelBtn.ZA.accesskey, and it should be taken from
+     the "Sort Selection:" part of the label, not from the sort direction part. -->
+<!ENTITY sortAttachmentsTogglePanelBtn.AZ.accesskey "o">
+<!ENTITY sortAttachmentsTogglePanelBtn.ZA.label "Sort Selection: Z - A">
+<!ENTITY sortAttachmentsTogglePanelBtn.ZA.accesskey "o">
+
 <!-- View Menu -->
 <!ENTITY viewMenu.label "View">
 <!ENTITY viewMenu.accesskey "v">
 <!ENTITY viewToolbarsMenuNew.label "Toolbars">
 <!ENTITY viewToolbarsMenuNew.accesskey "T">
 <!ENTITY menubarCmd.label "Menu Bar">
 <!ENTITY menubarCmd.accesskey "M">
 <!ENTITY showCompositionToolbarCmd.label "Composition Toolbar">
@@ -278,16 +305,18 @@
 <!ENTITY openAttachment.label "Open">
 <!ENTITY openAttachment.accesskey "O">
 <!ENTITY delete.label "Delete">
 <!ENTITY delete.accesskey "D">
 <!ENTITY removeAttachment.label "Remove Attachment">
 <!ENTITY removeAttachment.accesskey "M">
 <!ENTITY renameAttachment.label "Rename…">
 <!ENTITY renameAttachment.accesskey "R">
+<!ENTITY reorderAttachments.label "Reorder Attachments…">
+<!ENTITY reorderAttachments.accesskey "s">
 <!ENTITY selectAll.label "Select All">
 <!ENTITY selectAll.accesskey "A">
 <!ENTITY attachFile.label "Attach File(s)…">
 <!ENTITY attachFile.accesskey "F">
 <!ENTITY attachCloud.label "Filelink…">
 <!ENTITY attachCloud.accesskey "i">
 <!ENTITY convertCloud.label "Convert to…">
 <!ENTITY convertCloud.accesskey "C">
--- a/mail/themes/linux/mail/compose/messengercompose.css
+++ b/mail/themes/linux/mail/compose/messengercompose.css
@@ -821,8 +821,42 @@ toolbarbutton.formatting-button {
 }
 
 menu[command="cmd_attachCloud"] .menu-iconic-left,
 menu[command="cmd_convertCloud"] .menu-iconic-left {
   /* Ensure that the provider icons are visible even if the Gnome theme says
      menus shouldn't have icons. */
   visibility: visible;
 }
+
+/* ::::: Reorder Attachments Panel ::::: */
+
+#reorderAttachmentsPanel > .panel-arrowcontainer > .panel-arrowcontent {
+  --arrowpanel-padding: 4px;
+}
+
+#btn_moveAttachmentTop {
+  list-style-image: url("chrome://messenger/skin/icons/move-top.svg");
+}
+
+#btn_moveAttachmentUp {
+  list-style-image: url("chrome://messenger/skin/icons/move-up.svg");
+}
+
+#btn_moveAttachmentDown {
+  list-style-image: url("chrome://messenger/skin/icons/move-down.svg");
+}
+
+#btn_moveAttachmentBottom {
+  list-style-image: url("chrome://messenger/skin/icons/move-bottom.svg");
+}
+
+#btn_moveAttachmentBundleUp {
+  list-style-image: url("chrome://messenger/skin/icons/move-together.svg");
+}
+
+#btn_sortAttachmentsToggle {
+  list-style-image: url("chrome://messenger/skin/icons/sort.svg");
+}
+
+#btn_sortAttachmentsToggle[data-sortdirection="descending"] > .toolbarbutton-icon {
+  transform: scaleY(-1);
+}
--- a/mail/themes/linux/mail/messenger.css
+++ b/mail/themes/linux/mail/messenger.css
@@ -34,16 +34,18 @@
   --toolbarbutton-active-background: rgba(154, 154, 154, .5) linear-gradient(rgba(255, 255, 255, .7), rgba(255, 255, 255, .4));
   --toolbarbutton-active-bordercolor: rgba(0, 0, 0, .3);
   --toolbarbutton-active-boxshadow: 0 1px 1px rgba(0, 0, 0, .1) inset, 0 0 1px rgba(0, 0, 0, .3) inset;
 
   --toolbarbutton-checkedhover-backgroundcolor: rgba(200, 200, 200, .5);
   --toolbarbutton-icon-fill-attention: #0a84ff;
 
   --lwt-header-image: none;
+  --arrowpanel-dimmed: hsla(0, 0%, 80%, .3);
+  --arrowpanel-dimmed-further: hsla(0, 0%, 80%, .45);
 }
 
 :root:-moz-lwtheme {
   --toolbar-bgcolor: rgba(255,255,255,.4);
   --toolbar-bgimage: none;
 
   --toolbarbutton-icon-fill-opacity: 1;
 }
--- a/mail/themes/osx/mail/compose/messengercompose.css
+++ b/mail/themes/osx/mail/compose/messengercompose.css
@@ -1215,8 +1215,42 @@ toolbarbutton.toolbarbutton-1 > .toolbar
 #msgcomposeWindow[sizemode="fullscreen"] > #titlebar {
   display: none;
 }
 
 #titlebar-buttonbox-container {
   margin-top: 3px;
   margin-inline-start: 7px;
 }
+
+/* ::::: Reorder Attachments Panel ::::: */
+
+#reorderAttachmentsPanel > .panel-arrowcontainer > .panel-arrowcontent {
+  --arrowpanel-padding: 4px;
+}
+
+#btn_moveAttachmentTop {
+  list-style-image: url("chrome://messenger/skin/icons/move-top.svg");
+}
+
+#btn_moveAttachmentUp {
+  list-style-image: url("chrome://messenger/skin/icons/move-up.svg");
+}
+
+#btn_moveAttachmentDown {
+  list-style-image: url("chrome://messenger/skin/icons/move-down.svg");
+}
+
+#btn_moveAttachmentBottom {
+  list-style-image: url("chrome://messenger/skin/icons/move-bottom.svg");
+}
+
+#btn_moveAttachmentBundleUp {
+  list-style-image: url("chrome://messenger/skin/icons/move-together.svg");
+}
+
+#btn_sortAttachmentsToggle {
+  list-style-image: url("chrome://messenger/skin/icons/sort.svg");
+}
+
+#btn_sortAttachmentsToggle[data-sortdirection="descending"] > .toolbarbutton-icon {
+  transform: scaleY(-1);
+}
--- a/mail/themes/osx/mail/messenger.css
+++ b/mail/themes/osx/mail/messenger.css
@@ -39,16 +39,18 @@
                                     0 1px 0 hsla(0, 0%, 0%, .05) inset,
                                     0 1px 1px hsla(0, 0%, 0%, .2) inset;
   --toolbarbutton-inactive-bordercolor: rgba(0, 0, 0, 0.1);
   --toolbarbutton-inactive-boxshadow: 0 1px 0 hsla(0, 0%, 0%, .05) inset;
   --toolbarbutton-checkedhover-backgroundcolor: hsla(0, 0%, 0%, .09);
   --toolbarbutton-icon-fill-attention: #0a84ff;
 
   --lwt-header-image: none;
+  --arrowpanel-dimmed: hsla(210, 4%, 10%, .07);
+  --arrowpanel-dimmed-further: hsla(210, 4%, 10%, .12);
 }
 
 :root:-moz-window-inactive {
   --toolbar-bgcolor: -moz-mac-chrome-inactive;
 }
 
 :root:-moz-lwtheme {
   --toolbar-bgcolor: rgba(255,255,255,.4);
--- a/mail/themes/shared/jar.inc.mn
+++ b/mail/themes/shared/jar.inc.mn
@@ -36,16 +36,21 @@
   skin/classic/messenger/icons/forward.svg                    (../shared/mail/icons/forward.svg)
   skin/classic/messenger/icons/getmsg.svg                     (../shared/mail/icons/getmsg.svg)
   skin/classic/messenger/icons/goback.svg                     (../shared/mail/icons/goback.svg)
   skin/classic/messenger/icons/goforward.svg                  (../shared/mail/icons/goforward.svg)
   skin/classic/messenger/icons/join.svg                       (../shared/mail/icons/join.svg)
   skin/classic/messenger/icons/junk.svg                       (../shared/mail/icons/junk.svg)
   skin/classic/messenger/icons/mark.svg                       (../shared/mail/icons/mark.svg)
   skin/classic/messenger/icons/message.svg                    (../shared/mail/icons/message.svg)
+  skin/classic/messenger/icons/move-bottom.svg                (../shared/mail/icons/move-bottom.svg)
+  skin/classic/messenger/icons/move-down.svg                  (../shared/mail/icons/move-down.svg)
+  skin/classic/messenger/icons/move-together.svg              (../shared/mail/icons/move-together.svg)
+  skin/classic/messenger/icons/move-top.svg                   (../shared/mail/icons/move-top.svg)
+  skin/classic/messenger/icons/move-up.svg                    (../shared/mail/icons/move-up.svg)
   skin/classic/messenger/icons/newmsg.svg                     (../shared/mail/icons/newmsg.svg)
   skin/classic/messenger/icons/nextmsg.svg                    (../shared/mail/icons/nextmsg.svg)
   skin/classic/messenger/icons/nextunread.svg                 (../shared/mail/icons/nextunread.svg)
   skin/classic/messenger/icons/overflow-indicator.png         (../shared/mail/icons/overflow-indicator.png)
   skin/classic/messenger/icons/paste.svg                      (../shared/mail/icons/paste.svg)
   skin/classic/messenger/icons/pluginBlocked.svg              (../shared/mail/icons/pluginBlocked.svg)
   skin/classic/messenger/icons/previousmsg.svg                (../shared/mail/icons/previousmsg.svg)
   skin/classic/messenger/icons/previousunread.svg             (../shared/mail/icons/previousunread.svg)
@@ -54,16 +59,17 @@
   skin/classic/messenger/icons/remote-blocked.svg             (../shared/mail/icons/remote-blocked.svg)
   skin/classic/messenger/icons/reply.svg                      (../shared/mail/icons/reply.svg)
   skin/classic/messenger/icons/replyall.svg                   (../shared/mail/icons/replyall.svg)
   skin/classic/messenger/icons/replylist.svg                  (../shared/mail/icons/replylist.svg)
   skin/classic/messenger/icons/search-glass.svg               (../shared/mail/icons/search-glass.svg)
   skin/classic/messenger/icons/save.svg                       (../shared/mail/icons/save.svg)
   skin/classic/messenger/icons/security.svg                   (../shared/mail/icons/security.svg)
   skin/classic/messenger/icons/send.svg                       (../shared/mail/icons/send.svg)
+  skin/classic/messenger/icons/sort.svg                       (../shared/mail/icons/sort.svg)
   skin/classic/messenger/icons/spelling.svg                   (../shared/mail/icons/spelling.svg)
   skin/classic/messenger/icons/star.svg                       (../shared/mail/icons/star.svg)
   skin/classic/messenger/icons/sticky.svg                     (../shared/mail/icons/sticky.svg)
   skin/classic/messenger/icons/stop.svg                       (../shared/mail/icons/stop.svg)
   skin/classic/messenger/icons/tag.svg                        (../shared/mail/icons/tag.svg)
   skin/classic/messenger/icons/toolbarbutton-arrow.svg        (../shared/mail/icons/toolbarbutton-arrow.svg)
   skin/classic/messenger/shared/accountProvisioner.css        (../shared/mail/accountProvisioner.css)
   skin/classic/messenger/shared/addressbook.css               (../shared/mail/addressbook.css)
new file mode 100644
--- /dev/null
+++ b/mail/themes/shared/mail/icons/move-bottom.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <path fill="context-fill" fill-opacity="context-fill-opacity" d="M3.7 3.3c-.94-.88-2.3.47-1.4 1.4l5 5.02c.38.4 1.02.4 1.4 0l5-5c.9-.95-.47-2.3-1.4-1.42L8 7.6zM13 11a1 1 0 1 1 0 2H3a1 1 0 1 1 0-2z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/mail/themes/shared/mail/icons/move-down.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <path fill="context-fill" fill-opacity="context-fill-opacity" d="M3.7 4.7c-.94-.88-2.3.47-1.4 1.4l5 5.02c.38.4 1.02.4 1.4 0l5-5c.9-.95-.47-2.3-1.4-1.42L8 9z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/mail/themes/shared/mail/icons/move-together.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <path fill="context-fill" fill-opacity="context-fill-opacity" d="M2 4h4c1.3-.04 1.3-1.96 0-2H2C.7 2.04.7 3.96 2 4zm4 3H2C.67 7 .67 9 2 9h4c1.33 0 1.33-2 0-2zm0 5H2c-1.33 0-1.33 2 0 2h4c1.33 0 1.33-2 0-2zM10 6h4c1.3-.04 1.3-1.96 0-2h-4c-1.3.04-1.3 1.96 0 2zm4 1h-4c-1.34 0-1.34 2 0 2h4c1.33 0 1.33-2 0-2zm0 3h-4c-1.33 0-1.33 2 0 2h4c1.33 0 1.33-2 0-2z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/mail/themes/shared/mail/icons/move-top.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <path fill="context-fill" fill-opacity="context-fill-opacity" d="M3.7 12.7c-.94.9-2.3-.46-1.4-1.4l5-5c.38-.4 1.02-.4 1.4 0l5 5c.9.94-.47 2.3-1.4 1.4L8 8.43zM13 5a1 1 0 1 0 0-2H3a1 1 0 1 0 0 2z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/mail/themes/shared/mail/icons/move-up.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <path fill="context-fill" fill-opacity="context-fill-opacity" d="M3.7 11.3c-.94.88-2.3-.47-1.4-1.4l5-5.02c.38-.4 1.02-.4 1.4 0l5 5c.9.95-.47 2.3-1.4 1.42L8 7z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/mail/themes/shared/mail/icons/sort.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <path fill="context-fill" fill-opacity="context-fill-opacity" d="M13 13c1.33 0 1.33-2 0-2H3c-1.33 0-1.33 2 0 2zm-2-4c1.33 0 1.33-2 0-2H5C3.67 7 3.67 9 5 9zM9 5c1.33 0 1.33-2 0-2H7C5.67 3 5.67 5 7 5z"/>
+</svg>
--- a/mail/themes/shared/mail/messenger.css
+++ b/mail/themes/shared/mail/messenger.css
@@ -101,8 +101,51 @@ notification[type="warning"] {
 
 .abMenuItem[AddrBook="true"][IsRemote="true"] {
   list-style-image: url("chrome://messenger/skin/addressbook/icons/remote-addrbook.png");
 }
 
 .abMenuItem[AddrBook="true"][IsRemote="true"][IsSecure="true"] {
   list-style-image: url("chrome://messenger/skin/addressbook/icons/secure-remote-addrbook.png");
 }
+
+/* ::::: Panel toolbarbuttons ::::: */
+
+.panelTitle {
+  margin-top: 8px;
+  margin-inline-start: 7px;
+  margin-bottom: 6px;
+}
+
+.panelButton {
+  -moz-appearance: none;
+  min-height: 24px;
+  padding: 4px 6px;
+  background-color: transparent;
+  -moz-context-properties: fill, fill-opacity;
+  fill: currentColor;
+  fill-opacity: var(--toolbarbutton-icon-fill-opacity);
+}
+
+.panelButton:focus {
+  outline: 0;
+}
+
+.panelButton:not(:-moz-any([disabled],[open],:active)):-moz-any(:hover,:focus) {
+  background-color: var(--arrowpanel-dimmed);
+}
+
+.panelButton:not([disabled]):-moz-any([open],:hover:active) {
+  background-color: var(--arrowpanel-dimmed-further);
+  box-shadow: 0 1px 0 hsla(210, 4%, 10%, .03) inset;
+}
+
+.panelButton > .toolbarbutton-text {
+  text-align: start;
+  padding-inline-start: 6px;
+  padding-inline-end: 6px;
+}
+
+.panelButton[prettykey]::after {
+  content: attr(prettykey);
+  float: right;
+  color: GrayText;
+}
--- a/mail/themes/windows/mail/compose/messengercompose.css
+++ b/mail/themes/windows/mail/compose/messengercompose.css
@@ -1009,8 +1009,42 @@ treechildren::-moz-tree-image(subscribed
   #headers-box {
     border-bottom-color: #aabccf;
   }
 
   #composeContentBox {
     background-image: url("chrome://messenger/skin/messengercompose/noise.png");
   }
 }
+
+/* ::::: Reorder Attachments Panel ::::: */
+
+#reorderAttachmentsPanel > .panel-arrowcontainer > .panel-arrowcontent {
+  --arrowpanel-padding: 4px;
+}
+
+#btn_moveAttachmentTop {
+  list-style-image: url("chrome://messenger/skin/icons/move-top.svg");
+}
+
+#btn_moveAttachmentUp {
+  list-style-image: url("chrome://messenger/skin/icons/move-up.svg");
+}
+
+#btn_moveAttachmentDown {
+  list-style-image: url("chrome://messenger/skin/icons/move-down.svg");
+}
+
+#btn_moveAttachmentBottom {
+  list-style-image: url("chrome://messenger/skin/icons/move-bottom.svg");
+}
+
+#btn_moveAttachmentBundleUp {
+  list-style-image: url("chrome://messenger/skin/icons/move-together.svg");
+}
+
+#btn_sortAttachmentsToggle {
+  list-style-image: url("chrome://messenger/skin/icons/sort.svg");
+}
+
+#btn_sortAttachmentsToggle[data-sortdirection="descending"] > .toolbarbutton-icon {
+  transform: scaleY(-1);
+}
--- a/mail/themes/windows/mail/messenger.css
+++ b/mail/themes/windows/mail/messenger.css
@@ -31,16 +31,18 @@
   --toolbarbutton-active-background: rgba(0, 0, 0, .15);
   --toolbarbutton-active-bordercolor: rgba(0, 0, 0, .15);
   --toolbarbutton-active-boxshadow: 0 0 0 1px rgba(0, 0, 0, .15) inset;
 
   --toolbarbutton-checkedhover-backgroundcolor: rgba(0, 0, 0, .2);
   --toolbarbutton-icon-fill-attention: #0a84ff;
 
   --lwt-header-image: none;
+  --arrowpanel-dimmed: hsla(0, 0%, 80%, .3);
+  --arrowpanel-dimmed-further: hsla(0, 0%, 80%, .45);
 }
 
 @media (-moz-windows-default-theme) {
   :root {
     --tabs-border-color: rgba(0,0,0,.3);
     --tab-line-color: #0a84ff;
 
     --toolbar-non-lwt-bgcolor: #f9f9fa;