Initial commit

This commit is contained in:
Toby Zerner 2015-05-07 22:26:02 +09:30
commit 3787cc413e
22 changed files with 624 additions and 0 deletions

4
extensions/sticky/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/vendor
composer.phar
.DS_Store
Thumbs.db

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014-2015 Toby Zerner
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,9 @@
<?php
// Require the extension's composer autoload file. This will enable all of our
// classes in the src directory to be autoloaded.
require __DIR__.'/vendor/autoload.php';
// Register our service provider with the Flarum application. In here we can
// register bindings and execute code when the application boots.
return $app->register('Flarum\Sticky\StickyServiceProvider');

View File

@ -0,0 +1,19 @@
{
"name": "flarum/sticky",
"description": "",
"authors": [
{
"name": "Toby Zerner",
"email": "toby@flarum.org"
}
],
"require": {
"php": ">=5.4.0"
},
"autoload": {
"psr-4": {
"Flarum\\Sticky\\": "src/"
}
},
"minimum-stability": "dev"
}

View File

@ -0,0 +1,15 @@
{
"name": "sticky",
"description": "Pin discussions to the top of the list.",
"version": "0.1.0",
"author": {
"name": "Toby Zerner",
"email": "toby@flarum.org",
"website": "http://tobyzerner.com"
},
"license": "MIT",
"require": {
"php": ">=5.4.0",
"flarum": ">1.0.0"
}
}

4
extensions/sticky/js/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
bower_components
node_modules
mithril.js
dist

View File

@ -0,0 +1,45 @@
var gulp = require('gulp');
var livereload = require('gulp-livereload');
var concat = require('gulp-concat');
var argv = require('yargs').argv;
var uglify = require('gulp-uglify');
var gulpif = require('gulp-if');
var babel = require('gulp-babel');
var cached = require('gulp-cached');
var remember = require('gulp-remember');
var merge = require('merge-stream');
var streamqueue = require('streamqueue');
var staticFiles = [
'bootstrap.js'
];
var moduleFiles = [
'src/**/*.js'
];
var modulePrefix = 'sticky';
gulp.task('default', function() {
return streamqueue({objectMode: true},
gulp.src(moduleFiles)
.pipe(cached('scripts'))
.pipe(babel({ modules: 'amd', moduleIds: true, moduleRoot: modulePrefix }))
.pipe(remember('scripts')),
gulp.src(staticFiles)
.pipe(babel())
)
.pipe(concat('extension.js'))
.pipe(gulpif(argv.production, uglify()))
.pipe(gulp.dest('dist'))
.pipe(livereload());
});
gulp.task('watch', ['default'], function () {
livereload.listen();
var watcher = gulp.watch(moduleFiles.concat(staticFiles), ['default']);
watcher.on('change', function (event) {
if (event.type === 'deleted') {
delete cached.caches.scripts[event.path];
remember.forget('scripts', event.path);
}
});
});

60
extensions/sticky/js/bootstrap.js vendored Normal file
View File

@ -0,0 +1,60 @@
import { extend } from 'flarum/extension-utils';
import Model from 'flarum/model';
import Discussion from 'flarum/models/discussion';
import DiscussionPage from 'flarum/components/discussion-page';
import Badge from 'flarum/components/badge';
import ActionButton from 'flarum/components/action-button';
import SettingsPage from 'flarum/components/settings-page';
import icon from 'flarum/helpers/icon';
import app from 'flarum/app';
import PostDiscussionStickied from 'sticky/components/post-discussion-stickied';
import NotificationDiscussionStickied from 'sticky/components/notification-discussion-stickied';
app.initializers.add('sticky', function() {
// Register components.
app.postComponentRegistry['discussionStickied'] = PostDiscussionStickied;
app.notificationComponentRegistry['discussionStickied'] = NotificationDiscussionStickied;
Discussion.prototype.isSticky = Model.prop('isSticky');
// Add a sticky badge to discussions.
extend(Discussion.prototype, 'badges', function(badges) {
if (this.isSticky()) {
badges.add('sticky', Badge.component({
label: 'Sticky',
icon: 'thumb-tack',
className: 'badge-sticky',
}));
}
});
function toggleSticky() {
this.save({isSticky: !this.isSticky()}).then(discussion => {
if (app.current instanceof DiscussionPage) {
app.current.stream().sync();
}
m.redraw();
});
}
// Add a sticky control to discussions.
extend(Discussion.prototype, 'controls', function(items) {
if (this.canEdit()) {
items.add('sticky', ActionButton.component({
label: this.isSticky() ? 'Unsticky' : 'Sticky',
icon: 'thumb-tack',
onclick: toggleSticky.bind(this)
}), {after: 'rename'});
}
});
// Add a notification preference.
extend(SettingsPage.prototype, 'notificationTypes', function(items) {
items.add('discussionStickied', {
name: 'discussionStickied',
label: [icon('thumb-tack'), ' Someone stickies a discussion I started']
});
});
});

View File

@ -0,0 +1,18 @@
{
"name": "flarum-sticky",
"devDependencies": {
"gulp": "^3.8.11",
"gulp-babel": "^5.1.0",
"gulp-cached": "^1.0.4",
"gulp-concat": "^2.5.2",
"gulp-if": "^1.2.5",
"gulp-livereload": "^3.8.0",
"gulp-remember": "^0.3.0",
"gulp-uglify": "^1.2.0",
"merge-stream": "^0.1.7",
"yargs": "^3.7.2"
},
"dependencies": {
"streamqueue": "^0.1.3"
}
}

View File

@ -0,0 +1,21 @@
import Notification from 'flarum/components/notification';
import username from 'flarum/helpers/username';
export default class NotificationDiscussionStickied extends Notification {
view() {
var notification = this.props.notification;
var discussion = notification.subject();
return super.view({
href: app.route('discussion.near', {
id: discussion.id(),
slug: discussion.slug(),
near: notification.content().postNumber
}),
config: m.route,
title: discussion.title(),
icon: 'thumb-tack',
content: ['Stickied by ', username(notification.sender())]
});
}
}

View File

@ -0,0 +1,9 @@
import PostActivity from 'flarum/components/post-activity';
export default class PostDiscussionStickied extends PostActivity {
view() {
var post = this.props.post;
return super.view('thumb-tack', [post.content().sticky ? 'stickied' : 'unstickied', ' the discussion.']);
}
}

View File

@ -0,0 +1,8 @@
.badge-sticky {
background: #d13e32;
}
.post-discussion-stickied {
& .post-icon, & .post-activity-info, & .post-activity-info a {
color: #d13e32;
}
}

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddStickyToDiscussions extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('discussions', function (Blueprint $table) {
$table->boolean('is_sticky')->default(0);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('discussions', function (Blueprint $table) {
$table->dropColumn('is_sticky');
});
}
}

View File

@ -0,0 +1,39 @@
<?php namespace Flarum\Sticky;
use Flarum\Core\Models\User;
use Flarum\Core\Notifications\Types\Notification;
use Flarum\Core\Notifications\Types\AlertableNotification;
class DiscussionStickiedNotification extends Notification implements AlertableNotification
{
public $post;
public function __construct(User $recipient, User $sender, DiscussionStickiedPost $post)
{
$this->post = $post;
parent::__construct($recipient, $sender);
}
public function getSubject()
{
return $this->post->discussion;
}
public function getAlertData()
{
return [
'postNumber' => $this->post->number
];
}
public static function getType()
{
return 'discussionStickied';
}
public static function getSubjectModel()
{
return 'Flarum\Core\Models\Discussion';
}
}

View File

@ -0,0 +1,66 @@
<?php namespace Flarum\Sticky;
use Flarum\Core\Models\Model;
use Flarum\Core\Models\ActivityPost;
class DiscussionStickiedPost extends ActivityPost
{
/**
* The type of post this is, to be stored in the posts table.
*
* @var string
*/
public static $type = 'discussionStickied';
/**
* Merge the post into another post of the same type.
*
* @param \Flarum\Core\Models\DiscussionRenamedPost $previous
* @return \Flarum\Core\Models\Model|null The final model, or null if the
* previous post was deleted.
*/
protected function mergeInto(Model $previous)
{
if ($this->user_id === $previous->user_id) {
if ($previous->content['sticky'] != $this->content['sticky']) {
return;
}
$previous->content = $this->content;
return $previous;
}
return $this;
}
/**
* Create a new instance in reply to a discussion.
*
* @param integer $discussionId
* @param integer $userId
* @param boolean $isSticky
* @return static
*/
public static function reply($discussionId, $userId, $isSticky)
{
$post = new static;
$post->content = static::buildContent($isSticky);
$post->time = time();
$post->discussion_id = $discussionId;
$post->user_id = $userId;
return $post;
}
/**
* Build the content attribute.
*
* @param boolean $isSticky Whether or not the discussion is stickied.
* @return array
*/
public static function buildContent($isSticky)
{
return ['sticky' => (bool) $isSticky];
}
}

View File

@ -0,0 +1,27 @@
<?php namespace Flarum\Sticky\Events;
use Flarum\Core\Models\Discussion;
use Flarum\Core\Models\User;
class DiscussionWasStickied
{
/**
* @var \Flarum\Core\Models\Discussion
*/
public $discussion;
/**
* @var \Flarum\Core\Models\User
*/
public $user;
/**
* @param \Flarum\Core\Models\Discussion $discussion
* @param \Flarum\Core\Models\User $user
*/
public function __construct(Discussion $discussion, User $user)
{
$this->discussion = $discussion;
$this->user = $user;
}
}

View File

@ -0,0 +1,27 @@
<?php namespace Flarum\Sticky\Events;
use Flarum\Core\Models\Discussion;
use Flarum\Core\Models\User;
class DiscussionWasUnstickied
{
/**
* @var \Flarum\Core\Models\Discussion
*/
public $discussion;
/**
* @var \Flarum\Core\Models\User
*/
public $user;
/**
* @param \Flarum\Core\Models\Discussion $discussion
* @param \Flarum\Core\Models\User $user
*/
public function __construct(Discussion $discussion, User $user)
{
$this->discussion = $discussion;
$this->user = $user;
}
}

View File

@ -0,0 +1,67 @@
<?php namespace Flarum\Sticky\Handlers;
use Flarum\Sticky\DiscussionStickiedPost;
use Flarum\Sticky\DiscussionStickiedNotification;
use Flarum\Sticky\Events\DiscussionWasStickied;
use Flarum\Sticky\Events\DiscussionWasUnstickied;
use Flarum\Core\Notifications\Notifier;
use Illuminate\Contracts\Events\Dispatcher;
class DiscussionStickiedNotifier
{
protected $notifier;
public function __construct(Notifier $notifier)
{
$this->notifier = $notifier;
}
/**
* Register the listeners for the subscriber.
*
* @param \Illuminate\Contracts\Events\Dispatcher $events
*/
public function subscribe(Dispatcher $events)
{
$events->listen('Flarum\Sticky\Events\DiscussionWasStickied', __CLASS__.'@whenDiscussionWasStickied');
$events->listen('Flarum\Sticky\Events\DiscussionWasUnstickied', __CLASS__.'@whenDiscussionWasUnstickied');
}
public function whenDiscussionWasStickied(DiscussionWasStickied $event)
{
$post = $this->createPost($event->discussion->id, $event->user->id, true);
$post = $event->discussion->addPost($post);
if ($event->discussion->start_user_id !== $event->user->id) {
$this->sendNotification($post);
}
}
public function whenDiscussionWasUnstickied(DiscussionWasUnstickied $event)
{
$post = $this->createPost($event->discussion->id, $event->user->id, false);
$event->discussion->addPost($post);
}
protected function createPost($discussionId, $userId, $isSticky)
{
return DiscussionStickiedPost::reply(
$discussionId,
$userId,
$isSticky
);
}
protected function sendNotification(DiscussionStickiedPost $post)
{
$notification = new DiscussionStickiedNotification(
$post->discussion->startUser,
$post->user,
$post
);
$this->notifier->send($notification);
}
}

View File

@ -0,0 +1,34 @@
<?php namespace Flarum\Sticky\Handlers;
use Flarum\Sticky\Events\DiscussionWasStickied;
use Flarum\Sticky\Events\DiscussionWasUnstickied;
use Flarum\Core\Events\DiscussionWillBeSaved;
class StickySaver
{
public function subscribe($events)
{
$events->listen('Flarum\Core\Events\DiscussionWillBeSaved', __CLASS__.'@whenDiscussionWillBeSaved');
}
public function whenDiscussionWillBeSaved(DiscussionWillBeSaved $event)
{
if (isset($event->command->data['isSticky'])) {
$isSticky = (bool) $event->command->data['isSticky'];
$discussion = $event->discussion;
$user = $event->command->user;
if ((bool) $discussion->is_sticky === $isSticky) {
return;
}
$discussion->is_sticky = $isSticky;
$discussion->raise(
$discussion->is_sticky
? new DiscussionWasStickied($discussion, $user)
: new DiscussionWasUnstickied($discussion, $user)
);
}
}
}

View File

@ -0,0 +1,37 @@
<?php namespace Flarum\Sticky\Handlers;
use Flarum\Core\Events\DiscussionSearchWillBePerformed;
use Flarum\Categories\CategoryGambit;
class StickySearchModifier
{
public function subscribe($events)
{
$events->listen('Flarum\Core\Events\DiscussionSearchWillBePerformed', __CLASS__.'@reorderSearch');
}
public function reorderSearch(DiscussionSearchWillBePerformed $event)
{
if ($event->criteria->sort === null) {
$query = $event->searcher->query();
foreach ($event->searcher->getActiveGambits() as $gambit) {
if ($gambit instanceof CategoryGambit) {
array_unshift($query->orders, ['column' => 'is_sticky', 'direction' => 'desc']);
return;
}
}
$query->leftJoin('users_discussions', function ($join) use ($event) {
$join->on('users_discussions.discussion_id', '=', 'discussions.id')
->where('discussions.is_sticky', '=', true)
->where('users_discussions.user_id', '=', $event->criteria->user->id);
});
// might be quicker to do a subquery in the order clause than a join?
array_unshift(
$query->orders,
['type' => 'raw', 'sql' => '(is_sticky AND (users_discussions.read_number IS NULL OR discussions.last_post_number > users_discussions.read_number)) desc']
);
}
}
}

View File

@ -0,0 +1,29 @@
<?php namespace Flarum\Sticky;
use Flarum\Core\Search\SearcherInterface;
use Flarum\Core\Search\GambitAbstract;
class StickyGambit extends GambitAbstract
{
/**
* The gambit's regex pattern.
*
* @var string
*/
protected $pattern = 'sticky:(true|false)';
/**
* Apply conditions to the searcher, given matches from the gambit's
* regex.
*
* @param array $matches The matches from the gambit's regex.
* @param \Flarum\Core\Search\SearcherInterface $searcher
* @return void
*/
public function conditions($matches, SearcherInterface $searcher)
{
$sticky = $matches[1] === 'true';
$searcher->query()->where('is_sticky', $sticky);
}
}

View File

@ -0,0 +1,34 @@
<?php namespace Flarum\Sticky;
use Flarum\Support\ServiceProvider;
use Illuminate\Contracts\Events\Dispatcher;
class StickyServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application events.
*
* @return void
*/
public function boot(Dispatcher $events)
{
$events->subscribe('Flarum\Sticky\Handlers\StickySaver');
$events->subscribe('Flarum\Sticky\Handlers\StickySearchModifier');
$events->subscribe('Flarum\Sticky\Handlers\DiscussionStickiedNotifier');
$this->forumAssets([
__DIR__.'/../js/dist/extension.js',
__DIR__.'/../less/sticky.less'
]);
$this->postType('Flarum\Sticky\DiscussionStickiedPost');
$this->serializeAttributes('Flarum\Api\Serializers\DiscussionSerializer', function (&$attributes, $model) {
$attributes['isSticky'] = (bool) $model->is_sticky;
});
$this->discussionGambit('Flarum\Sticky\StickyGambit');
$this->notificationType('Flarum\Sticky\DiscussionStickiedNotification', ['alert' => true]);
}
}