diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index e4fb7c52b9..fbb13222c9 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -1,570 +1,621 @@ getApplicationTransactionType()); if ($type) { return $type->getTypeName(); } return pht('Object'); } public function getApplicationTransactionCommentObject() { throw new Exception("Not implemented!"); } public function getApplicationTransactionViewObject() { return new PhabricatorApplicationTransactionView(); } public function getMetadataValue($key, $default = null) { return idx($this->metadata, $key, $default); } public function setMetadataValue($key, $value) { $this->metadata[$key] = $value; return $this; } public function generatePHID() { $type = PhabricatorApplicationTransactionPHIDTypeTransaction::TYPECONST; $subtype = $this->getApplicationTransactionType(); return PhabricatorPHID::generateNewPHID($type, $subtype); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'oldValue' => self::SERIALIZATION_JSON, 'newValue' => self::SERIALIZATION_JSON, 'metadata' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source->serialize(); return $this; } public function getContentSource() { return PhabricatorContentSource::newFromSerialized($this->contentSource); } public function hasComment() { return $this->getComment() && strlen($this->getComment()->getContent()); } public function getComment() { if ($this->commentNotLoaded) { throw new Exception("Comment for this transaction was not loaded."); } return $this->comment; } public function attachComment( PhabricatorApplicationTransactionComment $comment) { $this->comment = $comment; $this->commentNotLoaded = false; return $this; } public function setCommentNotLoaded($not_loaded) { $this->commentNotLoaded = $not_loaded; return $this; } /* -( Rendering )---------------------------------------------------------- */ public function setRenderingTarget($rendering_target) { $this->renderingTarget = $rendering_target; return $this; } public function getRenderingTarget() { return $this->renderingTarget; } public function attachViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->assertAttached($this->viewer); } public function getRequiredHandlePHIDs() { $phids = array(); $old = $this->getOldValue(); $new = $this->getNewValue(); $phids[] = array($this->getAuthorPHID()); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_SUBSCRIBERS: $phids[] = $old; $phids[] = $new; break; case PhabricatorTransactions::TYPE_EDGE: $phids[] = ipull($old, 'dst'); $phids[] = ipull($new, 'dst'); break; case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: if (!PhabricatorPolicyQuery::isGlobalPolicy($old)) { $phids[] = array($old); } if (!PhabricatorPolicyQuery::isGlobalPolicy($new)) { $phids[] = array($new); } break; } return array_mergev($phids); } public function setHandles(array $handles) { $this->handles = $handles; return $this; } public function getHandle($phid) { if (empty($this->handles[$phid])) { throw new Exception( "Transaction requires a handle ('{$phid}') it did not load."); } return $this->handles[$phid]; } public function getHandleIfExists($phid) { return idx($this->handles, $phid); } public function getHandles() { if ($this->handles === null) { throw new Exception( 'Transaction requires handles and it did not load them.' ); } return $this->handles; } public function renderHandleLink($phid) { if ($this->renderingTarget == self::TARGET_HTML) { return $this->getHandle($phid)->renderLink(); } else { return $this->getHandle($phid)->getLinkName(); } } public function renderHandleList(array $phids) { $links = array(); foreach ($phids as $phid) { $links[] = $this->renderHandleLink($phid); } if ($this->renderingTarget == self::TARGET_HTML) { return phutil_implode_html(', ', $links); } else { return implode(', ', $links); } } public function renderPolicyName($phid) { $policy = PhabricatorPolicy::newFromPolicyAndHandle( $phid, $this->getHandleIfExists($phid)); if ($this->renderingTarget == self::TARGET_HTML) { $output = $policy->renderDescription(); } else { $output = hsprintf('%s', $policy->getFullName()); } return $output; } public function getIcon() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return 'comment'; case PhabricatorTransactions::TYPE_SUBSCRIBERS: return 'message'; case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: return 'lock'; case PhabricatorTransactions::TYPE_EDGE: return 'link'; } - return null; + return 'edit'; } public function getColor() { return null; } public function shouldHide() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: if ($this->getOldValue() === null) { return true; } else { return false; } break; } return false; } public function shouldHideForMail() { return $this->shouldHide(); } public function getNoEffectDescription() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return pht('You can not post an empty comment.'); case PhabricatorTransactions::TYPE_VIEW_POLICY: return pht( 'This %s already has that view policy.', $this->getApplicationObjectTypeName()); case PhabricatorTransactions::TYPE_EDIT_POLICY: return pht( 'This %s already has that edit policy.', $this->getApplicationObjectTypeName()); case PhabricatorTransactions::TYPE_JOIN_POLICY: return pht( 'This %s already has that join policy.', $this->getApplicationObjectTypeName()); case PhabricatorTransactions::TYPE_SUBSCRIBERS: return pht( 'All users are already subscribed to this %s.', $this->getApplicationObjectTypeName()); case PhabricatorTransactions::TYPE_EDGE: return pht('Edges already exist; transaction has no effect.'); } return pht('Transaction has no effect.'); } public function getTitle() { $author_phid = $this->getAuthorPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return pht( '%s added a comment.', $this->renderHandleLink($author_phid)); case PhabricatorTransactions::TYPE_VIEW_POLICY: return pht( '%s changed the visibility of this %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->getApplicationObjectTypeName(), $this->renderPolicyName($old), $this->renderPolicyName($new)); case PhabricatorTransactions::TYPE_EDIT_POLICY: return pht( '%s changed the edit policy of this %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->getApplicationObjectTypeName(), $this->renderPolicyName($old), $this->renderPolicyName($new)); case PhabricatorTransactions::TYPE_JOIN_POLICY: return pht( '%s changed the join policy of this %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->getApplicationObjectTypeName(), $this->renderPolicyName($old), $this->renderPolicyName($new)); case PhabricatorTransactions::TYPE_SUBSCRIBERS: $add = array_diff($new, $old); $rem = array_diff($old, $new); if ($add && $rem) { return pht( '%s edited subscriber(s), added %d: %s; removed %d: %s.', $this->renderHandleLink($author_phid), count($add), $this->renderHandleList($add), count($rem), $this->renderHandleList($rem)); } else if ($add) { return pht( '%s added %d subscriber(s): %s.', $this->renderHandleLink($author_phid), count($add), $this->renderHandleList($add)); } else if ($rem) { return pht( '%s removed %d subscriber(s): %s.', $this->renderHandleLink($author_phid), count($rem), $this->renderHandleList($rem)); } else { // This is used when rendering previews, before the user actually // selects any CCs. return pht( '%s updated subscribers...', $this->renderHandleLink($author_phid)); } break; case PhabricatorTransactions::TYPE_EDGE: $new = ipull($new, 'dst'); $old = ipull($old, 'dst'); $add = array_diff($new, $old); $rem = array_diff($old, $new); $type = $this->getMetadata('edge:type'); $type = head($type); if ($add && $rem) { $string = PhabricatorEdgeConfig::getEditStringForEdgeType($type); return pht( $string, $this->renderHandleLink($author_phid), count($add), $this->renderHandleList($add), count($rem), $this->renderHandleList($rem)); } else if ($add) { $string = PhabricatorEdgeConfig::getAddStringForEdgeType($type); return pht( $string, $this->renderHandleLink($author_phid), count($add), $this->renderHandleList($add)); } else { $string = PhabricatorEdgeConfig::getRemoveStringForEdgeType($type); return pht( $string, $this->renderHandleLink($author_phid), count($rem), $this->renderHandleList($rem)); } case PhabricatorTransactions::TYPE_CUSTOMFIELD: $key = $this->getMetadataValue('customfield:key'); $field = PhabricatorCustomField::getObjectField( // TODO: This is a giant hack, but we currently don't have a way to // get the contextual object and this pathway is only hit by // Maniphest. We should provide a way to get the actual object here. new ManiphestTask(), PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS, $key); if ($field) { $field->setViewer($this->getViewer()); return $field->getApplicationTransactionTitle($this); } else { return pht( '%s edited a custom field.', $this->renderHandleLink($author_phid)); } default: return pht( '%s edited this %s.', $this->renderHandleLink($author_phid), $this->getApplicationObjectTypeName()); } } public function getTitleForFeed(PhabricatorFeedStory $story) { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return pht( '%s added a comment to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_VIEW_POLICY: return pht( '%s changed the visibility for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_EDIT_POLICY: return pht( '%s changed the edit policy for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_JOIN_POLICY: return pht( '%s changed the join policy for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_SUBSCRIBERS: return pht( '%s updated subscribers of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_EDGE: $type = $this->getMetadata('edge:type'); $type = head($type); $string = PhabricatorEdgeConfig::getFeedStringForEdgeType($type); return pht( $string, $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_CUSTOMFIELD: $key = $this->getMetadataValue('customfield:key'); $field = PhabricatorCustomField::getObjectField( // TODO: This is a giant hack, but we currently don't have a way to // get the contextual object and this pathway is only hit by // Maniphest. We should provide a way to get the actual object here. new ManiphestTask(), PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS, $key); if ($field) { $field->setViewer($this->getViewer()); return $field->getApplicationTransactionTitleForFeed($this, $story); } else { return pht( '%s edited a custom field on %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } } return $this->getTitle(); } public function getBodyForFeed(PhabricatorFeedStory $story) { $old = $this->getOldValue(); $new = $this->getNewValue(); $body = null; switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $text = $this->getComment()->getContent(); $body = phutil_escape_html_newlines( phutil_utf8_shorten($text, 128)); break; } return $body; } public function getActionStrength() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return 0.5; } return 1.0; } public function getActionName() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return pht('Commented On'); case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: return pht('Changed Policy'); case PhabricatorTransactions::TYPE_SUBSCRIBERS: return pht('Changed Subscribers'); default: return pht('Updated'); } } public function getMailTags() { return array(); } public function hasChangeDetails() { return false; } public function renderChangeDetails(PhabricatorUser $viewer) { return null; } public function attachTransactionGroup(array $group) { assert_instances_of($group, 'PhabricatorApplicationTransaction'); $this->transactionGroup = $group; return $this; } public function getTransactionGroup() { return $this->transactionGroup; } + /** + * Should this transaction be visually grouped with an existing transaction + * group? + * + * @param list List of transactions. + * @return bool True to display in a group with the other transactions. + */ + public function shouldDisplayGroupWith(array $group) { + $type_comment = PhabricatorTransactions::TYPE_COMMENT; + + $this_source = null; + if ($this->getContentSource()) { + $this_source = $this->getContentSource()->getSource(); + } + + foreach ($group as $xaction) { + // Don't group transactions by different authors. + if ($xaction->getAuthorPHID() != $this->getAuthorPHID()) { + return false; + } + + // Don't group transactions for different objects. + if ($xaction->getObjectPHID() != $this->getObjectPHID()) { + return false; + } + + // Don't group anything into a group which already has a comment. + if ($xaction->getTransactionType() == $type_comment) { + return false; + } + + // Don't group transactions from different content sources. + $other_source = null; + if ($xaction->getContentSource()) { + $other_source = $xaction->getContentSource()->getSource(); + } + + if ($other_source != $this_source) { + return false; + } + + // Don't group transactions which happened more than 2 minutes apart. + $apart = abs($xaction->getDateCreated() - $this->getDateCreated()); + if ($apart > (60 * 2)) { + return false; + } + } + + return true; + } + /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getAuthorPHID()); } public function describeAutomaticCapability($capability) { // TODO: (T603) Exact policies are unclear here. return null; } } diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php index e64b5b86ff..aa1d856c12 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php @@ -1,299 +1,377 @@ isDetailView = $is_detail_view; return $this; } public function setObjectPHID($object_phid) { $this->objectPHID = $object_phid; return $this; } public function getObjectPHID() { return $this->objectPHID; } public function setIsPreview($is_preview) { $this->isPreview = $is_preview; return $this; } public function setShowEditActions($show_edit_actions) { $this->showEditActions = $show_edit_actions; return $this; } public function getShowEditActions() { return $this->showEditActions; } public function setAnchorOffset($anchor_offset) { $this->anchorOffset = $anchor_offset; return $this; } public function setMarkupEngine(PhabricatorMarkupEngine $engine) { $this->engine = $engine; return $this; } public function setTransactions(array $transactions) { assert_instances_of($transactions, 'PhabricatorApplicationTransaction'); $this->transactions = $transactions; return $this; } public function buildEvents() { $user = $this->getUser(); $anchor = $this->anchorOffset; - $events = array(); $xactions = $this->transactions; - foreach ($xactions as $key => $xaction) { - if ($xaction->shouldHide()) { - unset($xactions[$key]); - } - } - - $last = null; - $last_key = null; - $groups = array(); - foreach ($xactions as $key => $xaction) { - if ($last && $this->shouldGroupTransactions($last, $xaction)) { - $groups[$last_key][] = $xaction; - unset($xactions[$key]); - } else { - $last = $xaction; - $last_key = $key; - } - } - - foreach ($xactions as $key => $xaction) { - $xaction->attachTransactionGroup(idx($groups, $key, array())); - - $event = id(new PhabricatorTimelineEventView()) - ->setUser($user) - ->setTransactionPHID($xaction->getPHID()) - ->setUserHandle($xaction->getHandle($xaction->getAuthorPHID())) - ->setIcon($xaction->getIcon()) - ->setColor($xaction->getColor()); - - $title = $xaction->getTitle(); - if ($xaction->hasChangeDetails()) { - if ($this->isPreview || $this->isDetailView) { - $details = $this->buildChangeDetails($xaction); - } else { - $details = $this->buildChangeDetailsLink($xaction); - } - $title = array( - $title, - ' ', - $details, - ); - } - $event->setTitle($title); - if ($this->isPreview) { - $event->setIsPreview(true); - } else { - $event - ->setDateCreated($xaction->getDateCreated()) - ->setContentSource($xaction->getContentSource()) - ->setAnchor($anchor); + $xactions = $this->filterHiddenTransactions($xactions); + $xactions = $this->groupRelatedTransactions($xactions); + $groups = $this->groupDisplayTransactions($xactions); + $events = array(); + foreach ($groups as $group) { + $group_event = null; + foreach ($group as $xaction) { + $event = $this->renderEvent($xaction, $group, $anchor); $anchor++; - } - - $has_deleted_comment = $xaction->getComment() && - $xaction->getComment()->getIsDeleted(); - - if ($this->getShowEditActions() && !$this->isPreview) { - if ($xaction->getCommentVersion() > 1) { - $event->setIsEdited(true); - } - - $can_edit = PhabricatorPolicyCapability::CAN_EDIT; - - if ($xaction->hasComment() || $has_deleted_comment) { - $has_edit_capability = PhabricatorPolicyFilter::hasCapability( - $user, - $xaction, - $can_edit); - if ($has_edit_capability) { - $event->setIsEditable(true); - } + if (!$group_event) { + $group_event = $event; + } else { + $group_event->addEventToGroup($event); } } - - $content = $this->renderTransactionContent($xaction); - if ($content) { - $event->appendChild($content); - } - - $events[] = $event; + $events[] = $group_event; } return $events; } public function render() { if (!$this->getObjectPHID()) { throw new Exception("Call setObjectPHID() before render()!"); } $view = new PhabricatorTimelineView(); $events = $this->buildEvents(); foreach ($events as $event) { $view->addEvent($event); } if ($this->getShowEditActions()) { $list_id = celerity_generate_unique_node_id(); $view->setID($list_id); Javelin::initBehavior( 'phabricator-transaction-list', array( 'listID' => $list_id, 'objectPHID' => $this->getObjectPHID(), 'nextAnchor' => $this->anchorOffset + count($events), )); } return $view->render(); } protected function getOrBuildEngine() { if ($this->engine) { return $this->engine; } $field = PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT; $engine = id(new PhabricatorMarkupEngine()) ->setViewer($this->getUser()); foreach ($this->transactions as $xaction) { if (!$xaction->hasComment()) { continue; } $engine->addObject($xaction->getComment(), $field); } $engine->process(); return $engine; } private function buildChangeDetails( PhabricatorApplicationTransaction $xaction) { Javelin::initBehavior('phabricator-reveal-content'); $show_id = celerity_generate_unique_node_id(); $hide_id = celerity_generate_unique_node_id(); $content_id = celerity_generate_unique_node_id(); $show_more = javelin_tag( 'a', array( 'href' => '#', 'sigil' => 'reveal-content', 'mustcapture' => true, 'id' => $show_id, 'style' => 'display: none', 'meta' => array( 'hideIDs' => array($show_id), 'showIDs' => array($hide_id, $content_id), ), ), pht('(Show Details)')); $hide_more = javelin_tag( 'a', array( 'href' => '#', 'sigil' => 'reveal-content', 'mustcapture' => true, 'id' => $hide_id, 'meta' => array( 'hideIDs' => array($hide_id, $content_id), 'showIDs' => array($show_id), ), ), pht('(Hide Details)')); $content = phutil_tag( 'div', array( 'id' => $content_id, 'class' => 'phabricator-timeline-change-details', ), $xaction->renderChangeDetails($this->getUser())); return array( $show_more, $hide_more, $content, ); } private function buildChangeDetailsLink( PhabricatorApplicationTransaction $xaction) { return javelin_tag( 'a', array( 'href' => '/transactions/detail/'.$xaction->getPHID().'/', 'sigil' => 'transaction-detail', 'mustcapture' => true, 'meta' => array( 'anchor' => $this->anchorOffset, ), ), pht('(Show Details)')); } protected function shouldGroupTransactions( PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { return false; } protected function renderTransactionContent( PhabricatorApplicationTransaction $xaction) { $field = PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT; $engine = $this->getOrBuildEngine(); $comment = $xaction->getComment(); if ($xaction->hasComment()) { if ($comment->getIsDeleted()) { return phutil_tag( 'em', array(), pht('This comment has been deleted.')); } else { return $engine->getOutput($comment, $field); } } return null; } + private function filterHiddenTransactions(array $xactions) { + foreach ($xactions as $key => $xaction) { + if ($xaction->shouldHide()) { + unset($xactions[$key]); + } + } + return $xactions; + } + + private function groupRelatedTransactions(array $xactions) { + $last = null; + $last_key = null; + $groups = array(); + foreach ($xactions as $key => $xaction) { + if ($last && $this->shouldGroupTransactions($last, $xaction)) { + $groups[$last_key][] = $xaction; + unset($xactions[$key]); + } else { + $last = $xaction; + $last_key = $key; + } + } + + foreach ($xactions as $key => $xaction) { + $xaction->attachTransactionGroup(idx($groups, $key, array())); + } + + return $xactions; + } + + private function groupDisplayTransactions(array $xactions) { + $groups = array(); + $group = array(); + foreach ($xactions as $xaction) { + if ($xaction->shouldDisplayGroupWith($group)) { + $group[] = $xaction; + } else { + if ($group) { + $groups[] = $group; + } + $group = array($xaction); + } + } + + if ($group) { + $groups[] = $group; + } + + foreach ($groups as $key => $group) { + $group = msort($group, 'getActionStrength'); + $group = array_reverse($group); + $groups[$key] = $group; + } + + return $groups; + } + + private function renderEvent( + PhabricatorApplicationTransaction $xaction, + array $group, + $anchor) { + $viewer = $this->getUser(); + + $event = id(new PhabricatorTimelineEventView()) + ->setUser($viewer) + ->setTransactionPHID($xaction->getPHID()) + ->setUserHandle($xaction->getHandle($xaction->getAuthorPHID())) + ->setIcon($xaction->getIcon()) + ->setColor($xaction->getColor()); + + if (!$this->shouldSuppressTitle($xaction, $group)) { + $title = $xaction->getTitle(); + if ($xaction->hasChangeDetails()) { + if ($this->isPreview || $this->isDetailView) { + $details = $this->buildChangeDetails($xaction); + } else { + $details = $this->buildChangeDetailsLink($xaction); + } + $title = array( + $title, + ' ', + $details, + ); + } + $event->setTitle($title); + } + + if ($this->isPreview) { + $event->setIsPreview(true); + } else { + $event + ->setDateCreated($xaction->getDateCreated()) + ->setContentSource($xaction->getContentSource()) + ->setAnchor($anchor); + } + + $has_deleted_comment = $xaction->getComment() && + $xaction->getComment()->getIsDeleted(); + + if ($this->getShowEditActions() && !$this->isPreview) { + if ($xaction->getCommentVersion() > 1) { + $event->setIsEdited(true); + } + + $can_edit = PhabricatorPolicyCapability::CAN_EDIT; + + if ($xaction->hasComment() || $has_deleted_comment) { + $has_edit_capability = PhabricatorPolicyFilter::hasCapability( + $viewer, + $xaction, + $can_edit); + if ($has_edit_capability) { + $event->setIsEditable(true); + } + } + } + + $content = $this->renderTransactionContent($xaction); + if ($content) { + $event->appendChild($content); + } + + return $event; + } + + private function shouldSuppressTitle( + PhabricatorApplicationTransaction $xaction, + array $group) { + + // This is a little hard-coded, but we don't have any other reasonable + // cases for now. Suppress "commented on" if there are other actions in + // the display group. + + if (count($group) > 1) { + $type_comment = PhabricatorTransactions::TYPE_COMMENT; + if ($xaction->getTransactionType() == $type_comment) { + return true; + } + } + + return false; + } + } diff --git a/src/view/layout/PhabricatorTimelineEventView.php b/src/view/layout/PhabricatorTimelineEventView.php index f7999c7e91..54f226dbbb 100644 --- a/src/view/layout/PhabricatorTimelineEventView.php +++ b/src/view/layout/PhabricatorTimelineEventView.php @@ -1,374 +1,376 @@ transactionPHID = $transaction_phid; return $this; } public function getTransactionPHID() { return $this->transactionPHID; } public function setIsEdited($is_edited) { $this->isEdited = $is_edited; return $this; } public function getIsEdited() { return $this->isEdited; } public function setIsPreview($is_preview) { $this->isPreview = $is_preview; return $this; } public function getIsPreview() { return $this->isPreview; } public function setIsEditable($is_editable) { $this->isEditable = $is_editable; return $this; } public function getIsEditable() { return $this->isEditable; } public function setDateCreated($date_created) { $this->dateCreated = $date_created; return $this; } public function getDateCreated() { return $this->dateCreated; } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source; return $this; } public function getContentSource() { return $this->contentSource; } public function setUserHandle(PhabricatorObjectHandle $handle) { $this->userHandle = $handle; return $this; } public function setAnchor($anchor) { $this->anchor = $anchor; return $this; } public function setTitle($title) { $this->title = $title; return $this; } public function addClass($class) { $this->classes[] = $class; return $this; } public function setIcon($icon) { $this->icon = $icon; return $this; } public function setColor($color) { $this->color = $color; return $this; } public function getEventGroup() { return array_merge(array($this), $this->eventGroup); } public function addEventToGroup(PhabricatorTimelineEventView $event) { $this->eventGroup[] = $event; return $this; } protected function renderEventTitle($is_first_event, $force_icon) { $title = $this->title; if (($title === null) && !$this->hasChildren()) { $title = ''; } if ($is_first_event) { $extra = array(); $is_first_extra = true; foreach ($this->getEventGroup() as $event) { - $extra[] = $this->renderExtra($is_first_extra); + $extra[] = $event->renderExtra($is_first_extra); $is_first_extra = false; } + $extra = array_reverse($extra); + $extra = array_mergev($extra); $extra = phutil_tag( 'span', array( 'class' => 'phabricator-timeline-extra', ), - phutil_implode_html(" \xC2\xB7 ", array_mergev($extra))); + phutil_implode_html(" \xC2\xB7 ", $extra)); } else { $extra = null; } if ($title !== null || $extra) { $title_classes = array(); $title_classes[] = 'phabricator-timeline-title'; $icon = null; if ($this->icon || $force_icon) { $title_classes[] = 'phabricator-timeline-title-with-icon'; } if ($this->icon) { $fill_classes = array(); $fill_classes[] = 'phabricator-timeline-icon-fill'; if ($this->color) { $fill_classes[] = 'phabricator-timeline-icon-fill-'.$this->color; } $icon = phutil_tag( 'span', array( 'class' => implode(' ', $fill_classes), ), phutil_tag( 'span', array( 'class' => 'phabricator-timeline-icon sprite-icons '. 'icons-'.$this->icon.'-white', ), '')); } $title = phutil_tag( 'div', array( 'class' => implode(' ', $title_classes), ), array($icon, $title, $extra)); } return $title; } public function render() { $events = $this->getEventGroup(); // Move events with icons first. $icon_keys = array(); foreach ($this->getEventGroup() as $key => $event) { if ($event->icon) { $icon_keys[] = $key; } } $events = array_select_keys($events, $icon_keys) + $events; $force_icon = (bool)$icon_keys; $group_titles = array(); $group_children = array(); $is_first_event = true; foreach ($events as $event) { $group_titles[] = $event->renderEventTitle($is_first_event, $force_icon); $is_first_event = false; if ($event->hasChildren()) { $group_children[] = $event->renderChildren(); } } $wedge = phutil_tag( 'div', array( 'class' => 'phabricator-timeline-wedge phabricator-timeline-border', ), ''); $image_uri = $this->userHandle->getImageURI(); $image = phutil_tag( 'div', array( 'style' => 'background-image: url('.$image_uri.')', 'class' => 'phabricator-timeline-image', ), ''); $content_classes = array(); $content_classes[] = 'phabricator-timeline-content'; $classes = array(); $classes[] = 'phabricator-timeline-event-view'; if ($group_children) { $classes[] = 'phabricator-timeline-major-event'; $content = phutil_tag( 'div', array( 'class' => 'phabricator-timeline-inner-content', ), array( $group_titles, phutil_tag( 'div', array( 'class' => 'phabricator-timeline-core-content', ), $group_children), )); } else { $classes[] = 'phabricator-timeline-minor-event'; $content = $group_titles; } $content = phutil_tag( 'div', array( 'class' => 'phabricator-timeline-group phabricator-timeline-border', ), $content); $content = phutil_tag( 'div', array( 'class' => implode(' ', $content_classes), ), array($image, $wedge, $content)); $outer_classes = $this->classes; $outer_classes[] = 'phabricator-timeline-shell'; $color = null; foreach ($this->getEventGroup() as $event) { if ($event->color) { $color = $event->color; break; } } if ($color) { $outer_classes[] = 'phabricator-timeline-'.$color; } $sigil = null; $meta = null; if ($this->getTransactionPHID()) { $sigil = 'transaction'; $meta = array( 'phid' => $this->getTransactionPHID(), 'anchor' => $this->anchor, ); } return javelin_tag( 'div', array( 'class' => implode(' ', $outer_classes), 'id' => $this->anchor ? 'anchor-'.$this->anchor : null, 'sigil' => $sigil, 'meta' => $meta, ), phutil_tag( 'div', array( 'class' => implode(' ', $classes), ), $content)); } private function renderExtra($is_first_extra) { $extra = array(); if ($this->getIsPreview()) { $extra[] = pht('PREVIEW'); } else { $xaction_phid = $this->getTransactionPHID(); if ($this->getIsEdited()) { $extra[] = javelin_tag( 'a', array( 'href' => '/transactions/history/'.$xaction_phid.'/', 'sigil' => 'workflow', ), pht('Edited')); } if ($this->getIsEditable()) { $extra[] = javelin_tag( 'a', array( 'href' => '/transactions/edit/'.$xaction_phid.'/', 'sigil' => 'workflow transaction-edit', ), pht('Edit')); } if ($is_first_extra) { $source = $this->getContentSource(); if ($source) { $extra[] = id(new PhabricatorContentSourceView()) ->setContentSource($source) ->setUser($this->getUser()) ->render(); } $date_created = null; foreach ($this->getEventGroup() as $event) { if ($event->getDateCreated()) { if ($date_created === null) { $date_created = $event->getDateCreated(); } else { $date_created = min($event->getDateCreated(), $date_created); } } } if ($date_created) { $date = phabricator_datetime( $this->getDateCreated(), $this->getUser()); if ($this->anchor) { Javelin::initBehavior('phabricator-watch-anchor'); $anchor = id(new PhabricatorAnchorView()) ->setAnchorName($this->anchor) ->render(); $date = array( $anchor, phutil_tag( 'a', array( 'href' => '#'.$this->anchor, ), $date), ); } $extra[] = $date; } } } return $extra; } }