mirror of
https://github.com/flarum/framework.git
synced 2024-11-26 10:14:16 +08:00
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.
This commit is contained in:
parent
fa3523ac74
commit
011ae3603e
48
ember/app/components/discussion/post-renamed.js
Normal file
48
ember/app/components/discussion/post-renamed.js
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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() {
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"}}
|
||||
|
||||
<header class="post-header">
|
||||
{{ui/item-list items=header}}
|
||||
|
|
|
@ -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}}
|
|
@ -0,0 +1,7 @@
|
|||
{{partial "components/discussion/post-controls"}}
|
||||
|
||||
{{fa-icon "pencil" class="post-icon"}}
|
||||
|
||||
<div class="post-activity-info">{{#link-to "user" post.user class="post-user"}}{{post.user.username}}{{/link-to}} changed the title from <strong class="old-title">{{oldTitle}}</strong> to <strong class="new-title">{{newTitle}}</strong>.</div>
|
||||
|
||||
<div class="post-activity-time">{{human-time post.time}}</div>
|
|
@ -1,4 +0,0 @@
|
|||
<div class="activity">
|
||||
{{fa-icon "pencil" class="post-icon"}}
|
||||
{{#link-to "user" post.user}}{{post.user.username}}{{/link-to}} changed the title from <strong class="old-title">{{post.oldTitle}}</strong> to <strong class="new-title">{{post.newTitle}}</strong>.
|
||||
</div>
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user