Initial commit

This commit is contained in:
Toby Zerner 2015-05-14 22:01:03 +09:30
commit 57bb8702de
30 changed files with 1073 additions and 0 deletions

4
extensions/mentions/.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 $this->app->register('Flarum\Mentions\MentionsServiceProvider');

View File

@ -0,0 +1,18 @@
{
"name": "flarum/mentions",
"description": "",
"authors": [
{
"name": "Toby Zerner",
"email": "toby@flarum.org"
}
],
"require": {
"php": ">=5.4.0"
},
"autoload": {
"psr-4": {
"Flarum\\Mentions\\": "src/"
}
}
}

View File

@ -0,0 +1,15 @@
{
"name": "mentions",
"description": "",
"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/mentions/js/.gitignore vendored Normal file
View File

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

View File

@ -0,0 +1,46 @@
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',
'bower_components/textarea-caret-position/index.js'
];
var moduleFiles = [
'src/**/*.js'
];
var modulePrefix = 'mentions';
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);
}
});
});

29
extensions/mentions/js/bootstrap.js vendored Normal file
View File

@ -0,0 +1,29 @@
import app from 'flarum/app';
import postMentionPreviews from 'mentions/post-mention-previews';
import mentionedByList from 'mentions/mentioned-by-list';
import postReplyAction from 'mentions/post-reply-action';
import composerAutocomplete from 'mentions/composer-autocomplete';
import NotificationPostMentioned from 'mentions/components/notification-post-mentioned';
import NotificationUserMentioned from 'mentions/components/notification-user-mentioned';
app.initializers.add('mentions', function() {
// For every mention of a post inside a post's content, set up a hover handler
// that shows a preview of the mentioned post.
postMentionPreviews();
// In the footer of each post, show information about who has replied (i.e.
// who the post has been mentioned by).
mentionedByList();
// Add a 'reply' control to the footer of each post. When clicked, it will
// open up the composer and add a post mention to its contents.
postReplyAction();
// After typing '@' in the composer, show a dropdown suggesting a bunch of
// posts or users that the user could mention.
composerAutocomplete();
app.notificationComponentRegistry['postMentioned'] = NotificationPostMentioned;
app.notificationComponentRegistry['userMentioned'] = NotificationUserMentioned;
});

View File

@ -0,0 +1,6 @@
{
"name": "flarum-mentions",
"dev-dependencies": {
"textarea-caret-position": "~3.0.0"
}
}

View File

@ -0,0 +1,16 @@
{
"name": "flarum-replies",
"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",
"streamqueue": "^0.1.3"
}
}

View File

@ -0,0 +1,88 @@
import Component from 'flarum/component';
export default class AutocompleteDropdown extends Component {
constructor(props) {
super(props);
this.active = m.prop(false);
this.index = m.prop(0);
}
view() {
return m('ul.dropdown-menu.mentions-dropdown', {config: this.element}, this.props.items.map(item => m('li', item)));
}
show(left, top) {
this.$().show().css({
left: left+'px',
top: top+'px'
});
this.active(true);
}
hide() {
this.$().hide();
this.active(false);
}
navigate(e) {
if (!this.active()) return;
switch (e.which) {
case 40: // Down
this.setIndex(this.index() + 1, true);
e.preventDefault();
break;
case 38: // Up
this.setIndex(this.index() - 1, true);
e.preventDefault();
break;
case 13: case 9: // Enter/Tab
this.$('li').eq(this.index()).find('a').click();
e.preventDefault();
break;
case 27: // Escape
this.hide();
e.stopPropagation();
e.preventDefault();
break;
}
}
setIndex(index, scrollToItem) {
var $dropdown = this.$();
var $items = $dropdown.find('li');
if (index < 0) {
index = $items.length - 1;
} else if (index >= $items.length) {
index = 0;
}
this.index(index);
var $item = $items.removeClass('active').eq(index).addClass('active');
if (scrollToItem) {
var dropdownScroll = $dropdown.scrollTop();
var dropdownTop = $dropdown.offset().top;
var dropdownBottom = dropdownTop + $dropdown.outerHeight();
var itemTop = $item.offset().top;
var itemBottom = itemTop + $item.outerHeight();
var scrollTop;
if (itemTop < dropdownTop) {
scrollTop = dropdownScroll - dropdownTop + itemTop - parseInt($dropdown.css('padding-top'));
} else if (itemBottom > dropdownBottom) {
scrollTop = dropdownScroll - dropdownBottom + itemBottom + parseInt($dropdown.css('padding-bottom'));
}
if (typeof scrollTop !== 'undefined') {
$dropdown.stop(true).animate({scrollTop}, 100);
}
}
}
}

View File

@ -0,0 +1,17 @@
import Notification from 'flarum/components/notification';
import username from 'flarum/helpers/username';
export default class NotificationPostMentioned extends Notification {
view() {
var notification = this.props.notification;
var post = notification.subject();
var auc = notification.additionalUnreadCount();
var content = notification.content();
return super.view({
href: app.route.discussion(post.discussion(), auc ? post.number() : (content && content.replyNumber)),
icon: 'reply',
content: [username(notification.sender()), (auc ? ' and '+auc+' others' : '')+' replied to your post']
});
}
}

View File

@ -0,0 +1,15 @@
import Notification from 'flarum/components/notification';
import username from 'flarum/helpers/username';
export default class NotificationUserMentioned extends Notification {
view() {
var notification = this.props.notification;
var post = notification.subject();
return super.view({
href: app.route.discussion(post.discussion(), post.number()),
icon: 'at',
content: [username(notification.sender()), ' mentioned you']
});
}
}

View File

@ -0,0 +1,120 @@
import { extend } from 'flarum/extension-utils';
import ComposerBody from 'flarum/components/composer-body';
import ComposerReply from 'flarum/components/composer-reply';
import ComposerEdit from 'flarum/components/composer-edit';
import avatar from 'flarum/helpers/avatar';
import username from 'flarum/helpers/username';
import AutocompleteDropdown from 'mentions/components/autocomplete-dropdown';
export default function() {
extend(ComposerBody.prototype, 'onload', function(original, element, isInitialized, context) {
if (isInitialized) return;
var composer = this;
var $container = $('<div class="mentions-dropdown-container"></div>');
var dropdown = new AutocompleteDropdown({items: []});
this.$('textarea')
.after($container)
.on('keydown', dropdown.navigate.bind(dropdown))
.on('input', function() {
var cursor = this.selectionStart;
if (this.selectionEnd - cursor > 0) return;
// Search backwards from the cursor for an '@' symbol, without any
// intervening whitespace. If we find one, we will want to show the
// autocomplete dropdown!
var value = this.value;
var mentionStart;
for (var i = cursor - 1; i >= 0; i--) {
var character = value.substr(i, 1);
if (/\s/.test(character)) break;
if (character == '@') {
mentionStart = i + 1;
break;
}
}
dropdown.hide();
if (mentionStart) {
var typed = value.substring(mentionStart, cursor).toLowerCase();
var suggestions = [];
var applySuggestion = function(replacement) {
replacement += ' ';
var content = composer.content();
composer.editor.setContent(content.substring(0, mentionStart - 1)+replacement+content.substr(cursor));
var index = mentionStart + replacement.length;
composer.editor.setSelectionRange(index, index);
dropdown.hide();
};
var makeSuggestion = function(user, replacement, index, content) {
return m('a[href=javascript:;].post-preview', {
onclick: () => applySuggestion(replacement),
onmouseover: () => dropdown.setIndex(index)
}, m('div.post-preview-content', [
avatar(user),
username(user), ' ',
content
]));
};
// If the user is replying to a discussion, or if they are editing a
// post, then we can suggest other posts in the discussion to mention.
// We will add the 5 most recent comments in the discussion which
// match any username characters that have been typed.
var composerPost = composer.props.post;
var discussion = (composerPost && composerPost.discussion()) || composer.props.discussion;
if (discussion) {
discussion.posts()
.filter(post => post && post.contentType() === 'comment' && (!composerPost || post.number() < composerPost.number()))
.sort((a, b) => b.time() - a.time())
.filter(post => {
var user = post.user();
return user && user.username().toLowerCase().substr(0, typed.length) === typed;
})
.splice(0, 5)
.forEach((post, i) => {
var user = post.user();
suggestions.push(
makeSuggestion(user, '@'+user.username()+'#'+post.number(), i, [
'Reply to #', post.number(), ' — ',
post.excerpt()
])
);
});
}
// If the user has started to type a username, then suggest users
// matching that username.
if (typed) {
app.store.all('users').forEach((user, i) => {
if (user.username().toLowerCase().substr(0, typed.length) !== typed) return;
suggestions.push(
makeSuggestion(user, '@'+user.username(), i, '@mention')
);
});
}
if (suggestions.length) {
dropdown.props.items = suggestions;
m.render($container[0], dropdown.view());
var coordinates = getCaretCoordinates(this, mentionStart);
dropdown.show(coordinates.left, coordinates.top + 15);
dropdown.setIndex(0);
dropdown.$().scrollTop(0);
}
}
});
});
}

View File

@ -0,0 +1,90 @@
import { extend } from 'flarum/extension-utils';
import Model from 'flarum/model';
import Post from 'flarum/models/post';
import DiscussionPage from 'flarum/components/discussion-page';
import PostComment from 'flarum/components/post-comment';
import PostPreview from 'flarum/components/post-preview';
import punctuate from 'flarum/helpers/punctuate';
export default function mentionedByList() {
Post.prototype.mentionedBy = Model.many('mentionedBy');
extend(DiscussionPage.prototype, 'params', function(params) {
params.include.push('posts.mentionedBy', 'posts.mentionedBy.user');
});
extend(PostComment.prototype, 'footerItems', function(items) {
var replies = this.props.post.mentionedBy();
if (replies && replies.length) {
var hidePreview = () => {
this.$('.mentioned-by-preview').removeClass('in').one('transitionend', function() { $(this).hide(); });
};
var config = function(element, isInitialized) {
if (isInitialized) return;
var $this = $(element);
var timeout;
var $preview = $('<ul class="dropdown-menu mentioned-by-preview fade"/>');
$this.append($preview);
$this.children().hover(function() {
clearTimeout(timeout);
timeout = setTimeout(function() {
if (!$preview.hasClass('in') && $preview.is(':visible')) return;
// When the user hovers their mouse over the list of people who have
// replied to the post, render a list of reply previews into a
// popup.
m.render($preview[0], replies.map(post => {
return m('li', {'data-number': post.number()}, PostPreview.component({post, onclick: hidePreview}));
}));
$preview.show();
setTimeout(() => $preview.off('transitionend').addClass('in'));
}, 500);
}, function() {
clearTimeout(timeout);
timeout = setTimeout(hidePreview, 250);
});
// Whenever the user hovers their mouse over a particular name in the
// list of repliers, highlight the corresponding post in the preview
// popup.
$this.find('.summary a').hover(function() {
$preview.find('[data-number='+$(this).data('number')+']').addClass('active');
}, function() {
$preview.find('[data-number]').removeClass('active');
});
};
// Create a list of unique users who have replied. So even if a user has
// replied twice, they will only be in this array once.
var used = [];
var repliers = replies.filter(reply => {
if (used.indexOf(reply.user().id()) === -1) {
used.push(reply.user().id());
return true;
}
});
items.add('replies',
m('div.mentioned-by', {config}, [
m('span.summary', [
punctuate(repliers.map(reply => {
return m('a', {
href: app.route.post(reply),
config: m.route,
onclick: hidePreview,
'data-number': reply.number()
}, [
reply.user() === app.session.user() ? 'You' : username(reply.user())
])
})),
' replied to this.'
])
])
);
}
});
}

View File

@ -0,0 +1,81 @@
import { extend } from 'flarum/extension-utils';
import PostComment from 'flarum/components/post-comment';
import PostPreview from 'flarum/components/post-preview';
import LoadingIndicator from 'flarum/components/loading-indicator';
export default function postMentionPreviews() {
extend(PostComment.prototype, 'config', function() {
var contentHtml = this.props.post.contentHtml();
if (contentHtml === this.oldPostContentHtml) return;
this.oldPostContentHtml = contentHtml;
var discussion = this.props.post.discussion();
this.$('.mention-post').each(function() {
var $this = $(this);
var number = $this.data('number');
var timeout;
// Wrap the mention link in a wrapper element so that we can insert a
// preview popup as its sibling and relatively position it.
var $preview = $('<ul class="dropdown-menu mention-post-preview fade"/>');
var $wrapper = $('<span class="mention-post-wrapper"/>');
$this.wrap($wrapper).after($preview);
var getPostElement = function() {
return $('.discussion-posts .item[data-number='+number+']');
};
$this.parent().hover(
function() {
clearTimeout(timeout);
timeout = setTimeout(function() {
// When the user hovers their mouse over the mention, look for the
// post that it's referring to in the stream, and determine if it's
// in the viewport. If it is, we will "pulsate" it.
var $post = getPostElement();
var visible = false;
if ($post.length) {
var top = $post.offset().top;
var scrollTop = window.pageYOffset;
if (top > scrollTop && top + $post.height() < scrollTop + $(window).height()) {
$post.addClass('pulsate');
visible = true;
}
}
// Otherwise, we will show a popup preview of the post. If the post
// hasn't yet been loaded, we will need to do that.
if (!visible) {
var showPost = function(post) {
m.render($preview[0], m('li', PostPreview.component({post})));
}
var post = discussion.posts().filter(post => post && post.number() == number)[0];
if (post) {
showPost(post);
} else {
m.render($preview[0], LoadingIndicator.component());
app.store.find('posts', {discussions: discussion.id(), number}).then(posts => showPost(posts[0]));
}
// Position the preview so that it appears above the mention.
// (The offsetParent should be .post-body.)
$preview.show().css('top', $this.offset().top - $this.offsetParent().offset().top - $preview.outerHeight(true));
setTimeout(() => $preview.off('transitionend').addClass('in'));
}
}, 500);
},
function() {
clearTimeout(timeout);
getPostElement().removeClass('pulsate');
timeout = setTimeout(() => {
if ($preview.hasClass('in')) {
$preview.removeClass('in').one('transitionend', () => $preview.hide());
}
}, 250);
}
);
});
});
}

View File

@ -0,0 +1,32 @@
import { extend } from 'flarum/extension-utils';
import ActionButton from 'flarum/components/action-button';
import PostComment from 'flarum/components/post-comment';
export default function() {
extend(PostComment.prototype, 'actionItems', function(items) {
var post = this.props.post;
if (post.isHidden()) return;
items.add('reply',
ActionButton.component({
icon: 'reply',
label: 'Reply',
onclick: () => {
var component = post.discussion().replyAction();
if (component) {
var quote = window.getSelection().toString();
var mention = '@'+post.user().username()+'#'+post.number()+' ';
component.editor.insertAtCursor(quote ? '> '+mention+quote+'\n\n' : mention);
// If the composer is empty, then assume we're starting a new reply.
// In which case we don't want the user to have to confirm if they
// close the composer straight away.
if (!component.content()) {
component.props.originalContent = mention;
}
}
}
})
);
});
}

View File

@ -0,0 +1,65 @@
.mention-post, .mention-user {
background: @fl-body-control-bg;
color: @fl-body-control-color;
border-radius: @border-radius-base;
padding: 2px 5px;
border: 0 !important;
blockquote & {
background: @fl-body-bg;
}
}
.mention-post {
margin: 0 3px;
&:first-child {
margin-left: 0;
}
&:before {
.fa();
content: @fa-var-reply;
margin-right: 5px;
}
}
.text-editor {
position: relative;
}
.mentions-dropdown {
max-width: 500px;
max-height: 200px;
overflow: auto;
position: absolute;
}
.post-preview {
color: @fl-body-muted-color !important;
& .avatar {
.avatar-size(32px);
margin: 3px 0 3px -45px;
}
& .username {
color: @fl-body-color;
font-weight: bold;
}
}
.post-preview-content {
padding-left: 45px;
overflow: hidden;
line-height: 1.7em;
}
.mentioned-by {
position: relative;
& .summary {
cursor: pointer;
}
}
.mentioned-by-preview, .mention-post-preview, .mentions-dropdown {
margin: 5px 0 !important;
& > li > a {
white-space: normal;
border-bottom: 0;
}
}

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateMentionsPostsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('mentionsPosts', function (Blueprint $table) {
$table->integer('post_id')->unsigned();
$table->integer('mentions_id')->unsigned();
$table->primary(['post_id', 'mentions_id']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('mentionsPosts');
}
}

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateMentionsUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('mentions_users', function (Blueprint $table) {
$table->integer('post_id')->unsigned();
$table->integer('mentions_id')->unsigned();
$table->primary(['post_id', 'mentions_id']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('mentions_users');
}
}

View File

@ -0,0 +1,46 @@
<?php namespace Flarum\Mentions\Handlers;
use Flarum\Mentions\PostMentionsParser;
use Flarum\Mentions\PostMentionedNotification;
use Flarum\Core\Events\PostWasPosted;
use Flarum\Core\Models\User;
use Flarum\Core\Notifications\Notifier;
use Illuminate\Contracts\Events\Dispatcher;
class PostMentionsMetadataUpdater
{
protected $parser;
protected $notifier;
public function __construct(PostMentionsParser $parser, Notifier $notifier)
{
$this->parser = $parser;
$this->notifier = $notifier;
}
public function subscribe(Dispatcher $events)
{
$events->listen('Flarum\Core\Events\PostWasPosted', __CLASS__.'@whenPostWasPosted');
// @todo listen for post edit/delete events and sync mentions as appropriate
}
public function whenPostWasPosted(PostWasPosted $event)
{
$reply = $event->post;
$matches = $this->parser->match($reply->content);
$mentioned = $reply->discussion->posts()->with('user')->whereIn('number', array_filter($matches['number']))->get();
$reply->mentionsPosts()->sync($mentioned->lists('id'));
// @todo convert this into a new event (PostWasMentioned) and send
// notification as a handler?
foreach ($mentioned as $post) {
if ($post->user->id !== $reply->user->id) {
$this->notifier->send(new PostMentionedNotification($post, $reply->user, $reply), [$post->user]);
}
}
}
}

View File

@ -0,0 +1,46 @@
<?php namespace Flarum\Mentions\Handlers;
use Flarum\Mentions\UserMentionsParser;
use Flarum\Mentions\UserMentionedNotification;
use Flarum\Core\Events\PostWasPosted;
use Flarum\Core\Models\User;
use Flarum\Core\Notifications\Notifier;
use Illuminate\Contracts\Events\Dispatcher;
class UserMentionsMetadataUpdater
{
protected $parser;
protected $notifier;
public function __construct(UserMentionsParser $parser, Notifier $notifier)
{
$this->parser = $parser;
$this->notifier = $notifier;
}
public function subscribe(Dispatcher $events)
{
$events->listen('Flarum\Core\Events\PostWasPosted', __CLASS__.'@whenPostWasPosted');
// @todo listen for post edit/delete events and sync mentions as appropriate
}
public function whenPostWasPosted(PostWasPosted $event)
{
$post = $event->post;
$matches = $this->parser->match($post->content);
$mentioned = User::whereIn('username', array_filter($matches['username']))->get();
$post->mentionsUsers()->sync($mentioned);
// @todo convert this into a new event (UserWasMentioned) and send
// notification as a handler?
foreach ($mentioned as $user) {
if ($user->id !== $post->user->id) {
$this->notifier->send(new UserMentionedNotification($post->user, $post), [$user]);
}
}
}
}

View File

@ -0,0 +1,20 @@
<?php namespace Flarum\Mentions;
abstract class MentionsParserAbstract
{
protected $pattern;
public function match($string)
{
preg_match_all($this->pattern, $string, $matches);
return $matches;
}
public function replace($string, $callback)
{
return preg_replace_callback($this->pattern, function ($matches) use ($callback) {
return $callback($matches);
}, $string);
}
}

View File

@ -0,0 +1,69 @@
<?php namespace Flarum\Mentions;
use Flarum\Support\ServiceProvider;
use Illuminate\Contracts\Events\Dispatcher;
use Flarum\Api\Actions\Discussions\ShowAction as DiscussionsShowAction;
use Flarum\Api\Actions\Posts\IndexAction as PostsIndexAction;
use Flarum\Api\Actions\Posts\ShowAction as PostsShowAction;
use Flarum\Api\Actions\Posts\CreateAction as PostsCreateAction;
class MentionsServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application events.
*
* @return void
*/
public function boot(Dispatcher $events)
{
$events->subscribe('Flarum\Mentions\Handlers\PostMentionsMetadataUpdater');
$events->subscribe('Flarum\Mentions\Handlers\UserMentionsMetadataUpdater');
$this->forumAssets([
__DIR__.'/../js/dist/extension.js',
__DIR__.'/../less/mentions.less'
]);
$this->relationship('Flarum\Core\Models\Post', function ($model) {
return $model->belongsToMany('Flarum\Core\Models\Post', 'mentions_posts', 'mentions_id');
}, 'mentionedBy');
$this->serializeRelationship('Flarum\Api\Serializers\PostSerializer', 'hasMany', 'mentionedBy', 'Flarum\Api\Serializers\PostBasicSerializer');
DiscussionsShowAction::$include['posts.mentionedBy'] = true;
DiscussionsShowAction::$include['posts.mentionedBy.user'] = true;
PostsShowAction::$include['mentionedBy'] = true;
PostsShowAction::$include['mentionedBy.user'] = true;
PostsIndexAction::$include['mentionedBy'] = true;
PostsIndexAction::$include['mentionedBy.user'] = true;
$this->relationship('Flarum\Core\Models\Post', function ($model) {
return $model->belongsToMany('Flarum\Core\Models\Post', 'mentions_posts', 'post_id', 'mentions_id');
}, 'mentionsPosts');
$this->relationship('Flarum\Core\Models\Post', function ($model) {
return $model->belongsToMany('Flarum\Core\Models\User', 'mentions_users', 'post_id', 'mentions_id');
}, 'mentionsUsers');
$this->serializeRelationship('Flarum\Api\Serializers\PostSerializer', 'hasMany', 'mentionsPosts', 'Flarum\Api\Serializers\PostBasicSerializer');
$this->serializeRelationship('Flarum\Api\Serializers\PostSerializer', 'hasMany', 'mentionsUsers', 'Flarum\Api\Serializers\UserBasicSerializer');
DiscussionsShowAction::$include['posts.mentionsPosts'] = true;
DiscussionsShowAction::$include['posts.mentionsPosts.user'] = true;
PostsCreateAction::$include['mentionsPosts'] = true;
PostsCreateAction::$include['mentionsPosts.mentionedBy'] = true;
DiscussionsShowAction::$include['posts.mentionsUsers'] = true;
$this->formatter('postMentions', 'Flarum\Mentions\PostMentionsFormatter');
$this->formatter('userMentions', 'Flarum\Mentions\UserMentionsFormatter');
$this->notificationType('Flarum\Mentions\PostMentionedNotification', ['alert' => true]);
$this->notificationType('Flarum\Mentions\UserMentionedNotification', ['alert' => true]);
}
}

View File

@ -0,0 +1,47 @@
<?php namespace Flarum\Mentions;
use Flarum\Core\Models\User;
use Flarum\Core\Models\Post;
use Flarum\Core\Notifications\Types\Notification;
use Flarum\Core\Notifications\Types\AlertableNotification;
class PostMentionedNotification extends Notification implements AlertableNotification
{
protected $post;
protected $sender;
protected $reply;
public function __construct(Post $post, User $sender, Post $reply)
{
$this->post = $post;
$this->sender = $sender;
$this->reply = $reply;
}
public function getSubject()
{
return $this->post;
}
public function getSender()
{
return $this->sender;
}
public function getAlertData()
{
return ['replyNumber' => $this->reply->number];
}
public static function getType()
{
return 'postMentioned';
}
public static function getSubjectModel()
{
return 'Flarum\Core\Models\Post';
}
}

View File

@ -0,0 +1,31 @@
<?php namespace Flarum\Mentions;
class PostMentionsFormatter
{
protected $parser;
public function __construct(PostMentionsParser $parser)
{
$this->parser = $parser;
}
public function format($text, $post = null)
{
if ($post) {
$text = $this->parser->replace($text, function ($match) use ($post) {
return '<a href="#/d/'.$post->discussion_id.'/-/'.$match['number'].'" class="mention-post" data-number="'.$match['number'].'">'.$match['username'].'</a>';
}, $text);
}
return $text;
}
public function strip($text)
{
$text = $this->parser->replace($text, function () {
return ' ';
});
return $text;
}
}

View File

@ -0,0 +1,6 @@
<?php namespace Flarum\Mentions;
class PostMentionsParser extends MentionsParserAbstract
{
protected $pattern = '/\B@(?P<username>[a-z0-9_-]+)#(?P<number>\d+)/i';
}

View File

@ -0,0 +1,44 @@
<?php namespace Flarum\Mentions;
use Flarum\Core\Models\User;
use Flarum\Core\Models\Post;
use Flarum\Core\Notifications\Types\Notification;
use Flarum\Core\Notifications\Types\AlertableNotification;
class UserMentionedNotification extends Notification implements AlertableNotification
{
protected $sender;
protected $post;
public function __construct(User $sender, Post $post)
{
$this->sender = $sender;
$this->post = $post;
}
public function getSubject()
{
return $this->post;
}
public function getSender()
{
return $this->sender;
}
public function getAlertData()
{
return null;
}
public static function getType()
{
return 'userMentioned';
}
public static function getSubjectModel()
{
return 'Flarum\Core\Models\Post';
}
}

View File

@ -0,0 +1,20 @@
<?php namespace Flarum\Mentions;
class UserMentionsFormatter
{
protected $parser;
public function __construct(UserMentionsParser $parser)
{
$this->parser = $parser;
}
public function format($text, $post = null)
{
$text = $this->parser->replace($text, function ($match) {
return '<a href="#/u/'.$match['username'].'" class="mention-user" data-user="'.$match['username'].'">'.$match['username'].'</a>';
}, $text);
return $text;
}
}

View File

@ -0,0 +1,6 @@
<?php namespace Flarum\Mentions;
class UserMentionsParser extends MentionsParserAbstract
{
protected $pattern = '/\B@(?P<username>[a-z0-9_-]+)/i';
}