diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 03de550fb9..eed3233591 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -337,6 +337,7 @@ phutil_register_library_map(array(
'LiskIsolationTestCase' => 'storage/lisk/dao/__tests__',
'LiskIsolationTestDAO' => 'storage/lisk/dao/__tests__',
'LiskIsolationTestDAOException' => 'storage/lisk/dao/__tests__',
+ 'ManiphestAction' => 'applications/maniphest/constants/action',
'ManiphestAuxiliaryFieldDefaultSpecification' => 'applications/maniphest/auxiliaryfield/default',
'ManiphestAuxiliaryFieldSpecification' => 'applications/maniphest/auxiliaryfield/base',
'ManiphestAuxiliaryFieldTypeException' => 'applications/maniphest/auxiliaryfield/typeexception',
@@ -438,6 +439,7 @@ phutil_register_library_map(array(
'PhabricatorFeedStory' => 'applications/feed/story/base',
'PhabricatorFeedStoryData' => 'applications/feed/storage/story',
'PhabricatorFeedStoryDifferential' => 'applications/feed/story/differential',
+ 'PhabricatorFeedStoryManiphest' => 'applications/feed/story/maniphest',
'PhabricatorFeedStoryPhriction' => 'applications/feed/story/phriction',
'PhabricatorFeedStoryPublisher' => 'applications/feed/publisher',
'PhabricatorFeedStoryReference' => 'applications/feed/storage/storyreference',
@@ -1017,6 +1019,7 @@ phutil_register_library_map(array(
'JavelinViewExampleServerView' => 'AphrontView',
'LiskIsolationTestCase' => 'PhabricatorTestCase',
'LiskIsolationTestDAO' => 'LiskDAO',
+ 'ManiphestAction' => 'PhrictionConstants',
'ManiphestAuxiliaryFieldDefaultSpecification' => 'ManiphestAuxiliaryFieldSpecification',
'ManiphestController' => 'PhabricatorController',
'ManiphestDAO' => 'PhabricatorLiskDAO',
@@ -1101,6 +1104,7 @@ phutil_register_library_map(array(
'PhabricatorFeedPublicStreamController' => 'PhabricatorFeedController',
'PhabricatorFeedStoryData' => 'PhabricatorFeedDAO',
'PhabricatorFeedStoryDifferential' => 'PhabricatorFeedStory',
+ 'PhabricatorFeedStoryManiphest' => 'PhabricatorFeedStory',
'PhabricatorFeedStoryPhriction' => 'PhabricatorFeedStory',
'PhabricatorFeedStoryReference' => 'PhabricatorFeedDAO',
'PhabricatorFeedStoryStatus' => 'PhabricatorFeedStory',
diff --git a/src/applications/feed/constants/story/PhabricatorFeedStoryTypeConstants.php b/src/applications/feed/constants/story/PhabricatorFeedStoryTypeConstants.php
index 0ac7f802e8..4ab63dd596 100644
--- a/src/applications/feed/constants/story/PhabricatorFeedStoryTypeConstants.php
+++ b/src/applications/feed/constants/story/PhabricatorFeedStoryTypeConstants.php
@@ -23,5 +23,6 @@ final class PhabricatorFeedStoryTypeConstants
const STORY_STATUS = 'PhabricatorFeedStoryStatus';
const STORY_DIFFERENTIAL = 'PhabricatorFeedStoryDifferential';
const STORY_PHRICTION = 'PhabricatorFeedStoryPhriction';
+ const STORY_MANIPHEST = 'PhabricatorFeedStoryManiphest';
}
diff --git a/src/applications/feed/story/maniphest/PhabricatorFeedStoryManiphest.php b/src/applications/feed/story/maniphest/PhabricatorFeedStoryManiphest.php
new file mode 100644
index 0000000000..ef27673a63
--- /dev/null
+++ b/src/applications/feed/story/maniphest/PhabricatorFeedStoryManiphest.php
@@ -0,0 +1,94 @@
+getStoryData();
+ return array(
+ $this->getStoryData()->getAuthorPHID(),
+ $data->getValue('taskPHID'),
+ $data->getValue('ownerPHID'),
+ );
+ }
+
+ public function getRequiredObjectPHIDs() {
+ return array(
+ $this->getStoryData()->getAuthorPHID(),
+ );
+ }
+
+ public function renderView() {
+ $data = $this->getStoryData();
+
+ $handles = $this->getHandles();
+ $author_phid = $data->getAuthorPHID();
+ $owner_phid = $data->getValue('ownerPHID');
+ $task_phid = $data->getValue('taskPHID');
+
+ $objects = $this->getObjects();
+ $action = $data->getValue('action');
+
+ $view = new PhabricatorFeedStoryView();
+
+ $verb = ManiphestAction::getActionPastTenseVerb($action);
+ $title =
+ ''.$handles[$author_phid]->renderLink().''.
+ " {$verb} ".
+ ''.$handles[$task_phid]->renderLink().'';
+ switch ($action) {
+ case ManiphestAction::ACTION_ASSIGN:
+ $title .=
+ ' to '.
+ ''.$handles[$owner_phid]->renderLink().'';
+ break;
+ }
+ $title .= '.';
+ $view->setTitle($title);
+
+ switch ($action) {
+ case ManiphestAction::ACTION_CREATE:
+ $full_size = true;
+ break;
+ default:
+ $full_size = false;
+ break;
+ }
+
+ $view->setEpoch($data->getEpoch());
+
+ if ($full_size) {
+ if (!empty($objects[$author_phid])) {
+ $image_phid = $objects[$author_phid]->getProfileImagePHID();
+ $image_uri = PhabricatorFileURI::getViewURIForPHID($image_phid);
+ $view->setImage($image_uri);
+ }
+
+ $content = phutil_escape_html(
+ phutil_utf8_shorten($data->getValue('description'), 128));
+ $content = str_replace("\n", '
', $content);
+
+ $view->appendChild($content);
+ } else {
+ $view->setOneLineStory(true);
+ }
+
+ return $view;
+ }
+
+}
diff --git a/src/applications/feed/story/maniphest/__init__.php b/src/applications/feed/story/maniphest/__init__.php
new file mode 100644
index 0000000000..5a8b993c9f
--- /dev/null
+++ b/src/applications/feed/story/maniphest/__init__.php
@@ -0,0 +1,18 @@
+ 'created',
+ self::ACTION_CLOSE => 'closed',
+ self::ACTION_UPDATE => 'updated',
+ self::ACTION_ASSIGN => 'assigned',
+ );
+
+ return idx($map, $action, "brazenly {$action}'d");
+ }
+
+ /**
+ * If a group of transactions contain several actions, select the "strongest"
+ * action. For instance, a close is stronger than an update, because we want
+ * to render "User U closed task T" instead of "User U updated task T" when
+ * a user closes a task.
+ */
+ public static function selectStrongestAction(array $actions) {
+ static $strengths = array(
+ self::ACTION_UPDATE => 0,
+ self::ACTION_ASSIGN => 1,
+ self::ACTION_CREATE => 2,
+ self::ACTION_CLOSE => 3,
+ );
+
+ $strongest = null;
+ $strength = -1;
+ foreach ($actions as $action) {
+ if ($strengths[$action] > $strength) {
+ $strength = $strengths[$action];
+ $strongest = $action;
+ }
+ }
+ return $strongest;
+ }
+
+}
diff --git a/src/applications/maniphest/constants/action/__init__.php b/src/applications/maniphest/constants/action/__init__.php
new file mode 100644
index 0000000000..712d81ed5f
--- /dev/null
+++ b/src/applications/maniphest/constants/action/__init__.php
@@ -0,0 +1,14 @@
+getCCPHIDs());
+ $this->publishFeedStory($task, $transactions);
+
// TODO: Do this offline via timeline
PhabricatorSearchManiphestIndexer::indexTask($task);
@@ -207,15 +209,7 @@ class ManiphestTransactionEditor {
$view->setHandles($handles);
list($action, $body) = $view->renderForEmail($with_date = false);
- $is_create = false;
- foreach ($transactions as $transaction) {
- $type = $transaction->getTransactionType();
- if (($type == ManiphestTransactionType::TYPE_STATUS) &&
- ($transaction->getOldValue() === null) &&
- ($transaction->getNewValue() == ManiphestTaskStatus::STATUS_OPEN)) {
- $is_create = true;
- }
- }
+ $is_create = $this->isCreate($transactions);
$task_uri = PhabricatorEnv::getURI('/T'.$task->getID());
@@ -276,4 +270,71 @@ class ManiphestTransactionEditor {
return $handler_object;
}
+
+ private function publishFeedStory(ManiphestTask $task, array $transactions) {
+ $actions = array(ManiphestAction::ACTION_UPDATE);
+ $comments = null;
+ foreach ($transactions as $transaction) {
+ if ($transaction->hasComments()) {
+ $comments = $transaction->getComments();
+ }
+ switch ($transaction->getTransactionType()) {
+ case ManiphestTransactionType::TYPE_OWNER:
+ $actions[] = ManiphestAction::ACTION_ASSIGN;
+ break;
+ case ManiphestTransactionType::TYPE_STATUS:
+ if ($task->getStatus() != ManiphestTaskStatus::STATUS_OPEN) {
+ $actions[] = ManiphestAction::ACTION_CLOSE;
+ } else if ($this->isCreate($transactions)) {
+ $actions[] = ManiphestAction::ACTION_CREATE;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ $action_type = ManiphestAction::selectStrongestAction($actions);
+ $owner_phid = $task->getOwnerPHID();
+ $actor_phid = head($transactions)->getAuthorPHID();
+ $author_phid = $task->getAuthorPHID();
+
+ id(new PhabricatorFeedStoryPublisher())
+ ->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_MANIPHEST)
+ ->setStoryData(array(
+ 'taskPHID' => $task->getPHID(),
+ 'transactionIDs' => mpull($transactions, 'getID'),
+ 'ownerPHID' => $owner_phid,
+ 'action' => $action_type,
+ 'comments' => $comments,
+ 'description' => $task->getDescription(),
+ ))
+ ->setStoryTime(time())
+ ->setStoryAuthorPHID($actor_phid)
+ ->setRelatedPHIDs(
+ array_merge(
+ array_filter(
+ array(
+ $task->getPHID(),
+ $author_phid,
+ $actor_phid,
+ $owner_phid,
+ )),
+ $task->getProjectPHIDs()))
+ ->publish();
+ }
+
+ private function isCreate(array $transactions) {
+ $is_create = false;
+ foreach ($transactions as $transaction) {
+ $type = $transaction->getTransactionType();
+ if (($type == ManiphestTransactionType::TYPE_STATUS) &&
+ ($transaction->getOldValue() === null) &&
+ ($transaction->getNewValue() == ManiphestTaskStatus::STATUS_OPEN)) {
+ $is_create = true;
+ }
+ }
+ return $is_create;
+ }
+
}
diff --git a/src/applications/maniphest/editor/transaction/__init__.php b/src/applications/maniphest/editor/transaction/__init__.php
index 75f2b7d20e..09a4a08fb1 100644
--- a/src/applications/maniphest/editor/transaction/__init__.php
+++ b/src/applications/maniphest/editor/transaction/__init__.php
@@ -6,6 +6,9 @@
+phutil_require_module('phabricator', 'applications/feed/constants/story');
+phutil_require_module('phabricator', 'applications/feed/publisher');
+phutil_require_module('phabricator', 'applications/maniphest/constants/action');
phutil_require_module('phabricator', 'applications/maniphest/constants/status');
phutil_require_module('phabricator', 'applications/maniphest/constants/transactiontype');
phutil_require_module('phabricator', 'applications/maniphest/view/transactiondetail');