diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
index 115d8e7b50..85f458aacf 100644
--- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php
+++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
@@ -1,649 +1,644 @@
 <?php
 
 final class ManiphestTransactionEditor
   extends PhabricatorApplicationTransactionEditor {
 
   private $heraldEmailPHIDs = array();
 
   public function getTransactionTypes() {
     $types = parent::getTransactionTypes();
 
     $types[] = PhabricatorTransactions::TYPE_COMMENT;
     $types[] = PhabricatorTransactions::TYPE_EDGE;
     $types[] = ManiphestTransaction::TYPE_PRIORITY;
     $types[] = ManiphestTransaction::TYPE_STATUS;
     $types[] = ManiphestTransaction::TYPE_TITLE;
     $types[] = ManiphestTransaction::TYPE_DESCRIPTION;
     $types[] = ManiphestTransaction::TYPE_OWNER;
     $types[] = ManiphestTransaction::TYPE_CCS;
     $types[] = ManiphestTransaction::TYPE_SUBPRIORITY;
     $types[] = ManiphestTransaction::TYPE_PROJECT_COLUMN;
     $types[] = ManiphestTransaction::TYPE_UNBLOCK;
     $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
     $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
 
     return $types;
   }
 
   protected function getCustomTransactionOldValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case ManiphestTransaction::TYPE_PRIORITY:
         if ($this->getIsNewObject()) {
           return null;
         }
         return (int)$object->getPriority();
       case ManiphestTransaction::TYPE_STATUS:
         if ($this->getIsNewObject()) {
           return null;
         }
         return $object->getStatus();
       case ManiphestTransaction::TYPE_TITLE:
         if ($this->getIsNewObject()) {
           return null;
         }
         return $object->getTitle();
       case ManiphestTransaction::TYPE_DESCRIPTION:
         if ($this->getIsNewObject()) {
           return null;
         }
         return $object->getDescription();
       case ManiphestTransaction::TYPE_OWNER:
         return nonempty($object->getOwnerPHID(), null);
       case ManiphestTransaction::TYPE_CCS:
         return array_values(array_unique($object->getCCPHIDs()));
       case ManiphestTransaction::TYPE_PROJECT_COLUMN:
         // These are pre-populated.
         return $xaction->getOldValue();
       case ManiphestTransaction::TYPE_SUBPRIORITY:
         return $object->getSubpriority();
     }
   }
 
   protected function getCustomTransactionNewValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case ManiphestTransaction::TYPE_PRIORITY:
         return (int)$xaction->getNewValue();
       case ManiphestTransaction::TYPE_CCS:
         return array_values(array_unique($xaction->getNewValue()));
       case ManiphestTransaction::TYPE_OWNER:
         return nonempty($xaction->getNewValue(), null);
       case ManiphestTransaction::TYPE_STATUS:
       case ManiphestTransaction::TYPE_TITLE:
       case ManiphestTransaction::TYPE_DESCRIPTION:
       case ManiphestTransaction::TYPE_SUBPRIORITY:
       case ManiphestTransaction::TYPE_PROJECT_COLUMN:
       case ManiphestTransaction::TYPE_UNBLOCK:
         return $xaction->getNewValue();
     }
   }
 
   protected function transactionHasEffect(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     $old = $xaction->getOldValue();
     $new = $xaction->getNewValue();
 
     switch ($xaction->getTransactionType()) {
       case ManiphestTransaction::TYPE_CCS:
         sort($old);
         sort($new);
         return ($old !== $new);
       case ManiphestTransaction::TYPE_PROJECT_COLUMN:
         $new_column_phids = $new['columnPHIDs'];
         $old_column_phids = $old['columnPHIDs'];
         sort($new_column_phids);
         sort($old_column_phids);
         return ($old !== $new);
     }
 
     return parent::transactionHasEffect($object, $xaction);
   }
 
   protected function applyCustomInternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case ManiphestTransaction::TYPE_PRIORITY:
         return $object->setPriority($xaction->getNewValue());
       case ManiphestTransaction::TYPE_STATUS:
         return $object->setStatus($xaction->getNewValue());
       case ManiphestTransaction::TYPE_TITLE:
         return $object->setTitle($xaction->getNewValue());
       case ManiphestTransaction::TYPE_DESCRIPTION:
         return $object->setDescription($xaction->getNewValue());
       case ManiphestTransaction::TYPE_OWNER:
         $phid = $xaction->getNewValue();
 
         // Update the "ownerOrdering" column to contain the full name of the
         // owner, if the task is assigned.
 
         $handle = null;
         if ($phid) {
           $handle = id(new PhabricatorHandleQuery())
             ->setViewer($this->getActor())
             ->withPHIDs(array($phid))
             ->executeOne();
         }
 
         if ($handle) {
           $object->setOwnerOrdering($handle->getName());
         } else {
           $object->setOwnerOrdering(null);
         }
 
         return $object->setOwnerPHID($phid);
       case ManiphestTransaction::TYPE_CCS:
         return $object->setCCPHIDs($xaction->getNewValue());
       case ManiphestTransaction::TYPE_SUBPRIORITY:
         $data = $xaction->getNewValue();
         $new_sub = $this->getNextSubpriority(
           $data['newPriority'],
           $data['newSubpriorityBase'],
           $data['direction']);
         $object->setSubpriority($new_sub);
         return;
       case ManiphestTransaction::TYPE_PROJECT_COLUMN:
         // these do external (edge) updates
         return;
     }
   }
 
   protected function expandTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     $xactions = parent::expandTransaction($object, $xaction);
     switch ($xaction->getTransactionType()) {
       case ManiphestTransaction::TYPE_SUBPRIORITY:
         $data = $xaction->getNewValue();
         $new_pri = $data['newPriority'];
         if ($new_pri != $object->getPriority()) {
           $xactions[] = id(new ManiphestTransaction())
             ->setTransactionType(ManiphestTransaction::TYPE_PRIORITY)
             ->setNewValue($new_pri);
         }
         break;
       default:
         break;
     }
 
     return $xactions;
   }
 
   protected function applyCustomExternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case ManiphestTransaction::TYPE_PROJECT_COLUMN:
         $board_phid = idx($xaction->getNewValue(), 'projectPHID');
         if (!$board_phid) {
           throw new Exception(
             pht("Expected 'projectPHID' in column transaction."));
         }
 
         $old_phids = idx($xaction->getOldValue(), 'columnPHIDs', array());
         $new_phids = idx($xaction->getNewValue(), 'columnPHIDs', array());
         if (count($new_phids) !== 1) {
           throw new Exception(
             pht("Expected exactly one 'columnPHIDs' in column transaction."));
         }
 
         $columns = id(new PhabricatorProjectColumnQuery())
           ->setViewer($this->requireActor())
           ->withPHIDs($new_phids)
           ->execute();
         $columns = mpull($columns, null, 'getPHID');
 
         $positions = id(new PhabricatorProjectColumnPositionQuery())
           ->setViewer($this->requireActor())
           ->withObjectPHIDs(array($object->getPHID()))
           ->withBoardPHIDs(array($board_phid))
           ->execute();
 
         $before_phid = idx($xaction->getNewValue(), 'beforePHID');
         $after_phid = idx($xaction->getNewValue(), 'afterPHID');
 
         if (!$before_phid && !$after_phid && ($old_phids == $new_phids)) {
           // If we are not moving the object between columns and also not
           // reordering the position, this is a move on some other order
           // (like priority). We can leave the positions untouched and just
           // bail, there's no work to be done.
           return;
         }
 
         // Otherwise, we're either moving between columns or adjusting the
         // object's position in the "natural" ordering, so we do need to update
         // some rows.
 
         // Remove all existing column positions on the board.
 
         foreach ($positions as $position) {
           $position->delete();
         }
 
         // Add the new column positions.
 
         foreach ($new_phids as $phid) {
           $column = idx($columns, $phid);
           if (!$column) {
             throw new Exception(
               pht('No such column "%s" exists!', $phid));
           }
 
           // Load the other object positions in the column. Note that we must
           // skip implicit column creation to avoid generating a new position
           // if the target column is a backlog column.
 
           $other_positions = id(new PhabricatorProjectColumnPositionQuery())
             ->setViewer($this->requireActor())
             ->withColumns(array($column))
             ->withBoardPHIDs(array($board_phid))
             ->setSkipImplicitCreate(true)
             ->execute();
           $other_positions = msort($other_positions, 'getOrderingKey');
 
           // Set up the new position object. We're going to figure out the
           // right sequence number and then persist this object with that
           // sequence number.
           $new_position = id(new PhabricatorProjectColumnPosition())
             ->setBoardPHID($board_phid)
             ->setColumnPHID($column->getPHID())
             ->setObjectPHID($object->getPHID());
 
           $updates = array();
           $sequence = 0;
 
           // If we're just dropping this into the column without any specific
           // position information, put it at the top.
           if (!$before_phid && !$after_phid) {
             $new_position->setSequence($sequence)->save();
             $sequence++;
           }
 
           foreach ($other_positions as $position) {
             $object_phid = $position->getObjectPHID();
 
             // If this is the object we're moving before and we haven't
             // saved yet, insert here.
             if (($before_phid == $object_phid) && !$new_position->getID()) {
               $new_position->setSequence($sequence)->save();
               $sequence++;
             }
 
             // This object goes here in the sequence; we might need to update
             // the row.
             if ($sequence != $position->getSequence()) {
               $updates[$position->getID()] = $sequence;
             }
             $sequence++;
 
             // If this is the object we're moving after and we haven't saved
             // yet, insert here.
             if (($after_phid == $object_phid) && !$new_position->getID()) {
               $new_position->setSequence($sequence)->save();
               $sequence++;
             }
           }
 
           // We should have found a place to put it.
           if (!$new_position->getID()) {
             throw new Exception(
               pht('Unable to find a place to insert object on column!'));
           }
 
           // If we changed other objects' column positions, bulk reorder them.
 
           if ($updates) {
             $position = new PhabricatorProjectColumnPosition();
             $conn_w = $position->establishConnection('w');
 
             $pairs = array();
             foreach ($updates as $id => $sequence) {
               // This is ugly because MySQL gets upset with us if it is
               // configured strictly and we attempt inserts which can't work.
               // We'll never actually do these inserts since they'll always
               // collide (triggering the ON DUPLICATE KEY logic), so we just
               // provide dummy values in order to get there.
 
               $pairs[] = qsprintf(
                 $conn_w,
                 '(%d, %d, "", "", "")',
                 $id,
                 $sequence);
             }
 
             queryfx(
               $conn_w,
               'INSERT INTO %T (id, sequence, boardPHID, columnPHID, objectPHID)
                 VALUES %Q ON DUPLICATE KEY UPDATE sequence = VALUES(sequence)',
               $position->getTableName(),
               implode(', ', $pairs));
           }
         }
         break;
       default:
         break;
     }
   }
 
   protected function applyFinalEffects(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     // When we change the status of a task, update tasks this tasks blocks
     // with a message to the effect of "alincoln resolved blocking task Txxx."
     $unblock_xaction = null;
     foreach ($xactions as $xaction) {
       switch ($xaction->getTransactionType()) {
         case ManiphestTransaction::TYPE_STATUS:
           $unblock_xaction = $xaction;
           break;
       }
     }
 
     if ($unblock_xaction !== null) {
       $blocked_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
         $object->getPHID(),
         PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK);
       if ($blocked_phids) {
         // In theory we could apply these through policies, but that seems a
         // little bit surprising. For now, use the actor's vision.
         $blocked_tasks = id(new ManiphestTaskQuery())
           ->setViewer($this->getActor())
           ->withPHIDs($blocked_phids)
           ->execute();
 
         $old = $unblock_xaction->getOldValue();
         $new = $unblock_xaction->getNewValue();
 
         foreach ($blocked_tasks as $blocked_task) {
           $unblock_xactions = array();
 
           $unblock_xactions[] = id(new ManiphestTransaction())
             ->setTransactionType(ManiphestTransaction::TYPE_UNBLOCK)
             ->setOldValue(array($object->getPHID() => $old))
             ->setNewValue(array($object->getPHID() => $new));
 
-          // TODO: We should avoid notifiying users about these indirect
-          // changes if they are getting a notification about the current
-          // change, so you don't get a pile of extra notifications if you are
-          // subscribed to this task.
-
           id(new ManiphestTransactionEditor())
             ->setActor($this->getActor())
             ->setActingAsPHID($this->getActingAsPHID())
             ->setContentSource($this->getContentSource())
             ->setContinueOnNoEffect(true)
             ->setContinueOnMissingFields(true)
             ->applyTransactions($blocked_task, $unblock_xactions);
         }
       }
     }
 
     return $xactions;
   }
 
   protected function shouldSendMail(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $xactions = mfilter($xactions, 'shouldHide', true);
     return $xactions;
   }
 
   protected function getMailSubjectPrefix() {
     return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix');
   }
 
   protected function getMailThreadID(PhabricatorLiskDAO $object) {
     return 'maniphest-task-'.$object->getPHID();
   }
 
   protected function getMailTo(PhabricatorLiskDAO $object) {
     return array(
       $object->getOwnerPHID(),
       $this->getActingAsPHID(),
     );
   }
 
   protected function getMailCC(PhabricatorLiskDAO $object) {
     $phids = array();
 
     foreach ($object->getCCPHIDs() as $phid) {
       $phids[] = $phid;
     }
 
     foreach (parent::getMailCC($object) as $phid) {
       $phids[] = $phid;
     }
 
     foreach ($this->heraldEmailPHIDs as $phid) {
       $phids[] = $phid;
     }
 
     return $phids;
   }
 
   protected function buildReplyHandler(PhabricatorLiskDAO $object) {
     return id(new ManiphestReplyHandler())
       ->setMailReceiver($object);
   }
 
   protected function buildMailTemplate(PhabricatorLiskDAO $object) {
     $id = $object->getID();
     $title = $object->getTitle();
 
     return id(new PhabricatorMetaMTAMail())
       ->setSubject("T{$id}: {$title}")
       ->addHeader('Thread-Topic', "T{$id}: ".$object->getOriginalTitle());
   }
 
   protected function buildMailBody(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $body = parent::buildMailBody($object, $xactions);
 
     if ($this->getIsNewObject()) {
       $body->addTextSection(
         pht('TASK DESCRIPTION'),
         $object->getDescription());
     }
 
     $body->addTextSection(
       pht('TASK DETAIL'),
       PhabricatorEnv::getProductionURI('/T'.$object->getID()));
 
     return $body;
   }
 
   protected function shouldPublishFeedStory(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return $this->shouldSendMail($object, $xactions);
   }
 
   protected function supportsSearch() {
     return true;
   }
 
   protected function shouldApplyHeraldRules(
     PhabricatorLiskDAO $object,
     array $xactions) {
     return true;
   }
 
   protected function buildHeraldAdapter(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     return id(new HeraldManiphestTaskAdapter())
       ->setTask($object);
   }
 
   protected function didApplyHeraldRules(
     PhabricatorLiskDAO $object,
     HeraldAdapter $adapter,
     HeraldTranscript $transcript) {
 
     // TODO: Convert these to transactions. The way Maniphest deals with these
     // transactions is currently unconventional and messy.
 
     $save_again = false;
     $cc_phids = $adapter->getCcPHIDs();
     if ($cc_phids) {
       $existing_cc = $object->getCCPHIDs();
       $new_cc = array_unique(array_merge($cc_phids, $existing_cc));
       $object->setCCPHIDs($new_cc);
       $object->save();
     }
 
     $this->heraldEmailPHIDs = $adapter->getEmailPHIDs();
 
     $xactions = array();
 
     $assign_phid = $adapter->getAssignPHID();
     if ($assign_phid) {
       $xactions[] = id(new ManiphestTransaction())
         ->setTransactionType(ManiphestTransaction::TYPE_OWNER)
         ->setNewValue($assign_phid);
     }
 
     $project_phids = $adapter->getProjectPHIDs();
     if ($project_phids) {
       $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
       $xactions[] = id(new ManiphestTransaction())
         ->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
         ->setMetadataValue('edge:type', $project_type)
         ->setNewValue(
           array(
             '+' => array_fuse($project_phids),
           ));
     }
 
     return $xactions;
   }
 
   protected function requireCapabilities(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     parent::requireCapabilities($object, $xaction);
 
     $app_capability_map = array(
       ManiphestTransaction::TYPE_PRIORITY =>
         ManiphestEditPriorityCapability::CAPABILITY,
       ManiphestTransaction::TYPE_STATUS =>
         ManiphestEditStatusCapability::CAPABILITY,
       ManiphestTransaction::TYPE_OWNER =>
         ManiphestEditAssignCapability::CAPABILITY,
       PhabricatorTransactions::TYPE_EDIT_POLICY =>
         ManiphestEditPoliciesCapability::CAPABILITY,
       PhabricatorTransactions::TYPE_VIEW_POLICY =>
         ManiphestEditPoliciesCapability::CAPABILITY,
     );
 
 
     $transaction_type = $xaction->getTransactionType();
 
     $app_capability = null;
     if ($transaction_type == PhabricatorTransactions::TYPE_EDGE) {
       switch ($xaction->getMetadataValue('edge:type')) {
         case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST:
           $app_capability = ManiphestEditProjectsCapability::CAPABILITY;
           break;
       }
     } else {
       $app_capability = idx($app_capability_map, $transaction_type);
     }
 
     if ($app_capability) {
       $app = id(new PhabricatorApplicationQuery())
         ->setViewer($this->getActor())
         ->withClasses(array('PhabricatorManiphestApplication'))
         ->executeOne();
       PhabricatorPolicyFilter::requireCapability(
         $this->getActor(),
         $app,
         $app_capability);
     }
   }
 
   protected function adjustObjectForPolicyChecks(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $copy = parent::adjustObjectForPolicyChecks($object, $xactions);
     foreach ($xactions as $xaction) {
       switch ($xaction->getTransactionType()) {
         case ManiphestTransaction::TYPE_OWNER:
           $copy->setOwnerPHID($xaction->getNewValue());
           break;
         default:
           continue;
       }
     }
 
     return $copy;
   }
 
   private function getNextSubpriority($pri, $sub, $dir = '>') {
     switch ($dir) {
       case '>':
         $order = 'ASC';
         break;
       case '<':
         $order = 'DESC';
         break;
       default:
         throw new Exception('$dir must be ">" or "<".');
         break;
     }
 
     if ($sub === null) {
       $base = 0;
     } else {
       $base = $sub;
     }
 
     if ($sub === null) {
       $next = id(new ManiphestTask())->loadOneWhere(
         'priority = %d ORDER BY subpriority %Q LIMIT 1',
         $pri,
         $order);
       if ($next) {
         if ($dir == '>') {
           return $next->getSubpriority() - ((double)(2 << 16));
         } else {
           return $next->getSubpriority() + ((double)(2 << 16));
         }
       }
     } else {
       $next = id(new ManiphestTask())->loadOneWhere(
         'priority = %d AND subpriority %Q %f ORDER BY subpriority %Q LIMIT 1',
         $pri,
         $dir,
         $sub,
         $order);
       if ($next) {
         return ($sub + $next->getSubpriority()) / 2;
       }
     }
 
     if ($dir == '>') {
       return $base + (double)(2 << 32);
     } else {
       return $base - (double)(2 << 32);
     }
   }
 
 }
diff --git a/src/applications/maniphest/storage/ManiphestTransaction.php b/src/applications/maniphest/storage/ManiphestTransaction.php
index ac0ada0178..3d5716c656 100644
--- a/src/applications/maniphest/storage/ManiphestTransaction.php
+++ b/src/applications/maniphest/storage/ManiphestTransaction.php
@@ -1,852 +1,878 @@
 <?php
 
 final class ManiphestTransaction
   extends PhabricatorApplicationTransaction {
 
   const TYPE_TITLE = 'title';
   const TYPE_STATUS = 'status';
   const TYPE_DESCRIPTION = 'description';
   const TYPE_OWNER  = 'reassign';
   const TYPE_CCS = 'ccs';
   const TYPE_PROJECTS = 'projects';
   const TYPE_PRIORITY = 'priority';
   const TYPE_EDGE = 'edge';
   const TYPE_SUBPRIORITY = 'subpriority';
   const TYPE_PROJECT_COLUMN = 'projectcolumn';
 
   const TYPE_UNBLOCK = 'unblock';
 
   // NOTE: this type is deprecated. Keep it around for legacy installs
   // so any transactions render correctly.
   const TYPE_ATTACH = 'attach';
 
   public function getApplicationName() {
     return 'maniphest';
   }
 
   public function getApplicationTransactionType() {
     return ManiphestTaskPHIDType::TYPECONST;
   }
 
   public function getApplicationTransactionCommentObject() {
     return new ManiphestTransactionComment();
   }
 
   public function shouldGenerateOldValue() {
     switch ($this->getTransactionType()) {
       case self::TYPE_PROJECT_COLUMN:
       case self::TYPE_EDGE:
       case self::TYPE_UNBLOCK:
         return false;
     }
 
     return parent::shouldGenerateOldValue();
   }
 
   public function getRemarkupBlocks() {
     $blocks = parent::getRemarkupBlocks();
 
     switch ($this->getTransactionType()) {
       case self::TYPE_DESCRIPTION:
         $blocks[] = $this->getNewValue();
         break;
     }
 
     return $blocks;
   }
 
   public function getRequiredHandlePHIDs() {
     $phids = parent::getRequiredHandlePHIDs();
 
     $new = $this->getNewValue();
     $old = $this->getOldValue();
 
     switch ($this->getTransactionType()) {
       case self::TYPE_OWNER:
         if ($new) {
           $phids[] = $new;
         }
 
         if ($old) {
           $phids[] = $old;
         }
         break;
       case self::TYPE_CCS:
       case self::TYPE_PROJECTS:
         $phids = array_mergev(
           array(
             $phids,
             nonempty($old, array()),
             nonempty($new, array()),
           ));
         break;
       case self::TYPE_PROJECT_COLUMN:
         $phids[] = $new['projectPHID'];
         $phids[] = head($new['columnPHIDs']);
         break;
       case self::TYPE_EDGE:
         $phids = array_mergev(
           array(
             $phids,
             array_keys(nonempty($old, array())),
             array_keys(nonempty($new, array())),
           ));
         break;
       case self::TYPE_ATTACH:
         $old = nonempty($old, array());
         $new = nonempty($new, array());
         $phids = array_mergev(
           array(
             $phids,
             array_keys(idx($new, 'FILE', array())),
             array_keys(idx($old, 'FILE', array())),
           ));
         break;
       case self::TYPE_UNBLOCK:
         foreach (array_keys($new) as $phid) {
           $phids[] = $phid;
         }
         break;
     }
 
     return $phids;
   }
 
   public function shouldHide() {
     switch ($this->getTransactionType()) {
       case self::TYPE_DESCRIPTION:
       case self::TYPE_PRIORITY:
       case self::TYPE_STATUS:
         if ($this->getOldValue() === null) {
           return true;
         } else {
           return false;
         }
         break;
       case self::TYPE_SUBPRIORITY:
         return true;
       case self::TYPE_PROJECT_COLUMN:
         $old_cols = idx($this->getOldValue(), 'columnPHIDs');
         $new_cols = idx($this->getNewValue(), 'columnPHIDs');
 
         $old_cols = array_values($old_cols);
         $new_cols = array_values($new_cols);
         sort($old_cols);
         sort($new_cols);
 
         return ($old_cols === $new_cols);
     }
 
     return parent::shouldHide();
   }
 
   public function getActionStrength() {
     switch ($this->getTransactionType()) {
       case self::TYPE_TITLE:
         return 1.4;
       case self::TYPE_STATUS:
         return 1.3;
       case self::TYPE_OWNER:
         return 1.2;
       case self::TYPE_PRIORITY:
         return 1.1;
     }
 
     return parent::getActionStrength();
   }
 
 
   public function getColor() {
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
       case self::TYPE_OWNER:
         if ($this->getAuthorPHID() == $new) {
           return 'green';
         } else if (!$new) {
           return 'black';
         } else if (!$old) {
           return 'green';
         } else {
           return 'green';
         }
 
       case self::TYPE_STATUS:
         $color = ManiphestTaskStatus::getStatusColor($new);
         if ($color !== null) {
           return $color;
         }
 
         if (ManiphestTaskStatus::isOpenStatus($new)) {
           return 'green';
         } else {
           return 'black';
         }
 
       case self::TYPE_PRIORITY:
         if ($old == ManiphestTaskPriority::getDefaultPriority()) {
           return 'green';
         } else if ($old > $new) {
           return 'grey';
         } else {
           return 'yellow';
         }
 
     }
 
     return parent::getColor();
   }
 
   public function getActionName() {
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
       case self::TYPE_TITLE:
         if ($old === null) {
           return pht('Created');
         }
 
         return pht('Retitled');
 
       case self::TYPE_STATUS:
         $action = ManiphestTaskStatus::getStatusActionName($new);
         if ($action) {
           return $action;
         }
 
         $old_closed = ManiphestTaskStatus::isClosedStatus($old);
         $new_closed = ManiphestTaskStatus::isClosedStatus($new);
 
         if ($new_closed && !$old_closed) {
           return pht('Closed');
         } else if (!$new_closed && $old_closed) {
           return pht('Reopened');
         } else {
           return pht('Changed Status');
         }
 
       case self::TYPE_DESCRIPTION:
         return pht('Edited');
 
       case self::TYPE_OWNER:
         if ($this->getAuthorPHID() == $new) {
           return pht('Claimed');
         } else if (!$new) {
           return pht('Up For Grabs');
         } else if (!$old) {
           return pht('Assigned');
         } else {
           return pht('Reassigned');
         }
 
       case self::TYPE_CCS:
         return pht('Changed CC');
 
       case self::TYPE_PROJECTS:
         return pht('Changed Projects');
 
       case self::TYPE_PROJECT_COLUMN:
         return pht('Changed Project Column');
 
       case self::TYPE_PRIORITY:
         if ($old == ManiphestTaskPriority::getDefaultPriority()) {
           return pht('Triaged');
         } else if ($old > $new) {
           return pht('Lowered Priority');
         } else {
           return pht('Raised Priority');
         }
 
       case self::TYPE_EDGE:
       case self::TYPE_ATTACH:
         return pht('Attached');
 
       case self::TYPE_UNBLOCK:
         $old_status = head($old);
         $new_status = head($new);
 
         $old_closed = ManiphestTaskStatus::isClosedStatus($old_status);
         $new_closed = ManiphestTaskStatus::isClosedStatus($new_status);
 
         if ($old_closed && !$new_closed) {
           return pht('Block');
         } else if (!$old_closed && $new_closed) {
           return pht('Unblock');
         } else {
           return pht('Blocker');
         }
 
     }
 
     return parent::getActionName();
   }
 
   public function getIcon() {
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
       case self::TYPE_OWNER:
         return 'fa-user';
 
       case self::TYPE_CCS:
         return 'fa-envelope';
 
       case self::TYPE_TITLE:
         if ($old === null) {
           return 'fa-pencil';
         }
 
         return 'fa-pencil';
 
       case self::TYPE_STATUS:
         $action = ManiphestTaskStatus::getStatusIcon($new);
         if ($action !== null) {
           return $action;
         }
 
         if (ManiphestTaskStatus::isClosedStatus($new)) {
           return 'fa-check';
         } else {
           return 'fa-pencil';
         }
 
       case self::TYPE_DESCRIPTION:
         return 'fa-pencil';
 
       case self::TYPE_PROJECTS:
         return 'fa-briefcase';
 
       case self::TYPE_PROJECT_COLUMN:
         return 'fa-columns';
 
       case self::TYPE_PRIORITY:
         if ($old == ManiphestTaskPriority::getDefaultPriority()) {
           return 'fa-arrow-right';
         } else if ($old > $new) {
           return 'fa-arrow-down';
         } else {
           return 'fa-arrow-up';
         }
 
       case self::TYPE_EDGE:
       case self::TYPE_ATTACH:
         return 'fa-thumb-tack';
 
       case self::TYPE_UNBLOCK:
         return 'fa-shield';
 
     }
 
     return parent::getIcon();
   }
 
 
 
   public function getTitle() {
     $author_phid = $this->getAuthorPHID();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
       case self::TYPE_TITLE:
         if ($old === null) {
           return pht(
             '%s created this task.',
             $this->renderHandleLink($author_phid));
         }
         return pht(
           '%s changed the title from "%s" to "%s".',
           $this->renderHandleLink($author_phid),
           $old,
           $new);
 
       case self::TYPE_DESCRIPTION:
         return pht(
           '%s edited the task description.',
           $this->renderHandleLink($author_phid));
 
       case self::TYPE_STATUS:
         $old_closed = ManiphestTaskStatus::isClosedStatus($old);
         $new_closed = ManiphestTaskStatus::isClosedStatus($new);
 
         $old_name = ManiphestTaskStatus::getTaskStatusName($old);
         $new_name = ManiphestTaskStatus::getTaskStatusName($new);
 
         if ($new_closed && !$old_closed) {
           if ($new == ManiphestTaskStatus::getDuplicateStatus()) {
             return pht(
               '%s closed this task as a duplicate.',
               $this->renderHandleLink($author_phid));
           } else {
             return pht(
               '%s closed this task as "%s".',
               $this->renderHandleLink($author_phid),
               $new_name);
           }
         } else if (!$new_closed && $old_closed) {
           return pht(
             '%s reopened this task as "%s".',
             $this->renderHandleLink($author_phid),
             $new_name);
         } else {
           return pht(
             '%s changed the task status from "%s" to "%s".',
             $this->renderHandleLink($author_phid),
             $old_name,
             $new_name);
         }
 
       case self::TYPE_UNBLOCK:
         $blocker_phid = key($new);
         $old_status = head($old);
         $new_status = head($new);
 
         $old_closed = ManiphestTaskStatus::isClosedStatus($old_status);
         $new_closed = ManiphestTaskStatus::isClosedStatus($new_status);
 
         $old_name = ManiphestTaskStatus::getTaskStatusName($old_status);
         $new_name = ManiphestTaskStatus::getTaskStatusName($new_status);
 
         if ($old_closed && !$new_closed) {
           return pht(
             '%s reopened blocking task %s as "%s".',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($blocker_phid),
             $new_name);
         } else if (!$old_closed && $new_closed) {
           return pht(
             '%s closed blocking task %s as "%s".',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($blocker_phid),
             $new_name);
         } else {
           return pht(
             '%s changed the status of blocking task %s from "%s" to "%s".',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($blocker_phid),
             $old_name,
             $new_name);
         }
 
       case self::TYPE_OWNER:
         if ($author_phid == $new) {
           return pht(
             '%s claimed this task.',
             $this->renderHandleLink($author_phid));
         } else if (!$new) {
           return pht(
             '%s placed this task up for grabs.',
             $this->renderHandleLink($author_phid));
         } else if (!$old) {
           return pht(
             '%s assigned this task to %s.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($new));
         } else {
           return pht(
             '%s reassigned this task from %s to %s.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($old),
             $this->renderHandleLink($new));
         }
 
       case self::TYPE_PROJECTS:
         $added = array_diff($new, $old);
         $removed = array_diff($old, $new);
         if ($added && !$removed) {
           return pht(
             '%s added %d project(s): %s',
             $this->renderHandleLink($author_phid),
             count($added),
             $this->renderHandleList($added));
         } else if ($removed && !$added) {
           return pht(
             '%s removed %d project(s): %s',
             $this->renderHandleLink($author_phid),
             count($removed),
             $this->renderHandleList($removed));
         } else if ($removed && $added) {
           return pht(
             '%s changed project(s), added %d: %s; removed %d: %s',
             $this->renderHandleLink($author_phid),
             count($added),
             $this->renderHandleList($added),
             count($removed),
             $this->renderHandleList($removed));
         } else {
           // This is hit when rendering previews.
           return pht(
             '%s changed projects...',
             $this->renderHandleLink($author_phid));
         }
 
       case self::TYPE_PRIORITY:
         $old_name = ManiphestTaskPriority::getTaskPriorityName($old);
         $new_name = ManiphestTaskPriority::getTaskPriorityName($new);
 
         if ($old == ManiphestTaskPriority::getDefaultPriority()) {
           return pht(
             '%s triaged this task as "%s" priority.',
             $this->renderHandleLink($author_phid),
             $new_name);
         } else if ($old > $new) {
           return pht(
             '%s lowered the priority of this task from "%s" to "%s".',
             $this->renderHandleLink($author_phid),
             $old_name,
             $new_name);
         } else {
           return pht(
             '%s raised the priority of this task from "%s" to "%s".',
             $this->renderHandleLink($author_phid),
             $old_name,
             $new_name);
         }
 
       case self::TYPE_CCS:
         // TODO: Remove this when we switch to subscribers. Just reuse the
         // code in the parent.
         $clone = clone $this;
         $clone->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
         return $clone->getTitle();
 
       case self::TYPE_EDGE:
         // TODO: Remove this when we switch to real edges. Just reuse the
         // code in the parent;
         $clone = clone $this;
         $clone->setTransactionType(PhabricatorTransactions::TYPE_EDGE);
         return $clone->getTitle();
 
       case self::TYPE_ATTACH:
         $old = nonempty($old, array());
         $new = nonempty($new, array());
         $new = array_keys(idx($new, 'FILE', array()));
         $old = array_keys(idx($old, 'FILE', array()));
 
         $added = array_diff($new, $old);
         $removed = array_diff($old, $new);
         if ($added && !$removed) {
           return pht(
             '%s attached %d file(s): %s',
             $this->renderHandleLink($author_phid),
             count($added),
             $this->renderHandleList($added));
         } else if ($removed && !$added) {
           return pht(
             '%s detached %d file(s): %s',
             $this->renderHandleLink($author_phid),
             count($removed),
             $this->renderHandleList($removed));
         } else {
           return pht(
             '%s changed file(s), attached %d: %s; detached %d: %s',
             $this->renderHandleLink($author_phid),
             count($added),
             $this->renderHandleList($added),
             count($removed),
             $this->renderHandleList($removed));
         }
 
       case self::TYPE_PROJECT_COLUMN:
         $project_phid = $new['projectPHID'];
         $column_phid = head($new['columnPHIDs']);
         return pht(
           '%s moved this task to %s on the %s workboard.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($column_phid),
           $this->renderHandleLink($project_phid));
        break;
 
 
     }
 
     return parent::getTitle();
   }
 
   public function getTitleForFeed(PhabricatorFeedStory $story) {
     $author_phid = $this->getAuthorPHID();
     $object_phid = $this->getObjectPHID();
 
     $old = $this->getOldValue();
     $new = $this->getNewValue();
 
     switch ($this->getTransactionType()) {
       case self::TYPE_TITLE:
         if ($old === null) {
           return pht(
             '%s created %s.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid));
         }
 
         return pht(
           '%s renamed %s from "%s" to "%s".',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid),
           $old,
           $new);
 
       case self::TYPE_DESCRIPTION:
         return pht(
           '%s edited the description of %s.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid));
 
       case self::TYPE_STATUS:
         $old_closed = ManiphestTaskStatus::isClosedStatus($old);
         $new_closed = ManiphestTaskStatus::isClosedStatus($new);
 
         $old_name = ManiphestTaskStatus::getTaskStatusName($old);
         $new_name = ManiphestTaskStatus::getTaskStatusName($new);
 
         if ($new_closed && !$old_closed) {
           if ($new == ManiphestTaskStatus::getDuplicateStatus()) {
             return pht(
               '%s closed %s as a duplicate.',
               $this->renderHandleLink($author_phid),
               $this->renderHandleLink($object_phid));
           } else {
             return pht(
               '%s closed %s as "%s".',
               $this->renderHandleLink($author_phid),
               $this->renderHandleLink($object_phid),
               $new_name);
           }
         } else if (!$new_closed && $old_closed) {
           return pht(
             '%s reopened %s as "%s".',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid),
             $new_name);
         } else {
           return pht(
             '%s changed the status of %s from "%s" to "%s".',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid),
             $old_name,
             $new_name);
         }
 
       case self::TYPE_UNBLOCK:
+        $blocker_phid = key($new);
+        $old_status = head($old);
+        $new_status = head($new);
 
-        // TODO: We should probably not show these in feed; they're highly
-        // redundant. For now, just use the normal titles. Right now, we can't
-        // publish something to noficiations without also publishing it to feed.
-        // Fix that, then stop these from rendering in feed only.
+        $old_closed = ManiphestTaskStatus::isClosedStatus($old_status);
+        $new_closed = ManiphestTaskStatus::isClosedStatus($new_status);
 
-        break;
+        $old_name = ManiphestTaskStatus::getTaskStatusName($old_status);
+        $new_name = ManiphestTaskStatus::getTaskStatusName($new_status);
 
+        if ($old_closed && !$new_closed) {
+          return pht(
+            '%s reopened %s, a task blocking %s, as "%s".',
+            $this->renderHandleLink($author_phid),
+            $this->renderHandleLink($blocker_phid),
+            $this->renderHandleLink($object_phid),
+            $new_name);
+        } else if (!$old_closed && $new_closed) {
+          return pht(
+            '%s closed %s, a task blocking %s, as "%s".',
+            $this->renderHandleLink($author_phid),
+            $this->renderHandleLink($blocker_phid),
+            $this->renderHandleLink($object_phid),
+            $new_name);
+        } else {
+          return pht(
+            '%s changed the status of %s, a task blocking %s, '.
+            'from "%s" to "%s".',
+            $this->renderHandleLink($author_phid),
+            $this->renderHandleLink($blocker_phid),
+            $this->renderHandleLink($object_phid),
+            $old_name,
+            $new_name);
+        }
 
       case self::TYPE_OWNER:
         if ($author_phid == $new) {
           return pht(
             '%s claimed %s.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid));
         } else if (!$new) {
           return pht(
             '%s placed %s up for grabs.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid));
         } else if (!$old) {
           return pht(
             '%s assigned %s to %s.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid),
             $this->renderHandleLink($new));
         } else {
           return pht(
             '%s reassigned %s from %s to %s.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid),
             $this->renderHandleLink($old),
             $this->renderHandleLink($new));
         }
 
       case self::TYPE_PROJECTS:
         $added = array_diff($new, $old);
         $removed = array_diff($old, $new);
         if ($added && !$removed) {
           return pht(
             '%s added %d project(s) to %s: %s',
             $this->renderHandleLink($author_phid),
             count($added),
             $this->renderHandleLink($object_phid),
             $this->renderHandleList($added));
         } else if ($removed && !$added) {
           return pht(
             '%s removed %d project(s) from %s: %s',
             $this->renderHandleLink($author_phid),
             count($removed),
             $this->renderHandleLink($object_phid),
             $this->renderHandleList($removed));
         } else if ($removed && $added) {
           return pht(
             '%s changed project(s) of %s, added %d: %s; removed %d: %s',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid),
             count($added),
             $this->renderHandleList($added),
             count($removed),
             $this->renderHandleList($removed));
         }
 
       case self::TYPE_PRIORITY:
         $old_name = ManiphestTaskPriority::getTaskPriorityName($old);
         $new_name = ManiphestTaskPriority::getTaskPriorityName($new);
 
         if ($old == ManiphestTaskPriority::getDefaultPriority()) {
           return pht(
             '%s triaged %s as "%s" priority.',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid),
             $new_name);
         } else if ($old > $new) {
           return pht(
             '%s lowered the priority of %s from "%s" to "%s".',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid),
             $old_name,
             $new_name);
         } else {
           return pht(
             '%s raised the priority of %s from "%s" to "%s".',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid),
             $old_name,
             $new_name);
         }
 
       case self::TYPE_CCS:
         // TODO: Remove this when we switch to subscribers. Just reuse the
         // code in the parent.
         $clone = clone $this;
         $clone->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS);
         return $clone->getTitleForFeed($story);
 
       case self::TYPE_EDGE:
         // TODO: Remove this when we switch to real edges. Just reuse the
         // code in the parent;
         $clone = clone $this;
         $clone->setTransactionType(PhabricatorTransactions::TYPE_EDGE);
         return $clone->getTitleForFeed($story);
 
       case self::TYPE_ATTACH:
         $old = nonempty($old, array());
         $new = nonempty($new, array());
         $new = array_keys(idx($new, 'FILE', array()));
         $old = array_keys(idx($old, 'FILE', array()));
 
         $added = array_diff($new, $old);
         $removed = array_diff($old, $new);
         if ($added && !$removed) {
           return pht(
             '%s attached %d file(s) of %s: %s',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid),
             count($added),
             $this->renderHandleList($added));
         } else if ($removed && !$added) {
           return pht(
             '%s detached %d file(s) of %s: %s',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid),
             count($removed),
             $this->renderHandleList($removed));
         } else {
           return pht(
             '%s changed file(s) for %s, attached %d: %s; detached %d: %s',
             $this->renderHandleLink($author_phid),
             $this->renderHandleLink($object_phid),
             count($added),
             $this->renderHandleList($added),
             count($removed),
             $this->renderHandleList($removed));
         }
 
       case self::TYPE_PROJECT_COLUMN:
         $project_phid = $new['projectPHID'];
         $column_phid = head($new['columnPHIDs']);
         return pht(
           '%s moved %s to %s on the %s workboard.',
           $this->renderHandleLink($author_phid),
           $this->renderHandleLink($object_phid),
           $this->renderHandleLink($column_phid),
           $this->renderHandleLink($project_phid));
        break;
     }
 
     return parent::getTitleForFeed($story);
   }
 
   public function hasChangeDetails() {
     switch ($this->getTransactionType()) {
       case self::TYPE_DESCRIPTION:
         return true;
     }
     return parent::hasChangeDetails();
   }
 
   public function renderChangeDetails(PhabricatorUser $viewer) {
     return $this->renderTextCorpusChangeDetails(
       $viewer,
       $this->getOldValue(),
       $this->getNewValue());
   }
 
   public function getMailTags() {
     $tags = array();
     switch ($this->getTransactionType()) {
       case self::TYPE_STATUS:
         $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_STATUS;
         break;
       case self::TYPE_OWNER:
         $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_OWNER;
         break;
       case self::TYPE_CCS:
         $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_CC;
         break;
       case PhabricatorTransactions::TYPE_EDGE:
         switch ($this->getMetadataValue('edge:type')) {
           case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST:
             $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PROJECTS;
             break;
           default:
             $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_OTHER;
             break;
         }
         break;
       case self::TYPE_PRIORITY:
         $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PRIORITY;
         break;
       case PhabricatorTransactions::TYPE_COMMENT:
         $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_COMMENT;
         break;
       default:
         $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_OTHER;
         break;
     }
     return $tags;
   }
 
   public function getNoEffectDescription() {
 
     switch ($this->getTransactionType()) {
       case self::TYPE_STATUS:
         return pht('The task already has the selected status.');
       case self::TYPE_OWNER:
         return pht('The task already has the selected owner.');
       case self::TYPE_PROJECTS:
         return pht('The task is already associated with those projects.');
       case self::TYPE_PRIORITY:
         return pht('The task already has the selected priority.');
     }
 
     return parent::getNoEffectDescription();
   }
 
 }