diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -2972,26 +2972,32 @@
     'PhabricatorLogoutController' => 'applications/auth/controller/PhabricatorLogoutController.php',
     'PhabricatorLunarPhasePolicyRule' => 'applications/policy/rule/PhabricatorLunarPhasePolicyRule.php',
     'PhabricatorMacroApplication' => 'applications/macro/application/PhabricatorMacroApplication.php',
+    'PhabricatorMacroAudioBehaviorTransaction' => 'applications/macro/xaction/PhabricatorMacroAudioBehaviorTransaction.php',
     'PhabricatorMacroAudioController' => 'applications/macro/controller/PhabricatorMacroAudioController.php',
+    'PhabricatorMacroAudioTransaction' => 'applications/macro/xaction/PhabricatorMacroAudioTransaction.php',
     'PhabricatorMacroCommentController' => 'applications/macro/controller/PhabricatorMacroCommentController.php',
     'PhabricatorMacroConfigOptions' => 'applications/macro/config/PhabricatorMacroConfigOptions.php',
     'PhabricatorMacroController' => 'applications/macro/controller/PhabricatorMacroController.php',
     'PhabricatorMacroDatasource' => 'applications/macro/typeahead/PhabricatorMacroDatasource.php',
     'PhabricatorMacroDisableController' => 'applications/macro/controller/PhabricatorMacroDisableController.php',
+    'PhabricatorMacroDisabledTransaction' => 'applications/macro/xaction/PhabricatorMacroDisabledTransaction.php',
     'PhabricatorMacroEditController' => 'applications/macro/controller/PhabricatorMacroEditController.php',
     'PhabricatorMacroEditor' => 'applications/macro/editor/PhabricatorMacroEditor.php',
+    'PhabricatorMacroFileTransaction' => 'applications/macro/xaction/PhabricatorMacroFileTransaction.php',
     'PhabricatorMacroListController' => 'applications/macro/controller/PhabricatorMacroListController.php',
     'PhabricatorMacroMacroPHIDType' => 'applications/macro/phid/PhabricatorMacroMacroPHIDType.php',
     'PhabricatorMacroMailReceiver' => 'applications/macro/mail/PhabricatorMacroMailReceiver.php',
     'PhabricatorMacroManageCapability' => 'applications/macro/capability/PhabricatorMacroManageCapability.php',
     'PhabricatorMacroMemeController' => 'applications/macro/controller/PhabricatorMacroMemeController.php',
     'PhabricatorMacroMemeDialogController' => 'applications/macro/controller/PhabricatorMacroMemeDialogController.php',
+    'PhabricatorMacroNameTransaction' => 'applications/macro/xaction/PhabricatorMacroNameTransaction.php',
     'PhabricatorMacroQuery' => 'applications/macro/query/PhabricatorMacroQuery.php',
     'PhabricatorMacroReplyHandler' => 'applications/macro/mail/PhabricatorMacroReplyHandler.php',
     'PhabricatorMacroSearchEngine' => 'applications/macro/query/PhabricatorMacroSearchEngine.php',
     'PhabricatorMacroTransaction' => 'applications/macro/storage/PhabricatorMacroTransaction.php',
     'PhabricatorMacroTransactionComment' => 'applications/macro/storage/PhabricatorMacroTransactionComment.php',
     'PhabricatorMacroTransactionQuery' => 'applications/macro/query/PhabricatorMacroTransactionQuery.php',
+    'PhabricatorMacroTransactionType' => 'applications/macro/xaction/PhabricatorMacroTransactionType.php',
     'PhabricatorMacroViewController' => 'applications/macro/controller/PhabricatorMacroViewController.php',
     'PhabricatorMailEmailHeraldField' => 'applications/metamta/herald/PhabricatorMailEmailHeraldField.php',
     'PhabricatorMailEmailHeraldFieldGroup' => 'applications/metamta/herald/PhabricatorMailEmailHeraldFieldGroup.php',
@@ -8162,26 +8168,32 @@
     'PhabricatorLogoutController' => 'PhabricatorAuthController',
     'PhabricatorLunarPhasePolicyRule' => 'PhabricatorPolicyRule',
     'PhabricatorMacroApplication' => 'PhabricatorApplication',
+    'PhabricatorMacroAudioBehaviorTransaction' => 'PhabricatorMacroTransactionType',
     'PhabricatorMacroAudioController' => 'PhabricatorMacroController',
+    'PhabricatorMacroAudioTransaction' => 'PhabricatorMacroTransactionType',
     'PhabricatorMacroCommentController' => 'PhabricatorMacroController',
     'PhabricatorMacroConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorMacroController' => 'PhabricatorController',
     'PhabricatorMacroDatasource' => 'PhabricatorTypeaheadDatasource',
     'PhabricatorMacroDisableController' => 'PhabricatorMacroController',
+    'PhabricatorMacroDisabledTransaction' => 'PhabricatorMacroTransactionType',
     'PhabricatorMacroEditController' => 'PhabricatorMacroController',
     'PhabricatorMacroEditor' => 'PhabricatorApplicationTransactionEditor',
+    'PhabricatorMacroFileTransaction' => 'PhabricatorMacroTransactionType',
     'PhabricatorMacroListController' => 'PhabricatorMacroController',
     'PhabricatorMacroMacroPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorMacroMailReceiver' => 'PhabricatorObjectMailReceiver',
     'PhabricatorMacroManageCapability' => 'PhabricatorPolicyCapability',
     'PhabricatorMacroMemeController' => 'PhabricatorMacroController',
     'PhabricatorMacroMemeDialogController' => 'PhabricatorMacroController',
+    'PhabricatorMacroNameTransaction' => 'PhabricatorMacroTransactionType',
     'PhabricatorMacroQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorMacroReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler',
     'PhabricatorMacroSearchEngine' => 'PhabricatorApplicationSearchEngine',
-    'PhabricatorMacroTransaction' => 'PhabricatorApplicationTransaction',
+    'PhabricatorMacroTransaction' => 'PhabricatorModularTransaction',
     'PhabricatorMacroTransactionComment' => 'PhabricatorApplicationTransactionComment',
     'PhabricatorMacroTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+    'PhabricatorMacroTransactionType' => 'PhabricatorModularTransactionType',
     'PhabricatorMacroViewController' => 'PhabricatorMacroController',
     'PhabricatorMailEmailHeraldField' => 'HeraldField',
     'PhabricatorMailEmailHeraldFieldGroup' => 'HeraldFieldGroup',
diff --git a/src/applications/macro/controller/PhabricatorMacroAudioController.php b/src/applications/macro/controller/PhabricatorMacroAudioController.php
--- a/src/applications/macro/controller/PhabricatorMacroAudioController.php
+++ b/src/applications/macro/controller/PhabricatorMacroAudioController.php
@@ -34,7 +34,7 @@
       if ($request->getBool('behaviorForm')) {
         $xactions[] = id(new PhabricatorMacroTransaction())
           ->setTransactionType(
-            PhabricatorMacroTransaction::TYPE_AUDIO_BEHAVIOR)
+            PhabricatorMacroAudioBehaviorTransaction::TRANSACTIONTYPE)
           ->setNewValue($request->getStr('audioBehavior'));
       } else {
         $file = null;
@@ -54,7 +54,8 @@
             $e_file = pht('Invalid');
           } else {
             $xactions[] = id(new PhabricatorMacroTransaction())
-              ->setTransactionType(PhabricatorMacroTransaction::TYPE_AUDIO)
+              ->setTransactionType(
+                PhabricatorMacroAudioTransaction::TRANSACTIONTYPE)
               ->setNewValue($file->getPHID());
           }
         } else {
diff --git a/src/applications/macro/controller/PhabricatorMacroDisableController.php b/src/applications/macro/controller/PhabricatorMacroDisableController.php
--- a/src/applications/macro/controller/PhabricatorMacroDisableController.php
+++ b/src/applications/macro/controller/PhabricatorMacroDisableController.php
@@ -22,7 +22,8 @@
 
     if ($request->isDialogFormPost() || $macro->getIsDisabled()) {
       $xaction = id(new PhabricatorMacroTransaction())
-        ->setTransactionType(PhabricatorMacroTransaction::TYPE_DISABLED)
+        ->setTransactionType(
+          PhabricatorMacroDisabledTransaction::TRANSACTIONTYPE)
         ->setNewValue($macro->getIsDisabled() ? 0 : 1);
 
       $editor = id(new PhabricatorMacroEditor())
diff --git a/src/applications/macro/controller/PhabricatorMacroEditController.php b/src/applications/macro/controller/PhabricatorMacroEditController.php
--- a/src/applications/macro/controller/PhabricatorMacroEditController.php
+++ b/src/applications/macro/controller/PhabricatorMacroEditController.php
@@ -135,13 +135,15 @@
 
           if ($new_name !== null) {
             $xactions[] = id(new PhabricatorMacroTransaction())
-              ->setTransactionType(PhabricatorMacroTransaction::TYPE_NAME)
+              ->setTransactionType(
+                PhabricatorMacroNameTransaction::TRANSACTIONTYPE)
               ->setNewValue($new_name);
           }
 
           if ($file) {
             $xactions[] = id(new PhabricatorMacroTransaction())
-              ->setTransactionType(PhabricatorMacroTransaction::TYPE_FILE)
+              ->setTransactionType(
+                PhabricatorMacroFileTransaction::TRANSACTIONTYPE)
               ->setNewValue($file->getPHID());
           }
 
diff --git a/src/applications/macro/controller/PhabricatorMacroViewController.php b/src/applications/macro/controller/PhabricatorMacroViewController.php
--- a/src/applications/macro/controller/PhabricatorMacroViewController.php
+++ b/src/applications/macro/controller/PhabricatorMacroViewController.php
@@ -45,7 +45,7 @@
     if (!$macro->getIsDisabled()) {
       $header->setStatus('fa-check', 'bluegrey', pht('Active'));
     } else {
-      $header->setStatus('fa-ban', 'red', pht('Archived'));
+      $header->setStatus('fa-ban', 'indigo', pht('Archived'));
     }
 
     $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
diff --git a/src/applications/macro/editor/PhabricatorMacroEditor.php b/src/applications/macro/editor/PhabricatorMacroEditor.php
--- a/src/applications/macro/editor/PhabricatorMacroEditor.php
+++ b/src/applications/macro/editor/PhabricatorMacroEditor.php
@@ -13,79 +13,18 @@
 
   public function getTransactionTypes() {
     $types = parent::getTransactionTypes();
-
     $types[] = PhabricatorTransactions::TYPE_COMMENT;
-    $types[] = PhabricatorMacroTransaction::TYPE_NAME;
-    $types[] = PhabricatorMacroTransaction::TYPE_DISABLED;
-    $types[] = PhabricatorMacroTransaction::TYPE_FILE;
-    $types[] = PhabricatorMacroTransaction::TYPE_AUDIO;
-    $types[] = PhabricatorMacroTransaction::TYPE_AUDIO_BEHAVIOR;
 
     return $types;
   }
 
-  protected function getCustomTransactionOldValue(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case PhabricatorMacroTransaction::TYPE_NAME:
-        return $object->getName();
-      case PhabricatorMacroTransaction::TYPE_DISABLED:
-        return $object->getIsDisabled();
-      case PhabricatorMacroTransaction::TYPE_FILE:
-        return $object->getFilePHID();
-      case PhabricatorMacroTransaction::TYPE_AUDIO:
-        return $object->getAudioPHID();
-      case PhabricatorMacroTransaction::TYPE_AUDIO_BEHAVIOR:
-        return $object->getAudioBehavior();
-    }
-  }
-
-  protected function getCustomTransactionNewValue(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case PhabricatorMacroTransaction::TYPE_NAME:
-      case PhabricatorMacroTransaction::TYPE_DISABLED:
-      case PhabricatorMacroTransaction::TYPE_FILE:
-      case PhabricatorMacroTransaction::TYPE_AUDIO:
-      case PhabricatorMacroTransaction::TYPE_AUDIO_BEHAVIOR:
-        return $xaction->getNewValue();
-    }
-  }
-
-  protected function applyCustomInternalTransaction(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case PhabricatorMacroTransaction::TYPE_NAME:
-        $object->setName($xaction->getNewValue());
-        break;
-      case PhabricatorMacroTransaction::TYPE_DISABLED:
-        $object->setIsDisabled($xaction->getNewValue());
-        break;
-      case PhabricatorMacroTransaction::TYPE_FILE:
-        $object->setFilePHID($xaction->getNewValue());
-        break;
-      case PhabricatorMacroTransaction::TYPE_AUDIO:
-        $object->setAudioPHID($xaction->getNewValue());
-        break;
-      case PhabricatorMacroTransaction::TYPE_AUDIO_BEHAVIOR:
-        $object->setAudioBehavior($xaction->getNewValue());
-        break;
-    }
-  }
-
   protected function applyCustomExternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
-      case PhabricatorMacroTransaction::TYPE_FILE:
-      case PhabricatorMacroTransaction::TYPE_AUDIO:
+      case PhabricatorMacroFileTransaction::TRANSACTIONTYPE:
+      case PhabricatorMacroAudioTransaction::TRANSACTIONTYPE:
         // When changing a macro's image or audio, attach the underlying files
         // to the macro (and detach the old files).
         $old = $xaction->getOldValue();
@@ -117,34 +56,9 @@
     }
   }
 
-  protected function mergeTransactions(
-    PhabricatorApplicationTransaction $u,
-    PhabricatorApplicationTransaction $v) {
-
-    $type = $u->getTransactionType();
-    switch ($type) {
-      case PhabricatorMacroTransaction::TYPE_NAME:
-      case PhabricatorMacroTransaction::TYPE_DISABLED:
-      case PhabricatorMacroTransaction::TYPE_FILE:
-      case PhabricatorMacroTransaction::TYPE_AUDIO:
-      case PhabricatorMacroTransaction::TYPE_AUDIO_BEHAVIOR:
-        return $v;
-    }
-
-    return parent::mergeTransactions($u, $v);
-  }
-
   protected function shouldSendMail(
     PhabricatorLiskDAO $object,
     array $xactions) {
-    foreach ($xactions as $xaction) {
-      switch ($xaction->getTransactionType()) {
-        case PhabricatorMacroTransaction::TYPE_NAME;
-          return ($xaction->getOldValue() !== null);
-        default:
-          break;
-      }
-    }
     return true;
   }
 
diff --git a/src/applications/macro/storage/PhabricatorMacroTransaction.php b/src/applications/macro/storage/PhabricatorMacroTransaction.php
--- a/src/applications/macro/storage/PhabricatorMacroTransaction.php
+++ b/src/applications/macro/storage/PhabricatorMacroTransaction.php
@@ -1,14 +1,7 @@
 <?php
 
 final class PhabricatorMacroTransaction
-  extends PhabricatorApplicationTransaction {
-
-  const TYPE_NAME       = 'macro:name';
-  const TYPE_DISABLED   = 'macro:disabled';
-  const TYPE_FILE       = 'macro:file';
-
-  const TYPE_AUDIO = 'macro:audio';
-  const TYPE_AUDIO_BEHAVIOR = 'macro:audiobehavior';
+  extends PhabricatorModularTransaction {
 
   public function getApplicationName() {
     return 'file';
@@ -26,284 +19,8 @@
     return new PhabricatorMacroTransactionComment();
   }
 
-  public function getRequiredHandlePHIDs() {
-    $phids = parent::getRequiredHandlePHIDs();
-
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_FILE:
-      case self::TYPE_AUDIO:
-        if ($old !== null) {
-          $phids[] = $old;
-        }
-        $phids[] = $new;
-        break;
-    }
-
-    return $phids;
-  }
-
-  public function shouldHide() {
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_NAME:
-        return ($old === null);
-    }
-
-    return parent::shouldHide();
-  }
-
-  public function getTitle() {
-    $author_phid = $this->getAuthorPHID();
-
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_NAME:
-        return pht(
-          '%s renamed this macro from "%s" to "%s".',
-          $this->renderHandleLink($author_phid),
-          $old,
-          $new);
-        break;
-      case self::TYPE_DISABLED:
-        if ($new) {
-          return pht(
-            '%s disabled this macro.',
-            $this->renderHandleLink($author_phid));
-        } else {
-          return pht(
-            '%s restored this macro.',
-            $this->renderHandleLink($author_phid));
-        }
-        break;
-
-      case self::TYPE_AUDIO:
-        if (!$old) {
-          return pht(
-            '%s attached audio: %s.',
-            $this->renderHandleLink($author_phid),
-            $this->renderHandleLink($new));
-        } else {
-          return pht(
-            '%s changed the audio for this macro from %s to %s.',
-            $this->renderHandleLink($author_phid),
-            $this->renderHandleLink($old),
-            $this->renderHandleLink($new));
-        }
-
-      case self::TYPE_AUDIO_BEHAVIOR:
-        switch ($new) {
-          case PhabricatorFileImageMacro::AUDIO_BEHAVIOR_ONCE:
-            return pht(
-              '%s set the audio to play once.',
-              $this->renderHandleLink($author_phid));
-          case PhabricatorFileImageMacro::AUDIO_BEHAVIOR_LOOP:
-            return pht(
-              '%s set the audio to loop.',
-              $this->renderHandleLink($author_phid));
-          default:
-            return pht(
-              '%s disabled the audio for this macro.',
-              $this->renderHandleLink($author_phid));
-        }
-
-      case self::TYPE_FILE:
-        if ($old === null) {
-          return pht(
-            '%s created this macro.',
-            $this->renderHandleLink($author_phid));
-        } else {
-          return pht(
-            '%s changed the image for this macro from %s to %s.',
-            $this->renderHandleLink($author_phid),
-            $this->renderHandleLink($old),
-            $this->renderHandleLink($new));
-        }
-        break;
-    }
-
-    return parent::getTitle();
-  }
-
-  public function getTitleForFeed() {
-    $author_phid = $this->getAuthorPHID();
-    $object_phid = $this->getObjectPHID();
-
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_NAME:
-        return pht(
-          '%s renamed %s from "%s" to "%s".',
-          $this->renderHandleLink($author_phid),
-          $this->renderHandleLink($object_phid),
-          $old,
-          $new);
-      case self::TYPE_DISABLED:
-        if ($new) {
-          return pht(
-            '%s disabled %s.',
-            $this->renderHandleLink($author_phid),
-            $this->renderHandleLink($object_phid));
-        } else {
-          return pht(
-            '%s restored %s.',
-            $this->renderHandleLink($author_phid),
-            $this->renderHandleLink($object_phid));
-        }
-      case self::TYPE_FILE:
-        if ($old === null) {
-          return pht(
-            '%s created %s.',
-            $this->renderHandleLink($author_phid),
-            $this->renderHandleLink($object_phid));
-        } else {
-          return pht(
-            '%s updated the image for %s.',
-            $this->renderHandleLink($author_phid),
-            $this->renderHandleLink($object_phid));
-        }
-
-      case self::TYPE_AUDIO:
-        if (!$old) {
-          return pht(
-            '%s attached audio to %s: %s.',
-            $this->renderHandleLink($author_phid),
-            $this->renderHandleLink($object_phid),
-            $this->renderHandleLink($new));
-        } else {
-          return pht(
-            '%s changed the audio for %s from %s to %s.',
-            $this->renderHandleLink($author_phid),
-            $this->renderHandleLink($object_phid),
-            $this->renderHandleLink($old),
-            $this->renderHandleLink($new));
-        }
-
-      case self::TYPE_AUDIO_BEHAVIOR:
-        switch ($new) {
-          case PhabricatorFileImageMacro::AUDIO_BEHAVIOR_ONCE:
-            return pht(
-              '%s set the audio for %s to play once.',
-              $this->renderHandleLink($author_phid),
-              $this->renderHandleLink($object_phid));
-          case PhabricatorFileImageMacro::AUDIO_BEHAVIOR_LOOP:
-            return pht(
-              '%s set the audio for %s to loop.',
-              $this->renderHandleLink($author_phid),
-              $this->renderHandleLink($object_phid));
-          default:
-            return pht(
-              '%s disabled the audio for %s.',
-              $this->renderHandleLink($author_phid),
-              $this->renderHandleLink($object_phid));
-        }
-
-    }
-
-    return parent::getTitleForFeed();
-  }
-
-  public function getActionName() {
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_NAME:
-        if ($old === null) {
-          return pht('Created');
-        } else {
-          return pht('Renamed');
-        }
-      case self::TYPE_DISABLED:
-        if ($new) {
-          return pht('Disabled');
-        } else {
-          return pht('Restored');
-        }
-      case self::TYPE_FILE:
-        if ($old === null) {
-          return pht('Created');
-        } else {
-          return pht('Edited Image');
-        }
-
-      case self::TYPE_AUDIO:
-        return pht('Audio');
-
-      case self::TYPE_AUDIO_BEHAVIOR:
-        return pht('Audio Behavior');
-
-    }
-
-    return parent::getActionName();
-  }
-
-  public function getActionStrength() {
-    switch ($this->getTransactionType()) {
-      case self::TYPE_DISABLED:
-        return 2.0;
-      case self::TYPE_FILE:
-        return 1.5;
-    }
-    return parent::getActionStrength();
-  }
-
-  public function getIcon() {
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_NAME:
-        return 'fa-pencil';
-      case self::TYPE_FILE:
-        if ($old === null) {
-          return 'fa-plus';
-        } else {
-          return 'fa-pencil';
-        }
-      case self::TYPE_DISABLED:
-        if ($new) {
-          return 'fa-times';
-        } else {
-          return 'fa-undo';
-        }
-      case self::TYPE_AUDIO:
-        return 'fa-headphones';
-    }
-
-    return parent::getIcon();
-  }
-
-  public function getColor() {
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_NAME:
-        return PhabricatorTransactions::COLOR_BLUE;
-      case self::TYPE_FILE:
-        if ($old === null) {
-          return PhabricatorTransactions::COLOR_GREEN;
-        } else {
-          return PhabricatorTransactions::COLOR_BLUE;
-        }
-      case self::TYPE_DISABLED:
-        if ($new) {
-          return PhabricatorTransactions::COLOR_RED;
-        } else {
-          return PhabricatorTransactions::COLOR_SKY;
-        }
-    }
-
-    return parent::getColor();
+  public function getBaseTransactionClass() {
+    return 'PhabricatorMacroTransactionType';
   }
 
 
diff --git a/src/applications/macro/xaction/PhabricatorMacroAudioBehaviorTransaction.php b/src/applications/macro/xaction/PhabricatorMacroAudioBehaviorTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/macro/xaction/PhabricatorMacroAudioBehaviorTransaction.php
@@ -0,0 +1,69 @@
+<?php
+
+final class PhabricatorMacroAudioBehaviorTransaction
+  extends PhabricatorMacroTransactionType {
+
+  const TRANSACTIONTYPE = 'macro:audiobehavior';
+
+  public function generateOldValue($object) {
+    return $object->getAudioBehavior();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setAudioBehavior($value);
+  }
+
+  public function getTitle() {
+    $new = $this->getNewValue();
+    $old = $this->getOldValue();
+    switch ($new) {
+      case PhabricatorFileImageMacro::AUDIO_BEHAVIOR_ONCE:
+        return pht(
+          '%s set the audio to play once.',
+          $this->renderAuthor());
+      case PhabricatorFileImageMacro::AUDIO_BEHAVIOR_LOOP:
+        return pht(
+          '%s set the audio to loop.',
+          $this->renderAuthor());
+      default:
+        return pht(
+          '%s disabled the audio for this macro.',
+          $this->renderAuthor());
+    }
+  }
+
+  public function getTitleForFeed() {
+    $new = $this->getNewValue();
+    $old = $this->getOldValue();
+    switch ($new) {
+      case PhabricatorFileImageMacro::AUDIO_BEHAVIOR_ONCE:
+        return pht(
+          '%s set the audio for %s to play once.',
+          $this->renderAuthor(),
+          $this->renderObject());
+      case PhabricatorFileImageMacro::AUDIO_BEHAVIOR_LOOP:
+        return pht(
+          '%s set the audio for %s to loop.',
+          $this->renderAuthor(),
+          $this->renderObject());
+      default:
+        return pht(
+          '%s disabled the audio for %s.',
+          $this->renderAuthor(),
+          $this->renderObject());
+    }
+  }
+
+  public function getIcon() {
+    $new = $this->getNewValue();
+    switch ($new) {
+      case PhabricatorFileImageMacro::AUDIO_BEHAVIOR_ONCE:
+        return 'fa-play-circle';
+      case PhabricatorFileImageMacro::AUDIO_BEHAVIOR_LOOP:
+        return 'fa-repeat';
+      default:
+        return 'fa-pause-circle';
+    }
+  }
+
+}
diff --git a/src/applications/macro/xaction/PhabricatorMacroAudioTransaction.php b/src/applications/macro/xaction/PhabricatorMacroAudioTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/macro/xaction/PhabricatorMacroAudioTransaction.php
@@ -0,0 +1,56 @@
+<?php
+
+final class PhabricatorMacroAudioTransaction
+  extends PhabricatorMacroTransactionType {
+
+  const TRANSACTIONTYPE = 'macro:audio';
+
+  public function generateOldValue($object) {
+    return $object->getAudioPHID();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setAudioPHID($value);
+  }
+
+  public function getTitle() {
+    $new = $this->getNewValue();
+    $old = $this->getOldValue();
+    if (!$old) {
+      return pht(
+        '%s attached audio: %s.',
+        $this->renderAuthor(),
+        $this->renderHandle($new));
+    } else {
+      return pht(
+        '%s changed the audio for this macro from %s to %s.',
+        $this->renderAuthor(),
+        $this->renderHandle($old),
+        $this->renderHandle($new));
+    }
+  }
+
+  public function getTitleForFeed() {
+    $new = $this->getNewValue();
+    $old = $this->getOldValue();
+    if (!$old) {
+      return pht(
+        '%s attached audio to %s: %s.',
+        $this->renderAuthor(),
+        $this->renderObject(),
+        $this->renderHandle($new));
+    } else {
+      return pht(
+        '%s changed the audio for %s from %s to %s.',
+        $this->renderAuthor(),
+        $this->renderObject(),
+        $this->renderHandle($old),
+        $this->renderHandle($new));
+    }
+  }
+
+  public function getIcon() {
+    return 'fa-music';
+  }
+
+}
diff --git a/src/applications/macro/xaction/PhabricatorMacroDisabledTransaction.php b/src/applications/macro/xaction/PhabricatorMacroDisabledTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/macro/xaction/PhabricatorMacroDisabledTransaction.php
@@ -0,0 +1,50 @@
+<?php
+
+final class PhabricatorMacroDisabledTransaction
+  extends PhabricatorMacroTransactionType {
+
+  const TRANSACTIONTYPE = 'macro:disabled';
+
+  public function generateOldValue($object) {
+    return $object->getIsDisabled();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setIsDisabled($value);
+  }
+
+  public function getTitle() {
+    if ($this->getNewValue()) {
+      return pht(
+        '%s disabled this macro.',
+        $this->renderAuthor());
+    } else {
+      return pht(
+        '%s restored this macro.',
+        $this->renderAuthor());
+    }
+  }
+
+  public function getTitleForFeed() {
+    if ($this->getNewValue()) {
+      return pht(
+        '%s disabled %s.',
+        $this->renderAuthor(),
+        $this->renderObject());
+    } else {
+      return pht(
+        '%s restored %s.',
+        $this->renderAuthor(),
+        $this->renderObject());
+    }
+  }
+
+  public function getIcon() {
+    if ($this->getNewValue()) {
+      return 'fa-ban';
+    } else {
+      return 'fa-check';
+    }
+  }
+
+}
diff --git a/src/applications/macro/xaction/PhabricatorMacroFileTransaction.php b/src/applications/macro/xaction/PhabricatorMacroFileTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/macro/xaction/PhabricatorMacroFileTransaction.php
@@ -0,0 +1,33 @@
+<?php
+
+final class PhabricatorMacroFileTransaction
+  extends PhabricatorMacroTransactionType {
+
+  const TRANSACTIONTYPE = 'macro:file';
+
+  public function generateOldValue($object) {
+    return $object->getFilePHID();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setFilePHID($value);
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s changed the image for this macro.',
+      $this->renderAuthor());
+  }
+
+  public function getTitleForFeed() {
+    return pht(
+      '%s changed the image for macro %s.',
+      $this->renderAuthor(),
+      $this->renderObject());
+  }
+
+  public function getIcon() {
+    return 'fa-file-image-o';
+  }
+
+}
diff --git a/src/applications/macro/xaction/PhabricatorMacroNameTransaction.php b/src/applications/macro/xaction/PhabricatorMacroNameTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/macro/xaction/PhabricatorMacroNameTransaction.php
@@ -0,0 +1,55 @@
+<?php
+
+final class PhabricatorMacroNameTransaction
+  extends PhabricatorMacroTransactionType {
+
+  const TRANSACTIONTYPE = 'macro:name';
+
+  public function generateOldValue($object) {
+    return $object->getName();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setName($value);
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s renamed this macro from %s to %s.',
+      $this->renderAuthor(),
+      $this->renderOldValue(),
+      $this->renderNewValue());
+  }
+
+  public function getTitleForFeed() {
+    return pht(
+      '%s renamed %s macro %s to %s.',
+      $this->renderAuthor(),
+      $this->renderObject(),
+      $this->renderOldValue(),
+      $this->renderNewValue());
+  }
+
+  public function validateTransactions($object, array $xactions) {
+    $errors = array();
+
+    if ($this->isEmptyTextTransaction($object->getName(), $xactions)) {
+      $errors[] = $this->newRequiredError(
+        pht('Macros must have a name.'));
+    }
+
+    $max_length = $object->getColumnMaximumByteLength('name');
+    foreach ($xactions as $xaction) {
+      $new_value = $xaction->getNewValue();
+      $new_length = strlen($new_value);
+      if ($new_length > $max_length) {
+        $errors[] = $this->newInvalidError(
+          pht('The name can be no longer than %s characters.',
+          new PhutilNumber($max_length)));
+      }
+    }
+
+    return $errors;
+  }
+
+}
diff --git a/src/applications/macro/xaction/PhabricatorMacroTransactionType.php b/src/applications/macro/xaction/PhabricatorMacroTransactionType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/macro/xaction/PhabricatorMacroTransactionType.php
@@ -0,0 +1,4 @@
+<?php
+
+abstract class PhabricatorMacroTransactionType
+  extends PhabricatorModularTransactionType {}