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(); // Track why we're hiding older results. $hide_reason = null; $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 ($more) { $hide_reason = 'comment'; } if ($this->getPager()) { if ($this->getPager()->getHasMoreResults()) { if (!$more) { $hide_reason = 'limit'; } $more = true; } } $events = array(); if ($more && $this->getPager()) { switch ($hide_reason) { case 'comment': $hide_help = pht( 'Changes from before your most recent comment are hidden.'); break; case 'limit': default: $hide_help = pht( 'There are a very large number of changes, so older changes are '. 'hidden.'); break; } $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( $hide_help, ' ', 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()) ->setTipDirection('E') ->setHref('/badges/view/'.$badge->getID()); $event->addBadge($badge_view); } } } }