diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php index 021abfb91b..7c3c8125b7 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php @@ -1,523 +1,528 @@ renderAsFeed = $feed; return $this; } public function setQuoteRef($quote_ref) { $this->quoteRef = $quote_ref; return $this; } public function getQuoteRef() { return $this->quoteRef; } public function setQuoteTargetID($quote_target_id) { $this->quoteTargetID = $quote_target_id; return $this; } public function getQuoteTargetID() { return $this->quoteTargetID; } 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 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 getTransactions() { return $this->transactions; } public function setShouldTerminate($term) { $this->shouldTerminate = $term; return $this; } public function setPager(AphrontCursorPagerView $pager) { $this->pager = $pager; return $this; } public function getPager() { return $this->pager; } /** * This is additional data that may be necessary to render the next set * of transactions. Objects that implement * PhabricatorApplicationTransactionInterface use this data in * willRenderTimeline. */ public function setRenderData(array $data) { $this->renderData = $data; return $this; } public function getRenderData() { return $this->renderData; } public function setHideCommentOptions($hide_comment_options) { $this->hideCommentOptions = $hide_comment_options; return $this; } public function getHideCommentOptions() { return $this->hideCommentOptions; } public function buildEvents($with_hiding = false) { $user = $this->getUser(); $xactions = $this->transactions; $xactions = $this->filterHiddenTransactions($xactions); $xactions = $this->groupRelatedTransactions($xactions); $groups = $this->groupDisplayTransactions($xactions); // If the viewer has interacted with this object, we hide things from // before their most recent interaction by default. This tends to make // very long threads much more manageable, because you don't have to // scroll through a lot of history and can focus on just new stuff. $show_group = null; if ($with_hiding) { // Find the most recent comment by the viewer. $group_keys = array_keys($groups); $group_keys = array_reverse($group_keys); // If we would only hide a small number of transactions, don't hide // anything. Just don't examine the last few keys. Also, we always // want to show the most recent pieces of activity, so don't examine // the first few keys either. $group_keys = array_slice($group_keys, 2, -2); $type_comment = PhabricatorTransactions::TYPE_COMMENT; foreach ($group_keys as $group_key) { $group = $groups[$group_key]; foreach ($group as $xaction) { if ($xaction->getAuthorPHID() == $user->getPHID() && $xaction->getTransactionType() == $type_comment) { // This is the most recent group where the user commented. $show_group = $group_key; break 2; } } } } $events = array(); $hide_by_default = ($show_group !== null); $set_next_page_id = false; foreach ($groups as $group_key => $group) { if ($hide_by_default && ($show_group === $group_key)) { $hide_by_default = false; $set_next_page_id = true; } $group_event = null; foreach ($group as $xaction) { $event = $this->renderEvent($xaction, $group); $event->setHideByDefault($hide_by_default); if (!$group_event) { $group_event = $event; } else { $group_event->addEventToGroup($event); } if ($set_next_page_id) { $set_next_page_id = false; $pager = $this->getPager(); if ($pager) { $pager->setNextPageID($xaction->getID()); } } } $events[] = $group_event; } return $events; } public function render() { if (!$this->getObjectPHID()) { throw new PhutilInvalidStateException('setObjectPHID'); } $view = $this->buildPHUITimelineView(); if ($this->getShowEditActions()) { Javelin::initBehavior('phabricator-transaction-list'); } return $view->render(); } public function buildPHUITimelineView($with_hiding = true) { if (!$this->getObjectPHID()) { throw new PhutilInvalidStateException('setObjectPHID'); } - $view = new PHUITimelineView(); - $view->setShouldTerminate($this->shouldTerminate); - $view->setQuoteTargetID($this->getQuoteTargetID()); - $view->setQuoteRef($this->getQuoteRef()); + $view = id(new PHUITimelineView()) + ->setUser($this->getUser()) + ->setShouldTerminate($this->shouldTerminate) + ->setQuoteTargetID($this->getQuoteTargetID()) + ->setQuoteRef($this->getQuoteRef()); + $events = $this->buildEvents($with_hiding); foreach ($events as $event) { $view->addEvent($event); } + if ($this->getPager()) { $view->setPager($this->getPager()); } + if ($this->getRenderData()) { $view->setRenderData($this->getRenderData()); } return $view; } protected function getOrBuildEngine() { if (!$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(); $this->engine = $engine; } return $this->engine; } private function buildChangeDetailsLink( PhabricatorApplicationTransaction $xaction) { return javelin_tag( 'a', array( 'href' => '/transactions/detail/'.$xaction->getPHID().'/', 'sigil' => 'workflow', ), pht('(Show Details)')); } private function buildExtraInformationLink( PhabricatorApplicationTransaction $xaction) { $link = $xaction->renderExtraInformationLink(); if (!$link) { return null; } return phutil_tag( 'span', array( 'class' => 'phui-timeline-extra-information', ), array(" \xC2\xB7 ", $link)); } 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 ($comment) { if ($comment->getIsRemoved()) { return javelin_tag( 'span', array( 'class' => 'comment-deleted', 'sigil' => 'transaction-comment', 'meta' => array('phid' => $comment->getTransactionPHID()), ), pht( 'This comment was removed by %s.', $xaction->getHandle($comment->getAuthorPHID())->renderLink())); } else if ($comment->getIsDeleted()) { return javelin_tag( 'span', array( 'class' => 'comment-deleted', 'sigil' => 'transaction-comment', 'meta' => array('phid' => $comment->getTransactionPHID()), ), pht('This comment has been deleted.')); } else if ($xaction->hasComment()) { return javelin_tag( 'span', array( 'class' => 'transaction-comment', 'sigil' => 'transaction-comment', 'meta' => array('phid' => $comment->getTransactionPHID()), ), $engine->getOutput($comment, $field)); } else { // This is an empty, non-deleted comment. Usually this happens when // rendering previews. return null; } } 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) { $viewer = $this->getUser(); $event = id(new PHUITimelineEventView()) ->setUser($viewer) + ->setAuthorPHID($xaction->getAuthorPHID()) ->setTransactionPHID($xaction->getPHID()) ->setUserHandle($xaction->getHandle($xaction->getAuthorPHID())) ->setIcon($xaction->getIcon()) ->setColor($xaction->getColor()) ->setHideCommentOptions($this->getHideCommentOptions()); list($token, $token_removed) = $xaction->getToken(); if ($token) { $event->setToken($token, $token_removed); } if (!$this->shouldSuppressTitle($xaction, $group)) { if ($this->renderAsFeed) { $title = $xaction->getTitleForFeed(); } else { $title = $xaction->getTitle(); } if ($xaction->hasChangeDetails()) { if (!$this->isPreview) { $details = $this->buildChangeDetailsLink($xaction); $title = array( $title, ' ', $details, ); } } if (!$this->isPreview) { $more = $this->buildExtraInformationLink($xaction); if ($more) { $title = array($title, ' ', $more); } } $event->setTitle($title); } if ($this->isPreview) { $event->setIsPreview(true); } else { $event ->setDateCreated($xaction->getDateCreated()) ->setContentSource($xaction->getContentSource()) ->setAnchor($xaction->getID()); } $transaction_type = $xaction->getTransactionType(); $comment_type = PhabricatorTransactions::TYPE_COMMENT; $is_normal_comment = ($transaction_type == $comment_type); if ($this->getShowEditActions() && !$this->isPreview && $is_normal_comment) { $has_deleted_comment = $xaction->getComment() && $xaction->getComment()->getIsDeleted(); $has_removed_comment = $xaction->getComment() && $xaction->getComment()->getIsRemoved(); if ($xaction->getCommentVersion() > 1 && !$has_removed_comment) { $event->setIsEdited(true); } if (!$has_removed_comment) { $event->setIsNormalComment(true); } // If we have a place for quoted text to go and this is a quotable // comment, pass the quote target ID to the event view. if ($this->getQuoteTargetID()) { if ($xaction->hasComment()) { if (!$has_removed_comment && !$has_deleted_comment) { $event->setQuoteTargetID($this->getQuoteTargetID()); $event->setQuoteRef($this->getQuoteRef()); } } } $can_edit = PhabricatorPolicyCapability::CAN_EDIT; if ($xaction->hasComment() || $has_deleted_comment) { $has_edit_capability = PhabricatorPolicyFilter::hasCapability( $viewer, $xaction, $can_edit); if ($has_edit_capability && !$has_removed_comment) { $event->setIsEditable(true); } if ($has_edit_capability || $viewer->getIsAdmin()) { if (!$has_removed_comment) { $event->setIsRemovable(true); } } } } $comment = $this->renderTransactionContent($xaction); if ($comment) { $event->appendChild($comment); } 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/phui/PHUIBadgeMiniView.php b/src/view/phui/PHUIBadgeMiniView.php index df9579461f..5807336ca2 100644 --- a/src/view/phui/PHUIBadgeMiniView.php +++ b/src/view/phui/PHUIBadgeMiniView.php @@ -1,65 +1,63 @@ icon = $icon; return $this; } public function setHref($href) { $this->href = $href; return $this; } public function setQuality($quality) { $this->quality = $quality; return $this; } public function setHeader($header) { $this->header = $header; return $this; } protected function getTagName() { if ($this->href) { return 'a'; } else { return 'span'; } } protected function getTagAttributes() { require_celerity_resource('phui-badge-view-css'); Javelin::initBehavior('phabricator-tooltips'); $classes = array(); $classes[] = 'phui-badge-mini'; if ($this->quality) { $classes[] = 'phui-badge-mini-'.$this->quality; } return array( - 'class' => implode(' ', $classes), - 'sigil' => 'has-tooltip', - 'href' => $this->href, - 'meta' => array( - 'tip' => $this->header, - ), - ); + 'class' => implode(' ', $classes), + 'sigil' => 'has-tooltip', + 'href' => $this->href, + 'meta' => array( + 'tip' => $this->header, + ), + ); } protected function getTagContent() { - return id(new PHUIIconView()) ->setIconFont($this->icon); - } } diff --git a/src/view/phui/PHUITimelineEventView.php b/src/view/phui/PHUITimelineEventView.php index cf1ae8b247..519cbdf768 100644 --- a/src/view/phui/PHUITimelineEventView.php +++ b/src/view/phui/PHUITimelineEventView.php @@ -1,648 +1,658 @@ authorPHID = $author_phid; + return $this; + } + + public function getAuthorPHID() { + return $this->authorPHID; + } + public function setQuoteRef($quote_ref) { $this->quoteRef = $quote_ref; return $this; } public function getQuoteRef() { return $this->quoteRef; } public function setQuoteTargetID($quote_target_id) { $this->quoteTargetID = $quote_target_id; return $this; } public function getQuoteTargetID() { return $this->quoteTargetID; } public function setIsNormalComment($is_normal_comment) { $this->isNormalComment = $is_normal_comment; return $this; } public function getIsNormalComment() { return $this->isNormalComment; } public function setHideByDefault($hide_by_default) { $this->hideByDefault = $hide_by_default; return $this; } public function getHideByDefault() { return $this->hideByDefault; } public function setTransactionPHID($transaction_phid) { $this->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 setIsRemovable($is_removable) { $this->isRemovable = $is_removable; return $this; } public function getIsRemovable() { return $this->isRemovable; } 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 getAnchor() { return $this->anchor; } public function setTitle($title) { $this->title = $title; return $this; } public function addClass($class) { $this->classes[] = $class; return $this; } public function addBadge(PHUIBadgeMiniView $badge) { $this->badges[] = $badge; return $this; } public function setIcon($icon) { $this->icon = $icon; return $this; } public function setColor($color) { $this->color = $color; return $this; } public function setReallyMajorEvent($me) { $this->reallyMajorEvent = $me; return $this; } public function setHideCommentOptions($hide_comment_options) { $this->hideCommentOptions = $hide_comment_options; return $this; } public function getHideCommentOptions() { return $this->hideCommentOptions; } public function setToken($token, $removed = false) { $this->token = $token; $this->tokenRemoved = $removed; return $this; } public function getEventGroup() { return array_merge(array($this), $this->eventGroup); } public function addEventToGroup(PHUITimelineEventView $event) { $this->eventGroup[] = $event; return $this; } protected function shouldRenderEventTitle() { if ($this->title === null) { return false; } return true; } protected function renderEventTitle($force_icon, $has_menu, $extra) { $title = $this->title; $title_classes = array(); $title_classes[] = 'phui-timeline-title'; $icon = null; if ($this->icon || $force_icon) { $title_classes[] = 'phui-timeline-title-with-icon'; } if ($has_menu) { $title_classes[] = 'phui-timeline-title-with-menu'; } if ($this->icon) { $fill_classes = array(); $fill_classes[] = 'phui-timeline-icon-fill'; if ($this->color) { $fill_classes[] = 'phui-timeline-icon-fill-'.$this->color; } $icon = id(new PHUIIconView()) ->setIconFont($this->icon.' white') ->addClass('phui-timeline-icon'); $icon = phutil_tag( 'span', array( 'class' => implode(' ', $fill_classes), ), $icon); } $token = null; if ($this->token) { $token = id(new PHUIIconView()) ->addClass('phui-timeline-token') ->setSpriteSheet(PHUIIconView::SPRITE_TOKENS) ->setSpriteIcon($this->token); if ($this->tokenRemoved) { $token->addClass('strikethrough'); } } $title = phutil_tag( 'div', array( 'class' => implode(' ', $title_classes), ), array($icon, $token, $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; $menu = null; $items = array(); $has_menu = false; if (!$this->getIsPreview() && !$this->getHideCommentOptions()) { foreach ($this->getEventGroup() as $event) { $items[] = $event->getMenuItems($this->anchor); if ($event->hasChildren()) { $has_menu = true; } } $items = array_mergev($items); } if ($items || $has_menu) { $icon = id(new PHUIIconView()) ->setIconFont('fa-caret-down'); $aural = javelin_tag( 'span', array( 'aural' => true, ), pht('Comment Actions')); if ($items) { $sigil = 'phui-dropdown-menu'; Javelin::initBehavior('phui-dropdown-menu'); } else { $sigil = null; } $action_list = id(new PhabricatorActionListView()) ->setUser($this->getUser()); foreach ($items as $item) { $action_list->addAction($item); } $menu = javelin_tag( $items ? 'a' : 'span', array( 'href' => '#', 'class' => 'phui-timeline-menu', 'sigil' => $sigil, 'aria-haspopup' => 'true', 'aria-expanded' => 'false', 'meta' => array( 'items' => hsprintf('%s', $action_list), ), ), array( $aural, $icon, )); $has_menu = true; } // Render "extra" information (timestamp, etc). $extra = $this->renderExtra($events); $group_titles = array(); $group_items = array(); $group_children = array(); foreach ($events as $event) { if ($event->shouldRenderEventTitle()) { $group_titles[] = $event->renderEventTitle( $force_icon, $has_menu, $extra); // Don't render this information more than once. $extra = null; } if ($event->hasChildren()) { $group_children[] = $event->renderChildren(); } } $image_uri = $this->userHandle->getImageURI(); $wedge = phutil_tag( 'div', array( 'class' => 'phui-timeline-wedge phui-timeline-border', 'style' => (nonempty($image_uri)) ? '' : 'display: none;', ), ''); $image = null; $badges = null; if ($image_uri) { $image = phutil_tag( 'div', array( 'style' => 'background-image: url('.$image_uri.')', 'class' => 'phui-timeline-image', ), ''); if ($this->badges) { $flex = new PHUIBadgeBoxView(); $flex->addItems($this->badges); $flex->setCollapsed(true); $badges = phutil_tag( 'div', array( 'class' => 'phui-timeline-badges', ), $flex); } } $content_classes = array(); $content_classes[] = 'phui-timeline-content'; $classes = array(); $classes[] = 'phui-timeline-event-view'; if ($group_children) { $classes[] = 'phui-timeline-major-event'; $content = phutil_tag( 'div', array( 'class' => 'phui-timeline-inner-content', ), array( $group_titles, $menu, phutil_tag( 'div', array( 'class' => 'phui-timeline-core-content', ), $group_children), )); } else { $classes[] = 'phui-timeline-minor-event'; $content = $group_titles; } $content = phutil_tag( 'div', array( 'class' => 'phui-timeline-group phui-timeline-border', ), $content); $content = phutil_tag( 'div', array( 'class' => implode(' ', $content_classes), ), array($image, $badges, $wedge, $content)); $outer_classes = $this->classes; $outer_classes[] = 'phui-timeline-shell'; $color = null; foreach ($this->getEventGroup() as $event) { if ($event->color) { $color = $event->color; break; } } if ($color) { $outer_classes[] = 'phui-timeline-'.$color; } $sigil = null; $meta = null; if ($this->getTransactionPHID()) { $sigil = 'transaction'; $meta = array( 'phid' => $this->getTransactionPHID(), 'anchor' => $this->anchor, ); } $major_event = null; if ($this->reallyMajorEvent) { $major_event = phutil_tag( 'div', array( 'class' => 'phui-timeline-event-view '. 'phui-timeline-spacer '. 'phui-timeline-spacer-bold', '', )); } return array( 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)), $major_event, ); } private function renderExtra(array $events) { $extra = array(); if ($this->getIsPreview()) { $extra[] = pht('PREVIEW'); } else { foreach ($events as $event) { if ($event->getIsEdited()) { $extra[] = pht('Edited'); break; } } $source = $this->getContentSource(); if ($source) { $extra[] = id(new PhabricatorContentSourceView()) ->setContentSource($source) ->setUser($this->getUser()) ->render(); } $date_created = null; foreach ($events 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( $date_created, $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; } } $extra = javelin_tag( 'span', array( 'class' => 'phui-timeline-extra', ), phutil_implode_html( javelin_tag( 'span', array( 'aural' => false, ), self::DELIMITER), $extra)); return $extra; } private function getMenuItems($anchor) { $xaction_phid = $this->getTransactionPHID(); $items = array(); if ($this->getIsEditable()) { $items[] = id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setHref('/transactions/edit/'.$xaction_phid.'/') ->setName(pht('Edit Comment')) ->addSigil('transaction-edit') ->setMetadata( array( 'anchor' => $anchor, )); } if ($this->getQuoteTargetID()) { $ref = null; if ($this->getQuoteRef()) { $ref = $this->getQuoteRef(); if ($anchor) { $ref = $ref.'#'.$anchor; } } $items[] = id(new PhabricatorActionView()) ->setIcon('fa-quote-left') ->setHref('#') ->setName(pht('Quote')) ->addSigil('transaction-quote') ->setMetadata( array( 'targetID' => $this->getQuoteTargetID(), 'uri' => '/transactions/quote/'.$xaction_phid.'/', 'ref' => $ref, )); } if ($this->getIsNormalComment()) { $items[] = id(new PhabricatorActionView()) ->setIcon('fa-cutlery') ->setHref('/transactions/raw/'.$xaction_phid.'/') ->setName(pht('View Raw')) ->addSigil('transaction-raw') ->setMetadata( array( 'anchor' => $anchor, )); $content_source = $this->getContentSource(); $source_email = PhabricatorContentSource::SOURCE_EMAIL; if ($content_source->getSource() == $source_email) { $source_id = $content_source->getParam('id'); if ($source_id) { $items[] = id(new PhabricatorActionView()) ->setIcon('fa-envelope-o') ->setHref('/transactions/raw/'.$xaction_phid.'/?email') ->setName(pht('View Email Body')) ->addSigil('transaction-raw') ->setMetadata( array( 'anchor' => $anchor, )); } } } if ($this->getIsRemovable()) { $items[] = id(new PhabricatorActionView()) ->setIcon('fa-times') ->setHref('/transactions/remove/'.$xaction_phid.'/') ->setName(pht('Remove Comment')) ->addSigil('transaction-remove') ->setMetadata( array( 'anchor' => $anchor, )); } if ($this->getIsEdited()) { $items[] = id(new PhabricatorActionView()) ->setIcon('fa-list') ->setHref('/transactions/history/'.$xaction_phid.'/') ->setName(pht('View Edit History')) ->setWorkflow(true); } return $items; } } diff --git a/src/view/phui/PHUITimelineView.php b/src/view/phui/PHUITimelineView.php index b17e075711..17178b8135 100644 --- a/src/view/phui/PHUITimelineView.php +++ b/src/view/phui/PHUITimelineView.php @@ -1,186 +1,265 @@ id = $id; return $this; } public function setShouldTerminate($term) { $this->shouldTerminate = $term; return $this; } public function setShouldAddSpacers($bool) { $this->shouldAddSpacers = $bool; return $this; } public function setPager(AphrontCursorPagerView $pager) { $this->pager = $pager; return $this; } public function getPager() { return $this->pager; } public function addEvent(PHUITimelineEventView $event) { $this->events[] = $event; return $this; } public function setRenderData(array $data) { $this->renderData = $data; return $this; } public function setQuoteTargetID($quote_target_id) { $this->quoteTargetID = $quote_target_id; return $this; } public function getQuoteTargetID() { return $this->quoteTargetID; } public function setQuoteRef($quote_ref) { $this->quoteRef = $quote_ref; return $this; } public function getQuoteRef() { return $this->quoteRef; } public function render() { if ($this->getPager()) { if ($this->id === null) { $this->id = celerity_generate_unique_node_id(); } Javelin::initBehavior( 'phabricator-show-older-transactions', array( 'timelineID' => $this->id, 'renderData' => $this->renderData, )); } $events = $this->buildEvents(); return phutil_tag( 'div', array( 'class' => 'phui-timeline-view', 'id' => $this->id, ), $events); } public function buildEvents() { require_celerity_resource('phui-timeline-view-css'); $spacer = self::renderSpacer(); $hide = array(); $show = array(); // Bucket timeline events into events we'll hide by default (because they // predate your most recent interaction with the object) and events we'll // show by default. foreach ($this->events as $event) { if ($event->getHideByDefault()) { $hide[] = $event; } else { $show[] = $event; } } // If you've never interacted with the object, all the events will be shown // by default. We may still need to paginate if there are a large number // of events. $more = (bool)$hide; if ($this->getPager()) { if ($this->getPager()->getHasMoreResults()) { $more = true; } } $events = array(); if ($more && $this->getPager()) { $uri = $this->getPager()->getNextPageURI(); $uri->setQueryParam('quoteTargetID', $this->getQuoteTargetID()); $uri->setQueryParam('quoteRef', $this->getQuoteRef()); $events[] = javelin_tag( 'div', array( 'sigil' => 'show-older-block', 'class' => 'phui-timeline-older-transactions-are-hidden', ), array( pht('Older changes are hidden. '), ' ', javelin_tag( 'a', array( 'href' => (string)$uri, 'mustcapture' => true, 'sigil' => 'show-older-link', ), pht('Show older changes.')), )); if ($show) { $events[] = $spacer; } } if ($show) { + $this->prepareBadgeData($show); $events[] = phutil_implode_html($spacer, $show); } if ($events) { if ($this->shouldAddSpacers) { $events = array($spacer, $events, $spacer); } } else { $events = array($spacer); } if ($this->shouldTerminate) { $events[] = self::renderEnder(true); } return $events; } public static function renderSpacer() { return phutil_tag( 'div', array( 'class' => 'phui-timeline-event-view '. 'phui-timeline-spacer', ), ''); } public static function renderEnder() { return phutil_tag( 'div', array( 'class' => 'phui-timeline-event-view '. 'the-worlds-end', ), ''); } + private function prepareBadgeData(array $events) { + assert_instances_of($events, 'PHUITimelineEventView'); + + $viewer = $this->getUser(); + $can_use_badges = PhabricatorApplication::isClassInstalledForViewer( + 'PhabricatorBadgesApplication', + $viewer); + if (!$can_use_badges) { + return; + } + + $user_phid_type = PhabricatorPeopleUserPHIDType::TYPECONST; + $badge_edge_type = PhabricatorRecipientHasBadgeEdgeType::EDGECONST; + + $user_phids = array(); + foreach ($events as $key => $event) { + if (!$event->hasChildren()) { + // This is a minor event, so we don't have space to show badges. + unset($events[$key]); + continue; + } + + $author_phid = $event->getAuthorPHID(); + if (!$author_phid) { + unset($events[$key]); + continue; + } + + if (phid_get_type($author_phid) != $user_phid_type) { + // This is likely an application actor, like "Herald" or "Harbormaster". + // They can't have badges. + unset($events[$key]); + continue; + } + + $user_phids[$author_phid] = $author_phid; + } + + if (!$user_phids) { + return; + } + + $edges = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($user_phids) + ->withEdgeTypes(array($badge_edge_type)); + $edges->execute(); + + $badge_phids = $edges->getDestinationPHIDs(); + if (!$badge_phids) { + return; + } + + $all_badges = id(new PhabricatorBadgesQuery()) + ->setViewer($viewer) + ->withPHIDs($badge_phids) + ->execute(); + $all_badges = mpull($all_badges, null, 'getPHID'); + + foreach ($events as $event) { + $author_phid = $event->getAuthorPHID(); + $event_phids = $edges->getDestinationPHIDs(array($author_phid)); + $badges = array_select_keys($all_badges, $event_phids); + + // TODO: Pick the "best" badges in some smart way. For now, just pick + // the first two. + $badges = array_slice($badges, 0, 2); + foreach ($badges as $badge) { + $badge_view = id(new PHUIBadgeMiniView()) + ->setIcon($badge->getIcon()) + ->setQuality($badge->getQuality()) + ->setHeader($badge->getName()) + ->setHref('/badges/view/'.$badge->getID()); + + $event->addBadge($badge_view); + } + } + } + }