From 011ae3603e2ec39f0d5680064ae663e71ecff293 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Fri, 13 Feb 2015 10:23:38 +1030 Subject: [PATCH] Implement "renamed" posts Record when the discussion was renamed, from what, and by whom. Information is stored in the `content` field as a serialised JSON object because proper polymorphism will be too difficult with Ember Data and especially when extensions try to add new post types. --- .../app/components/discussion/post-renamed.js | 48 ++++++++++++++ ember/app/controllers/discussion.js | 11 +++- ember/app/models/discussion.js | 1 + ember/app/routes/discussion.js | 34 +++++----- ember/app/styles/flarum/discussion.less | 22 ++++++- .../components/discussion/post-comment.hbs | 9 +-- .../components/discussion/post-controls.hbs | 8 +++ .../components/discussion/post-renamed.hbs | 7 +++ .../components/discussion/post-title.hbs | 4 -- ember/app/views/discussion.js | 5 +- src/Flarum/Api/Actions/Discussions/Update.php | 9 +-- .../Api/Serializers/DiscussionSerializer.php | 13 ++++ .../Api/Serializers/PostBasicSerializer.php | 17 +++-- src/Flarum/Api/Serializers/PostSerializer.php | 6 +- src/Flarum/Core/CoreServiceProvider.php | 62 +++++++++---------- src/Flarum/Core/Discussions/Discussion.php | 23 +++++-- .../Events/DiscussionWasRenamed.php | 5 +- ...PostCreator.php => RenamedPostCreator.php} | 13 ++-- .../{TitleChangePost.php => RenamedPost.php} | 18 ++++-- 19 files changed, 225 insertions(+), 90 deletions(-) create mode 100644 ember/app/components/discussion/post-renamed.js create mode 100644 ember/app/templates/components/discussion/post-controls.hbs create mode 100644 ember/app/templates/components/discussion/post-renamed.hbs delete mode 100644 ember/app/templates/components/discussion/post-title.hbs rename src/Flarum/Core/Listeners/{TitleChangePostCreator.php => RenamedPostCreator.php} (64%) rename src/Flarum/Core/Posts/{TitleChangePost.php => RenamedPost.php} (51%) diff --git a/ember/app/components/discussion/post-renamed.js b/ember/app/components/discussion/post-renamed.js new file mode 100644 index 000000000..ea8c68a68 --- /dev/null +++ b/ember/app/components/discussion/post-renamed.js @@ -0,0 +1,48 @@ +import Ember from 'ember'; + +import FadeIn from 'flarum/mixins/fade-in'; +import HasItemLists from 'flarum/mixins/has-item-lists'; + +var precompileTemplate = Ember.Handlebars.compile; + +/** + Component for a `renamed`-typed post. + */ +export default Ember.Component.extend(FadeIn, HasItemLists, { + layoutName: 'components/discussion/post-renamed', + tagName: 'article', + classNames: ['post', 'post-renamed', 'post-activity'], + itemLists: ['controls'], + + // The stream-content component instansiates this component and sets the + // `content` property to the content of the item in the post-stream object. + // This happens to be our post model! + post: Ember.computed.alias('content'), + + decodedContent: Ember.computed('post.content', function() { + return JSON.parse(this.get('post.content')); + }), + oldTitle: Ember.computed.alias('decodedContent.0'), + newTitle: Ember.computed.alias('decodedContent.1'), + + populateControls: function(items) { + this.addActionItem(items, 'delete', 'Delete', 'times', 'post.canDelete'); + }, + + actions: { + // In the template, we render the "controls" dropdown with the contents of + // the `renderControls` property. This way, when a post is initially + // rendered, it doesn't have to go to the trouble of rendering the + // controls right away, which speeds things up. When the dropdown button + // is clicked, this will fill in the actual controls. + renderControls: function() { + this.set('renderControls', this.get('controls')); + }, + + delete: function() { + var post = this.get('post'); + post.destroyRecord(); + this.sendAction('postRemoved', post); + } + } +}); diff --git a/ember/app/controllers/discussion.js b/ember/app/controllers/discussion.js index 42183059f..e992e0f02 100644 --- a/ember/app/controllers/discussion.js +++ b/ember/app/controllers/discussion.js @@ -107,7 +107,16 @@ export default Ember.Controller.extend(Ember.Evented, UseComposerMixin, { rename: function(title) { var discussion = this.get('model'); discussion.set('title', title); - discussion.save(); + + // When we save the title, we should get back an 'added post' in the + // response which documents the title change. We'll add this to the post + // stream. + var controller = this; + discussion.save().then(function(discussion) { + discussion.get('addedPosts').forEach(function(post) { + controller.get('stream').addPostToEnd(post); + }); + }); }, delete: function() { diff --git a/ember/app/models/discussion.js b/ember/app/models/discussion.js index 1ede0cb4f..fa2875d1f 100644 --- a/ember/app/models/discussion.js +++ b/ember/app/models/discussion.js @@ -36,6 +36,7 @@ export default DS.Model.extend({ }), loadedPosts: DS.hasMany('post'), relevantPosts: DS.hasMany('post'), + addedPosts: DS.hasMany('post'), readTime: DS.attr('date'), readNumber: DS.attr('number'), diff --git a/ember/app/routes/discussion.js b/ember/app/routes/discussion.js index e7143e6d0..c1f7c810f 100644 --- a/ember/app/routes/discussion.js +++ b/ember/app/routes/discussion.js @@ -7,10 +7,19 @@ export default Ember.Route.extend({ start: {replace: true} }, - model: function(params) { - return this.store.findQueryOne('discussion', params.id, { + discussion: function(id, start) { + return this.store.findQueryOne('discussion', id, { include: 'posts', - near: params.start + near: start + }); + }, + + // When we fetch the discussion from the model hook (i.e. on a fresh page + // load), we'll wrap it in an object proxy and set a `loaded` flag to true + // so that it won't be reloaded later on. + model: function(params) { + return this.discussion(params.id, params.start).then(function(discussion) { + return Ember.ObjectProxy.create({content: discussion, loaded: true}); }); }, @@ -36,18 +45,18 @@ export default Ember.Route.extend({ }); controller.set('stream', stream); - // Next, we need to make sure we have a list of the discussion's post - // IDs. If we don't already have this information, we'll need to - // reload the discussion model. - var promise = discussion.get('posts') ? Ember.RSVP.resolve(discussion) : this.model({ - id: discussion.get('id'), - start: controller.get('start') - }); + // We need to make sure we have an up-to-date list of the discussion's + // post IDs. If we didn't enter this route using the model hook (like if + // clicking on a discussion in the index), then we'll reload the model. + var promise = discussion.get('loaded') ? + Ember.RSVP.resolve(discussion.get('content')) : + this.discussion(discussion.get('id'), controller.get('start')); // When we know we have the post IDs, we can set up the post stream with // them. Then we will tell the view that we have finished loading so that // it can scroll down to the appropriate post. promise.then(function(discussion) { + controller.set('model', discussion); var postIds = discussion.get('postIds'); stream.setup(postIds); @@ -70,11 +79,6 @@ export default Ember.Route.extend({ })); } - // Clear the list of post IDs for this discussion (without - // dirtying the record), so that next time we load the discussion, - // the discussion details and post IDs will be refreshed. - controller.store.push('discussion', {id: discussion.get('id'), posts: ''}); - // It's possible for this promise to have resolved but the user // has clicked away to a different discussion. So only if we're // still on the original one, we will tell the view that we're diff --git a/ember/app/styles/flarum/discussion.less b/ember/app/styles/flarum/discussion.less index 82a13f621..1399246cf 100644 --- a/ember/app/styles/flarum/discussion.less +++ b/ember/app/styles/flarum/discussion.less @@ -152,10 +152,9 @@ } .post-icon { float: left; - margin-top: -2px; margin-left: -90px; width: 64px; - text-align: center; + text-align: right; font-size: 22px; } .post.is-hidden { @@ -194,6 +193,25 @@ } } +.post-activity { + &, & a { + color: @fl-body-muted-color; + } + & a { + font-weight: bold; + } +} +.post-activity-info { + font-size: 15px; + margin-bottom: 5px; +} +.post-renamed { + & .old-title, & .new-title { + font-weight: normal; + font-style: italic; + } +} + // ------------------------------------ // Scrubber diff --git a/ember/app/templates/components/discussion/post-comment.hbs b/ember/app/templates/components/discussion/post-comment.hbs index 8658e7e6a..7a031f541 100644 --- a/ember/app/templates/components/discussion/post-comment.hbs +++ b/ember/app/templates/components/discussion/post-comment.hbs @@ -1,11 +1,4 @@ -{{#if controls}} - {{ui/dropdown-button - items=renderControls - class="contextual-controls" - buttonClass="btn btn-default btn-icon btn-sm btn-naked" - buttonClick="renderControls" - menuClass="pull-right"}} -{{/if}} +{{partial "components/discussion/post-controls"}}
{{ui/item-list items=header}} diff --git a/ember/app/templates/components/discussion/post-controls.hbs b/ember/app/templates/components/discussion/post-controls.hbs new file mode 100644 index 000000000..f4a8b920d --- /dev/null +++ b/ember/app/templates/components/discussion/post-controls.hbs @@ -0,0 +1,8 @@ +{{#if controls}} + {{ui/dropdown-button + items=renderControls + class="contextual-controls" + buttonClass="btn btn-default btn-icon btn-sm btn-naked" + buttonClick="renderControls" + menuClass="pull-right"}} +{{/if}} diff --git a/ember/app/templates/components/discussion/post-renamed.hbs b/ember/app/templates/components/discussion/post-renamed.hbs new file mode 100644 index 000000000..bb5ffaa82 --- /dev/null +++ b/ember/app/templates/components/discussion/post-renamed.hbs @@ -0,0 +1,7 @@ +{{partial "components/discussion/post-controls"}} + +{{fa-icon "pencil" class="post-icon"}} + +
{{#link-to "user" post.user class="post-user"}}{{post.user.username}}{{/link-to}} changed the title from {{oldTitle}} to {{newTitle}}.
+ +
{{human-time post.time}}
diff --git a/ember/app/templates/components/discussion/post-title.hbs b/ember/app/templates/components/discussion/post-title.hbs deleted file mode 100644 index 65ec1a122..000000000 --- a/ember/app/templates/components/discussion/post-title.hbs +++ /dev/null @@ -1,4 +0,0 @@ -
- {{fa-icon "pencil" class="post-icon"}} - {{#link-to "user" post.user}}{{post.user.username}}{{/link-to}} changed the title from {{post.oldTitle}} to {{post.newTitle}}. -
diff --git a/ember/app/views/discussion.js b/ember/app/views/discussion.js index f3a3fd9d6..ecdf68313 100644 --- a/ember/app/views/discussion.js +++ b/ember/app/views/discussion.js @@ -71,8 +71,9 @@ export default Ember.View.extend(HasItemLists, { this.addActionItem(items, 'rename', 'Rename', 'pencil', 'discussion.canEdit', function() { var discussion = view.get('controller.model'); - var title = prompt('Enter a new title for this discussion:', discussion.get('title')); - if (title) { + var currentTitle = discussion.get('title'); + var title = prompt('Enter a new title for this discussion:', currentTitle); + if (title && title !== currentTitle) { view.get('controller').send('rename', title); } }); diff --git a/src/Flarum/Api/Actions/Discussions/Update.php b/src/Flarum/Api/Actions/Discussions/Update.php index 5e6d235f2..02b7c6bc5 100644 --- a/src/Flarum/Api/Actions/Discussions/Update.php +++ b/src/Flarum/Api/Actions/Discussions/Update.php @@ -20,12 +20,13 @@ class Update extends Base { $discussionId = $this->param('id'); $readNumber = $this->input('discussions.readNumber'); + $user = User::current(); // First, we will run the EditDiscussionCommand. This will update the // discussion's direct properties; by default, this is just the title. // As usual, however, we will fire an event to allow plugins to update // additional properties. - $command = new EditDiscussionCommand($discussionId, User::current()); + $command = new EditDiscussionCommand($discussionId, $user); $this->fillCommandWithInput($command, 'discussions'); Event::fire('Flarum.Api.Actions.Discussions.Update.WillExecuteCommand', [$command]); @@ -36,14 +37,14 @@ class Update extends Base // ReadDiscussionCommand. We won't bother firing an event for this one, // because it's pretty specific. (This may need to change in the future.) if ($readNumber) { - $command = new ReadDiscussionCommand($discussionId, User::current(), $readNumber); - $discussion = $this->commandBus->execute($command); + $command = new ReadDiscussionCommand($discussionId, $user, $readNumber); + $this->commandBus->execute($command); } // Presumably, the discussion was updated successfully. (One of the command // handlers would have thrown an exception if not.) We set this // discussion as our document's primary element. - $serializer = new DiscussionSerializer; + $serializer = new DiscussionSerializer(['addedPosts', 'addedPosts.user']); $this->document->setPrimaryElement($serializer->resource($discussion)); return $this->respondWithDocument(); diff --git a/src/Flarum/Api/Serializers/DiscussionSerializer.php b/src/Flarum/Api/Serializers/DiscussionSerializer.php index 0d8ce84b2..3149564ef 100644 --- a/src/Flarum/Api/Serializers/DiscussionSerializer.php +++ b/src/Flarum/Api/Serializers/DiscussionSerializer.php @@ -130,4 +130,17 @@ class DiscussionSerializer extends DiscussionBasicSerializer { return (new PostBasicSerializer($relations))->resource($discussion->lastPost); } + + /** + * Get a resource containing a discussion's list of posts that have been + * added during this request. + * + * @param Discussion $discussion + * @param array $relations + * @return Tobscure\JsonApi\Collection + */ + public function includeAddedPosts(Discussion $discussion, $relations) + { + return (new PostBasicSerializer($relations))->collection($discussion->getAddedPosts()); + } } diff --git a/src/Flarum/Api/Serializers/PostBasicSerializer.php b/src/Flarum/Api/Serializers/PostBasicSerializer.php index a76dc7ce6..858af1d0c 100644 --- a/src/Flarum/Api/Serializers/PostBasicSerializer.php +++ b/src/Flarum/Api/Serializers/PostBasicSerializer.php @@ -30,7 +30,7 @@ class PostBasicSerializer extends BaseSerializer /** * Serialize attributes of a Post model for JSON output. - * + * * @param Post $post The Post model to serialize. * @return array */ @@ -40,17 +40,22 @@ class PostBasicSerializer extends BaseSerializer 'id' => (int) $post->id, 'number' => (int) $post->number, 'time' => $post->time->toRFC3339String(), - 'type' => $post->type, - 'content' => str_limit($post->content, 200) + 'type' => $post->type ]; + if ($post->type === 'comment') { + $attributes['content'] = str_limit($post->content, 200); + } else { + $attributes['content'] = json_encode($post->content); + } + return $this->attributesEvent($post, $attributes); } /** * Get the URL templates where this resource and its related resources can * be accessed. - * + * * @return array */ public function href() @@ -62,7 +67,7 @@ class PostBasicSerializer extends BaseSerializer /** * Get a resource containing a post's user. - * + * * @param Post $post * @param array $relations * @return Tobscure\JsonApi\Resource @@ -74,7 +79,7 @@ class PostBasicSerializer extends BaseSerializer /** * Get a resource containing a post's discussion ID. - * + * * @param Post $post * @return Tobscure\JsonApi\Resource */ diff --git a/src/Flarum/Api/Serializers/PostSerializer.php b/src/Flarum/Api/Serializers/PostSerializer.php index ece2385ea..5d2dfd7bc 100644 --- a/src/Flarum/Api/Serializers/PostSerializer.php +++ b/src/Flarum/Api/Serializers/PostSerializer.php @@ -38,13 +38,13 @@ class PostSerializer extends PostBasicSerializer $canEdit = $post->can($user, 'edit'); - if ($post->type != 'comment') { - $attributes['content'] = $post->content; - } else { + if ($post->type === 'comment') { $attributes['contentHtml'] = $post->content_html; if ($canEdit) { $attributes['content'] = $post->content; } + } else { + $attributes['content'] = json_encode($post->content); } if ($post->edit_time) { diff --git a/src/Flarum/Core/CoreServiceProvider.php b/src/Flarum/Core/CoreServiceProvider.php index d5af47b4c..43cc86991 100644 --- a/src/Flarum/Core/CoreServiceProvider.php +++ b/src/Flarum/Core/CoreServiceProvider.php @@ -9,17 +9,17 @@ use Flarum\Core\Formatter\FormatterManager; class CoreServiceProvider extends ServiceProvider { /** - * Indicates if loading of the provider is deferred. - * - * @var bool - */ + * Indicates if loading of the provider is deferred. + * + * @var bool + */ protected $defer = false; /** - * Bootstrap the application events. - * - * @return void - */ + * Bootstrap the application events. + * + * @return void + */ public function boot() { $this->package('flarum/core', 'flarum'); @@ -30,14 +30,14 @@ class CoreServiceProvider extends ServiceProvider Event::listen('Flarum.Core.*', 'Flarum\Core\Listeners\DiscussionMetadataUpdater'); Event::listen('Flarum.Core.*', 'Flarum\Core\Listeners\UserMetadataUpdater'); - Event::listen('Flarum.Core.*', 'Flarum\Core\Listeners\TitleChangePostCreator'); + Event::listen('Flarum.Core.*', 'Flarum\Core\Listeners\RenamedPostCreator'); } /** - * Register the service provider. - * - * @return void - */ + * Register the service provider. + * + * @return void + */ public function register() { // Start up the Laracasts Commander package. This is used as the basis @@ -69,36 +69,36 @@ class CoreServiceProvider extends ServiceProvider $formatter->add('basic', 'Flarum\Core\Formatter\BasicFormatter'); return $formatter; }); - - + + // $this->app->singleton( - // 'Flarum\Core\Repositories\Contracts\DiscussionRepository', - // function($app) - // { - // $discussion = new \Flarum\Core\Repositories\EloquentDiscussionRepository; - // return new DiscussionCacheDecorator($discussion); - // } + // 'Flarum\Core\Repositories\Contracts\DiscussionRepository', + // function($app) + // { + // $discussion = new \Flarum\Core\Repositories\EloquentDiscussionRepository; + // return new DiscussionCacheDecorator($discussion); + // } // ); // $this->app->singleton( - // 'Flarum\Core\Repositories\Contracts\UserRepository', - // 'Flarum\Core\Repositories\EloquentUserRepository' + // 'Flarum\Core\Repositories\Contracts\UserRepository', + // 'Flarum\Core\Repositories\EloquentUserRepository' // ); // $this->app->singleton( - // 'Flarum\Core\Repositories\Contracts\PostRepository', - // 'Flarum\Core\Repositories\EloquentPostRepository' + // 'Flarum\Core\Repositories\Contracts\PostRepository', + // 'Flarum\Core\Repositories\EloquentPostRepository' // ); // $this->app->singleton( - // 'Flarum\Core\Repositories\Contracts\GroupRepository', - // 'Flarum\Core\Repositories\EloquentGroupRepository' + // 'Flarum\Core\Repositories\Contracts\GroupRepository', + // 'Flarum\Core\Repositories\EloquentGroupRepository' // ); } /** - * Get the services provided by the provider. - * - * @return array - */ + * Get the services provided by the provider. + * + * @return array + */ public function provides() { return array(); diff --git a/src/Flarum/Core/Discussions/Discussion.php b/src/Flarum/Core/Discussions/Discussion.php index 35d00ed83..aee6a90fc 100755 --- a/src/Flarum/Core/Discussions/Discussion.php +++ b/src/Flarum/Core/Discussions/Discussion.php @@ -8,6 +8,7 @@ use Flarum\Core\Forum; use Flarum\Core\Permission; use Flarum\Core\Support\Exceptions\PermissionDeniedException; use Flarum\Core\Users\User; +use Flarum\Core\Posts\Post; class Discussion extends Entity { @@ -28,6 +29,8 @@ class Discussion extends Entity 'last_post_number' => 'integer' ]; + protected $addedPosts = []; + public static function boot() { parent::boot(); @@ -69,7 +72,7 @@ class Discussion extends Entity return $discussion; } - public function setLastPost($post) + public function setLastPost(Post $post) { $this->last_time = $post->time; $this->last_user_id = $post->user_id; @@ -79,8 +82,19 @@ class Discussion extends Entity public function refreshLastPost() { - $lastPost = $this->comments()->orderBy('time', 'desc')->first(); - $this->setLastPost($lastPost); + if ($lastPost = $this->comments()->orderBy('time', 'desc')->first()) { + $this->setLastPost($lastPost); + } + } + + public function getAddedPosts() + { + return $this->addedPosts; + } + + public function postWasAdded(Post $post) + { + $this->addedPosts[] = $post; } public function refreshCommentsCount() @@ -94,9 +108,10 @@ class Discussion extends Entity return; } + $oldTitle = $this->title; $this->title = $title; - $this->raise(new Events\DiscussionWasRenamed($this, $user)); + $this->raise(new Events\DiscussionWasRenamed($this, $user, $oldTitle)); } public function getDates() diff --git a/src/Flarum/Core/Discussions/Events/DiscussionWasRenamed.php b/src/Flarum/Core/Discussions/Events/DiscussionWasRenamed.php index 3409c4848..f36bdd134 100644 --- a/src/Flarum/Core/Discussions/Events/DiscussionWasRenamed.php +++ b/src/Flarum/Core/Discussions/Events/DiscussionWasRenamed.php @@ -9,9 +9,12 @@ class DiscussionWasRenamed public $user; - public function __construct(Discussion $discussion, User $user) + public $oldTitle; + + public function __construct(Discussion $discussion, User $user, $oldTitle) { $this->discussion = $discussion; $this->user = $user; + $this->oldTitle = $oldTitle; } } diff --git a/src/Flarum/Core/Listeners/TitleChangePostCreator.php b/src/Flarum/Core/Listeners/RenamedPostCreator.php similarity index 64% rename from src/Flarum/Core/Listeners/TitleChangePostCreator.php rename to src/Flarum/Core/Listeners/RenamedPostCreator.php index 18a2c1b19..349e94ee8 100755 --- a/src/Flarum/Core/Listeners/TitleChangePostCreator.php +++ b/src/Flarum/Core/Listeners/RenamedPostCreator.php @@ -3,10 +3,10 @@ use Laracasts\Commander\Events\EventListener; use Flarum\Core\Posts\PostRepository; -use Flarum\Core\Posts\TitleChangePost; +use Flarum\Core\Posts\RenamedPost; use Flarum\Core\Discussions\Events\DiscussionWasRenamed; -class TitleChangePostCreator extends EventListener +class RenamedPostCreator extends EventListener { protected $postRepo; @@ -17,12 +17,15 @@ class TitleChangePostCreator extends EventListener public function whenDiscussionWasRenamed(DiscussionWasRenamed $event) { - $post = TitleChangePost::reply( + $post = RenamedPost::reply( $event->discussion->id, - $event->discussion->title, - $event->user->id + $event->user->id, + $event->oldTitle, + $event->discussion->title ); $this->postRepo->save($post); + + $event->discussion->postWasAdded($post); } } diff --git a/src/Flarum/Core/Posts/TitleChangePost.php b/src/Flarum/Core/Posts/RenamedPost.php similarity index 51% rename from src/Flarum/Core/Posts/TitleChangePost.php rename to src/Flarum/Core/Posts/RenamedPost.php index a9eaa6550..f42ecb33a 100755 --- a/src/Flarum/Core/Posts/TitleChangePost.php +++ b/src/Flarum/Core/Posts/RenamedPost.php @@ -8,18 +8,28 @@ use Flarum\Core\Permission; use Flarum\Core\Support\Exceptions\PermissionDeniedException; use Flarum\Core\Users\User; -class TitleChangePost extends Post +class RenamedPost extends Post { - public static function reply($discussionId, $content, $userId) + public static function reply($discussionId, $userId, $oldTitle, $newTitle) { $post = new static; - $post->content = $content; + $post->content = [$oldTitle, $newTitle]; $post->time = time(); $post->discussion_id = $discussionId; $post->user_id = $userId; - $post->type = 'titleChange'; + $post->type = 'renamed'; return $post; } + + public function getContentAttribute($value) + { + return json_decode($value); + } + + public function setContentAttribute($value) + { + $this->attributes['content'] = json_encode($value); + } }