');
- var dropdown = new AutocompleteDropdown({items: []});
- var typed;
- var mentionStart;
- var $textarea = this.$('textarea');
- var searched = [];
- var searchTimeout;
+ const composer = this;
+ const $container = $('');
+ const dropdown = new AutocompleteDropdown({items: []});
+ const $textarea = this.$('textarea');
+ const searched = [];
+ let mentionStart;
+ let typed;
+ let searchTimeout;
- var applySuggestion = function(replacement) {
- replacement += ' ';
+ const applySuggestion = function(replacement) {
+ const insert = replacement + ' ';
- var content = composer.content();
- composer.editor.setContent(content.substring(0, mentionStart - 1)+replacement+content.substr($textarea[0].selectionStart));
+ const content = composer.content();
+ composer.editor.setValue(content.substring(0, mentionStart - 1) + insert + content.substr($textarea[0].selectionStart));
- var index = mentionStart - 1 + replacement.length;
+ const index = mentionStart - 1 + insert.length;
composer.editor.setSelectionRange(index, index);
dropdown.hide();
@@ -41,73 +41,76 @@ export default function() {
// Up, down, enter, tab, escape, left, right.
if ([9, 13, 27, 40, 38, 37, 39].indexOf(e.which) !== -1) return;
- var cursor = this.selectionStart;
+ const 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;
+ const value = this.value;
mentionStart = 0;
- for (var i = cursor - 1; i >= 0; i--) {
- var character = value.substr(i, 1);
+ for (let i = cursor - 1; i >= 0; i--) {
+ const character = value.substr(i, 1);
if (/\s/.test(character)) break;
- if (character == '@') {
+ if (character === '@') {
mentionStart = i + 1;
break;
}
}
dropdown.hide();
- dropdown.active(false);
+ dropdown.active = false;
if (mentionStart) {
typed = value.substring(mentionStart, cursor).toLowerCase();
- var makeSuggestion = function(user, replacement, content, className) {
- return m('a[href=javascript:;].post-preview', {
- className,
- onclick: () => applySuggestion(replacement),
- onmouseenter: function() { dropdown.setIndex($(this).parent().index()); }
- }, m('div.post-preview-content', [
- avatar(user),
- (function() {
- var vdom = username(user);
- if (typed) {
- vdom.children[0] = highlight(vdom.children[0], typed);
- }
- return vdom;
- })(), ' ',
- content
- ]));
+ const makeSuggestion = function(user, replacement, content, className = '') {
+ const username = usernameHelper(user);
+ if (typed) {
+ username.children[0] = highlight(username.children[0], typed);
+ }
+
+ return (
+
+ );
};
- var buildSuggestions = () => {
- var suggestions = [];
+ const buildSuggestions = () => {
+ const suggestions = [];
// 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;
+ const composerPost = composer.props.post;
+ const 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();
+ const user = post.user();
return user && user.username().toLowerCase().substr(0, typed.length) === typed;
})
.splice(0, 5)
.forEach(post => {
- var user = post.user();
+ const user = post.user();
suggestions.push(
- makeSuggestion(user, '@'+user.username()+'#'+post.number(), [
- 'Reply to #', post.number(), ' — ',
+ makeSuggestion(user, '@' + user.username() + '#' + post.number(), [
+ app.trans('mentions.reply_to_post', {number: post.number()}), ' — ',
truncate(post.contentPlain(), 200)
- ], 'suggestion-post')
+ ], 'MentionsDropdown-post')
);
});
}
@@ -119,7 +122,7 @@ export default function() {
if (user.username().toLowerCase().substr(0, typed.length) !== typed) return;
suggestions.push(
- makeSuggestion(user, '@'+user.username(), '', 'suggestion-user')
+ makeSuggestion(user, '@' + user.username(), '', 'MentionsDropdown-user')
);
});
}
@@ -129,12 +132,12 @@ export default function() {
m.render($container[0], dropdown.render());
dropdown.show();
- var coordinates = getCaretCoordinates(this, mentionStart);
- var left = coordinates.left;
- var top = coordinates.top + 15;
- var width = dropdown.$().outerWidth();
- var height = dropdown.$().outerHeight();
- var parent = dropdown.$().offsetParent();
+ const coordinates = getCaretCoordinates(this, mentionStart);
+ const width = dropdown.$().outerWidth();
+ const height = dropdown.$().outerHeight();
+ const parent = dropdown.$().offsetParent();
+ let left = coordinates.left;
+ let top = coordinates.top + 15;
if (top + height > parent.height()) {
top = coordinates.top - height - 15;
}
@@ -149,15 +152,15 @@ export default function() {
dropdown.setIndex(0);
dropdown.$().scrollTop(0);
- dropdown.active(true);
+ dropdown.active = true;
clearTimeout(searchTimeout);
if (typed) {
searchTimeout = setTimeout(function() {
- var typedLower = typed.toLowerCase();
+ const typedLower = typed.toLowerCase();
if (searched.indexOf(typedLower) === -1) {
- app.store.find('users', {q: typed, page: {limit: 5}}).then(users => {
- if (dropdown.active()) buildSuggestions();
+ app.store.find('users', {q: typed, page: {limit: 5}}).then(() => {
+ if (dropdown.active) buildSuggestions();
});
searched.push(typedLower);
}
diff --git a/extensions/mentions/js/forum/src/addMentionedByList.js b/extensions/mentions/js/forum/src/addMentionedByList.js
new file mode 100644
index 000000000..b8c43407e
--- /dev/null
+++ b/extensions/mentions/js/forum/src/addMentionedByList.js
@@ -0,0 +1,112 @@
+import { extend } from 'flarum/extend';
+import Model from 'flarum/Model';
+import Post from 'flarum/models/Post';
+import CommentPost from 'flarum/components/CommentPost';
+import PostPreview from 'flarum/components/PostPreview';
+import punctuate from 'flarum/helpers/punctuate';
+import username from 'flarum/helpers/username';
+import icon from 'flarum/helpers/icon';
+
+export default function addMentionedByList() {
+ Post.prototype.mentionedBy = Model.hasMany('mentionedBy');
+
+ extend(CommentPost.prototype, 'footerItems', function(items) {
+ const post = this.props.post;
+ const replies = post.mentionedBy();
+
+ if (replies && replies.length) {
+ // If there is only one reply, and it's adjacent to this post, we don't
+ // really need to show the list.
+ if (replies.length === 1 && replies[0].number() === post.number() + 1) {
+ return;
+ }
+
+ const hidePreview = () => {
+ this.$('.Post-mentionedBy-preview')
+ .removeClass('in')
+ .one('transitionend', function() { $(this).hide(); });
+ };
+
+ const config = function(element, isInitialized) {
+ if (isInitialized) return;
+
+ const $this = $(element);
+ let timeout;
+
+ const $preview = $('
');
+ $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(reply => (
+
+ )));
+ $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('.Post-mentionedBy-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.
+ const used = [];
+ const repliers = replies.filter(reply => {
+ const user = reply.user();
+ const id = user && user.id();
+ if (used.indexOf(id) === -1) {
+ used.push(id);
+ return true;
+ }
+ });
+
+ const names = repliers.sort(a => a === app.session.user ? -1 : 1)
+ .map(reply => {
+ const user = reply.user();
+
+ return (
+
+ {app.session.user === user ? app.trans('mentions.you') : username(user)}
+
+ );
+ });
+
+ items.add('replies',
+
+ );
+ }
+ });
+}
diff --git a/extensions/mentions/js/src/post-mention-previews.js b/extensions/mentions/js/forum/src/addPostMentionPreviews.js
similarity index 58%
rename from extensions/mentions/js/src/post-mention-previews.js
rename to extensions/mentions/js/forum/src/addPostMentionPreviews.js
index 1b3d36a43..ccd486e38 100644
--- a/extensions/mentions/js/src/post-mention-previews.js
+++ b/extensions/mentions/js/forum/src/addPostMentionPreviews.js
@@ -1,40 +1,46 @@
-import { extend } from 'flarum/extension-utils';
-import CommentPost from 'flarum/components/comment-post';
-import PostPreview from 'flarum/components/post-preview';
-import LoadingIndicator from 'flarum/components/loading-indicator';
+import { extend } from 'flarum/extend';
+import CommentPost from 'flarum/components/CommentPost';
+import PostPreview from 'flarum/components/PostPreview';
+import LoadingIndicator from 'flarum/components/LoadingIndicator';
-export default function postMentionPreviews() {
+export default function addPostMentionPreviews() {
extend(CommentPost.prototype, 'config', function() {
- var contentHtml = this.props.post.contentHtml();
+ const contentHtml = this.props.post.contentHtml();
+
if (contentHtml === this.oldPostContentHtml) return;
+
this.oldPostContentHtml = contentHtml;
- var discussion = this.props.post.discussion();
+ const discussion = this.props.post.discussion();
- this.$('.mention-post').each(function() {
- var $this = $(this);
- var number = $this.data('number');
- var timeout;
+ this.$('.UserMention').each(function() {
+ m.route.call(this, this, false, {}, {attrs: {href: this.getAttribute('href')}});
+ });
+
+ this.$('.PostMention').each(function() {
+ const $this = $(this);
+ const number = $this.data('number');
+ let 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 = $('
');
+ const $wrapper = $('');
$this.wrap($wrapper).after($preview);
- var getPostElement = function() {
- return $('.discussion-posts .item[data-number='+number+']');
+ const getPostElement = () => {
+ return $(`.PostStream-item[data-number="${number}"]`);
};
- var showPreview = function() {
+ const showPreview = () => {
// 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;
+ const $post = getPostElement();
+ let visible = false;
if ($post.length) {
- var top = $post.offset().top;
- var scrollTop = window.pageYOffset;
+ const top = $post.offset().top;
+ const scrollTop = window.pageYOffset;
if (top > scrollTop && top + $post.height() < scrollTop + $(window).height()) {
$post.addClass('pulsate');
visible = true;
@@ -44,30 +50,33 @@ export default function postMentionPreviews() {
// 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})));
- positionPreview();
- };
-
// Position the preview so that it appears above the mention.
// (The offsetParent should be .post-body.)
- var positionPreview = function() {
+ const positionPreview = () => {
$preview.show().css('top', $this.offset().top - $this.offsetParent().offset().top - $preview.outerHeight(true));
};
- var post = discussion.posts().filter(post => post && post.number() == number)[0];
+ const showPost = post => {
+ m.render($preview[0],
{PostPreview.component({post})}
);
+ positionPreview();
+ };
+
+ const post = discussion.posts().filter(p => p && p.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]));
+ app.store.find('posts', {
+ filter: {discussion: discussion.id(), number}
+ }).then(posts => showPost(posts[0]));
positionPreview();
}
setTimeout(() => $preview.off('transitionend').addClass('in'));
}
};
- var hidePreview = () => {
+
+ const hidePreview = () => {
getPostElement().removeClass('pulsate');
if ($preview.hasClass('in')) {
$preview.removeClass('in').one('transitionend', () => $preview.hide());
@@ -75,11 +84,11 @@ export default function postMentionPreviews() {
};
$this.parent().hover(
- function() {
+ () => {
clearTimeout(timeout);
timeout = setTimeout(showPreview, 500);
},
- function() {
+ () => {
clearTimeout(timeout);
getPostElement().removeClass('pulsate');
timeout = setTimeout(hidePreview, 250);
diff --git a/extensions/mentions/js/forum/src/addPostReplyAction.js b/extensions/mentions/js/forum/src/addPostReplyAction.js
new file mode 100644
index 000000000..8be0001d6
--- /dev/null
+++ b/extensions/mentions/js/forum/src/addPostReplyAction.js
@@ -0,0 +1,48 @@
+import { extend } from 'flarum/extend';
+import Button from 'flarum/components/Button';
+import CommentPost from 'flarum/components/CommentPost';
+import DiscussionControls from 'flarum/utils/DiscussionControls';
+
+export default function() {
+ extend(CommentPost.prototype, 'actionItems', function(items) {
+ const post = this.props.post;
+
+ if (post.isHidden() || (app.session.user && !post.discussion().canReply())) return;
+
+ function insertMention(component, quote) {
+ const mention = '@' + post.user().username() + '#' + post.number() + ' ';
+
+ // 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;
+ }
+
+ component.editor.insertAtCursor(
+ (component.editor.getSelectionRange()[0] > 0 ? '\n\n' : '') +
+ (quote
+ ? '> ' + mention + quote.trim().replace(/\n/g, '\n> ') + '\n\n'
+ : mention)
+ );
+ }
+
+ items.add('reply',
+ Button.component({
+ className: 'Button Button--text',
+ children: app.trans('mentions.reply_link'),
+ onclick: () => {
+ const quote = window.getSelection().toString();
+
+ const component = app.composer.component;
+ if (component && component.props.post && component.props.post.discussion() === post.discussion()) {
+ insertMention(component, quote);
+ } else {
+ DiscussionControls.replyAction.call(post.discussion())
+ .then(newComponent => insertMention(newComponent, quote));
+ }
+ }
+ })
+ );
+ });
+}
diff --git a/extensions/mentions/js/forum/src/components/AutocompleteDropdown.js b/extensions/mentions/js/forum/src/components/AutocompleteDropdown.js
new file mode 100644
index 000000000..296b882b5
--- /dev/null
+++ b/extensions/mentions/js/forum/src/components/AutocompleteDropdown.js
@@ -0,0 +1,97 @@
+import Component from 'flarum/Component';
+
+export default class AutocompleteDropdown extends Component {
+ constructor(...args) {
+ super(...args);
+
+ this.active = false;
+ this.index = 0;
+ this.keyWasJustPressed = false;
+ }
+
+ view() {
+ return (
+
+ {this.props.items.map(item =>
{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: case 38: // Down/Up
+ this.keyWasJustPressed = true;
+ this.setIndex(this.index + (e.which === 40 ? 1 : -1), true);
+ clearTimeout(this.keyWasJustPressedTimeout);
+ this.keyWasJustPressedTimeout = setTimeout(() => this.keyWasJustPressed = false, 500);
+ e.preventDefault();
+ break;
+
+ case 13: case 9: // Enter/Tab
+ this.$('li').eq(this.index).find('button').click();
+ e.preventDefault();
+ break;
+
+ case 27: // Escape
+ this.hide();
+ e.stopPropagation();
+ e.preventDefault();
+ break;
+
+ default:
+ // no default
+ }
+ }
+
+ setIndex(index, scrollToItem) {
+ if (this.keyWasJustPressed && !scrollToItem) return;
+
+ const $dropdown = this.$();
+ const $items = $dropdown.find('li');
+ let rangedIndex = index;
+
+ if (rangedIndex < 0) {
+ rangedIndex = $items.length - 1;
+ } else if (rangedIndex >= $items.length) {
+ rangedIndex = 0;
+ }
+
+ this.index = rangedIndex;
+
+ const $item = $items.removeClass('active').eq(rangedIndex).addClass('active');
+
+ if (scrollToItem) {
+ const dropdownScroll = $dropdown.scrollTop();
+ const dropdownTop = $dropdown.offset().top;
+ const dropdownBottom = dropdownTop + $dropdown.outerHeight();
+ const itemTop = $item.offset().top;
+ const itemBottom = itemTop + $item.outerHeight();
+
+ let scrollTop;
+ if (itemTop < dropdownTop) {
+ scrollTop = dropdownScroll - dropdownTop + itemTop - parseInt($dropdown.css('padding-top'), 10);
+ } else if (itemBottom > dropdownBottom) {
+ scrollTop = dropdownScroll - dropdownBottom + itemBottom + parseInt($dropdown.css('padding-bottom'), 10);
+ }
+
+ if (typeof scrollTop !== 'undefined') {
+ $dropdown.stop(true).animate({scrollTop}, 100);
+ }
+ }
+ }
+}
diff --git a/extensions/mentions/js/forum/src/components/PostMentionedNotification.js b/extensions/mentions/js/forum/src/components/PostMentionedNotification.js
new file mode 100644
index 000000000..3b6cd929d
--- /dev/null
+++ b/extensions/mentions/js/forum/src/components/PostMentionedNotification.js
@@ -0,0 +1,34 @@
+import Notification from 'flarum/components/Notification';
+import username from 'flarum/helpers/username';
+import punctuate from 'flarum/helpers/punctuate';
+
+export default class PostMentionedNotification extends Notification {
+ icon() {
+ return 'reply';
+ }
+
+ href() {
+ const notification = this.props.notification;
+ const post = notification.subject();
+ const auc = notification.additionalUnreadCount();
+ const content = notification.content();
+
+ return app.route.discussion(post.discussion(), auc ? post.number() : (content && content.replyNumber));
+ }
+
+ content() {
+ const notification = this.props.notification;
+ const post = notification.subject();
+ const auc = notification.additionalUnreadCount();
+ const user = notification.sender();
+
+ return app.trans('mentions.post_mentioned_notification', {
+ user,
+ username: auc ? punctuate([
+ username(user),
+ app.trans('mentions.others', {count: auc})
+ ]) : undefined,
+ number: post.number()
+ });
+ }
+}
diff --git a/extensions/mentions/js/forum/src/components/UserMentionedNotification.js b/extensions/mentions/js/forum/src/components/UserMentionedNotification.js
new file mode 100644
index 000000000..939d791d7
--- /dev/null
+++ b/extensions/mentions/js/forum/src/components/UserMentionedNotification.js
@@ -0,0 +1,20 @@
+import Notification from 'flarum/components/Notification';
+import username from 'flarum/helpers/username';
+
+export default class UserMentionedNotification extends Notification {
+ icon() {
+ return 'at';
+ }
+
+ href() {
+ const post = this.props.notification.subject();
+
+ return app.route.discussion(post.discussion(), post.number());
+ }
+
+ content() {
+ const user = this.props.notification.sender();
+
+ return app.trans('mentions.user_mentioned_notification', {user});
+ }
+}
diff --git a/extensions/mentions/js/forum/src/main.js b/extensions/mentions/js/forum/src/main.js
new file mode 100644
index 000000000..848b2cf0f
--- /dev/null
+++ b/extensions/mentions/js/forum/src/main.js
@@ -0,0 +1,45 @@
+import { extend } from 'flarum/extend';
+import app from 'flarum/app';
+import NotificationGrid from 'flarum/components/NotificationGrid';
+
+import addPostMentionPreviews from 'mentions/addPostMentionPreviews';
+import addMentionedByList from 'mentions/addMentionedByList';
+import addPostReplyAction from 'mentions/addPostReplyAction';
+import addComposerAutocomplete from 'mentions/addComposerAutocomplete';
+import PostMentionedNotification from 'mentions/components/PostMentionedNotification';
+import UserMentionedNotification from 'mentions/components/UserMentionedNotification';
+
+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.
+ addPostMentionPreviews();
+
+ // In the footer of each post, show information about who has replied (i.e.
+ // who the post has been mentioned by).
+ addMentionedByList();
+
+ // 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.
+ addPostReplyAction();
+
+ // After typing '@' in the composer, show a dropdown suggesting a bunch of
+ // posts or users that the user could mention.
+ addComposerAutocomplete();
+
+ app.notificationComponents.postMentioned = PostMentionedNotification;
+ app.notificationComponents.userMentioned = UserMentionedNotification;
+
+ // Add notification preferences.
+ extend(NotificationGrid.prototype, 'notificationTypes', function(items) {
+ items.add('postMentioned', {
+ name: 'postMentioned',
+ icon: 'reply',
+ label: 'Someone replies to my post'
+ });
+ items.add('userMentioned', {
+ name: 'userMentioned',
+ icon: 'at',
+ label: 'Someone mentions me in a post'
+ });
+ });
+});
diff --git a/extensions/mentions/js/src/components/autocomplete-dropdown.js b/extensions/mentions/js/src/components/autocomplete-dropdown.js
deleted file mode 100644
index 429347aa3..000000000
--- a/extensions/mentions/js/src/components/autocomplete-dropdown.js
+++ /dev/null
@@ -1,89 +0,0 @@
-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);
- this.keyWasJustPressed = false;
- }
-
- view() {
- return m('ul.dropdown-menu.mentions-dropdown', 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: case 38: // Down/Up
- this.keyWasJustPressed = true;
- this.setIndex(this.index() + (e.which === 40 ? 1 : -1), true);
- clearTimeout(this.keyWasJustPressedTimeout);
- this.keyWasJustPressedTimeout = setTimeout(() => this.keyWasJustPressed = false, 500);
- 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) {
- if (this.keyWasJustPressed && !scrollToItem) return;
-
- 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);
- }
- }
- }
-}
diff --git a/extensions/mentions/js/src/components/post-mentioned-notification.js b/extensions/mentions/js/src/components/post-mentioned-notification.js
deleted file mode 100644
index 6e66a9a68..000000000
--- a/extensions/mentions/js/src/components/post-mentioned-notification.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import Notification from 'flarum/components/notification';
-import username from 'flarum/helpers/username';
-
-export default class PostMentionedNotification 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 #'+post.number()]
- });
- }
-}
diff --git a/extensions/mentions/js/src/components/user-mentioned-notification.js b/extensions/mentions/js/src/components/user-mentioned-notification.js
deleted file mode 100644
index 84f10023e..000000000
--- a/extensions/mentions/js/src/components/user-mentioned-notification.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import Notification from 'flarum/components/notification';
-import username from 'flarum/helpers/username';
-
-export default class UserMentionedNotification 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']
- });
- }
-}
diff --git a/extensions/mentions/js/src/mentioned-by-list.js b/extensions/mentions/js/src/mentioned-by-list.js
deleted file mode 100644
index 22f2f4eb8..000000000
--- a/extensions/mentions/js/src/mentioned-by-list.js
+++ /dev/null
@@ -1,102 +0,0 @@
-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 CommentPost from 'flarum/components/comment-post';
-import PostPreview from 'flarum/components/post-preview';
-import punctuate from 'flarum/helpers/punctuate';
-import username from 'flarum/helpers/username';
-import icon from 'flarum/helpers/icon';
-
-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(CommentPost.prototype, 'footerItems', function(items) {
- var post = this.props.post;
- var replies = post.mentionedBy();
- if (replies && replies.length) {
-
- // If there is only one reply, and it's adjacent to this post, we don't
- // really need to show the list.
- if (replies.length === 1 && replies[0].number() == post.number() + 1) {
- return;
- }
-
- 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 = $('
');
- $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 => {
- var user = reply.user();
- var id = user && user.id();
- if (used.indexOf(id) === -1) {
- used.push(id);
- return true;
- }
- });
-
- items.add('replies',
- m('div.mentioned-by', {config}, [
- m('span.summary', [
- icon('reply icon'),
- punctuate(repliers.map(reply => {
- return m('a', {
- href: app.route.post(reply),
- config: m.route,
- onclick: hidePreview,
- 'data-number': reply.number()
- }, [
- app.session.user() && reply.user() === app.session.user() ? 'You' : username(reply.user())
- ])
- })),
- ' replied to this.'
- ])
- ])
- );
- }
- });
-}
diff --git a/extensions/mentions/js/src/post-reply-action.js b/extensions/mentions/js/src/post-reply-action.js
deleted file mode 100644
index 14f196ab3..000000000
--- a/extensions/mentions/js/src/post-reply-action.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import { extend } from 'flarum/extension-utils';
-import ActionButton from 'flarum/components/action-button';
-import CommentPost from 'flarum/components/comment-post';
-
-export default function() {
- extend(CommentPost.prototype, 'actionItems', function(items) {
- var post = this.props.post;
- if (post.isHidden() || (app.session.user() && !post.discussion().canReply())) return;
-
- function insertMention(component, quote) {
- var mention = '@'+post.user().username()+'#'+post.number()+' ';
-
- // 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;
- }
-
- component.editor.insertAtCursor((component.editor.getSelectionRange()[0] > 0 ? '\n\n' : '')+(quote ? '> '+mention+quote.trim().replace(/\n/g, '\n> ')+'\n\n' : mention));
- }
-
- items.add('reply',
- ActionButton.component({
- icon: 'reply',
- label: 'Reply',
- onclick: () => {
- var quote = window.getSelection().toString();
-
- var component = app.composer.component;
- if (component && component.props.post && component.props.post.discussion() === post.discussion()) {
- insertMention(component, quote);
- } else {
- post.discussion().replyAction().then(component => insertMention(component, quote));
- }
- }
- })
- );
- });
-}
diff --git a/extensions/mentions/less/forum/extension.less b/extensions/mentions/less/forum/extension.less
new file mode 100644
index 000000000..5c5c7f521
--- /dev/null
+++ b/extensions/mentions/less/forum/extension.less
@@ -0,0 +1,83 @@
+.PostMention, .UserMention {
+ background: @control-bg;
+ color: @control-color;
+ border-radius: @border-radius;
+ padding: 2px 5px;
+ border: 0 !important;
+
+ blockquote & {
+ background: @body-bg;
+ }
+ &:hover,
+ &:active {
+ color: @link-color;
+ }
+}
+.PostMention {
+ margin: 0 3px;
+
+ &:first-child {
+ margin-left: 0;
+ }
+
+ &:before {
+ .fa();
+ content: @fa-var-reply;
+ margin-right: 5px;
+ }
+}
+.TextEditor {
+ position: relative;
+}
+.MentionsDropdown {
+ max-width: 500px;
+ max-height: 200px;
+ overflow: auto;
+ position: absolute;
+
+ mark {
+ padding: 0;
+ }
+ > li > a:hover {
+ background: none;
+ }
+}
+.MentionsDropdown, .PostMention-preview, .Post-mentionedBy-preview {
+ .PostPreview {
+ color: @muted-color;
+
+ .Avatar {
+ .Avatar--size(24px);
+ margin: 0 0 0 -37px;
+
+ .MentionsDropdown-post& {
+ margin-top: 3px;
+ margin-bottom: 3px;
+ }
+ }
+ .username {
+ color: @text-color;
+ font-weight: bold;
+ }
+ }
+ .PostPreview-content {
+ padding-left: 37px;
+ overflow: hidden;
+ line-height: 1.7em;
+ display: block;
+ }
+}
+.Post-mentionedBy {
+ position: relative;
+}
+.Post-mentionedBy-summary {
+ cursor: pointer;
+}
+.Post-mentionedBy-preview, .PostMention-preview, .MentionsDropdown {
+ margin: 5px 0 !important;
+
+ > li > a {
+ white-space: normal;
+ border-bottom: 0;
+ }
+}
diff --git a/extensions/mentions/less/mentions.less b/extensions/mentions/less/mentions.less
deleted file mode 100644
index b9fbb73d0..000000000
--- a/extensions/mentions/less/mentions.less
+++ /dev/null
@@ -1,77 +0,0 @@
-.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;
-
- & mark {
- padding: 0;
- }
- & > li > a:hover {
- background: none;
- }
-}
-.post-preview {
- color: @fl-body-muted-color !important;
-
- & .avatar {
- .avatar-size(24px);
- margin: 0 0 0 -37px;
-
- .suggestion-post& {
- margin-top: 3px;
- margin-bottom: 3px;
- }
- }
- & .username {
- color: @fl-body-color;
- font-weight: bold;
- }
-}
-.post-preview-content {
- padding-left: 37px;
- 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;
- }
-}
diff --git a/extensions/mentions/locale/en.yml b/extensions/mentions/locale/en.yml
new file mode 100644
index 000000000..8e6c327ed
--- /dev/null
+++ b/extensions/mentions/locale/en.yml
@@ -0,0 +1,8 @@
+mentions:
+ reply_to_post: "Reply to #{number}"
+ post_mentioned_notification: "{username} replied to your post #{number}"
+ others: "{count} others"
+ user_mentioned_notification: "{username} mentioned you"
+ post_mentioned_by: "{users} replied to this."
+ you: You
+ reply_link: Reply
diff --git a/extensions/mentions/src/Extension.php b/extensions/mentions/src/Extension.php
new file mode 100644
index 000000000..0103a48bc
--- /dev/null
+++ b/extensions/mentions/src/Extension.php
@@ -0,0 +1,18 @@
+subscribe('Flarum\Mentions\Listeners\AddClientAssets');
+ $events->subscribe('Flarum\Mentions\Listeners\AddModelRelationships');
+ $events->subscribe('Flarum\Mentions\Listeners\AddApiRelationships');
+ $events->subscribe('Flarum\Mentions\Listeners\AddUserMentionsFormatter');
+ $events->subscribe('Flarum\Mentions\Listeners\AddPostMentionsFormatter');
+ $events->subscribe('Flarum\Mentions\Listeners\UpdateUserMentionsMetadata');
+ $events->subscribe('Flarum\Mentions\Listeners\UpdatePostMentionsMetadata');
+ }
+}
diff --git a/extensions/mentions/src/Handlers/PostMentionsMetadataUpdater.php b/extensions/mentions/src/Handlers/PostMentionsMetadataUpdater.php
deleted file mode 100755
index bca95865e..000000000
--- a/extensions/mentions/src/Handlers/PostMentionsMetadataUpdater.php
+++ /dev/null
@@ -1,94 +0,0 @@
-parser = $parser;
- $this->activity = $activity;
- $this->notifications = $notifications;
- }
-
- public function subscribe(Dispatcher $events)
- {
- $events->listen('Flarum\Core\Events\PostWasPosted', __CLASS__.'@whenPostWasPosted');
- $events->listen('Flarum\Core\Events\PostWasRevised', __CLASS__.'@whenPostWasRevised');
- $events->listen('Flarum\Core\Events\PostWasHidden', __CLASS__.'@whenPostWasHidden');
- $events->listen('Flarum\Core\Events\PostWasRestored', __CLASS__.'@whenPostWasRestored');
- $events->listen('Flarum\Core\Events\PostWasDeleted', __CLASS__.'@whenPostWasDeleted');
- }
-
- public function whenPostWasPosted(PostWasPosted $event)
- {
- $this->replyBecameVisible($event->post);
- }
-
- public function whenPostWasRevised(PostWasRevised $event)
- {
- $this->replyBecameVisible($event->post);
- }
-
- public function whenPostWasHidden(PostWasHidden $event)
- {
- $this->replyBecameInvisible($event->post);
- }
-
- public function whenPostWasRestored(PostWasRestored $event)
- {
- $this->replyBecameVisible($event->post);
- }
-
- public function whenPostWasDeleted(PostWasDeleted $event)
- {
- $this->replyBecameInvisible($event->post);
- }
-
- protected function replyBecameVisible(Post $reply)
- {
- $matches = $this->parser->match($reply->content);
-
- $mentioned = $reply->discussion->posts()->with('user')->whereIn('number', array_filter($matches['number']))->get()->all();
-
- $this->sync($reply, $mentioned);
- }
-
- protected function replyBecameInvisible(Post $reply)
- {
- $this->sync($reply, []);
- }
-
- protected function sync(Post $reply, array $mentioned)
- {
- $reply->mentionsPosts()->sync(array_pluck($mentioned, 'id'));
-
- $mentioned = array_filter($mentioned, function ($post) use ($reply) {
- return $post->user->id !== $reply->user->id;
- });
-
- foreach ($mentioned as $post) {
- $this->activity->sync(new PostMentionedActivity($post, $reply), [$post->user]);
-
- $this->notifications->sync(new PostMentionedNotification($post, $reply), [$post->user]);
- }
- }
-}
diff --git a/extensions/mentions/src/Handlers/UserMentionsMetadataUpdater.php b/extensions/mentions/src/Handlers/UserMentionsMetadataUpdater.php
deleted file mode 100755
index 2d2aeab30..000000000
--- a/extensions/mentions/src/Handlers/UserMentionsMetadataUpdater.php
+++ /dev/null
@@ -1,92 +0,0 @@
-parser = $parser;
- $this->activity = $activity;
- $this->notifications = $notifications;
- }
-
- public function subscribe(Dispatcher $events)
- {
- $events->listen('Flarum\Core\Events\PostWasPosted', __CLASS__.'@whenPostWasPosted');
- $events->listen('Flarum\Core\Events\PostWasRevised', __CLASS__.'@whenPostWasRevised');
- $events->listen('Flarum\Core\Events\PostWasHidden', __CLASS__.'@whenPostWasHidden');
- $events->listen('Flarum\Core\Events\PostWasRestored', __CLASS__.'@whenPostWasRestored');
- $events->listen('Flarum\Core\Events\PostWasDeleted', __CLASS__.'@whenPostWasDeleted');
- }
-
- public function whenPostWasPosted(PostWasPosted $event)
- {
- $this->postBecameVisible($event->post);
- }
-
- public function whenPostWasRevised(PostWasRevised $event)
- {
- $this->postBecameVisible($event->post);
- }
-
- public function whenPostWasHidden(PostWasHidden $event)
- {
- $this->postBecameInvisible($event->post);
- }
-
- public function whenPostWasRestored(PostWasRestored $event)
- {
- $this->postBecameVisible($event->post);
- }
-
- public function whenPostWasDeleted(PostWasDeleted $event)
- {
- $this->postBecameInvisible($event->post);
- }
-
- protected function postBecameVisible(Post $post)
- {
- $matches = $this->parser->match($post->content);
-
- $mentioned = User::whereIn('username', array_filter($matches['username']))->get()->all();
-
- $this->sync($post, $mentioned);
- }
-
- protected function postBecameInvisible(Post $post)
- {
- $this->sync($post, []);
- }
-
- protected function sync(Post $post, array $mentioned)
- {
- $post->mentionsUsers()->sync(array_pluck($mentioned, 'id'));
-
- $mentioned = array_filter($mentioned, function ($user) use ($post) {
- return $user->id !== $post->user->id;
- });
-
- $this->activity->sync(new UserMentionedActivity($post), $mentioned);
-
- $this->notifications->sync(new UserMentionedNotification($post), $mentioned);
- }
-}
diff --git a/extensions/mentions/src/Listeners/AddApiRelationships.php b/extensions/mentions/src/Listeners/AddApiRelationships.php
new file mode 100755
index 000000000..c1878d882
--- /dev/null
+++ b/extensions/mentions/src/Listeners/AddApiRelationships.php
@@ -0,0 +1,55 @@
+listen(ApiRelationship::class, __CLASS__.'@addRelationships');
+ $events->listen(BuildApiAction::class, __CLASS__.'@includeRelationships');
+ }
+
+ public function addRelationships(ApiRelationship $event)
+ {
+ if ($event->serializer instanceof PostBasicSerializer) {
+ if ($event->relationship === 'mentionedBy') {
+ return $event->serializer->hasMany('Flarum\Api\Serializers\PostBasicSerializer', 'mentionedBy');
+ }
+
+ if ($event->relationship === 'mentionsPosts') {
+ return $event->serializer->hasMany('Flarum\Api\Serializers\PostBasicSerializer', 'mentionsPosts');
+ }
+
+ if ($event->relationship === 'mentionsUsers') {
+ return $event->serializer->hasMany('Flarum\Api\Serializers\PostBasicSerializer', 'mentionsUsers');
+ }
+ }
+ }
+
+ public function includeRelationships(BuildApiAction $event)
+ {
+ if ($event->action instanceof Discussions\ShowAction) {
+ $event->addInclude('posts.mentionedBy');
+ $event->addInclude('posts.mentionedBy.user');
+ $event->addLink('posts.mentionedBy.discussion');
+ }
+
+ if ($event->action instanceof Posts\ShowAction ||
+ $event->action instanceof Posts\IndexAction) {
+ $event->addInclude('mentionedBy');
+ $event->addInclude('mentionedBy.user');
+ $event->addLink('mentionedBy.discussion');
+ }
+
+ if ($event->action instanceof Posts\CreateAction) {
+ $event->addInclude('mentionsPosts');
+ $event->addInclude('mentionsPosts.mentionedBy');
+ }
+ }
+}
diff --git a/extensions/mentions/src/Listeners/AddClientAssets.php b/extensions/mentions/src/Listeners/AddClientAssets.php
new file mode 100755
index 000000000..200babf41
--- /dev/null
+++ b/extensions/mentions/src/Listeners/AddClientAssets.php
@@ -0,0 +1,39 @@
+listen(RegisterLocales::class, __CLASS__.'@addLocale');
+ $events->listen(BuildClientView::class, __CLASS__.'@addAssets');
+ }
+
+ public function addLocale(RegisterLocales $event)
+ {
+ $event->addTranslations('en', __DIR__.'/../../locale/en.yml');
+ }
+
+ public function addAssets(BuildClientView $event)
+ {
+ $event->forumAssets([
+ __DIR__.'/../../js/forum/dist/extension.js',
+ __DIR__.'/../../less/forum/extension.less'
+ ]);
+
+ $event->forumBootstrapper('mentions/main');
+
+ $event->forumTranslations([
+ 'mentions.reply_to_post',
+ 'mentions.post_mentioned_notification',
+ 'mentions.others',
+ 'mentions.user_mentioned_notification',
+ 'mentions.post_mentioned_by',
+ 'mentions.you',
+ 'mentions.reply_link'
+ ]);
+ }
+}
diff --git a/extensions/mentions/src/Listeners/AddModelRelationships.php b/extensions/mentions/src/Listeners/AddModelRelationships.php
new file mode 100755
index 000000000..2b1bbfa76
--- /dev/null
+++ b/extensions/mentions/src/Listeners/AddModelRelationships.php
@@ -0,0 +1,30 @@
+listen(ModelRelationship::class, __CLASS__.'@addRelationships');
+ }
+
+ public function addRelationships(ModelRelationship $event)
+ {
+ if ($event->model instanceof Post) {
+ if ($event->relationship === 'mentionedBy') {
+ return $event->model->belongsToMany(Post::class, 'mentions_posts', 'mentions_id', 'post_id', 'mentionedBy');
+ }
+
+ if ($event->relationship === 'mentionsPosts') {
+ return $event->model->belongsToMany(Post::class, 'mentions_posts', 'post_id', 'mentions_id', 'mentionsPosts');
+ }
+
+ if ($event->relationship === 'mentionsUsers') {
+ return $event->model->belongsToMany(User::class, 'mentions_users', 'post_id', 'mentions_id', 'mentionsUsers');
+ }
+ }
+ }
+}
diff --git a/extensions/mentions/src/Listeners/AddPostMentionsFormatter.php b/extensions/mentions/src/Listeners/AddPostMentionsFormatter.php
new file mode 100755
index 000000000..6d842b8e9
--- /dev/null
+++ b/extensions/mentions/src/Listeners/AddPostMentionsFormatter.php
@@ -0,0 +1,47 @@
+listen(FormatterConfigurator::class, __CLASS__.'@configure');
+ $events->listen(FormatterRenderer::class, __CLASS__.'@render');
+ }
+
+ public function configure(FormatterConfigurator $event)
+ {
+ $configurator = $event->configurator;
+
+ $tagName = 'POSTMENTION';
+
+ $tag = $configurator->tags->add($tagName);
+ $tag->attributes->add('username');
+ $tag->attributes->add('number');
+ $tag->attributes->add('id')->filterChain->append('#uint');
+ $tag->template = '';
+ $tag->filterChain->prepend([static::class, 'addId'])->addParameterByName('post');
+
+ $configurator->Preg->match('/\B@(?[a-z0-9_-]+)#(?\d+)/i', $tagName);
+ }
+
+ public function render(FormatterRenderer $event)
+ {
+ // TODO: use URL generator
+ $event->renderer->setParameter('DISCUSSION_URL', '/d/' . $event->post->discussion_id . '/-/');
+ }
+
+ public static function addId($tag, CommentPost $post)
+ {
+ $id = CommentPost::where('discussion_id', $post->discussion_id)
+ ->where('number', $tag->getAttribute('number'))
+ ->pluck('id');
+
+ $tag->setAttribute('id', $id);
+
+ return true;
+ }
+}
diff --git a/extensions/mentions/src/Listeners/AddUserMentionsFormatter.php b/extensions/mentions/src/Listeners/AddUserMentionsFormatter.php
new file mode 100755
index 000000000..f752f76cc
--- /dev/null
+++ b/extensions/mentions/src/Listeners/AddUserMentionsFormatter.php
@@ -0,0 +1,56 @@
+users = $users;
+ }
+
+ public function subscribe($events)
+ {
+ $events->listen(FormatterConfigurator::class, __CLASS__.'@configure');
+ $events->listen(FormatterParser::class, __CLASS__.'@parse');
+ $events->listen(FormatterRenderer::class, __CLASS__.'@render');
+ }
+
+ public function configure(FormatterConfigurator $event)
+ {
+ $configurator = $event->configurator;
+
+ $tagName = 'USERMENTION';
+
+ $tag = $configurator->tags->add($tagName);
+ $tag->attributes->add('username');
+ $tag->attributes->add('id')->filterChain->append('#uint');
+ $tag->template = '@';
+ $tag->filterChain->prepend([static::class, 'addId'])->addParameterByName('userRepository');
+
+ $configurator->Preg->match('/\B@(?[a-z0-9_-]+)(?!#)/i', $tagName);
+ }
+
+ public function parse(FormatterParser $event)
+ {
+ $event->parser->registeredVars['userRepository'] = $this->users;
+ }
+
+ public function render(FormatterRenderer $event)
+ {
+ // TODO: use URL generator
+ $event->renderer->setParameter('PROFILE_URL', '/u/');
+ }
+
+ public static function addId($tag, UserRepository $users)
+ {
+ $tag->setAttribute('id', $users->getIdForUsername($tag->getAttribute('username')));
+
+ return true;
+ }
+}
diff --git a/extensions/mentions/src/Listeners/UpdatePostMentionsMetadata.php b/extensions/mentions/src/Listeners/UpdatePostMentionsMetadata.php
new file mode 100755
index 000000000..8c9a2a5ca
--- /dev/null
+++ b/extensions/mentions/src/Listeners/UpdatePostMentionsMetadata.php
@@ -0,0 +1,97 @@
+notifications = $notifications;
+ }
+
+ public function subscribe(Dispatcher $events)
+ {
+ $events->listen(RegisterNotificationTypes::class, __CLASS__.'@registerNotificationType');
+
+ $events->listen(PostWasPosted::class, __CLASS__.'@whenPostWasPosted');
+ $events->listen(PostWasRevised::class, __CLASS__.'@whenPostWasRevised');
+ $events->listen(PostWasHidden::class, __CLASS__.'@whenPostWasHidden');
+ $events->listen(PostWasRestored::class, __CLASS__.'@whenPostWasRestored');
+ $events->listen(PostWasDeleted::class, __CLASS__.'@whenPostWasDeleted');
+ }
+
+ public function registerNotificationType(RegisterNotificationTypes $event)
+ {
+ $event->register(
+ PostMentionedBlueprint::class,
+ 'Flarum\Api\Serializers\PostBasicSerializer',
+ ['alert']
+ );
+ }
+
+ public function whenPostWasPosted(PostWasPosted $event)
+ {
+ $this->replyBecameVisible($event->post);
+ }
+
+ public function whenPostWasRevised(PostWasRevised $event)
+ {
+ $this->replyBecameVisible($event->post);
+ }
+
+ public function whenPostWasHidden(PostWasHidden $event)
+ {
+ $this->replyBecameInvisible($event->post);
+ }
+
+ public function whenPostWasRestored(PostWasRestored $event)
+ {
+ $this->replyBecameVisible($event->post);
+ }
+
+ public function whenPostWasDeleted(PostWasDeleted $event)
+ {
+ $this->replyBecameInvisible($event->post);
+ }
+
+ protected function replyBecameVisible(Post $reply)
+ {
+ $mentioned = Utils::getAttributeValues($reply->parsedContent, 'POSTMENTION', 'id');
+
+ $this->sync($reply, $mentioned);
+ }
+
+ protected function replyBecameInvisible(Post $reply)
+ {
+ $this->sync($reply, []);
+ }
+
+ protected function sync(Post $reply, array $mentioned)
+ {
+ $reply->mentionsPosts()->sync($mentioned);
+
+ $posts = Post::with('user')
+ ->whereIn('id', $mentioned)
+ ->get()
+ ->filter(function ($post) use ($reply) {
+ return $post->user->id !== $reply->user->id;
+ })
+ ->all();
+
+ foreach ($posts as $post) {
+ $this->notifications->sync(new PostMentionedBlueprint($post, $reply), [$post->user]);
+ }
+ }
+}
diff --git a/extensions/mentions/src/Listeners/UpdateUserMentionsMetadata.php b/extensions/mentions/src/Listeners/UpdateUserMentionsMetadata.php
new file mode 100755
index 000000000..233799d9b
--- /dev/null
+++ b/extensions/mentions/src/Listeners/UpdateUserMentionsMetadata.php
@@ -0,0 +1,95 @@
+notifications = $notifications;
+ }
+
+ public function subscribe(Dispatcher $events)
+ {
+ $events->listen(RegisterNotificationTypes::class, __CLASS__.'@registerNotificationType');
+
+ $events->listen(PostWasPosted::class, __CLASS__.'@whenPostWasPosted');
+ $events->listen(PostWasRevised::class, __CLASS__.'@whenPostWasRevised');
+ $events->listen(PostWasHidden::class, __CLASS__.'@whenPostWasHidden');
+ $events->listen(PostWasRestored::class, __CLASS__.'@whenPostWasRestored');
+ $events->listen(PostWasDeleted::class, __CLASS__.'@whenPostWasDeleted');
+ }
+
+ public function registerNotificationType(RegisterNotificationTypes $event)
+ {
+ $event->register(
+ UserMentionedBlueprint::class,
+ 'Flarum\Api\Serializers\PostBasicSerializer',
+ ['alert']
+ );
+ }
+
+ public function whenPostWasPosted(PostWasPosted $event)
+ {
+ $this->postBecameVisible($event->post);
+ }
+
+ public function whenPostWasRevised(PostWasRevised $event)
+ {
+ $this->postBecameVisible($event->post);
+ }
+
+ public function whenPostWasHidden(PostWasHidden $event)
+ {
+ $this->postBecameInvisible($event->post);
+ }
+
+ public function whenPostWasRestored(PostWasRestored $event)
+ {
+ $this->postBecameVisible($event->post);
+ }
+
+ public function whenPostWasDeleted(PostWasDeleted $event)
+ {
+ $this->postBecameInvisible($event->post);
+ }
+
+ protected function postBecameVisible(Post $post)
+ {
+ $mentioned = Utils::getAttributeValues($post->parsedContent, 'USERMENTION', 'id');
+
+ $this->sync($post, $mentioned);
+ }
+
+ protected function postBecameInvisible(Post $post)
+ {
+ $this->sync($post, []);
+ }
+
+ protected function sync(Post $post, array $mentioned)
+ {
+ $post->mentionsUsers()->sync($mentioned);
+
+ $users = User::whereIn('id', $mentioned)
+ ->get()
+ ->filter(function ($user) use ($post) {
+ return $user->id !== $post->user->id;
+ })
+ ->all();
+
+ $this->notifications->sync(new UserMentionedBlueprint($post), $users);
+ }
+}
diff --git a/extensions/mentions/src/MentionsParserAbstract.php b/extensions/mentions/src/MentionsParserAbstract.php
deleted file mode 100644
index e3b763df3..000000000
--- a/extensions/mentions/src/MentionsParserAbstract.php
+++ /dev/null
@@ -1,20 +0,0 @@
-pattern, $string, $matches);
-
- return $matches;
- }
-
- public function replace($string, $callback)
- {
- return preg_replace_callback($this->pattern, function ($matches) use ($callback) {
- return $callback($matches);
- }, $string);
- }
-}
diff --git a/extensions/mentions/src/MentionsServiceProvider.php b/extensions/mentions/src/MentionsServiceProvider.php
deleted file mode 100644
index b94df2739..000000000
--- a/extensions/mentions/src/MentionsServiceProvider.php
+++ /dev/null
@@ -1,69 +0,0 @@
-loadViewsFrom(__DIR__.'/../views', 'mentions');
-
- $this->extend(
- new Extend\EventSubscriber([
- 'Flarum\Mentions\Handlers\PostMentionsMetadataUpdater',
- 'Flarum\Mentions\Handlers\UserMentionsMetadataUpdater'
- ]),
-
- (new Extend\ForumClient())
- ->assets([
- __DIR__.'/../js/dist/extension.js',
- __DIR__.'/../less/mentions.less'
- ]),
-
- (new Extend\Model('Flarum\Core\Models\Post'))
- ->belongsToMany('mentionedBy', 'Flarum\Core\Models\Post', 'mentions_posts', 'mentions_id')
- ->belongsToMany('mentionsPosts', 'Flarum\Core\Models\Post', 'mentions_posts', 'post_id', 'mentions_id')
- ->belongsToMany('mentionsUsers', 'Flarum\Core\Models\User', 'mentions_users', 'post_id', 'mentions_id'),
-
- (new Extend\ApiSerializer('Flarum\Api\Serializers\PostSerializer'))
- ->hasMany('mentionedBy', 'Flarum\Api\Serializers\PostBasicSerializer')
- ->hasMany('mentionsPosts', 'Flarum\Api\Serializers\PostBasicSerializer')
- ->hasMany('mentionsUsers', 'Flarum\Api\Serializers\UserBasicSerializer'),
-
- (new Extend\ApiAction('Flarum\Api\Actions\Discussions\ShowAction'))
- ->addInclude('posts.mentionedBy')
- ->addInclude('posts.mentionedBy.user')
- ->addLink('posts.mentionedBy.discussion')
- ->addInclude('posts.mentionsPosts', false)
- ->addInclude('posts.mentionsPosts.user', false)
- ->addInclude('posts.mentionsUsers', false),
-
- (new Extend\ApiAction([
- 'Flarum\Api\Actions\Posts\IndexAction',
- 'Flarum\Api\Actions\Posts\ShowAction',
- ]))
- ->addInclude('mentionedBy')
- ->addInclude('mentionedBy.user')
- ->addLink('mentionedBy.discussion'),
-
- (new Extend\ApiAction('Flarum\Api\Actions\Posts\CreateAction'))
- ->addInclude('mentionsPosts')
- ->addInclude('mentionsPosts.mentionedBy'),
-
- new Extend\Formatter('postMentions', 'Flarum\Mentions\PostMentionsFormatter'),
-
- new Extend\Formatter('userMentions', 'Flarum\Mentions\UserMentionsFormatter'),
-
- new Extend\ActivityType('Flarum\Mentions\PostMentionedActivity', 'Flarum\Api\Serializers\PostBasicSerializer'),
-
- new Extend\ActivityType('Flarum\Mentions\UserMentionedActivity', 'Flarum\Api\Serializers\PostBasicSerializer'),
-
- (new Extend\NotificationType('Flarum\Mentions\PostMentionedNotification', 'Flarum\Api\Serializers\PostBasicSerializer'))
- ->enableByDefault('alert'),
-
- (new Extend\NotificationType('Flarum\Mentions\UserMentionedNotification', 'Flarum\Api\Serializers\PostBasicSerializer'))
- ->enableByDefault('alert')
- );
- }
-}
diff --git a/extensions/mentions/src/PostMentionedNotification.php b/extensions/mentions/src/Notifications/PostMentionedBlueprint.php
similarity index 69%
rename from extensions/mentions/src/PostMentionedNotification.php
rename to extensions/mentions/src/Notifications/PostMentionedBlueprint.php
index da6c7b256..1f9f84653 100644
--- a/extensions/mentions/src/PostMentionedNotification.php
+++ b/extensions/mentions/src/Notifications/PostMentionedBlueprint.php
@@ -1,9 +1,10 @@
- $this->reply->number];
+ return ['replyNumber' => (int) $this->reply->number];
}
public function getEmailView()
@@ -47,11 +48,6 @@ class PostMentionedNotification extends NotificationAbstract
public static function getSubjectModel()
{
- return 'Flarum\Core\Models\Post';
- }
-
- public static function isEmailable()
- {
- return true;
+ return Post::class;
}
}
diff --git a/extensions/mentions/src/UserMentionedNotification.php b/extensions/mentions/src/Notifications/UserMentionedBlueprint.php
similarity index 66%
rename from extensions/mentions/src/UserMentionedNotification.php
rename to extensions/mentions/src/Notifications/UserMentionedBlueprint.php
index 7a3592d11..c86f1dcfe 100644
--- a/extensions/mentions/src/UserMentionedNotification.php
+++ b/extensions/mentions/src/Notifications/UserMentionedBlueprint.php
@@ -1,10 +1,11 @@
-post->user;
}
+ public function getData()
+ {
+ return null;
+ }
+
public function getEmailView()
{
return ['text' => 'mentions::emails.userMentioned'];
@@ -40,11 +46,6 @@ class UserMentionedNotification extends NotificationAbstract
public static function getSubjectModel()
{
- return 'Flarum\Core\Models\Post';
- }
-
- public static function isEmailable()
- {
- return true;
+ return Post::class;
}
}
diff --git a/extensions/mentions/src/PostMentionedActivity.php b/extensions/mentions/src/PostMentionedActivity.php
deleted file mode 100644
index 3da847c9f..000000000
--- a/extensions/mentions/src/PostMentionedActivity.php
+++ /dev/null
@@ -1,37 +0,0 @@
-post = $post;
- $this->reply = $reply;
- }
-
- public function getSubject()
- {
- return $this->reply;
- }
-
- public function getTime()
- {
- return $this->reply->time;
- }
-
- public static function getType()
- {
- return 'postMentioned';
- }
-
- public static function getSubjectModel()
- {
- return 'Flarum\Core\Models\Post';
- }
-}
diff --git a/extensions/mentions/src/PostMentionsFormatter.php b/extensions/mentions/src/PostMentionsFormatter.php
deleted file mode 100644
index 4e91013ef..000000000
--- a/extensions/mentions/src/PostMentionsFormatter.php
+++ /dev/null
@@ -1,28 +0,0 @@
-parser = $parser;
- }
-
- public function afterPurification($text, Post $post = null)
- {
- if ($post) {
- $text = $this->ignoreTags($text, ['a', 'code', 'pre'], function ($text) use ($post) {
- return $this->parser->replace($text, function ($match) use ($post) {
- // TODO: use URL generator
- return ''.$match['username'].'';
- }, $text);
- });
- }
-
- return $text;
- }
-}
diff --git a/extensions/mentions/src/PostMentionsParser.php b/extensions/mentions/src/PostMentionsParser.php
deleted file mode 100644
index fb2b73e8a..000000000
--- a/extensions/mentions/src/PostMentionsParser.php
+++ /dev/null
@@ -1,6 +0,0 @@
-[a-z0-9_-]+)#(?P\d+)/i';
-}
diff --git a/extensions/mentions/src/UserMentionedActivity.php b/extensions/mentions/src/UserMentionedActivity.php
deleted file mode 100644
index 37c6b958b..000000000
--- a/extensions/mentions/src/UserMentionedActivity.php
+++ /dev/null
@@ -1,34 +0,0 @@
-post = $post;
- }
-
- public function getSubject()
- {
- return $this->post;
- }
-
- public function getTime()
- {
- return $this->post->time;
- }
-
- public static function getType()
- {
- return 'userMentioned';
- }
-
- public static function getSubjectModel()
- {
- return 'Flarum\Core\Models\Post';
- }
-}
diff --git a/extensions/mentions/src/UserMentionsFormatter.php b/extensions/mentions/src/UserMentionsFormatter.php
deleted file mode 100644
index 0a8a3ea0c..000000000
--- a/extensions/mentions/src/UserMentionsFormatter.php
+++ /dev/null
@@ -1,26 +0,0 @@
-parser = $parser;
- }
-
- public function afterPurification($text, Post $post = null)
- {
- $text = $this->ignoreTags($text, ['a', 'code', 'pre'], function ($text) {
- return $this->parser->replace($text, function ($match) {
- // TODO: use URL generator
- return ''.$match['username'].'';
- }, $text);
- });
-
- return $text;
- }
-}
diff --git a/extensions/mentions/src/UserMentionsParser.php b/extensions/mentions/src/UserMentionsParser.php
deleted file mode 100644
index 51c463162..000000000
--- a/extensions/mentions/src/UserMentionsParser.php
+++ /dev/null
@@ -1,6 +0,0 @@
-[a-z0-9_-]+)(?!#)/i';
-}