diff --git a/src/applications/project/controller/PhabricatorProjectEditDetailsController.php b/src/applications/project/controller/PhabricatorProjectEditDetailsController.php
--- a/src/applications/project/controller/PhabricatorProjectEditDetailsController.php
+++ b/src/applications/project/controller/PhabricatorProjectEditDetailsController.php
@@ -77,10 +77,6 @@
       $v_icon = $request->getStr('icon');
       $v_locked = $request->getInt('is_membership_locked', 0);
 
-      $xactions = $field_list->buildFieldTransactionsFromRequest(
-        new PhabricatorProjectTransaction(),
-        $request);
-
       $type_name = PhabricatorProjectTransaction::TYPE_NAME;
       $type_slugs = PhabricatorProjectTransaction::TYPE_SLUGS;
       $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY;
@@ -88,6 +84,8 @@
       $type_color = PhabricatorProjectTransaction::TYPE_COLOR;
       $type_locked = PhabricatorProjectTransaction::TYPE_LOCKED;
 
+      $xactions = array();
+
       $xactions[] = id(new PhabricatorProjectTransaction())
         ->setTransactionType($type_name)
         ->setNewValue($v_name);
@@ -120,6 +118,12 @@
         ->setTransactionType($type_locked)
         ->setNewValue($v_locked);
 
+      $xactions = array_merge(
+        $xactions,
+        $field_list->buildFieldTransactionsFromRequest(
+          new PhabricatorProjectTransaction(),
+          $request));
+
       $editor = id(new PhabricatorProjectTransactionEditor())
         ->setActor($viewer)
         ->setContentSourceFromRequest($request)
diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
--- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
+++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php
@@ -403,6 +403,19 @@
     return parent::requireCapabilities($object, $xaction);
   }
 
+  /**
+   * Note: this is implemented for Feed purposes.
+   */
+  protected function getMailTo(PhabricatorLiskDAO $object) {
+    return array();
+  }
+
+  protected function shouldPublishFeedStory(
+    PhabricatorLiskDAO $object,
+    array $xactions) {
+    return true;
+  }
+
   protected function supportsSearch() {
     return true;
   }
diff --git a/src/applications/project/storage/PhabricatorProjectTransaction.php b/src/applications/project/storage/PhabricatorProjectTransaction.php
--- a/src/applications/project/storage/PhabricatorProjectTransaction.php
+++ b/src/applications/project/storage/PhabricatorProjectTransaction.php
@@ -226,6 +226,125 @@
     return parent::getTitle();
   }
 
+  public function getTitleForFeed() {
+    $author_phid = $this->getAuthorPHID();
+    $object_phid = $this->getObjectPHID();
+    $author_handle = $this->renderHandleLink($author_phid);
+    $object_handle = $this->renderHandleLink($object_phid);
+
+    $old = $this->getOldValue();
+    $new = $this->getNewValue();
+
+    switch ($this->getTransactionType()) {
+      case self::TYPE_NAME:
+        if ($old === null) {
+          return pht(
+            '%s created %s.',
+            $author_handle,
+            $object_handle);
+        } else {
+          return pht(
+            '%s renamed %s from "%s" to "%s".',
+            $author_handle,
+            $object_handle,
+            $old,
+            $new);
+        }
+      case self::TYPE_STATUS:
+        if ($old == 0) {
+          return pht(
+            '%s archived %s.',
+            $author_handle,
+            $object_handle);
+        } else {
+          return pht(
+            '%s activated %s.',
+            $author_handle,
+            $object_handle);
+        }
+      case self::TYPE_IMAGE:
+        // TODO: Some day, it would be nice to show the images.
+        if (!$old) {
+          return pht(
+            '%s set the image for %s to %s.',
+            $author_handle,
+            $object_handle,
+            $this->renderHandleLink($new));
+        } else if (!$new) {
+          return pht(
+            '%s removed the image for %s.',
+            $author_handle,
+            $object_handle);
+        } else {
+          return pht(
+            '%s updated the image for %s from %s to %s.',
+            $author_handle,
+            $object_handle,
+            $this->renderHandleLink($old),
+            $this->renderHandleLink($new));
+        }
+
+      case self::TYPE_ICON:
+        return pht(
+          '%s set the icon for %s to %s.',
+          $author_handle,
+          $object_handle,
+          PhabricatorProjectIcon::getLabel($new));
+
+      case self::TYPE_COLOR:
+        return pht(
+          '%s set the color for %s to %s.',
+          $author_handle,
+          $object_handle,
+          PHUITagView::getShadeName($new));
+
+      case self::TYPE_LOCKED:
+        if ($new) {
+          return pht(
+            '%s locked %s membership.',
+            $author_handle,
+            $object_handle);
+        } else {
+          return pht(
+            '%s unlocked %s membership.',
+            $author_handle,
+            $object_handle);
+        }
+
+      case self::TYPE_SLUGS:
+        $add = array_diff($new, $old);
+        $rem = array_diff($old, $new);
+
+        if ($add && $rem) {
+          return pht(
+            '%s changed %s hashtag(s), added %d: %s; removed %d: %s.',
+            $author_handle,
+            $object_handle,
+            count($add),
+            $this->renderSlugList($add),
+            count($rem),
+            $this->renderSlugList($rem));
+        } else if ($add) {
+          return pht(
+            '%s added %d %s hashtag(s): %s.',
+            $author_handle,
+            count($add),
+            $object_handle,
+            $this->renderSlugList($add));
+        } else if ($rem) {
+          return pht(
+            '%s removed %d %s hashtag(s): %s.',
+            $author_handle,
+            count($rem),
+            $object_handle,
+            $this->renderSlugList($rem));
+        }
+
+    }
+
+    return parent::getTitleForFeed();
+  }
+
   private function renderSlugList($slugs) {
     return implode(', ', $slugs);
   }
diff --git a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
--- a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
+++ b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php
@@ -735,6 +735,23 @@
         ),
       ),
 
+      '%s changed %s hashtag(s), added %d: %s; removed %d: %s.' =>
+        '%s changed hashtags for %s, added %4$s; removed %6$s.',
+
+      '%s added %d %s hashtag(s): %s.' => array(
+        array(
+          '%s added a hashtag to %3$s: %4$s.',
+          '%s added hashtags to %3$s: %4$s.',
+        ),
+      ),
+
+      '%s removed %d %s hashtag(s): %s.' => array(
+        array(
+          '%s removed a hashtag from %3$s: %4$s.',
+          '%s removed hashtags from %3$s: %4$s.',
+        ),
+      ),
+
       '%d User(s) Need Approval' => array(
         '%d User Needs Approval',
         '%d Users Need Approval',
@@ -948,6 +965,23 @@
       '%s edited %s edge(s) for %s, added %s: %s; removed %s: %s.' =>
         '%s edited edges for %3$s, added: %5$s; removed %7$s.',
 
+      '%s added %s member(s) for %s: %s.' => array(
+        array(
+          '%s added a member for %3$s: %4$s.',
+          '%s added members for %3$s: %4$s.',
+        ),
+      ),
+
+      '%s removed %s member(s) for %s: %s.' => array(
+        array(
+          '%s removed a member for %3$s: %4$s.',
+          '%s removed members for %3$s: %4$s.',
+        ),
+      ),
+
+      '%s edited %s member(s) for %s, added %s: %s; removed %s: %s.' =>
+        '%s edited members for %3$s, added: %5$s; removed %7$s.',
+
       '%d related link(s):' => array(
         'Related link:',
         'Related links:',