diff --git a/src/applications/feed/story/PhabricatorFeedStory.php b/src/applications/feed/story/PhabricatorFeedStory.php index e81031e16b..4ee8a6372f 100644 --- a/src/applications/feed/story/PhabricatorFeedStory.php +++ b/src/applications/feed/story/PhabricatorFeedStory.php @@ -1,379 +1,428 @@ List of @{class:PhabricatorFeedStoryData} rows from the * database. * @return list List of @{class:PhabricatorFeedStory} * objects. * @task load */ public static function loadAllFromRows(array $rows, PhabricatorUser $viewer) { $stories = array(); $data = id(new PhabricatorFeedStoryData())->loadAllFromArray($rows); foreach ($data as $story_data) { $class = $story_data->getStoryType(); try { $ok = class_exists($class) && is_subclass_of($class, 'PhabricatorFeedStory'); } catch (PhutilMissingSymbolException $ex) { $ok = false; } // If the story type isn't a valid class or isn't a subclass of // PhabricatorFeedStory, decline to load it. if (!$ok) { continue; } $key = $story_data->getChronologicalKey(); $stories[$key] = newv($class, array($story_data)); } $object_phids = array(); $key_phids = array(); foreach ($stories as $key => $story) { $phids = array(); foreach ($story->getRequiredObjectPHIDs() as $phid) { $phids[$phid] = true; } if ($story->getPrimaryObjectPHID()) { $phids[$story->getPrimaryObjectPHID()] = true; } $key_phids[$key] = $phids; $object_phids += $phids; } $objects = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array_keys($object_phids)) ->execute(); foreach ($key_phids as $key => $phids) { if (!$phids) { continue; } $story_objects = array_select_keys($objects, array_keys($phids)); if (count($story_objects) != count($phids)) { // An object this story requires either does not exist or is not visible // to the user. Decline to render the story. unset($stories[$key]); unset($key_phids[$key]); continue; } $stories[$key]->setObjects($story_objects); } + // If stories are about PhabricatorProjectInterface objects, load the + // projects the objects are a part of so we can render project tags + // on the stories. + + $project_phids = array(); + foreach ($objects as $object) { + if ($object instanceof PhabricatorProjectInterface) { + $project_phids[$object->getPHID()] = array(); + } + } + + if ($project_phids) { + $edge_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs(array_keys($project_phids)) + ->withEdgeTypes( + array( + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, + )); + $edge_query->execute(); + foreach ($project_phids as $phid => $ignored) { + $project_phids[$phid] = $edge_query->getDestinationPHIDs(array($phid)); + } + } + $handle_phids = array(); foreach ($stories as $key => $story) { foreach ($story->getRequiredHandlePHIDs() as $phid) { $key_phids[$key][$phid] = true; } if ($story->getAuthorPHID()) { $key_phids[$key][$story->getAuthorPHID()] = true; } + + $object_phid = $story->getPrimaryObjectPHID(); + $object_project_phids = idx($project_phids, $object_phid, array()); + $story->setProjectPHIDs($object_project_phids); + foreach ($object_project_phids as $dst) { + $key_phids[$key][$dst] = true; + } + $handle_phids += $key_phids[$key]; } $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(array_keys($handle_phids)) ->execute(); foreach ($key_phids as $key => $phids) { if (!$phids) { continue; } $story_handles = array_select_keys($handles, array_keys($phids)); $stories[$key]->setHandles($story_handles); } return $stories; } public function setHovercard($hover) { $this->hovercard = $hover; return $this; } public function setRenderingTarget($target) { $this->validateRenderingTarget($target); $this->renderingTarget = $target; return $this; } public function getRenderingTarget() { return $this->renderingTarget; } private function validateRenderingTarget($target) { switch ($target) { case PhabricatorApplicationTransaction::TARGET_HTML: case PhabricatorApplicationTransaction::TARGET_TEXT: break; default: throw new Exception('Unknown rendering target: '.$target); break; } } public function setObjects(array $objects) { $this->objects = $objects; return $this; } public function getObject($phid) { $object = idx($this->objects, $phid); if (!$object) { throw new Exception( "Story is asking for an object it did not request ('{$phid}')!"); } return $object; } public function getPrimaryObject() { $phid = $this->getPrimaryObjectPHID(); if (!$phid) { throw new Exception('Story has no primary object!'); } return $this->getObject($phid); } public function getPrimaryObjectPHID() { return null; } final public function __construct(PhabricatorFeedStoryData $data) { $this->data = $data; } abstract public function renderView(); public function getRequiredHandlePHIDs() { return array(); } public function getRequiredObjectPHIDs() { return array(); } public function setHasViewed($has_viewed) { $this->hasViewed = $has_viewed; return $this; } public function getHasViewed() { return $this->hasViewed; } final public function setFramed($framed) { $this->framed = $framed; return $this; } final public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } final protected function getObjects() { return $this->objects; } final protected function getHandles() { return $this->handles; } final protected function getHandle($phid) { if (isset($this->handles[$phid])) { if ($this->handles[$phid] instanceof PhabricatorObjectHandle) { return $this->handles[$phid]; } } $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); $handle->setName("Unloaded Object '{$phid}'"); return $handle; } final public function getStoryData() { return $this->data; } final public function getEpoch() { return $this->getStoryData()->getEpoch(); } final public function getChronologicalKey() { return $this->getStoryData()->getChronologicalKey(); } final public function getValue($key, $default = null) { return $this->getStoryData()->getValue($key, $default); } final public function getAuthorPHID() { return $this->getStoryData()->getAuthorPHID(); } final protected function renderHandleList(array $phids) { $items = array(); foreach ($phids as $phid) { $items[] = $this->linkTo($phid); } $list = null; switch ($this->getRenderingTarget()) { case PhabricatorApplicationTransaction::TARGET_TEXT: $list = implode(', ', $items); break; case PhabricatorApplicationTransaction::TARGET_HTML: $list = phutil_implode_html(', ', $items); break; } return $list; } final protected function linkTo($phid) { $handle = $this->getHandle($phid); switch ($this->getRenderingTarget()) { case PhabricatorApplicationTransaction::TARGET_TEXT: return $handle->getLinkName(); } // NOTE: We render our own link here to customize the styling and add // the '_top' target for framed feeds. $class = null; if ($handle->getType() == PhabricatorPeopleUserPHIDType::TYPECONST) { $class = 'phui-link-person'; } return javelin_tag( 'a', array( 'href' => $handle->getURI(), 'target' => $this->framed ? '_top' : null, 'sigil' => $this->hovercard ? 'hovercard' : null, 'meta' => $this->hovercard ? array('hoverPHID' => $phid) : null, 'class' => $class, ), $handle->getLinkName()); } final protected function renderString($str) { switch ($this->getRenderingTarget()) { case PhabricatorApplicationTransaction::TARGET_TEXT: return $str; case PhabricatorApplicationTransaction::TARGET_HTML: return phutil_tag('strong', array(), $str); } } final protected function renderSummary($text, $len = 128) { if ($len) { $text = phutil_utf8_shorten($text, $len); } switch ($this->getRenderingTarget()) { case PhabricatorApplicationTransaction::TARGET_HTML: $text = phutil_escape_html_newlines($text); break; } return $text; } public function getNotificationAggregations() { return array(); } protected function newStoryView() { - return id(new PHUIFeedStoryView()) + $view = id(new PHUIFeedStoryView()) ->setChronologicalKey($this->getChronologicalKey()) ->setEpoch($this->getEpoch()) ->setViewed($this->getHasViewed()); + + $project_phids = $this->getProjectPHIDs(); + if ($project_phids) { + $view->setTags($this->renderHandleList($project_phids)); + } + + return $view; + } + + public function setProjectPHIDs(array $phids) { + $this->projectPHIDs = $phids; + return $this; + } + + public function getProjectPHIDs() { + return $this->projectPHIDs; } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getPHID() { return null; } /** * @task policy */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } /** * @task policy */ public function getPolicy($capability) { // If this story's primary object is a policy-aware object, use its policy // to control story visiblity. $primary_phid = $this->getPrimaryObjectPHID(); if (isset($this->objects[$primary_phid])) { $object = $this->objects[$primary_phid]; if ($object instanceof PhabricatorPolicyInterface) { return $object->getPolicy($capability); } } // TODO: Remove this once all objects are policy-aware. For now, keep // respecting the `feed.public` setting. return PhabricatorEnv::getEnvConfig('feed.public') ? PhabricatorPolicies::POLICY_PUBLIC : PhabricatorPolicies::POLICY_USER; } /** * @task policy */ public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/view/phui/PHUIFeedStoryView.php b/src/view/phui/PHUIFeedStoryView.php index 60d71005ae..de717e25d1 100644 --- a/src/view/phui/PHUIFeedStoryView.php +++ b/src/view/phui/PHUIFeedStoryView.php @@ -1,275 +1,294 @@ tags = $tags; + return $this; + } + + public function getTags() { + return $this->tags; + } public function setChronologicalKey($chronological_key) { $this->chronologicalKey = $chronological_key; return $this; } public function getChronologicalKey() { return $this->chronologicalKey; } public function setTitle($title) { $this->title = $title; return $this; } public function getTitle() { return $this->title; } public function setEpoch($epoch) { $this->epoch = $epoch; return $this; } public function setImage($image) { $this->image = $image; return $this; } public function setImageHref($image_href) { $this->imageHref = $image_href; return $this; } public function setAppIcon($icon) { $this->appIcon = $icon; return $this; } public function setViewed($viewed) { $this->viewed = $viewed; return $this; } public function getViewed() { return $this->viewed; } public function setHref($href) { $this->href = $href; return $this; } public function setTokenBar(array $tokens) { $this->tokenBar = $tokens; return $this; } public function addProject($project) { $this->projects[] = $project; return $this; } public function addAction(PHUIIconView $action) { $this->actions[] = $action; return $this; } public function setPontification($text, $title = null) { if ($title) { $title = phutil_tag('h3', array(), $title); } $copy = phutil_tag( 'div', array( 'class' => 'phui-feed-story-bigtext-post', ), array( $title, $text)); $this->appendChild($copy); return $this; } public function getHref() { return $this->href; } public function renderNotification($user) { $classes = array( 'phabricator-notification', ); if (!$this->viewed) { $classes[] = 'phabricator-notification-unread'; } if ($this->epoch) { if ($user) { $foot = phabricator_datetime($this->epoch, $user); $foot = phutil_tag( 'span', array( 'class' => 'phabricator-notification-date'), $foot); } else { $foot = null; } } else { $foot = pht('No time specified.'); } return javelin_tag( 'div', array( 'class' => implode(' ', $classes), 'sigil' => 'notification', 'meta' => array( 'href' => $this->getHref(), ), ), array($this->title, $foot)); } public function render() { require_celerity_resource('phui-feed-story-css'); Javelin::initBehavior('phabricator-hovercards'); $body = null; $foot = null; $image_style = null; $actor = ''; if ($this->image) { $actor = new PHUIIconView(); $actor->setImage($this->image); $actor->addClass('phui-feed-story-actor-image'); if ($this->imageHref) { $actor->setHref($this->imageHref); } } if ($this->epoch) { // TODO: This is really bad; when rendering through Conduit and via // renderText() we don't have a user. if ($this->user) { $foot = phabricator_datetime($this->epoch, $this->user); } else { $foot = null; } } else { $foot = pht('No time specified.'); } if ($this->chronologicalKey) { $foot = phutil_tag( 'a', array( 'href' => '/feed/'.$this->chronologicalKey.'/', ), $foot); } $icon = null; if ($this->appIcon) { $icon = new PHUIIconView(); $icon->setSpriteIcon($this->appIcon); $icon->setSpriteSheet(PHUIIconView::SPRITE_APPS); } $action_list = array(); $icons = null; foreach ($this->actions as $action) { $action_list[] = phutil_tag( 'li', array( 'class' => 'phui-feed-story-action-item' ), $action); } if (!empty($action_list)) { $icons = phutil_tag( 'ul', array( 'class' => 'phui-feed-story-action-list' ), $action_list); } $head = phutil_tag( 'div', array( 'class' => 'phui-feed-story-head', ), array( $actor, nonempty($this->title, pht('Untitled Story')), $icons, )); if (!empty($this->tokenBar)) { $tokenview = phutil_tag( 'div', array( 'class' => 'phui-feed-token-bar' ), $this->tokenBar); $this->appendChild($tokenview); } $body_content = $this->renderChildren(); if ($body_content) { $body = phutil_tag( 'div', array( 'class' => 'phui-feed-story-body', ), $body_content); } + $tags = null; + if ($this->tags) { + $tags = array( + " \xC2\xB7 ", + $this->tags); + } + $foot = phutil_tag( 'div', array( 'class' => 'phui-feed-story-foot', ), array( $icon, - $foot)); + $foot, + $tags, + )); $classes = array('phui-feed-story'); return id(new PHUIBoxView()) ->addClass(implode(' ', $classes)) ->setBorder(true) ->addMargin(PHUI::MARGIN_MEDIUM_BOTTOM) ->appendChild(array($head, $body, $foot)); } public function setAppIconFromPHID($phid) { switch (phid_get_type($phid)) { case PholioMockPHIDType::TYPECONST: $this->setAppIcon('pholio-dark'); break; case PhabricatorMacroMacroPHIDType::TYPECONST: $this->setAppIcon('macro-dark'); break; case ManiphestTaskPHIDType::TYPECONST: $this->setAppIcon('maniphest-dark'); break; case DifferentialRevisionPHIDType::TYPECONST: $this->setAppIcon('differential-dark'); break; case PhabricatorCalendarEventPHIDType::TYPECONST: $this->setAppIcon('calendar-dark'); break; } } }