From 662a4dc54fa3cd6a093542a40b8a6b494ba06f3d Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Tue, 7 Jul 2015 19:23:13 +0930 Subject: [PATCH] Finish client action refactoring. closes flarum/core#126 --- framework/core/src/Core/Users/User.php | 17 +- .../core/src/Forum/Actions/ClientAction.php | 130 +--------- .../src/Forum/Actions/DiscussionAction.php | 27 +- .../core/src/Forum/Actions/IndexAction.php | 60 +++-- .../core/src/Locale/LocaleServiceProvider.php | 4 +- framework/core/src/Support/ClientAction.php | 231 ++++++++++++++++++ framework/core/src/Support/ClientView.php | 177 ++++++++++++-- framework/core/views/app.blade.php | 4 + framework/core/views/forum.blade.php | 38 ++- 9 files changed, 518 insertions(+), 170 deletions(-) create mode 100644 framework/core/src/Support/ClientAction.php diff --git a/framework/core/src/Core/Users/User.php b/framework/core/src/Core/Users/User.php index fa45ff66a..7ced9ff8e 100755 --- a/framework/core/src/Core/Users/User.php +++ b/framework/core/src/Core/Users/User.php @@ -1,5 +1,6 @@ avatar_path ? app('Flarum\Http\UrlGeneratorInterface')->toAsset('assets/avatars/'.$this->avatar_path) : null; + $urlGenerator = app('Flarum\Http\UrlGeneratorInterface'); + + return $this->avatar_path ? $urlGenerator->toAsset('assets/avatars/'.$this->avatar_path) : null; + } + + /** + * Get the user's locale, falling back to the forum's default if they + * haven't set one. + * + * @param string $value + * @return string + */ + public function getLocaleAttribute($value) + { + return $value ?: Core::config('locale', 'en'); } /** diff --git a/framework/core/src/Forum/Actions/ClientAction.php b/framework/core/src/Forum/Actions/ClientAction.php index 06b3445d2..623e29169 100644 --- a/framework/core/src/Forum/Actions/ClientAction.php +++ b/framework/core/src/Forum/Actions/ClientAction.php @@ -1,134 +1,16 @@ apiClient = $apiClient; - $this->locales = $locales; - } - - /** - * @param Request $request - * @param array $routeParams - * @return \Flarum\Support\ClientView - */ - public function render(Request $request, array $routeParams = []) - { - $actor = app('flarum.actor'); - - $assets = $this->getAssets(); - $locale = $this->getLocaleCompiler($actor); - - $layout = 'flarum.forum::forum'; - - $view = new ClientView( - $request, - $actor, - $this->apiClient, - $layout, - $assets, - $locale - ); - - return $view; - } - - protected function getAssets() - { - $public = $this->getAssetDirectory(); - - $assets = new AssetManager( - new JsCompiler($public, 'forum.js'), - new LessCompiler($public, 'forum.css') - ); - - $root = __DIR__.'/../../..'; - $assets->addFile($root.'/js/forum/dist/app.js'); - $assets->addFile($root.'/less/forum/app.less'); - - foreach ($this->getLessVariables() as $name => $value) { - $assets->addLess("@$name: $value;"); - } - - $assets->addLess(Core::config('custom_less')); - - return $assets; - } - - protected function getLessVariables() - { - return [ - 'fl-primary-color' => Core::config('theme_primary_color', '#000'), - 'fl-secondary-color' => Core::config('theme_secondary_color', '#000'), - 'fl-dark-mode' => Core::config('theme_dark_mode') ? 'true' : 'false', - 'fl-colored-header' => Core::config('theme_colored_header') ? 'true' : 'false' - ]; - } - - protected function getLocaleCompiler(User $actor) - { - $locale = $actor->locale ?: Core::config('locale', 'en'); - -// $translations = $this->locales->getTranslations($locale); - $jsFiles = $this->locales->getJsFiles($locale); - - $compiler = new LocaleJsCompiler($this->getAssetDirectory(), 'locale-'.$locale.'.js'); -// $compiler->setTranslations(static::filterTranslations($translations)); - array_walk($jsFiles, [$compiler, 'addFile']); - - return $compiler; - } - - protected function getAssetDirectory() - { - return public_path().'/assets'; - } - - /** - * @param $translations - * @return array - */ -// protected static function filterTranslations($translations) -// { -// $filtered = []; -// -// foreach (static::$translations as $key) { -// $parts = explode('.', $key); -// $level = &$filtered; -// -// foreach ($parts as $part) { -// $level = &$level[$part]; -// } -// -// $level = array_get($translations, $key); -// } -// -// return $filtered; -// } + protected $layout = 'flarum.forum::forum'; } diff --git a/framework/core/src/Forum/Actions/DiscussionAction.php b/framework/core/src/Forum/Actions/DiscussionAction.php index 9a0d0e925..fff5a3b3b 100644 --- a/framework/core/src/Forum/Actions/DiscussionAction.php +++ b/framework/core/src/Forum/Actions/DiscussionAction.php @@ -4,18 +4,21 @@ use Psr\Http\Message\ServerRequestInterface as Request; class DiscussionAction extends ClientAction { + /** + * {@inheritdoc} + */ public function render(Request $request, array $routeParams = []) { $view = parent::render($request, $routeParams); - $actor = app('flarum.actor'); - $action = 'Flarum\Api\Actions\Discussions\ShowAction'; $params = [ - 'id' => $routeParams['id'], - 'page.near' => $routeParams['near'] + 'id' => array_get($routeParams, 'id'), + 'page.near' => array_get($routeParams, 'near') ]; - $document = $this->apiClient->send($actor, $action, $params)->getBody(); + // FIXME: make sure this is extensible. 404s, pagination. + + $document = $this->preload($params); $view->setTitle($document->data->attributes->title); $view->setDocument($document); @@ -23,4 +26,18 @@ class DiscussionAction extends ClientAction return $view; } + + /** + * Get the result of an API request to show a discussion. + * + * @param array $params + * @return object + */ + protected function preload(array $params) + { + $actor = app('flarum.actor'); + $action = 'Flarum\Api\Actions\Discussions\ShowAction'; + + return $this->apiClient->send($actor, $action, $params)->getBody(); + } } diff --git a/framework/core/src/Forum/Actions/IndexAction.php b/framework/core/src/Forum/Actions/IndexAction.php index 2195ec8ae..6e038ab4b 100644 --- a/framework/core/src/Forum/Actions/IndexAction.php +++ b/framework/core/src/Forum/Actions/IndexAction.php @@ -1,22 +1,23 @@ '-lastTime', + 'replies' => '-commentsCount', + 'newest' => '-startTime', + 'oldest' => '+startTime' + ]; + + /** + * {@inheritdoc} */ public function render(Request $request, array $routeParams = []) { @@ -24,18 +25,35 @@ class IndexAction extends ClientAction $queryParams = $request->getQueryParams(); - // Only preload data if we're viewing the default index with no filters, - // otherwise we have to do all kinds of crazy stuff - if (! count($queryParams) && $request->getUri()->getPath() === '/') { - $actor = app('flarum.actor'); - $action = 'Flarum\Api\Actions\Discussions\IndexAction'; + $sort = array_pull($queryParams, 'sort'); + $q = array_pull($queryParams, 'q'); - $document = $this->apiClient->send($actor, $action)->getBody(); + $params = [ + 'sort' => $sort ? $this->sortMap[$sort] : '', + 'q' => $q + ]; - $view->setDocument($document); - $view->setContent(app('view')->make('flarum.forum::index', compact('document'))); - } + // FIXME: make sure this is extensible. Support pagination. + + $document = $this->preload($params); + + $view->setDocument($document); + $view->setContent(app('view')->make('flarum.forum::index', compact('document'))); return $view; } + + /** + * Get the result of an API request to list discussions. + * + * @param array $params + * @return object + */ + protected function preload(array $params) + { + $actor = app('flarum.actor'); + $action = 'Flarum\Api\Actions\Discussions\IndexAction'; + + return $this->apiClient->send($actor, $action, $params)->getBody(); + } } diff --git a/framework/core/src/Locale/LocaleServiceProvider.php b/framework/core/src/Locale/LocaleServiceProvider.php index 4f0b5801d..ac339dca8 100644 --- a/framework/core/src/Locale/LocaleServiceProvider.php +++ b/framework/core/src/Locale/LocaleServiceProvider.php @@ -29,6 +29,8 @@ class LocaleServiceProvider extends ServiceProvider public function register() { - $this->app->singleton('flarum.localeManager', 'Flarum\Locale\LocaleManager'); + $this->app->singleton('Flarum\Locale\LocaleManager'); + + $this->app->alias('Flarum\Locale\LocaleManager', 'flarum.localeManager'); } } diff --git a/framework/core/src/Support/ClientAction.php b/framework/core/src/Support/ClientAction.php new file mode 100644 index 000000000..d75c1e744 --- /dev/null +++ b/framework/core/src/Support/ClientAction.php @@ -0,0 +1,231 @@ +apiClient = $apiClient; + $this->locales = $locales; + } + + /** + * {@inheritdoc} + * + * @return ClientView + */ + public function render(Request $request, array $routeParams = []) + { + $actor = app('flarum.actor'); + $assets = $this->getAssets(); + $locale = $this->getLocaleCompiler($actor); + + $view = new ClientView( + $this->apiClient, + $request, + $actor, + $assets, + $locale, + $this->layout + ); + + // Now that we've set up the ClientView instance, we can fire an event + // to give extensions the opportunity to add their own assets and + // translations. We will pass an array to the event which specifies + // which translations should be included in the locale file. Afterwards, + // we will filter all of the translations for the actor's locale and + // compile only the ones we need. + $translations = $this->locales->getTranslations($actor->locale); + $keys = $this->translationKeys; + + // TODO: event($this, $view, $keys) + + $translations = $this->filterTranslations($translations, $keys); + + $locale->setTranslations($translations); + + return $view; + } + + /** + * Set up the asset manager, preloaded with a JavaScript compiler and a LESS + * compiler. Automatically add the files necessary to boot a Flarum client, + * as well as any configured LESS customizations. + * + * @return AssetManager + */ + protected function getAssets() + { + $public = $this->getAssetDirectory(); + + $assets = new AssetManager( + new JsCompiler($public, "$this->clientName.js"), + new LessCompiler($public, "$this->clientName.css") + ); + + $this->addAssets($assets); + $this->addCustomizations($assets); + + return $assets; + } + + /** + * Add the assets necessary to boot a Flarum client, found within the + * directory specified by the $clientName property. + * + * @param AssetManager $assets + */ + protected function addAssets(AssetManager $assets) + { + $root = __DIR__.'/../..'; + + $assets->addFile("$root/js/$this->clientName/dist/app.js"); + $assets->addFile("$root/less/$this->clientName/app.less"); + } + + /** + * Add any configured JS/LESS customizations to the asset manager. + * + * @param AssetManager $assets + */ + protected function addCustomizations(AssetManager $assets) + { + foreach ($this->getLessVariables() as $name => $value) { + $assets->addLess("@$name: $value;"); + } + + $assets->addLess(Core::config('custom_less')); + } + + /** + * Get the values of any LESS variables to compile into the CSS, based on + * the forum's configuration. + * + * @return array + */ + protected function getLessVariables() + { + return [ + 'fl-primary-color' => Core::config('theme_primary_color', '#000'), + 'fl-secondary-color' => Core::config('theme_secondary_color', '#000'), + 'fl-dark-mode' => Core::config('theme_dark_mode') ? 'true' : 'false', + 'fl-colored-header' => Core::config('theme_colored_header') ? 'true' : 'false' + ]; + } + + /** + * Set up the locale compiler for the given user's locale. + * + * @param User $actor + * @return LocaleJsCompiler + */ + protected function getLocaleCompiler(User $actor) + { + $locale = $actor->locale; + + $compiler = new LocaleJsCompiler($this->getAssetDirectory(), "$this->clientName-$locale.js"); + + foreach ($this->locales->getJsFiles($locale) as $file) { + $compiler->addFile($file); + } + + return $compiler; + } + + /** + * Get the path to the directory where assets should be written. + * + * @return string + */ + protected function getAssetDirectory() + { + return public_path().'/assets'; + } + + /** + * Take a selection of keys from a collection of translations. + * + * @param array $translations + * @param array $keys + * @return array + */ + protected function filterTranslations(array $translations, array $keys) + { + $filtered = []; + + foreach ($keys as $key) { + $parts = explode('.', $key); + $level = &$filtered; + + foreach ($parts as $part) { + $level = &$level[$part]; + } + + $level = array_get($translations, $key); + } + + return $filtered; + } +} diff --git a/framework/core/src/Support/ClientView.php b/framework/core/src/Support/ClientView.php index 1b5832211..c36388c64 100644 --- a/framework/core/src/Support/ClientView.php +++ b/framework/core/src/Support/ClientView.php @@ -3,61 +3,174 @@ use Flarum\Api\Client; use Flarum\Assets\AssetManager; use Flarum\Core\Users\User; +use Illuminate\Contracts\Support\Renderable; use Psr\Http\Message\ServerRequestInterface as Request; use Flarum\Locale\JsCompiler; -class ClientView +/** + * This class represents a view which boots up Flarum's client. + */ +class ClientView implements Renderable { + /** + * The user who is using the client. + * + * @var User + */ protected $actor; - protected $apiClient; - + /** + * The title of the document, displayed in the tag. + * + * @var null|string + */ protected $title; + /** + * An API response that should be preloaded into the page. + * + * @var null|array|object + */ protected $document; + /** + * The SEO content of the page, displayed in <noscript> tags. + * + * @var string + */ protected $content; - protected $request; - + /** + * The name of the client layout view to display. + * + * @var string + */ protected $layout; + /** + * An array of strings to append to the page's <head>. + * + * @var array + */ + protected $headStrings = []; + + /** + * An array of strings to prepend before the page's </body>. + * + * @var array + */ + protected $footStrings = []; + + /** + * @var Client + */ + protected $apiClient; + + /** + * @var Request + */ + protected $request; + + /** + * @var AssetManager + */ + protected $assets; + + /** + * @var JsCompiler + */ + protected $locale; + + /** + * @param Client $apiClient + * @param Request $request + * @param User $actor + * @param AssetManager $assets + * @param JsCompiler $locale + * @param string $layout + */ public function __construct( + Client $apiClient, Request $request, User $actor, - Client $apiClient, - $layout, AssetManager $assets, - JsCompiler $locale + JsCompiler $locale, + $layout ) { + $this->apiClient = $apiClient; $this->request = $request; $this->actor = $actor; - $this->apiClient = $apiClient; - $this->layout = $layout; $this->assets = $assets; $this->locale = $locale; + $this->layout = $layout; } - public function setActor(User $actor) - { - $this->actor = $actor; - } - + /** + * The title of the document, to be displayed in the <title> tag. + * + * @param null|string $title + */ public function setTitle($title) { $this->title = $title; } + /** + * Set an API response to be preloaded into the page. This should be a + * JSON-API document. + * + * @param null|array|object $document + */ public function setDocument($document) { $this->document = $document; } + /** + * Set the SEO content of the page, to be displayed in <noscript> tags. + * + * @param null|string $content + */ public function setContent($content) { $this->content = $content; } + /** + * Add a string to be appended to the page's <head>. + * + * @param string $string + */ + public function addHeadString($string) + { + $this->headStrings[] = $string; + } + + /** + * Add a string to be prepended before the page's </body>. + * + * @param string $string + */ + public function addFootString($string) + { + $this->footStrings[] = $string; + } + + /** + * Get the view's asset manager. + * + * @return AssetManager + */ + public function getAssets() + { + return $this->assets; + } + + /** + * Get the string contents of the view. + * + * @return string + */ public function render() { $view = app('view')->file(__DIR__.'/../../views/app.blade.php'); @@ -81,25 +194,43 @@ class ClientView $view->styles = [$this->assets->getCssFile()]; $view->scripts = [$this->assets->getJsFile(), $this->locale->getFile()]; + $view->head = implode("\n", $this->headStrings); + $view->foot = implode("\n", $this->footStrings); + return $view->render(); } + /** + * Get the string contents of the view. + * + * @return string + */ public function __toString() { return $this->render(); } + /** + * Get the result of an API request to show the forum. + * + * @return object + */ protected function getForumDocument() { return $this->apiClient->send($this->actor, 'Flarum\Api\Actions\Forum\ShowAction')->getBody(); } + /** + * Get the result of an API request to show the current user. + * + * @return object + */ protected function getUserDocument() { // TODO: calling on the API here results in an extra query to get // the user + their groups, when we already have this information on // $this->actor. Can we simply run the CurrentUserSerializer - // manually? + // manually? Or can we somehow inject this data into the ShowAction? $document = $this->apiClient->send( $this->actor, 'Flarum\Api\Actions\Users\ShowAction', @@ -109,6 +240,13 @@ class ClientView return $document; } + /** + * Get an array of data by merging the 'data' and 'included' keys of a + * JSON-API document. + * + * @param object $document + * @return array + */ protected function getDataFromDocument($document) { $data[] = $document->data; @@ -120,11 +258,16 @@ class ClientView return $data; } + /** + * Get information about the current session. + * + * @return array + */ protected function getSession() { return [ 'userId' => $this->actor->id, - 'token' => $this->request->getCookieParams()['flarum_remember'], + 'token' => array_get($this->request->getCookieParams(), 'flarum_remember'), ]; } } diff --git a/framework/core/views/app.blade.php b/framework/core/views/app.blade.php index c7dad90c7..b59914251 100644 --- a/framework/core/views/app.blade.php +++ b/framework/core/views/app.blade.php @@ -11,6 +11,8 @@ @foreach ($styles as $file) <link rel="stylesheet" href="{{ str_replace(public_path(), '', $file) }}"> @endforeach + + {!! $head !!} </head> <body> @@ -37,5 +39,7 @@ {!! $content !!} </noscript> @endif + + {!! $foot !!} </body> </html> diff --git a/framework/core/views/forum.blade.php b/framework/core/views/forum.blade.php index a98d44901..55fba8f19 100644 --- a/framework/core/views/forum.blade.php +++ b/framework/core/views/forum.blade.php @@ -1,21 +1,56 @@ +<?php +/** + * Forum Client Template + * + * NOTE: You shouldn't edit this file directly. Your changes will be overwritten + * when you update Flarum. See flarum.org/docs/templates to learn how to + * customize your forum's layout. + * + * Flarum's JavaScript client mounts various components into key elements in + * this template. They are distinguished by their ID attributes: + * + * - #page + * - #page-back-button + * - #header + * - #header-back-button + * - #home-link + * - #header-primary-controls + * - #header-secondary-controls + * - #footer + * - #footer-primary-controls + * - #footer-secondary-controls + * - #content + * - #composer + */ +?> <div class="global-page" id="page"> + <div id="back-control"></div> + <div class="global-drawer"> + <header class="global-header" id="header"> <div id="back-button"></div> <div class="container"> - <h1 class="header-title"><a href="{{ $forum->attributes->baseUrl }}" id="home-link">{{ $forum->attributes->title }}</a></h1> + <h1 class="header-title"> + <a href="{{ $forum->attributes->baseUrl }}" id="home-link"> + {{ $forum->attributes->title }} + </a> + </h1> <div id="header-primary" class="header-primary"></div> <div id="header-secondary" class="header-secondary"></div> </div> </header> + <footer class="global-footer" id="footer"> <div class="container"> <div id="footer-primary" class="footer-primary"></div> <div id="footer-secondary" class="footer-secondary"></div> </div> </footer> + </div> + <main class="global-content"> <div id="content"></div> <div class="composer-container"> @@ -24,4 +59,5 @@ </div> </div> </main> + </div>