diff --git a/framework/core/CHANGELOG.md b/framework/core/CHANGELOG.md
index 3a8586e08..77d286c6d 100644
--- a/framework/core/CHANGELOG.md
+++ b/framework/core/CHANGELOG.md
@@ -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.
diff --git a/framework/core/js/forum/src/components/DiscussionListItem.js b/framework/core/js/forum/src/components/DiscussionListItem.js
index f82cbb32d..937cb777b 100644
--- a/framework/core/js/forum/src/components/DiscussionListItem.js
+++ b/framework/core/js/forum/src/components/DiscussionListItem.js
@@ -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 (
-
+
{controls.length ? Dropdown.component({
icon: 'ellipsis-v',
diff --git a/framework/core/js/forum/src/utils/DiscussionControls.js b/framework/core/js/forum/src/utils/DiscussionControls.js
index 7c2c5d2a0..073a140f1 100644
--- a/framework/core/js/forum/src/utils/DiscussionControls.js
+++ b/framework/core/js/forum/src/utils/DiscussionControls.js
@@ -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();
}
diff --git a/framework/core/js/forum/src/utils/PostControls.js b/framework/core/js/forum/src/utils/PostControls.js
index 7429e249b..acfc2b152 100644
--- a/framework/core/js/forum/src/utils/PostControls.js
+++ b/framework/core/js/forum/src/utils/PostControls.js
@@ -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();
}
};
diff --git a/framework/core/js/lib/components/Badge.js b/framework/core/js/lib/components/Badge.js
index 8da355982..2b42489b8 100644
--- a/framework/core/js/lib/components/Badge.js
+++ b/framework/core/js/lib/components/Badge.js
@@ -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.
*/
diff --git a/framework/core/js/lib/models/Discussion.js b/framework/core/js/lib/models/Discussion.js
index 631f261d3..472caf895 100644
--- a/framework/core/js/lib/models/Discussion.js
+++ b/framework/core/js/lib/models/Discussion.js
@@ -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', );
+ }
+
+ return items;
}
/**
diff --git a/framework/core/less/forum/DiscussionListItem.less b/framework/core/less/forum/DiscussionListItem.less
index 55d2c735c..b2fce8db7 100644
--- a/framework/core/less/forum/DiscussionListItem.less
+++ b/framework/core/less/forum/DiscussionListItem.less
@@ -3,6 +3,12 @@
max-width: none;
}
}
+.DiscussionListItem--hidden {
+ .DiscussionListItem-content {
+ opacity: 0.5;
+ }
+}
+
.DiscussionListItem a {
text-decoration: none;
}
diff --git a/framework/core/less/lib/Badge.less b/framework/core/less/lib/Badge.less
index ffb555d11..1864d80d6 100755
--- a/framework/core/less/lib/Badge.less
+++ b/framework/core/less/lib/Badge.less
@@ -33,3 +33,7 @@
display: inline-block;
}
}
+
+.Badge--hidden {
+ background: #888;
+}
diff --git a/framework/core/less/lib/Dropdown.less b/framework/core/less/lib/Dropdown.less
index 3c093ede0..c3b2c0e21 100755
--- a/framework/core/less/lib/Dropdown.less
+++ b/framework/core/less/lib/Dropdown.less
@@ -50,8 +50,8 @@
}
&.disabled {
- opacity: 0.5;
- background: none;
+ opacity: 0.4;
+ background: none !important;
}
}
> a, > button {
diff --git a/framework/core/migrations/2015_09_20_224327_add_hide_to_discussions.php b/framework/core/migrations/2015_09_20_224327_add_hide_to_discussions.php
new file mode 100644
index 000000000..0abd6caaa
--- /dev/null
+++ b/framework/core/migrations/2015_09_20_224327_add_hide_to_discussions.php
@@ -0,0 +1,34 @@
+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']);
+ });
+ }
+}
diff --git a/framework/core/src/Api/Actions/Posts/UpdateAction.php b/framework/core/src/Api/Actions/Posts/UpdateAction.php
index c119b6ab5..2187c3bcb 100644
--- a/framework/core/src/Api/Actions/Posts/UpdateAction.php
+++ b/framework/core/src/Api/Actions/Posts/UpdateAction.php
@@ -33,6 +33,7 @@ class UpdateAction extends SerializeResourceAction
*/
public $include = [
'editUser' => true,
+ 'discussion' => true
];
/**
diff --git a/framework/core/src/Api/Serializers/DiscussionSerializer.php b/framework/core/src/Api/Serializers/DiscussionSerializer.php
index 39af34ee8..ddf0743de 100644
--- a/framework/core/src/Api/Serializers/DiscussionSerializer.php
+++ b/framework/core/src/Api/Serializers/DiscussionSerializer.php
@@ -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');
+ }
}
diff --git a/framework/core/src/Core/Discussions/Commands/EditDiscussionHandler.php b/framework/core/src/Core/Discussions/Commands/EditDiscussionHandler.php
index c1a600b79..93574d8e0 100644
--- a/framework/core/src/Core/Discussions/Commands/EditDiscussionHandler.php
+++ b/framework/core/src/Core/Discussions/Commands/EditDiscussionHandler.php
@@ -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();
diff --git a/framework/core/src/Core/Discussions/Discussion.php b/framework/core/src/Core/Discussions/Discussion.php
index 75e1e5fee..ae3374c5d 100644
--- a/framework/core/src/Core/Discussions/Discussion.php
+++ b/framework/core/src/Core/Discussions/Discussion.php
@@ -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.
*
diff --git a/framework/core/src/Core/Discussions/DiscussionsServiceProvider.php b/framework/core/src/Core/Discussions/DiscussionsServiceProvider.php
index dc7f2c532..273aa0ae5 100644
--- a/framework/core/src/Core/Discussions/DiscussionsServiceProvider.php
+++ b/framework/core/src/Core/Discussions/DiscussionsServiceProvider.php
@@ -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));
diff --git a/framework/core/src/Core/Discussions/Search/Gambits/HiddenGambit.php b/framework/core/src/Core/Discussions/Search/Gambits/HiddenGambit.php
new file mode 100644
index 000000000..bd840857c
--- /dev/null
+++ b/framework/core/src/Core/Discussions/Search/Gambits/HiddenGambit.php
@@ -0,0 +1,42 @@
+
+ *
+ * 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);
+ }
+ });
+ }
+}
diff --git a/framework/core/src/Events/DiscussionWasHidden.php b/framework/core/src/Events/DiscussionWasHidden.php
new file mode 100644
index 000000000..9228bbb6e
--- /dev/null
+++ b/framework/core/src/Events/DiscussionWasHidden.php
@@ -0,0 +1,31 @@
+
+ *
+ * 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;
+ }
+}
diff --git a/framework/core/src/Events/DiscussionWasRestored.php b/framework/core/src/Events/DiscussionWasRestored.php
new file mode 100644
index 000000000..cd3c02019
--- /dev/null
+++ b/framework/core/src/Events/DiscussionWasRestored.php
@@ -0,0 +1,31 @@
+
+ *
+ * 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;
+ }
+}
diff --git a/framework/core/src/Events/ScopeEmptyDiscussionVisibility.php b/framework/core/src/Events/ScopeHiddenDiscussionVisibility.php
similarity index 66%
rename from framework/core/src/Events/ScopeEmptyDiscussionVisibility.php
rename to framework/core/src/Events/ScopeHiddenDiscussionVisibility.php
index 1f0299e25..cacebde5d 100644
--- a/framework/core/src/Events/ScopeEmptyDiscussionVisibility.php
+++ b/framework/core/src/Events/ScopeHiddenDiscussionVisibility.php
@@ -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;
}
}