mirror of
https://github.com/flarum/framework.git
synced 2025-01-31 15:15:15 +08:00
Allow discussions to be hidden and restored
This commit is contained in:
parent
c7ed189cf3
commit
264725d872
|
@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||
|
||||
## [Unreleased][unreleased]
|
||||
### Added
|
||||
- Allow discussions to be hidden and restored.
|
||||
- External authentication (social login) API.
|
||||
- API to set asset compiler filename.
|
||||
- Migration generator, available via generate:migration console command.
|
||||
|
|
|
@ -13,6 +13,7 @@ import SubtreeRetainer from 'flarum/utils/SubtreeRetainer';
|
|||
import DiscussionControls from 'flarum/utils/DiscussionControls';
|
||||
import slidable from 'flarum/utils/slidable';
|
||||
import extractText from 'flarum/utils/extractText';
|
||||
import classList from 'flarum/utils/classList';
|
||||
|
||||
/**
|
||||
* The `DiscussionListItem` component shows a single discussion in the
|
||||
|
@ -43,6 +44,16 @@ export default class DiscussionListItem extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
attrs() {
|
||||
return {
|
||||
className: classList([
|
||||
'DiscussionListItem',
|
||||
this.active() ? 'active' : '',
|
||||
this.props.discussion.isHidden() ? 'DiscussionListItem--hidden' : ''
|
||||
])
|
||||
};
|
||||
}
|
||||
|
||||
view() {
|
||||
const retain = this.subtree.retain();
|
||||
|
||||
|
@ -56,9 +67,10 @@ export default class DiscussionListItem extends Component {
|
|||
const jumpTo = Math.min(discussion.lastPostNumber(), (discussion.readNumber() || 0) + 1);
|
||||
const relevantPosts = this.props.params.q ? discussion.relevantPosts() : [];
|
||||
const controls = DiscussionControls.controls(discussion, this).toArray();
|
||||
const attrs = this.attrs();
|
||||
|
||||
return (
|
||||
<div className={'DiscussionListItem ' + (this.active() ? 'active' : '')}>
|
||||
<div {...attrs}>
|
||||
|
||||
{controls.length ? Dropdown.component({
|
||||
icon: 'ellipsis-v',
|
||||
|
|
|
@ -105,10 +105,25 @@ export default {
|
|||
destructiveControls(discussion) {
|
||||
const items = new ItemList();
|
||||
|
||||
if (discussion.canDelete()) {
|
||||
if (!discussion.isHidden()) {
|
||||
if (discussion.canHide()) {
|
||||
items.add('hide', Button.component({
|
||||
icon: 'trash-o',
|
||||
children: app.trans('core.delete'),
|
||||
onclick: this.hideAction.bind(discussion)
|
||||
}));
|
||||
}
|
||||
} else if (discussion.canDelete()) {
|
||||
items.add('restore', Button.component({
|
||||
icon: 'reply',
|
||||
children: app.trans('core.restore'),
|
||||
onclick: this.restoreAction.bind(discussion),
|
||||
disabled: discussion.commentsCount() === 0
|
||||
}));
|
||||
|
||||
items.add('delete', Button.component({
|
||||
icon: 'times',
|
||||
children: app.trans('core.delete'),
|
||||
children: app.trans('core.delete_forever'),
|
||||
onclick: this.deleteAction.bind(discussion)
|
||||
}));
|
||||
}
|
||||
|
@ -173,13 +188,35 @@ export default {
|
|||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Hide a discussion.
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
hideAction() {
|
||||
this.pushAttributes({ hideTime: new Date(), hideUser: app.session.user });
|
||||
|
||||
return this.save({ isHidden: true });
|
||||
},
|
||||
|
||||
/**
|
||||
* Restore a discussion.
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
restoreAction() {
|
||||
this.pushAttributes({ hideTime: null, hideUser: null });
|
||||
|
||||
return this.save({ isHidden: false });
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete the discussion after confirming with the user.
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
deleteAction() {
|
||||
if (confirm(extractText(app.trans('core.confirm_delete_discussion')))) {
|
||||
this.delete();
|
||||
|
||||
// If there is a discussion list in the cache, remove this discussion.
|
||||
if (app.cache.discussionList) {
|
||||
app.cache.discussionList.removeDiscussion(this);
|
||||
|
@ -190,11 +227,15 @@ export default {
|
|||
if (app.viewingDiscussion(this)) {
|
||||
app.history.back();
|
||||
}
|
||||
|
||||
return this.delete();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Rename the discussion.
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
renameAction() {
|
||||
const currentTitle = this.title();
|
||||
|
@ -204,7 +245,7 @@ export default {
|
|||
// save has completed, update the post stream as there will be a new post
|
||||
// indicating that the discussion was renamed.
|
||||
if (title && title !== currentTitle) {
|
||||
this.save({title}).then(() => {
|
||||
return this.save({title}).then(() => {
|
||||
if (app.viewingDiscussion(this)) {
|
||||
app.current.stream.update();
|
||||
}
|
||||
|
|
|
@ -114,25 +114,34 @@ export default {
|
|||
|
||||
/**
|
||||
* Hide a post.
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
hideAction() {
|
||||
this.save({ isHidden: true });
|
||||
this.pushAttributes({ hideTime: new Date(), hideUser: app.session.user });
|
||||
|
||||
return this.save({ isHidden: true }).then(() => m.redraw());
|
||||
},
|
||||
|
||||
/**
|
||||
* Restore a post.
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
restoreAction() {
|
||||
this.save({ isHidden: false });
|
||||
this.pushAttributes({ hideTime: null, hideUser: null });
|
||||
|
||||
return this.save({ isHidden: false }).then(() => m.redraw());
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a post.
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
deleteAction() {
|
||||
this.delete();
|
||||
this.discussion().removePost(this.id());
|
||||
|
||||
return this.delete();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -11,6 +11,7 @@ import extract from 'flarum/utils/extract';
|
|||
* - `type` The type of badge this is. This will be used to give the badge a
|
||||
* class name of `Badge--{type}`.
|
||||
* - `icon` The name of an icon to show inside the badge.
|
||||
* - `label`
|
||||
*
|
||||
* All other props will be assigned as attributes on the badge element.
|
||||
*/
|
||||
|
|
|
@ -3,6 +3,7 @@ import mixin from 'flarum/utils/mixin';
|
|||
import computed from 'flarum/utils/computed';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import { slug } from 'flarum/utils/string';
|
||||
import Badge from 'flarum/components/Badge';
|
||||
|
||||
export default class Discussion extends mixin(Model, {
|
||||
title: Model.attribute('title'),
|
||||
|
@ -27,8 +28,13 @@ export default class Discussion extends mixin(Model, {
|
|||
isUnread: computed('unreadCount', unreadCount => !!unreadCount),
|
||||
isRead: computed('unreadCount', unreadCount => app.session.user && !unreadCount),
|
||||
|
||||
hideTime: Model.attribute('hideTime', Model.transformDate),
|
||||
hideUser: Model.hasOne('hideUser'),
|
||||
isHidden: computed('hideTime', 'commentsCount', (hideTime, commentsCount) => !!hideTime || commentsCount === 0),
|
||||
|
||||
canReply: Model.attribute('canReply'),
|
||||
canRename: Model.attribute('canRename'),
|
||||
canHide: Model.attribute('canHide'),
|
||||
canDelete: Model.attribute('canDelete')
|
||||
}) {
|
||||
/**
|
||||
|
@ -75,7 +81,13 @@ export default class Discussion extends mixin(Model, {
|
|||
* @public
|
||||
*/
|
||||
badges() {
|
||||
return new ItemList();
|
||||
const items = new ItemList();
|
||||
|
||||
if (this.isHidden()) {
|
||||
items.add('hidden', <Badge type="hidden" icon="trash" label="Hidden"/>);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,6 +3,12 @@
|
|||
max-width: none;
|
||||
}
|
||||
}
|
||||
.DiscussionListItem--hidden {
|
||||
.DiscussionListItem-content {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.DiscussionListItem a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
|
|
@ -33,3 +33,7 @@
|
|||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.Badge--hidden {
|
||||
background: #888;
|
||||
}
|
||||
|
|
|
@ -50,8 +50,8 @@
|
|||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
background: none;
|
||||
opacity: 0.4;
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
> a, > button {
|
||||
|
|
34
migrations/2015_09_20_224327_add_hide_to_discussions.php
Normal file
34
migrations/2015_09_20_224327_add_hide_to_discussions.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace Flarum\Migrations\Core;
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Flarum\Migrations\Migration;
|
||||
|
||||
class AddHideToDiscussions extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$this->schema->table('discussions', function (Blueprint $table) {
|
||||
$table->dateTime('hide_time')->nullable();
|
||||
$table->integer('hide_user_id')->unsigned()->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
$this->schema->table('discussions', function (Blueprint $table) {
|
||||
$table->dropColumn(['hide_time', 'hide_user_id']);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -33,6 +33,7 @@ class UpdateAction extends SerializeResourceAction
|
|||
*/
|
||||
public $include = [
|
||||
'editUser' => true,
|
||||
'discussion' => true
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -27,9 +27,15 @@ class DiscussionSerializer extends DiscussionBasicSerializer
|
|||
'lastPostNumber' => $discussion->last_post_number,
|
||||
'canReply' => $discussion->can($this->actor, 'reply'),
|
||||
'canRename' => $discussion->can($this->actor, 'rename'),
|
||||
'canDelete' => $discussion->can($this->actor, 'delete')
|
||||
'canDelete' => $discussion->can($this->actor, 'delete'),
|
||||
'canHide' => $discussion->can($this->actor, 'hide')
|
||||
];
|
||||
|
||||
if ($discussion->hide_time) {
|
||||
$attributes['isHidden'] = true;
|
||||
$attributes['hideTime'] = $discussion->hide_time->toRFC3339String();
|
||||
}
|
||||
|
||||
Discussion::setStateUser($this->actor);
|
||||
|
||||
if ($state = $discussion->state) {
|
||||
|
@ -41,4 +47,12 @@ class DiscussionSerializer extends DiscussionBasicSerializer
|
|||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return callable
|
||||
*/
|
||||
public function hideUser()
|
||||
{
|
||||
return $this->hasOne('Flarum\Api\Serializers\UserSerializer');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,16 @@ class EditDiscussionHandler
|
|||
$discussion->rename($attributes['title'], $actor);
|
||||
}
|
||||
|
||||
if (isset($attributes['isHidden'])) {
|
||||
$discussion->assertCan($actor, 'hide');
|
||||
|
||||
if ($attributes['isHidden']) {
|
||||
$discussion->hide($actor);
|
||||
} else {
|
||||
$discussion->restore();
|
||||
}
|
||||
}
|
||||
|
||||
event(new DiscussionWillBeSaved($discussion, $actor, $data));
|
||||
|
||||
$discussion->save();
|
||||
|
|
|
@ -14,6 +14,8 @@ use Flarum\Core\Model;
|
|||
use Flarum\Events\DiscussionWasDeleted;
|
||||
use Flarum\Events\DiscussionWasStarted;
|
||||
use Flarum\Events\DiscussionWasRenamed;
|
||||
use Flarum\Events\DiscussionWasHidden;
|
||||
use Flarum\Events\DiscussionWasRestored;
|
||||
use Flarum\Events\PostWasDeleted;
|
||||
use Flarum\Events\ScopePostVisibility;
|
||||
use Flarum\Core\Posts\Post;
|
||||
|
@ -68,7 +70,7 @@ class Discussion extends Model
|
|||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $dates = ['start_time', 'last_time'];
|
||||
protected $dates = ['start_time', 'last_time', 'hide_time'];
|
||||
|
||||
/**
|
||||
* The user for which the state relationship should be loaded.
|
||||
|
@ -147,6 +149,41 @@ class Discussion extends Model
|
|||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the discussion.
|
||||
*
|
||||
* @param User $actor
|
||||
* @return $this
|
||||
*/
|
||||
public function hide(User $actor = null)
|
||||
{
|
||||
if (! $this->hide_time) {
|
||||
$this->hide_time = time();
|
||||
$this->hide_user_id = $actor ? $actor->id : null;
|
||||
|
||||
$this->raise(new DiscussionWasHidden($this));
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the discussion.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function restore()
|
||||
{
|
||||
if ($this->hide_time !== null) {
|
||||
$this->hide_time = null;
|
||||
$this->hide_user_id = null;
|
||||
|
||||
$this->raise(new DiscussionWasRestored($this));
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the discussion's start post details.
|
||||
*
|
||||
|
|
|
@ -15,7 +15,7 @@ use Flarum\Core\Users\User;
|
|||
use Flarum\Events\ModelAllow;
|
||||
use Flarum\Events\ScopeModelVisibility;
|
||||
use Flarum\Events\RegisterDiscussionGambits;
|
||||
use Flarum\Events\ScopeEmptyDiscussionVisibility;
|
||||
use Flarum\Events\ScopeHiddenDiscussionVisibility;
|
||||
use Flarum\Support\ServiceProvider;
|
||||
use Flarum\Extend;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
|
@ -58,12 +58,15 @@ class DiscussionsServiceProvider extends ServiceProvider
|
|||
|
||||
$events->listen(ScopeModelVisibility::class, function (ScopeModelVisibility $event) {
|
||||
if ($event->model instanceof Discussion) {
|
||||
if (! $event->actor->hasPermission('discussion.editPosts')) {
|
||||
$event->query->where(function ($query) use ($event) {
|
||||
$query->where('comments_count', '>', '0')
|
||||
->orWhere('start_user_id', $event->actor->id);
|
||||
$user = $event->actor;
|
||||
|
||||
event(new ScopeEmptyDiscussionVisibility($query, $event->actor));
|
||||
if (! $user->hasPermission('discussion.hide')) {
|
||||
$event->query->where(function ($query) use ($user) {
|
||||
$query->whereNull('discussions.hide_time')
|
||||
->where('comments_count', '>', 0)
|
||||
->orWhere('start_user_id', $user->id);
|
||||
|
||||
event(new ScopeHiddenDiscussionVisibility($query, $user, 'discussion.hide'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -86,8 +89,10 @@ class DiscussionsServiceProvider extends ServiceProvider
|
|||
->needs('Flarum\Core\Search\GambitManager')
|
||||
->give(function (Container $app) {
|
||||
$gambits = new GambitManager($app);
|
||||
|
||||
$gambits->setFulltextGambit('Flarum\Core\Discussions\Search\Gambits\FulltextGambit');
|
||||
$gambits->add('Flarum\Core\Discussions\Search\Gambits\AuthorGambit');
|
||||
$gambits->add('Flarum\Core\Discussions\Search\Gambits\HiddenGambit');
|
||||
$gambits->add('Flarum\Core\Discussions\Search\Gambits\UnreadGambit');
|
||||
|
||||
event(new RegisterDiscussionGambits($gambits));
|
||||
|
|
42
src/Core/Discussions/Search/Gambits/HiddenGambit.php
Normal file
42
src/Core/Discussions/Search/Gambits/HiddenGambit.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Core\Discussions\Search\Gambits;
|
||||
|
||||
use Flarum\Core\Discussions\Search\DiscussionSearch;
|
||||
use Flarum\Core\Search\RegexGambit;
|
||||
use Flarum\Core\Search\Search;
|
||||
use LogicException;
|
||||
|
||||
class HiddenGambit extends RegexGambit
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $pattern = 'is:hidden';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function conditions(Search $search, array $matches, $negate)
|
||||
{
|
||||
if (! $search instanceof DiscussionSearch) {
|
||||
throw new LogicException('This gambit can only be applied on a DiscussionSearch');
|
||||
}
|
||||
|
||||
$search->getQuery()->where(function ($query) use ($negate) {
|
||||
if ($negate) {
|
||||
$query->whereNull('hide_time')->where('comments_count', '>', 0);
|
||||
} else {
|
||||
$query->whereNotNull('hide_time')->orWhere('comments_count', 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
31
src/Events/DiscussionWasHidden.php
Normal file
31
src/Events/DiscussionWasHidden.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Events;
|
||||
|
||||
use Flarum\Core\Discussions\Discussion;
|
||||
|
||||
class DiscussionWasHidden
|
||||
{
|
||||
/**
|
||||
* The discussion that was hidden.
|
||||
*
|
||||
* @var Discussion
|
||||
*/
|
||||
public $discussion;
|
||||
|
||||
/**
|
||||
* @param Discussion $discussion
|
||||
*/
|
||||
public function __construct(Discussion $discussion)
|
||||
{
|
||||
$this->discussion = $discussion;
|
||||
}
|
||||
}
|
31
src/Events/DiscussionWasRestored.php
Normal file
31
src/Events/DiscussionWasRestored.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* (c) Toby Zerner <toby.zerner@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Events;
|
||||
|
||||
use Flarum\Core\Discussions\Discussion;
|
||||
|
||||
class DiscussionWasRestored
|
||||
{
|
||||
/**
|
||||
* The discussion that was restored.
|
||||
*
|
||||
* @var Discussion
|
||||
*/
|
||||
public $discussion;
|
||||
|
||||
/**
|
||||
* @param Discussion $discussion
|
||||
*/
|
||||
public function __construct(Discussion $discussion)
|
||||
{
|
||||
$this->discussion = $discussion;
|
||||
}
|
||||
}
|
|
@ -14,9 +14,9 @@ use Flarum\Core\Users\User;
|
|||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* The `ScopeEmptyDiscussionVisibility` event
|
||||
* The `ScopeHiddenDiscussionVisibility` event
|
||||
*/
|
||||
class ScopeEmptyDiscussionVisibility
|
||||
class ScopeHiddenDiscussionVisibility
|
||||
{
|
||||
/**
|
||||
* @var Builder
|
||||
|
@ -28,13 +28,20 @@ class ScopeEmptyDiscussionVisibility
|
|||
*/
|
||||
public $actor;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $permission;
|
||||
|
||||
/**
|
||||
* @param Builder $query
|
||||
* @param User $actor
|
||||
* @param string $permission
|
||||
*/
|
||||
public function __construct(Builder $query, User $actor)
|
||||
public function __construct(Builder $query, User $actor, $permission)
|
||||
{
|
||||
$this->query = $query;
|
||||
$this->actor = $actor;
|
||||
$this->permission = $permission;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user