mirror of
https://github.com/flarum/core.git
synced 2025-08-03 06:57:54 +02:00
Initial commit
This commit is contained in:
4
extensions/mentions/.gitignore
vendored
Normal file
4
extensions/mentions/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/vendor
|
||||||
|
composer.phar
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
21
extensions/mentions/LICENSE.txt
Normal file
21
extensions/mentions/LICENSE.txt
Normal 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.
|
9
extensions/mentions/bootstrap.php
Normal file
9
extensions/mentions/bootstrap.php
Normal 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');
|
18
extensions/mentions/composer.json
Normal file
18
extensions/mentions/composer.json
Normal 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/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
extensions/mentions/extension.json
Normal file
15
extensions/mentions/extension.json
Normal 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
4
extensions/mentions/js/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
bower_components
|
||||||
|
node_modules
|
||||||
|
mithril.js
|
||||||
|
dist
|
46
extensions/mentions/js/Gulpfile.js
Normal file
46
extensions/mentions/js/Gulpfile.js
Normal 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
29
extensions/mentions/js/bootstrap.js
vendored
Normal 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;
|
||||||
|
});
|
6
extensions/mentions/js/bower.json
Normal file
6
extensions/mentions/js/bower.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "flarum-mentions",
|
||||||
|
"dev-dependencies": {
|
||||||
|
"textarea-caret-position": "~3.0.0"
|
||||||
|
}
|
||||||
|
}
|
16
extensions/mentions/js/package.json
Normal file
16
extensions/mentions/js/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -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']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
120
extensions/mentions/js/src/composer-autocomplete.js
Normal file
120
extensions/mentions/js/src/composer-autocomplete.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
90
extensions/mentions/js/src/mentioned-by-list.js
Normal file
90
extensions/mentions/js/src/mentioned-by-list.js
Normal 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.'
|
||||||
|
])
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
81
extensions/mentions/js/src/post-mention-previews.js
Normal file
81
extensions/mentions/js/src/post-mention-previews.js
Normal 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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
32
extensions/mentions/js/src/post-reply-action.js
Normal file
32
extensions/mentions/js/src/post-reply-action.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
65
extensions/mentions/less/mentions.less
Normal file
65
extensions/mentions/less/mentions.less
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
46
extensions/mentions/src/Handlers/PostMentionsMetadataUpdater.php
Executable file
46
extensions/mentions/src/Handlers/PostMentionsMetadataUpdater.php
Executable 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
extensions/mentions/src/Handlers/UserMentionsMetadataUpdater.php
Executable file
46
extensions/mentions/src/Handlers/UserMentionsMetadataUpdater.php
Executable 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
extensions/mentions/src/MentionsParserAbstract.php
Normal file
20
extensions/mentions/src/MentionsParserAbstract.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
69
extensions/mentions/src/MentionsServiceProvider.php
Normal file
69
extensions/mentions/src/MentionsServiceProvider.php
Normal 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]);
|
||||||
|
}
|
||||||
|
}
|
47
extensions/mentions/src/PostMentionedNotification.php
Normal file
47
extensions/mentions/src/PostMentionedNotification.php
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
31
extensions/mentions/src/PostMentionsFormatter.php
Normal file
31
extensions/mentions/src/PostMentionsFormatter.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
6
extensions/mentions/src/PostMentionsParser.php
Normal file
6
extensions/mentions/src/PostMentionsParser.php
Normal 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';
|
||||||
|
}
|
44
extensions/mentions/src/UserMentionedNotification.php
Normal file
44
extensions/mentions/src/UserMentionedNotification.php
Normal 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';
|
||||||
|
}
|
||||||
|
}
|
20
extensions/mentions/src/UserMentionsFormatter.php
Normal file
20
extensions/mentions/src/UserMentionsFormatter.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
6
extensions/mentions/src/UserMentionsParser.php
Normal file
6
extensions/mentions/src/UserMentionsParser.php
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?php namespace Flarum\Mentions;
|
||||||
|
|
||||||
|
class UserMentionsParser extends MentionsParserAbstract
|
||||||
|
{
|
||||||
|
protected $pattern = '/\B@(?P<username>[a-z0-9_-]+)/i';
|
||||||
|
}
|
Reference in New Issue
Block a user