Update to sub in "display names" from database when rendering

This also improves the behaviour of mentions in the JS preview (no more
broken links, mention is only picked up if corresponding user/post is
found).

See flarum/core#1246

Closes flarum/core#315
This commit is contained in:
Toby Zerner 2017-09-19 20:16:30 +09:30
parent acd3195491
commit 408d882598
7 changed files with 164 additions and 62 deletions

View File

@ -179,14 +179,12 @@ System.register('flarum/mentions/addComposerAutocomplete', ['flarum/extend', 'fl
if (this.selectionEnd - cursor > 0) return; if (this.selectionEnd - cursor > 0) return;
// Search backwards from the cursor for an '@' symbol, without any // Search backwards from the cursor for an '@' symbol. If we find one,
// intervening whitespace. If we find one, we will want to show the // we will want to show the autocomplete dropdown!
// autocomplete dropdown!
var value = this.value; var value = this.value;
mentionStart = 0; mentionStart = 0;
for (var i = cursor - 1; i >= 0; i--) { for (var i = cursor - 1; i >= cursor - 30; i--) {
var character = value.substr(i, 1); var character = value.substr(i, 1);
if (/\s/.test(character)) break;
if (character === '@') { if (character === '@') {
mentionStart = i + 1; mentionStart = i + 1;
break; break;
@ -228,6 +226,14 @@ System.register('flarum/mentions/addComposerAutocomplete', ['flarum/extend', 'fl
); );
}; };
var userMatches = function userMatches(user) {
var names = [user.username(), user.displayName()];
return names.some(function (value) {
return value.toLowerCase().substr(0, typed.length) === typed;
});
};
var buildSuggestions = function buildSuggestions() { var buildSuggestions = function buildSuggestions() {
var suggestions = []; var suggestions = [];
@ -235,7 +241,7 @@ System.register('flarum/mentions/addComposerAutocomplete', ['flarum/extend', 'fl
// matching that username. // matching that username.
if (typed) { if (typed) {
app.store.all('users').forEach(function (user) { app.store.all('users').forEach(function (user) {
if (user.username().toLowerCase().substr(0, typed.length) !== typed) return; if (!userMatches(user)) return;
suggestions.push(makeSuggestion(user, '@' + user.username(), '', 'MentionsDropdown-user')); suggestions.push(makeSuggestion(user, '@' + user.username(), '', 'MentionsDropdown-user'));
}); });
@ -254,7 +260,7 @@ System.register('flarum/mentions/addComposerAutocomplete', ['flarum/extend', 'fl
return b.time() - a.time(); return b.time() - a.time();
}).filter(function (post) { }).filter(function (post) {
var user = post.user(); var user = post.user();
return user && user.username().toLowerCase().substr(0, typed.length) === typed; return user && userMatches(user);
}).splice(0, 5).forEach(function (post) { }).splice(0, 5).forEach(function (post) {
var user = post.user(); var user = post.user();
suggestions.push(makeSuggestion(user, '@' + user.username() + '#' + post.id(), [app.translator.trans('flarum-mentions.forum.composer.reply_to_post_text', { number: post.number() }), ' — ', truncate(post.contentPlain(), 200)], 'MentionsDropdown-post')); suggestions.push(makeSuggestion(user, '@' + user.username() + '#' + post.id(), [app.translator.trans('flarum-mentions.forum.composer.reply_to_post_text', { number: post.number() }), ' — ', truncate(post.contentPlain(), 200)], 'MentionsDropdown-post'));
@ -279,14 +285,18 @@ System.register('flarum/mentions/addComposerAutocomplete', ['flarum/extend', 'fl
left = parent.width() - width; left = parent.width() - width;
} }
dropdown.show(left, top); dropdown.show(left, top);
} else {
dropdown.active = false;
dropdown.hide();
} }
}; };
dropdown.active = true;
buildSuggestions(); buildSuggestions();
dropdown.setIndex(0); dropdown.setIndex(0);
dropdown.$().scrollTop(0); dropdown.$().scrollTop(0);
dropdown.active = true;
clearTimeout(searchTimeout); clearTimeout(searchTimeout);
if (typed) { if (typed) {
@ -1166,6 +1176,7 @@ System.register('flarum/mentions/main', ['flarum/extend', 'flarum/app', 'flarum/
}), 80); }), 80);
}); });
// Remove post mentions when rendering post previews.
getPlainContent.removeSelectors.push('a.PostMention'); getPlainContent.removeSelectors.push('a.PostMention');
}); });
} }
@ -1258,4 +1269,46 @@ System.register('flarum/mentions/utils/selectedText', [], function (_export, _co
setters: [], setters: [],
execute: function () {} execute: function () {}
}; };
});;
'use strict';
System.register('flarum/mentions/utils/textFormatter', ['flarum/helpers/username', 'flarum/utils/extractText'], function (_export, _context) {
"use strict";
var username, extractText;
function filterUserMentions(tag) {
var user = app.store.getBy('users', 'username', tag.getAttribute('username'));
if (user) {
tag.setAttribute('id', user.id());
tag.setAttribute('displayname', extractText(username(user)));
return true;
}
}
_export('filterUserMentions', filterUserMentions);
function filterPostMentions(tag) {
var post = app.store.getById('posts', tag.getAttribute('id'));
if (post) {
tag.setAttribute('discussionid', post.discussion().id());
tag.setAttribute('number', post.number());
tag.setAttribute('displayname', extractText(username(post.user())));
return true;
}
}
_export('filterPostMentions', filterPostMentions);
return {
setters: [function (_flarumHelpersUsername) {
username = _flarumHelpersUsername.default;
}, function (_flarumUtilsExtractText) {
extractText = _flarumUtilsExtractText.default;
}],
execute: function () {}
};
}); });

View File

@ -54,14 +54,12 @@ export default function addComposerAutocomplete() {
if (this.selectionEnd - cursor > 0) return; if (this.selectionEnd - cursor > 0) return;
// Search backwards from the cursor for an '@' symbol, without any // Search backwards from the cursor for an '@' symbol. If we find one,
// intervening whitespace. If we find one, we will want to show the // we will want to show the autocomplete dropdown!
// autocomplete dropdown!
const value = this.value; const value = this.value;
mentionStart = 0; mentionStart = 0;
for (let i = cursor - 1; i >= 0; i--) { for (let i = cursor - 1; i >= cursor - 30; i--) {
const character = value.substr(i, 1); const character = value.substr(i, 1);
if (/\s/.test(character)) break;
if (character === '@') { if (character === '@') {
mentionStart = i + 1; mentionStart = i + 1;
break; break;
@ -95,6 +93,15 @@ export default function addComposerAutocomplete() {
); );
}; };
const userMatches = function(user) {
const names = [
user.username(),
user.displayName()
];
return names.some(value => value.toLowerCase().substr(0, typed.length) === typed);
};
const buildSuggestions = () => { const buildSuggestions = () => {
const suggestions = []; const suggestions = [];
@ -102,7 +109,7 @@ export default function addComposerAutocomplete() {
// matching that username. // matching that username.
if (typed) { if (typed) {
app.store.all('users').forEach(user => { app.store.all('users').forEach(user => {
if (user.username().toLowerCase().substr(0, typed.length) !== typed) return; if (!userMatches(user)) return;
suggestions.push( suggestions.push(
makeSuggestion(user, '@' + user.username(), '', 'MentionsDropdown-user') makeSuggestion(user, '@' + user.username(), '', 'MentionsDropdown-user')
@ -122,7 +129,7 @@ export default function addComposerAutocomplete() {
.sort((a, b) => b.time() - a.time()) .sort((a, b) => b.time() - a.time())
.filter(post => { .filter(post => {
const user = post.user(); const user = post.user();
return user && user.username().toLowerCase().substr(0, typed.length) === typed; return user && userMatches(user);
}) })
.splice(0, 5) .splice(0, 5)
.forEach(post => { .forEach(post => {

View File

@ -67,5 +67,6 @@ app.initializers.add('flarum-mentions', function() {
); );
}); });
// Remove post mentions when rendering post previews.
getPlainContent.removeSelectors.push('a.PostMention'); getPlainContent.removeSelectors.push('a.PostMention');
}); });

View File

@ -0,0 +1,25 @@
import username from 'flarum/helpers/username';
import extractText from 'flarum/utils/extractText';
export function filterUserMentions(tag) {
const user = app.store.getBy('users', 'username', tag.getAttribute('username'));
if (user) {
tag.setAttribute('id', user.id());
tag.setAttribute('displayname', extractText(username(user)));
return true;
}
}
export function filterPostMentions(tag) {
const post = app.store.getById('posts', tag.getAttribute('id'));
if (post) {
tag.setAttribute('discussionid', post.discussion().id());
tag.setAttribute('number', post.number());
tag.setAttribute('displayname', extractText(username(post.user())));
return true;
}
}

View File

@ -11,12 +11,10 @@
namespace Flarum\Mentions\Listener; namespace Flarum\Mentions\Listener;
use Flarum\Api\Controller\CreatePostController; use Flarum\Api\Controller;
use Flarum\Api\Controller\ListPostsController;
use Flarum\Api\Controller\ShowDiscussionController;
use Flarum\Api\Controller\ShowPostController;
use Flarum\Api\Serializer\PostBasicSerializer; use Flarum\Api\Serializer\PostBasicSerializer;
use Flarum\Core\Post; use Flarum\Core\Post;
use Flarum\Core\Post\CommentPost;
use Flarum\Core\Repository\PostRepository; use Flarum\Core\Repository\PostRepository;
use Flarum\Core\User; use Flarum\Core\User;
use Flarum\Event\ConfigureApiController; use Flarum\Event\ConfigureApiController;
@ -24,6 +22,7 @@ use Flarum\Event\GetApiRelationship;
use Flarum\Event\GetModelRelationship; use Flarum\Event\GetModelRelationship;
use Flarum\Event\PrepareApiData; use Flarum\Event\PrepareApiData;
use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Eloquent\Collection;
class AddPostMentionedByRelationship class AddPostMentionedByRelationship
{ {
@ -94,7 +93,7 @@ class AddPostMentionedByRelationship
*/ */
public function includeRelationships(ConfigureApiController $event) public function includeRelationships(ConfigureApiController $event)
{ {
if ($event->isController(ShowDiscussionController::class)) { if ($event->isController(Controller\ShowDiscussionController::class)) {
$event->addInclude([ $event->addInclude([
'posts.mentionedBy', 'posts.mentionedBy',
'posts.mentionedBy.user', 'posts.mentionedBy.user',
@ -102,8 +101,8 @@ class AddPostMentionedByRelationship
]); ]);
} }
if ($event->isController(ShowPostController::class) if ($event->isController(Controller\ShowPostController::class)
|| $event->isController(ListPostsController::class)) { || $event->isController(Controller\ListPostsController::class)) {
$event->addInclude([ $event->addInclude([
'mentionedBy', 'mentionedBy',
'mentionedBy.user', 'mentionedBy.user',
@ -111,7 +110,7 @@ class AddPostMentionedByRelationship
]); ]);
} }
if ($event->isController(CreatePostController::class)) { if ($event->isController(Controller\CreatePostController::class)) {
$event->addInclude([ $event->addInclude([
'mentionsPosts', 'mentionsPosts',
'mentionsPosts.mentionedBy' 'mentionsPosts.mentionedBy'
@ -133,22 +132,33 @@ class AddPostMentionedByRelationship
{ {
// Firstly we gather a list of posts contained within the API document. // Firstly we gather a list of posts contained within the API document.
// This will vary according to the API endpoint that is being accessed. // This will vary according to the API endpoint that is being accessed.
if ($event->isController(ShowDiscussionController::class)) { if ($event->isController(Controller\ShowDiscussionController::class)) {
$posts = $event->data->posts; $posts = $event->data->posts;
} elseif ($event->isController(ShowPostController::class)) { } elseif ($event->isController(Controller\ShowPostController::class)
|| $event->isController(Controller\CreatePostController::class)
|| $event->isController(Controller\UpdatePostController::class)) {
$posts = [$event->data]; $posts = [$event->data];
} elseif ($event->isController(ListPostsController::class)) { } elseif ($event->isController(Controller\ListPostsController::class)) {
$posts = $event->data; $posts = $event->data;
} }
if (isset($posts)) { if (isset($posts)) {
$posts = array_filter((array) $posts, 'is_object'); $posts = new Collection($posts);
$posts = $posts->filter(function ($post) {
return $post instanceof CommentPost;
});
// Load all of the users that these posts mention. This way the data
// will be ready to go when we need to sub in current usernames
// during the rendering process.
$posts->load(['mentionsUsers', 'mentionsPosts.user']);
// Construct a list of the IDs of all of the posts that these posts
// have been mentioned in. We can then filter this list of IDs to
// weed out all of the ones which the user is not meant to see.
$ids = []; $ids = [];
// Once we have the posts, construct a list of the IDs of all of
// the posts that they have been mentioned in. We can then filter
// this list of IDs to weed out all of the ones which the user is
// not meant to see.
foreach ($posts as $post) { foreach ($posts as $post) {
$ids = array_merge($ids, $post->mentionedBy->pluck('id')->all()); $ids = array_merge($ids, $post->mentionedBy->pluck('id')->all());
} }

View File

@ -16,6 +16,7 @@ use Flarum\Event\ConfigureFormatter;
use Flarum\Event\ConfigureFormatterRenderer; use Flarum\Event\ConfigureFormatterRenderer;
use Flarum\Forum\UrlGenerator; use Flarum\Forum\UrlGenerator;
use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Events\Dispatcher;
use s9e\TextFormatter\Utils;
class FormatPostMentions class FormatPostMentions
{ {
@ -48,22 +49,23 @@ class FormatPostMentions
{ {
$configurator = $event->configurator; $configurator = $event->configurator;
$configurator->rendering->parameters['DISCUSSION_URL'] = $this->url->toRoute('discussion', ['id' => '']);
$tagName = 'POSTMENTION'; $tagName = 'POSTMENTION';
$tag = $configurator->tags->add($tagName); $tag = $configurator->tags->add($tagName);
$tag->attributes->add('username'); $tag->attributes->add('username');
$tag->attributes->add('displayname');
$tag->attributes->add('number')->filterChain->append('#uint'); $tag->attributes->add('number')->filterChain->append('#uint');
$tag->attributes->add('discussionid')->filterChain->append('#uint'); $tag->attributes->add('discussionid')->filterChain->append('#uint');
$tag->attributes->add('id')->filterChain->append('#uint'); $tag->attributes->add('id')->filterChain->append('#uint');
$tag->attributes['number']->required = false;
$tag->attributes['discussionid']->required = false;
$tag->template = '<a href="{$DISCUSSION_URL}{@discussionid}/{@number}" class="PostMention" data-id="{@id}"><xsl:value-of select="@username"/></a>'; $tag->template = '<a href="{$DISCUSSION_URL}{@discussionid}/{@number}" class="PostMention" data-id="{@id}"><xsl:value-of select="@displayname"/></a>';
$tag->filterChain $tag->filterChain
->prepend([static::class, 'addId']) ->prepend([static::class, 'addId'])
->setJS('function() { return true; }'); ->setJS('function(tag) { return System.get("flarum/mentions/utils/textFormatter").filterPostMentions(tag); }');
$configurator->Preg->match('/\B@(?<username>[a-z0-9_-]+)#(?<id>\d+)/i', $tagName); $configurator->Preg->match('/\B@(?<username>[a-z0-9_-]+)#(?<id>\d+)/i', $tagName);
} }
@ -73,7 +75,15 @@ class FormatPostMentions
*/ */
public function render(ConfigureFormatterRenderer $event) public function render(ConfigureFormatterRenderer $event)
{ {
$event->renderer->setParameter('DISCUSSION_URL', $this->url->toRoute('discussion', ['id' => ''])); $post = $event->context;
$event->xml = Utils::replaceAttributes($event->xml, 'POSTMENTION', function ($attributes) use ($post) {
$post = $post->mentionsPosts->find($attributes['id']);
if ($post && $post->user) {
$attributes['displayname'] = $post->user->display_name;
}
return $attributes;
});
} }
/** /**
@ -87,6 +97,7 @@ class FormatPostMentions
if ($post) { if ($post) {
$tag->setAttribute('discussionid', (int) $post->discussion_id); $tag->setAttribute('discussionid', (int) $post->discussion_id);
$tag->setAttribute('number', (int) $post->number); $tag->setAttribute('number', (int) $post->number);
$tag->setAttribute('displayname', $post->user->display_name);
return true; return true;
} }

View File

@ -11,32 +11,25 @@
namespace Flarum\Mentions\Listener; namespace Flarum\Mentions\Listener;
use Flarum\Core\Repository\UserRepository; use Flarum\Core\User;
use Flarum\Event\ConfigureFormatter; use Flarum\Event\ConfigureFormatter;
use Flarum\Event\ConfigureFormatterParser;
use Flarum\Event\ConfigureFormatterRenderer; use Flarum\Event\ConfigureFormatterRenderer;
use Flarum\Forum\UrlGenerator; use Flarum\Forum\UrlGenerator;
use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Events\Dispatcher;
use s9e\TextFormatter\Utils;
class FormatUserMentions class FormatUserMentions
{ {
/**
* @var UserRepository
*/
protected $users;
/** /**
* @var UrlGenerator * @var UrlGenerator
*/ */
protected $url; protected $url;
/** /**
* @param UserRepository $users
* @param UrlGenerator $url * @param UrlGenerator $url
*/ */
public function __construct(UserRepository $users, UrlGenerator $url) public function __construct(UrlGenerator $url)
{ {
$this->users = $users;
$this->url = $url; $this->url = $url;
} }
@ -46,7 +39,6 @@ class FormatUserMentions
public function subscribe(Dispatcher $events) public function subscribe(Dispatcher $events)
{ {
$events->listen(ConfigureFormatter::class, [$this, 'configure']); $events->listen(ConfigureFormatter::class, [$this, 'configure']);
$events->listen(ConfigureFormatterParser::class, [$this, 'parse']);
$events->listen(ConfigureFormatterRenderer::class, [$this, 'render']); $events->listen(ConfigureFormatterRenderer::class, [$this, 'render']);
} }
@ -57,35 +49,37 @@ class FormatUserMentions
{ {
$configurator = $event->configurator; $configurator = $event->configurator;
$configurator->rendering->parameters['PROFILE_URL'] = $this->url->toRoute('user', ['username' => '']);
$tagName = 'USERMENTION'; $tagName = 'USERMENTION';
$tag = $configurator->tags->add($tagName); $tag = $configurator->tags->add($tagName);
$tag->attributes->add('username'); $tag->attributes->add('username');
$tag->attributes->add('displayname');
$tag->attributes->add('id')->filterChain->append('#uint'); $tag->attributes->add('id')->filterChain->append('#uint');
$tag->attributes['id']->required = false;
$tag->template = '<a href="{$PROFILE_URL}{@username}" class="UserMention">@<xsl:value-of select="@username"/></a>'; $tag->template = '<a href="{$PROFILE_URL}{@username}" class="UserMention">@<xsl:value-of select="@displayname"/></a>';
$tag->filterChain->prepend([static::class, 'addId']) $tag->filterChain->prepend([static::class, 'addId'])
->addParameterByName('userRepository') ->addParameterByName('userRepository')
->setJS('function() { return true; }'); ->setJS('function(tag) { return System.get("flarum/mentions/utils/textFormatter").filterUserMentions(tag); }');
$configurator->Preg->match('/\B@(?<username>[a-z0-9_-]+)(?!#)/i', $tagName); $configurator->Preg->match('/\B@(?<username>[a-z0-9_-]+)(?!#)/i', $tagName);
} }
/**
* @param ConfigureFormatterParser $event
*/
public function parse(ConfigureFormatterParser $event)
{
$event->parser->registeredVars['userRepository'] = $this->users;
}
/** /**
* @param ConfigureFormatterRenderer $event * @param ConfigureFormatterRenderer $event
*/ */
public function render(ConfigureFormatterRenderer $event) public function render(ConfigureFormatterRenderer $event)
{ {
$event->renderer->setParameter('PROFILE_URL', $this->url->toRoute('user', ['username' => ''])); $post = $event->context;
$event->xml = Utils::replaceAttributes($event->xml, 'USERMENTION', function ($attributes) use ($post) {
$user = $post->mentionsUsers->find($attributes['id']);
if ($user) {
$attributes['displayname'] = $user->display_name;
}
return $attributes;
});
} }
/** /**
@ -93,10 +87,11 @@ class FormatUserMentions
* @param UserRepository $users * @param UserRepository $users
* @return bool * @return bool
*/ */
public static function addId($tag, UserRepository $users) public static function addId($tag)
{ {
if ($id = $users->getIdForUsername($tag->getAttribute('username'))) { if ($user = User::where('username', 'like', $tag->getAttribute('username'))->first()) {
$tag->setAttribute('id', $id); $tag->setAttribute('id', $user->id);
$tag->setAttribute('displayname', $user->display_name);
return true; return true;
} }