diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 76d042af86..a9e23c8b04 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -1406,7 +1406,7 @@ celerity_register_resource_map(array( ), 'phabricator-feed-css' => array( - 'uri' => '/res/7d1d0015/rsrc/css/application/feed/feed.css', + 'uri' => '/res/e4bf27b5/rsrc/css/application/feed/feed.css', 'type' => 'css', 'requires' => array( diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index d96123eff2..2d2ec5c4ec 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -512,7 +512,6 @@ phutil_register_library_map(array( 'PhabricatorFeedStoryTypeConstants' => 'applications/feed/constants/story', 'PhabricatorFeedStoryUnknown' => 'applications/feed/story/unknown', 'PhabricatorFeedStoryView' => 'applications/feed/view/story', - 'PhabricatorFeedStreamController' => 'applications/feed/controller/stream', 'PhabricatorFeedView' => 'applications/feed/view/base', 'PhabricatorFile' => 'applications/files/storage/file', 'PhabricatorFileController' => 'applications/files/controller/base', @@ -1242,7 +1241,6 @@ phutil_register_library_map(array( 'PhabricatorFeedStoryTypeConstants' => 'PhabricatorFeedConstants', 'PhabricatorFeedStoryUnknown' => 'PhabricatorFeedStory', 'PhabricatorFeedStoryView' => 'PhabricatorFeedView', - 'PhabricatorFeedStreamController' => 'PhabricatorFeedController', 'PhabricatorFeedView' => 'AphrontView', 'PhabricatorFile' => 'PhabricatorFileDAO', 'PhabricatorFileController' => 'PhabricatorController', diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index 270415aab1..bfdd855a01 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -32,9 +32,7 @@ class AphrontDefaultApplicationConfiguration public function getURIMap() { return $this->getResourceURIMapRules() + array( - '/' => array( - '$' => 'PhabricatorDirectoryMainController', - ), + '/(?:(?Pfeed)/)?$' => 'PhabricatorDirectoryMainController', '/directory/' => array( '(?P\d+)/$' => 'PhabricatorDirectoryCategoryViewController', @@ -359,10 +357,7 @@ class AphrontDefaultApplicationConfiguration => 'PhabricatorCountdownDeleteController' ), - '/feed/' => array( - '$' => 'PhabricatorFeedStreamController', - 'public/$' => 'PhabricatorFeedPublicStreamController', - ), + '/feed/public/$' => 'PhabricatorFeedPublicStreamController', '/V(?P\d+)$' => 'PhabricatorSlowvotePollController', '/vote/' => array( diff --git a/src/applications/directory/controller/base/PhabricatorDirectoryController.php b/src/applications/directory/controller/base/PhabricatorDirectoryController.php index 2c25cf4e6c..c426e267dd 100644 --- a/src/applications/directory/controller/base/PhabricatorDirectoryController.php +++ b/src/applications/directory/controller/base/PhabricatorDirectoryController.php @@ -44,6 +44,7 @@ abstract class PhabricatorDirectoryController extends PhabricatorController { $nav->addLabel('Phabricator'); $nav->addFilter('home', 'Tactical Command', '/'); + $nav->addFilter('feed', 'Feed'); $nav->addSpacer(); $nav->addLabel('Applications'); diff --git a/src/applications/directory/controller/main/PhabricatorDirectoryMainController.php b/src/applications/directory/controller/main/PhabricatorDirectoryMainController.php index 19534fdacf..951cca84c5 100644 --- a/src/applications/directory/controller/main/PhabricatorDirectoryMainController.php +++ b/src/applications/directory/controller/main/PhabricatorDirectoryMainController.php @@ -21,6 +21,10 @@ class PhabricatorDirectoryMainController private $filter; + public function willProcessRequest(array $data) { + $this->filter = idx($data, 'filter'); + } + public function shouldRequireAdmin() { // These controllers are admin-only by default, but this one is public, // so allow non-admin users to view it. @@ -30,18 +34,29 @@ class PhabricatorDirectoryMainController public function processRequest() { $user = $this->getRequest()->getUser(); + $nav = $this->buildNav(); + $this->filter = $nav->selectFilter($this->filter, 'home'); + $project_query = new PhabricatorProjectQuery(); $project_query->setMembers(array($user->getPHID())); $projects = $project_query->execute(); + switch ($this->filter) { + case 'feed': + return $this->buildFeedResponse($nav, $projects); + default: + return $this->buildMainResponse($nav, $projects); + } + + } + + private function buildMainResponse($nav, $projects) { $unbreak_panel = $this->buildUnbreakNowPanel(); $triage_panel = $this->buildNeedsTriagePanel($projects); $revision_panel = $this->buildRevisionPanel(); $tasks_panel = $this->buildTasksPanel(); - $feed_view = $this->buildFeedView($projects); + $feed_view = $this->buildFeedView($projects, $is_full = false); - $nav = $this->buildNav(); - $this->filter = $nav->selectFilter($this->filter, 'home'); $content = array( $unbreak_panel, @@ -56,8 +71,17 @@ class PhabricatorDirectoryMainController return $this->buildStandardPageResponse( $nav, array( - 'title' => 'Directory', - 'tab' => 'directory', + 'title' => 'Phabricator', + )); + } + + private function buildFeedResponse($nav, $projects) { + $view = $this->buildFeedView($projects, $is_full = true); + $nav->appendChild($view); + return $this->buildStandardPageResponse( + $nav, + array( + 'title' => 'Feed', )); } @@ -252,8 +276,9 @@ class PhabricatorDirectoryMainController return $view; } - private function buildFeedView(array $projects) { - $user = $this->getRequest()->getUser(); + private function buildFeedView(array $projects, $is_full) { + $request = $this->getRequest(); + $user = $request->getUser(); $user_phid = $user->getPHID(); $feed_query = new PhabricatorFeedQuery(); @@ -261,19 +286,77 @@ class PhabricatorDirectoryMainController array_merge( array($user_phid), mpull($projects, 'getPHID'))); + + // TODO: All this limit stuff should probably be consolidated into the + // feed query? + + $old_link = null; + $new_link = null; + + if ($is_full) { + $feed_query->setAfter($request->getStr('after')); + $feed_query->setBefore($request->getStr('before')); + $limit = 500; + } else { + $limit = 100; + } + + // Grab one more story than we intend to display so we can figure out + // if we need to render an "Older Posts" link or not (with reasonable + // accuracy, at least). + $feed_query->setLimit($limit + 1); $feed = $feed_query->execute(); + $extra_row = (count($feed) == $limit + 1); + + if ($is_full) { + $have_new = ($request->getStr('before')) || + ($request->getStr('after') && $extra_row); + } else { + $have_new = false; + } + + $have_old = ($request->getStr('after')) || + ($request->getStr('before') && $extra_row) || + (!$request->getStr('before') && + !$request->getStr('after') && + $extra_row); + $feed = array_slice($feed, 0, $limit, $preserve_keys = true); + + if ($have_old) { + $old_link = phutil_render_tag( + 'a', + array( + 'href' => '/feed/?before='.end($feed)->getChronologicalKey(), + 'class' => 'phabricator-feed-older-link', + ), + "Older Stories \xC2\xBB"); + } + if ($have_new) { + $new_link = phutil_render_tag( + 'a', + array( + 'href' => '/feed/?after='.reset($feed)->getChronologicalKey(), + 'class' => 'phabricator-feed-newer-link', + ), + "\xC2\xAB Newer Stories"); + } $builder = new PhabricatorFeedBuilder($feed); $builder->setUser($user); $feed_view = $builder->buildView(); - return - '
'. - '

Feed

'. + '
'. + '
'. + '

Feed

'. + '
'. $feed_view->render(). + '
'. + $new_link. + $old_link. + '
'. '
'; } diff --git a/src/applications/feed/controller/stream/PhabricatorFeedStreamController.php b/src/applications/feed/controller/stream/PhabricatorFeedStreamController.php deleted file mode 100644 index 6dafd5fd9c..0000000000 --- a/src/applications/feed/controller/stream/PhabricatorFeedStreamController.php +++ /dev/null @@ -1,74 +0,0 @@ -getRequest(); - $viewer = $request->getUser(); - - if ($request->isFormPost()) { - $story = id(new PhabricatorFeedStoryPublisher()) - ->setRelatedPHIDs(array($viewer->getPHID())) - ->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_STATUS) - ->setStoryTime(time()) - ->setStoryAuthorPHID($viewer->getPHID()) - ->setStoryData( - array( - 'content' => $request->getStr('status') - )) - ->publish(); - - return id(new AphrontRedirectResponse())->setURI( - $request->getRequestURI()); - } - - $query = new PhabricatorFeedQuery(); - $stories = $query->execute(); - - $builder = new PhabricatorFeedBuilder($stories); - $builder->setUser($request->getUser()); - $view = $builder->buildView(); - - $post_form = id(new AphrontFormView()) - ->setUser($viewer) - ->appendChild( - id(new AphrontFormTextAreaControl()) - ->setLabel('Pithy Wit') - ->setName('status')) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue('Publish')); - - $post = new AphrontPanelView(); - $post->setWidth(AphrontPanelView::WIDTH_FORM); - $post->setHeader('High Horse Soapbox'); - $post->appendChild($post_form); - - $page = array(); - $page[] = $post; - $page[] = $view; - - return $this->buildStandardPageResponse( - $page, - array( - 'title' => 'Feed', - )); - } -} diff --git a/src/applications/feed/controller/stream/__init__.php b/src/applications/feed/controller/stream/__init__.php deleted file mode 100644 index 2becac7d1d..0000000000 --- a/src/applications/feed/controller/stream/__init__.php +++ /dev/null @@ -1,23 +0,0 @@ -filterPHIDs = $phids; @@ -37,6 +38,11 @@ final class PhabricatorFeedQuery { return $this; } + public function setBefore($before) { + $this->before = $before; + return $this; + } + public function execute() { $ref_table = new PhabricatorFeedStoryReference(); @@ -52,6 +58,29 @@ final class PhabricatorFeedQuery { $this->filterPHIDs); } + // For "before" queries, we can just add a constraint to the WHERE clause. + // For "after" queries, we must also reverse the result ordering, since + // otherwise we'll always grab the first page of results if there's a limit. + // After MySQL applies the limit, we reverse the page in PHP (below) to + // ensure consistent ordering. + + $order = 'DESC'; + + if ($this->after) { + $where[] = qsprintf( + $conn, + 'ref.chronologicalKey > %s', + $this->after); + $order = 'ASC'; + } + + if ($this->before) { + $where[] = qsprintf( + $conn, + 'ref.chronologicalKey < %s', + $this->before); + } + if ($where) { $where = 'WHERE ('.implode(') AND (', $where).')'; } else { @@ -64,12 +93,20 @@ final class PhabricatorFeedQuery { JOIN %T story ON ref.chronologicalKey = story.chronologicalKey %Q GROUP BY story.chronologicalKey - ORDER BY story.chronologicalKey DESC + ORDER BY story.chronologicalKey %Q LIMIT %d', $ref_table->getTableName(), $story_table->getTableName(), $where, + $order, $this->limit); + + if ($order != 'DESC') { + // If we did order ASC to pull 'after' data, reverse the result set so + // that stories are returned in a consistent (descending) order. + $data = array_reverse($data); + } + $data = $story_table->loadAllFromArray($data); $stories = array(); diff --git a/src/applications/feed/story/base/PhabricatorFeedStory.php b/src/applications/feed/story/base/PhabricatorFeedStory.php index 5cfcd90b1b..7ab94f12c0 100644 --- a/src/applications/feed/story/base/PhabricatorFeedStory.php +++ b/src/applications/feed/story/base/PhabricatorFeedStory.php @@ -83,6 +83,10 @@ abstract class PhabricatorFeedStory { return $this->getStoryData()->getEpoch(); } + final public function getChronologicalKey() { + return $this->getStoryData()->getChronologicalKey(); + } + final protected function renderHandleList(array $phids) { $list = array(); foreach ($phids as $phid) { diff --git a/webroot/rsrc/css/application/feed/feed.css b/webroot/rsrc/css/application/feed/feed.css index e9fae34971..cb73e82d65 100644 --- a/webroot/rsrc/css/application/feed/feed.css +++ b/webroot/rsrc/css/application/feed/feed.css @@ -43,3 +43,14 @@ .phabricator-feed-story-date-separator { margin-top: 2em; } + +.phabricator-feed-newer-link { + float: left; + font-weight: bold; +} + +.phabricator-feed-older-link { + float: right; + font-weight: bold; +} +