diff --git a/js/forum/dist/app.js b/js/forum/dist/app.js index f3bccdee24..295c71f49b 100644 --- a/js/forum/dist/app.js +++ b/js/forum/dist/app.js @@ -19740,10 +19740,10 @@ System.register('flarum/components/CommentPost', ['flarum/components/Post', 'fla });; 'use strict'; -System.register('flarum/components/Composer', ['flarum/Component', 'flarum/utils/ItemList', 'flarum/components/ComposerButton', 'flarum/helpers/listItems', 'flarum/utils/classList', 'flarum/utils/computed'], function (_export, _context) { +System.register('flarum/components/Composer', ['flarum/Component', 'flarum/utils/ItemList', 'flarum/components/ComposerButton', 'flarum/helpers/listItems', 'flarum/utils/classList'], function (_export, _context) { "use strict"; - var Component, ItemList, ComposerButton, listItems, classList, computed, Composer; + var Component, ItemList, ComposerButton, listItems, classList, Composer; return { setters: [function (_flarumComponent) { Component = _flarumComponent.default; @@ -19755,8 +19755,6 @@ System.register('flarum/components/Composer', ['flarum/Component', 'flarum/utils listItems = _flarumHelpersListItems.default; }, function (_flarumUtilsClassList) { classList = _flarumUtilsClassList.default; - }, function (_flarumUtilsComputed) { - computed = _flarumUtilsComputed.default; }], execute: function () { Composer = function (_Component) { @@ -19791,28 +19789,6 @@ System.register('flarum/components/Composer', ['flarum/Component', 'flarum/utils * @type {Boolean} */ this.active = false; - - /** - * Computed the composer's current height, based on the intended height, and - * the composer's current state. This will be applied to the composer's - * content's DOM element. - * - * @return {Integer} - */ - this.computedHeight = computed('height', 'position', function (height, position) { - // If the composer is minimized, then we don't want to set a height; we'll - // let the CSS decide how high it is. If it's fullscreen, then we need to - // make it as high as the window. - if (position === Composer.PositionEnum.MINIMIZED) { - return ''; - } else if (position === Composer.PositionEnum.FULLSCREEN) { - return $(window).height(); - } - - // Otherwise, if it's normal or hidden, then we use the intended height. - // We don't let the composer get too small or too big, though. - return Math.max(200, Math.min(height, $(window).height() - $('#header').outerHeight())); - }); } }, { key: 'view', @@ -19853,12 +19829,6 @@ System.register('flarum/components/Composer', ['flarum/Component', 'flarum/utils value: function config(isInitialized, context) { var _this2 = this; - var defaultHeight = void 0; - - if (!isInitialized) { - defaultHeight = this.$().height(); - } - // Set the height of the Composer element and its contents on each redraw, // so that they do not lose it if their DOM elements are recreated. this.updateHeight(); @@ -19869,11 +19839,8 @@ System.register('flarum/components/Composer', ['flarum/Component', 'flarum/utils // routes, we will flag the DOM to be retained across route changes. context.retain = true; - // Initialize the composer's intended height based on what the user has set - // it at previously, or otherwise the composer's default height. After that, - // we'll hide the composer. - this.height = localStorage.getItem('composerHeight') || defaultHeight; - this.$().hide().css('bottom', -this.height); + this.initializeHeight(); + this.$().hide().css('bottom', -this.computedHeight()); // Whenever any of the inputs inside the composer are have focus, we want to // add a class to the composer to draw attention to it. @@ -19932,8 +19899,7 @@ System.register('flarum/components/Composer', ['flarum/Component', 'flarum/utils // height so that it fills the height of the composer, and update the // body's padding. var deltaPixels = this.mouseStart - e.clientY; - this.height = this.heightStart + deltaPixels; - this.updateHeight(); + this.changeHeight(this.heightStart + deltaPixels); // Update the body's padding-bottom so that no content on the page will ever // get permanently hidden behind the composer. If the user is already @@ -19942,8 +19908,6 @@ System.register('flarum/components/Composer', ['flarum/Component', 'flarum/utils var scrollTop = $(window).scrollTop(); var anchorToBottom = scrollTop > 0 && scrollTop + $(window).height() >= $(document).height(); this.updateBodyPadding(anchorToBottom); - - localStorage.setItem('composerHeight', this.height); } }, { key: 'onmouseup', @@ -20172,6 +20136,54 @@ System.register('flarum/components/Composer', ['flarum/Component', 'flarum/utils return items; } + }, { + key: 'initializeHeight', + value: function initializeHeight() { + this.height = localStorage.getItem('composerHeight'); + + if (!this.height) { + this.height = this.defaultHeight(); + } + } + }, { + key: 'defaultHeight', + value: function defaultHeight() { + return this.$().height(); + } + }, { + key: 'minimumHeight', + value: function minimumHeight() { + return 200; + } + }, { + key: 'maximumHeight', + value: function maximumHeight() { + return $(window).height() - $('#header').outerHeight(); + } + }, { + key: 'computedHeight', + value: function computedHeight() { + // If the composer is minimized, then we don't want to set a height; we'll + // let the CSS decide how high it is. If it's fullscreen, then we need to + // make it as high as the window. + if (this.position === Composer.PositionEnum.MINIMIZED) { + return ''; + } else if (this.position === Composer.PositionEnum.FULLSCREEN) { + return $(window).height(); + } + + // Otherwise, if it's normal or hidden, then we use the intended height. + // We don't let the composer get too small or too big, though. + return Math.max(this.minimumHeight(), Math.min(this.height, this.maximumHeight())); + } + }, { + key: 'changeHeight', + value: function changeHeight(height) { + this.height = height; + this.updateHeight(); + + localStorage.setItem('composerHeight', this.height); + } }]); return Composer; }(Component); @@ -20605,7 +20617,7 @@ System.register('flarum/components/DiscussionList', ['flarum/Component', 'flarum return m( 'div', - { className: 'DiscussionList' }, + { className: 'DiscussionList' + (this.props.params.q ? ' DiscussionList--searchResults' : '') }, m( 'ul', { className: 'DiscussionList-discussions' }, @@ -20634,7 +20646,7 @@ System.register('flarum/components/DiscussionList', ['flarum/Component', 'flarum if (this.props.params.q) { params.filter.q = this.props.params.q; - params.include.push('relevantPosts', 'relevantPosts.discussion', 'relevantPosts.user'); + params.include.push('mostRelevantPost', 'mostRelevantPost.user'); } return params; @@ -20809,8 +20821,6 @@ System.register('flarum/components/DiscussionListItem', ['flarum/Component', 'fl }, { key: 'view', value: function view() { - var _this3 = this; - var retain = this.subtree.retain(); if (retain) return retain; @@ -20820,11 +20830,22 @@ System.register('flarum/components/DiscussionListItem', ['flarum/Component', 'fl var isUnread = discussion.isUnread(); var isRead = discussion.isRead(); var showUnread = !this.showRepliesCount() && isUnread; - var jumpTo = Math.min(discussion.lastPostNumber(), (discussion.readNumber() || 0) + 1); - var relevantPosts = this.props.params.q ? discussion.relevantPosts() : []; + var jumpTo = 0; var controls = DiscussionControls.controls(discussion, this).toArray(); var attrs = this.attrs(); + if (this.props.params.q) { + var post = discussion.mostRelevantPost(); + if (post) { + jumpTo = post.number(); + } + + var phrase = this.props.params.q; + this.highlightRegExp = new RegExp(phrase + '|' + phrase.trim().replace(/\s+/g, '|'), 'gi'); + } else { + jumpTo = Math.min(discussion.lastPostNumber(), (discussion.readNumber() || 0) + 1); + } + return m( 'div', attrs, @@ -20867,7 +20888,7 @@ System.register('flarum/components/DiscussionListItem', ['flarum/Component', 'fl m( 'h3', { className: 'DiscussionListItem-title' }, - highlight(discussion.title(), this.props.params.q) + highlight(discussion.title(), this.highlightRegExp) ), m( 'ul', @@ -20881,14 +20902,7 @@ System.register('flarum/components/DiscussionListItem', ['flarum/Component', 'fl onclick: this.markAsRead.bind(this), title: showUnread ? app.translator.trans('core.forum.discussion_list.mark_as_read_tooltip') : '' }, abbreviateNumber(discussion[showUnread ? 'unreadCount' : 'repliesCount']()) - ), - relevantPosts && relevantPosts.length ? m( - 'div', - { className: 'DiscussionListItem-relevantPosts' }, - relevantPosts.map(function (post) { - return PostPreview.component({ post: post, highlight: _this3.props.params.q }); - }) - ) : '' + ) ) ); } @@ -20940,10 +20954,19 @@ System.register('flarum/components/DiscussionListItem', ['flarum/Component', 'fl value: function infoItems() { var items = new ItemList(); - items.add('terminalPost', TerminalPost.component({ - discussion: this.props.discussion, - lastPost: !this.showStartPost() - })); + if (this.props.params.q) { + var post = this.props.discussion.mostRelevantPost() || this.props.discussion.startPost(); + + if (post && post.contentType() === 'comment') { + var excerpt = highlight(post.contentPlain(), this.highlightRegExp, 175); + items.add('excerpt', excerpt, -100); + } + } else { + items.add('terminalPost', TerminalPost.component({ + discussion: this.props.discussion, + lastPost: !this.showStartPost() + })); + } return items; } @@ -21387,7 +21410,7 @@ System.register('flarum/components/DiscussionsSearchSource', ['flarum/helpers/hi var params = { filter: { q: query }, page: { limit: 3 }, - include: 'relevantPosts,relevantPosts.discussion,relevantPosts.user' + include: 'mostRelevantPost' }; return app.store.find('discussions', params).then(function (results) { @@ -21414,24 +21437,23 @@ System.register('flarum/components/DiscussionsSearchSource', ['flarum/helpers/hi href: app.route('index', { q: query }) }) ), results.map(function (discussion) { - var relevantPosts = discussion.relevantPosts(); - var post = relevantPosts && relevantPosts[0]; + var mostRelevantPost = discussion.mostRelevantPost(); return m( 'li', { className: 'DiscussionSearchResult', 'data-index': 'discussions' + discussion.id() }, m( 'a', - { href: app.route.discussion(discussion, post && post.number()), config: m.route }, + { href: app.route.discussion(discussion, mostRelevantPost && mostRelevantPost.number()), config: m.route }, m( 'div', { className: 'DiscussionSearchResult-title' }, highlight(discussion.title(), query) ), - post ? m( + mostRelevantPost ? m( 'div', { className: 'DiscussionSearchResult-excerpt' }, - highlight(post.contentPlain(), query, 100) + highlight(mostRelevantPost.contentPlain(), query, 100) ) : '' ) ); @@ -27313,6 +27335,11 @@ System.register('flarum/components/SignUpModal', ['flarum/components/Modal', 'fl this.footer() )]; } + }, { + key: 'isProvided', + value: function isProvided(field) { + return this.props.identificationFields && this.props.identificationFields.indexOf(field) !== -1; + } }, { key: 'body', value: function body() { @@ -27325,7 +27352,7 @@ System.register('flarum/components/SignUpModal', ['flarum/components/Modal', 'fl m('input', { className: 'FormControl', name: 'username', type: 'text', placeholder: extractText(app.translator.trans('core.forum.sign_up.username_placeholder')), value: this.username(), onchange: m.withAttr('value', this.username), - disabled: this.loading }) + disabled: this.loading || this.isProvided('username') }) ), m( 'div', @@ -27333,7 +27360,7 @@ System.register('flarum/components/SignUpModal', ['flarum/components/Modal', 'fl m('input', { className: 'FormControl', name: 'email', type: 'email', placeholder: extractText(app.translator.trans('core.forum.sign_up.email_placeholder')), value: this.email(), onchange: m.withAttr('value', this.email), - disabled: this.loading || this.props.token && this.props.email }) + disabled: this.loading || this.isProvided('email') }) ), this.props.token ? '' : m( 'div', @@ -29395,7 +29422,7 @@ System.register('flarum/models/Discussion', ['flarum/Model', 'flarum/utils/compu return Math.max(0, commentsCount - 1); }), posts: Model.hasMany('posts'), - relevantPosts: Model.hasMany('relevantPosts'), + mostRelevantPost: Model.hasOne('mostRelevantPost'), readTime: Model.attribute('readTime', Model.transformDate), readNumber: Model.attribute('readNumber'), diff --git a/js/forum/src/components/DiscussionList.js b/js/forum/src/components/DiscussionList.js index 35c5d2968b..978fbc05b7 100644 --- a/js/forum/src/components/DiscussionList.js +++ b/js/forum/src/components/DiscussionList.js @@ -62,7 +62,7 @@ export default class DiscussionList extends Component { } return ( -
+
); @@ -188,12 +191,21 @@ export default class DiscussionListItem extends Component { infoItems() { const items = new ItemList(); - items.add('terminalPost', - TerminalPost.component({ - discussion: this.props.discussion, - lastPost: !this.showStartPost() - }) - ); + if (this.props.params.q) { + const post = this.props.discussion.mostRelevantPost() || this.props.discussion.startPost(); + + if (post && post.contentType() === 'comment') { + const excerpt = highlight(post.contentPlain(), this.highlightRegExp, 175); + items.add('excerpt', excerpt, -100); + } + } else { + items.add('terminalPost', + TerminalPost.component({ + discussion: this.props.discussion, + lastPost: !this.showStartPost() + }) + ); + } return items; } diff --git a/js/forum/src/components/DiscussionsSearchSource.js b/js/forum/src/components/DiscussionsSearchSource.js index e1d1e1c4f1..a4b435e840 100644 --- a/js/forum/src/components/DiscussionsSearchSource.js +++ b/js/forum/src/components/DiscussionsSearchSource.js @@ -20,7 +20,7 @@ export default class DiscussionsSearchSource { const params = { filter: {q: query}, page: {limit: 3}, - include: 'relevantPosts,relevantPosts.discussion,relevantPosts.user' + include: 'mostRelevantPost' }; return app.store.find('discussions', params).then(results => this.results[query] = results); @@ -41,14 +41,13 @@ export default class DiscussionsSearchSource { })} , results.map(discussion => { - const relevantPosts = discussion.relevantPosts(); - const post = relevantPosts && relevantPosts[0]; + const mostRelevantPost = discussion.mostRelevantPost(); return (
  • - +
    {highlight(discussion.title(), query)}
    - {post ?
    {highlight(post.contentPlain(), query, 100)}
    : ''} + {mostRelevantPost ?
    {highlight(mostRelevantPost.contentPlain(), query, 100)}
    : ''}
  • ); diff --git a/js/lib/models/Discussion.js b/js/lib/models/Discussion.js index f5a6d27119..4c4072ba9d 100644 --- a/js/lib/models/Discussion.js +++ b/js/lib/models/Discussion.js @@ -21,7 +21,7 @@ Object.assign(Discussion.prototype, { commentsCount: Model.attribute('commentsCount'), repliesCount: computed('commentsCount', commentsCount => Math.max(0, commentsCount - 1)), posts: Model.hasMany('posts'), - relevantPosts: Model.hasMany('relevantPosts'), + mostRelevantPost: Model.hasOne('mostRelevantPost'), readTime: Model.attribute('readTime', Model.transformDate), readNumber: Model.attribute('readNumber'), diff --git a/less/forum/DiscussionListItem.less b/less/forum/DiscussionListItem.less index 5bd7ecc8db..a5c3d5182a 100644 --- a/less/forum/DiscussionListItem.less +++ b/less/forum/DiscussionListItem.less @@ -50,27 +50,29 @@ overflow: hidden; text-overflow: ellipsis; - .read & { + .DiscussionList:not(.DiscussionList--searchResults) .read { color: mix(@heading-color, @body-bg, 55%); } - .unread & { + .DiscussionList:not(.DiscussionList--searchResults) .unread & { font-weight: 600; } + + mark { + background: none; + box-shadow: none; + font-weight: bold; + color: @text-color; + } } .DiscussionListItem-info { list-style-type: none; padding: 0; margin: 0; - font-size: 12px; + font-size: 11px; + color: @muted-more-color; > li { display: inline; - opacity: 0.7; - .transition(opacity 0.2s); - - .DiscussionListItem:hover &, .DiscussionListItem.active & { - opacity: 1; - } } .username { font-weight: bold; @@ -79,6 +81,24 @@ font-size: 11px; margin-right: -1px; } + .item-excerpt { + margin-top: 4px; + margin-right: 170px; + white-space: normal; + font-size: 12px; + line-height: 1.5em; + display: block; + + .DiscussionPage-list & { + margin-right: 0; + } + mark { + background: none; + box-shadow: none; + font-weight: bold; + color: inherit; + } + } } .DiscussionListItem-count { float: right; @@ -89,41 +109,6 @@ cursor: pointer; } } -.DiscussionListItem-relevantPosts { - padding-bottom: 15px; - - @media @phone { - margin-left: -45px; - margin-right: -35px; - } - - .PostPreview { - background: @control-bg; - display: block; - padding: 10px 15px; - border-bottom: 2px dotted @body-bg; - color: @muted-color; - transition: border-color 0.2s; - - .DiscussionListItem:hover & { - border-color: lighten(@control-bg, 3%); - } - - .Avatar, time { - display: none; - } - .PostPreview-content { - padding-left: 0; - } - &:first-child { - border-radius: @border-radius @border-radius 0 0; - } - &:hover { - background: darken(@control-bg, 3%); - text-decoration: none; - } - } -} @media @phone { @@ -212,7 +197,7 @@ .DiscussionListItem-controls { position: absolute; right: 5px; - top: 15px; + top: 5px; z-index: 1; opacity: 0; transition: opacity 0.2s; @@ -244,10 +229,10 @@ margin-right: -65px; } .DiscussionListItem-title { - font-size: 15px; + font-size: 16px; } .DiscussionListItem-count { - margin-top: 21px; + margin-top: 12px; margin-right: -70px; width: 55px; color: @muted-color; diff --git a/migrations/2015_02_24_000000_create_posts_table.php b/migrations/2015_02_24_000000_create_posts_table.php index e6d026298f..9f8d87f41b 100644 --- a/migrations/2015_02_24_000000_create_posts_table.php +++ b/migrations/2015_02_24_000000_create_posts_table.php @@ -36,8 +36,9 @@ $table->engine = 'MyISAM'; }); - $prefix = $schema->getConnection()->getTablePrefix(); - $schema->getConnection()->statement('ALTER TABLE '.$prefix.'posts ADD FULLTEXT content (content)'); + $connection = $schema->getConnection(); + $prefix = $connection->getTablePrefix(); + $connection->statement('ALTER TABLE '.$prefix.'posts ADD FULLTEXT content (content)'); }, 'down' => function (Builder $schema) { diff --git a/migrations/2018_01_11_120604_change_posts_table_to_innodb.php b/migrations/2018_01_11_120604_change_posts_table_to_innodb.php new file mode 100644 index 0000000000..54aac3c4c2 --- /dev/null +++ b/migrations/2018_01_11_120604_change_posts_table_to_innodb.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Illuminate\Database\Schema\Builder; + +return [ + 'up' => function (Builder $schema) { + $connection = $schema->getConnection(); + $prefix = $connection->getTablePrefix(); + $connection->statement('ALTER TABLE '.$prefix.'posts ENGINE = InnoDB'); + }, + + 'down' => function (Builder $schema) { + $connection = $schema->getConnection(); + $prefix = $connection->getTablePrefix(); + $connection->statement('ALTER TABLE '.$prefix.'posts ENGINE = MyISAM'); + } +]; diff --git a/migrations/2018_01_30_112238_add_fulltext_index_to_discussions_title.php b/migrations/2018_01_30_112238_add_fulltext_index_to_discussions_title.php new file mode 100644 index 0000000000..c63138c5a0 --- /dev/null +++ b/migrations/2018_01_30_112238_add_fulltext_index_to_discussions_title.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Illuminate\Database\Schema\Builder; + +return [ + 'up' => function (Builder $schema) { + $connection = $schema->getConnection(); + $prefix = $connection->getTablePrefix(); + $connection->statement('ALTER TABLE '.$prefix.'discussions ADD FULLTEXT title (title)'); + }, + + 'down' => function (Builder $schema) { + $connection = $schema->getConnection(); + $prefix = $connection->getTablePrefix(); + $connection->statement('ALTER TABLE '.$prefix.'discussions DROP INDEX title'); + } +]; diff --git a/src/Api/Controller/ListDiscussionsController.php b/src/Api/Controller/ListDiscussionsController.php index 55cc4a4e9f..c5b98a2538 100644 --- a/src/Api/Controller/ListDiscussionsController.php +++ b/src/Api/Controller/ListDiscussionsController.php @@ -31,9 +31,8 @@ class ListDiscussionsController extends AbstractListController public $include = [ 'startUser', 'lastUser', - 'relevantPosts', - 'relevantPosts.discussion', - 'relevantPosts.user' + 'mostRelevantPost', + 'mostRelevantPost.user' ]; /** @@ -84,7 +83,7 @@ protected function data(ServerRequestInterface $request, Document $document) $offset = $this->extractOffset($request); $load = array_merge($this->extractInclude($request), ['state']); - $results = $this->searcher->search($criteria, $limit, $offset, $load); + $results = $this->searcher->search($criteria, $limit, $offset); $document->addPaginationLinks( $this->url->to('api')->route('discussions.index'), @@ -94,7 +93,7 @@ protected function data(ServerRequestInterface $request, Document $document) $results->areMoreResults() ? null : 0 ); - $results = $results->getResults(); + $results = $results->getResults()->load($load); if ($relations = array_intersect($load, ['startPost', 'lastPost'])) { foreach ($results as $discussion) { diff --git a/src/Api/Serializer/BasicDiscussionSerializer.php b/src/Api/Serializer/BasicDiscussionSerializer.php index b03d6d0026..026082a23a 100644 --- a/src/Api/Serializer/BasicDiscussionSerializer.php +++ b/src/Api/Serializer/BasicDiscussionSerializer.php @@ -84,9 +84,9 @@ protected function posts($discussion) /** * @return \Tobscure\JsonApi\Relationship */ - protected function relevantPosts($discussion) + protected function mostRelevantPost($discussion) { - return $this->hasMany($discussion, BasicPostSerializer::class); + return $this->hasOne($discussion, PostSerializer::class); } /** diff --git a/src/Discussion/Discussion.php b/src/Discussion/Discussion.php index a693940b58..116f1484ce 100644 --- a/src/Discussion/Discussion.php +++ b/src/Discussion/Discussion.php @@ -382,6 +382,16 @@ public function lastUser() return $this->belongsTo(User::class, 'last_user_id'); } + /** + * Define the relationship with the discussion's most relevant post. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function mostRelevantPost() + { + return $this->belongsTo(Post::class, 'most_relevant_post_id'); + } + /** * Define the relationship with the discussion's readers. * diff --git a/src/Discussion/Search/DiscussionSearcher.php b/src/Discussion/Search/DiscussionSearcher.php index c3b5fdb59c..d342acfcbe 100644 --- a/src/Discussion/Search/DiscussionSearcher.php +++ b/src/Discussion/Search/DiscussionSearcher.php @@ -11,26 +11,20 @@ namespace Flarum\Discussion\Search; -use Flarum\Discussion\Discussion; use Flarum\Discussion\DiscussionRepository; use Flarum\Discussion\Event\Searching; -use Flarum\Post\PostRepository; use Flarum\Search\ApplySearchParametersTrait; use Flarum\Search\GambitManager; use Flarum\Search\SearchCriteria; use Flarum\Search\SearchResults; -use Illuminate\Database\Eloquent\Collection; +use Illuminate\Contracts\Events\Dispatcher; -/** - * Takes a DiscussionSearchCriteria object, performs a search using gambits, - * and spits out a DiscussionSearchResults object. - */ class DiscussionSearcher { use ApplySearchParametersTrait; /** - * @var \Flarum\Search\GambitManager + * @var GambitManager */ protected $gambits; @@ -40,37 +34,34 @@ class DiscussionSearcher protected $discussions; /** - * @var PostRepository + * @var Dispatcher */ - protected $posts; + protected $events; /** - * @param \Flarum\Search\GambitManager $gambits + * @param GambitManager $gambits * @param DiscussionRepository $discussions - * @param PostRepository $posts + * @param Dispatcher $events */ - public function __construct( - GambitManager $gambits, - DiscussionRepository $discussions, - PostRepository $posts - ) { + public function __construct(GambitManager $gambits, DiscussionRepository $discussions, Dispatcher $events) + { $this->gambits = $gambits; $this->discussions = $discussions; - $this->posts = $posts; + $this->events = $events; } /** * @param SearchCriteria $criteria * @param int|null $limit * @param int $offset - * @param array $load An array of relationships to load on the results. + * * @return SearchResults */ - public function search(SearchCriteria $criteria, $limit = null, $offset = 0, array $load = []) + public function search(SearchCriteria $criteria, $limit = null, $offset = 0) { $actor = $criteria->actor; - $query = $this->discussions->query()->whereVisibleTo($actor); + $query = $this->discussions->query()->select('discussions.*')->whereVisibleTo($actor); // Construct an object which represents this search for discussions. // Apply gambits to it, sort, and paging criteria. Also give extensions @@ -82,8 +73,7 @@ public function search(SearchCriteria $criteria, $limit = null, $offset = 0, arr $this->applyOffset($search, $offset); $this->applyLimit($search, $limit + 1); - // TODO: inject dispatcher - event(new Searching($search, $criteria)); + $this->events->dispatch(new Searching($search, $criteria)); // Execute the search query and retrieve the results. We get one more // results than the user asked for, so that we can say if there are more @@ -96,42 +86,6 @@ public function search(SearchCriteria $criteria, $limit = null, $offset = 0, arr $discussions->pop(); } - // The relevant posts relationship isn't a typical Eloquent - // relationship; rather, we need to extract that information from our - // search object. We will delegate that task and prevent Eloquent - // from trying to load it. - if (in_array('relevantPosts', $load)) { - $this->loadRelevantPosts($discussions, $search); - - $load = array_diff($load, ['relevantPosts', 'relevantPosts.discussion', 'relevantPosts.user']); - } - - Discussion::setStateUser($actor); - $discussions->load($load); - return new SearchResults($discussions, $areMoreResults); } - - /** - * Load relevant posts onto each discussion using information from the - * search. - * - * @param Collection $discussions - * @param DiscussionSearch $search - */ - protected function loadRelevantPosts(Collection $discussions, DiscussionSearch $search) - { - $postIds = []; - foreach ($search->getRelevantPostIds() as $relevantPostIds) { - $postIds = array_merge($postIds, array_slice($relevantPostIds, 0, 2)); - } - - $posts = $postIds ? $this->posts->findByIds($postIds, $search->getActor())->load('user')->all() : []; - - foreach ($discussions as $discussion) { - $discussion->relevantPosts = array_filter($posts, function ($post) use ($discussion) { - return $post->discussion_id == $discussion->id; - }); - } - } } diff --git a/src/Discussion/Search/Fulltext/DriverInterface.php b/src/Discussion/Search/Fulltext/DriverInterface.php deleted file mode 100644 index bfcd370939..0000000000 --- a/src/Discussion/Search/Fulltext/DriverInterface.php +++ /dev/null @@ -1,24 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Discussion\Search\Fulltext; - -interface DriverInterface -{ - /** - * Return an array of arrays of post IDs, grouped by discussion ID, which - * match the given string. - * - * @param string $string - * @return array - */ - public function match($string); -} diff --git a/src/Discussion/Search/Fulltext/MySqlFulltextDriver.php b/src/Discussion/Search/Fulltext/MySqlFulltextDriver.php deleted file mode 100644 index f3e1a0be21..0000000000 --- a/src/Discussion/Search/Fulltext/MySqlFulltextDriver.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Flarum\Discussion\Search\Fulltext; - -use Flarum\Post\Post; - -class MySqlFulltextDriver implements DriverInterface -{ - /** - * {@inheritdoc} - */ - public function match($string) - { - $discussionIds = Post::where('type', 'comment') - ->whereRaw('MATCH (`content`) AGAINST (? IN BOOLEAN MODE)', [$string]) - ->orderByRaw('MATCH (`content`) AGAINST (?) DESC', [$string]) - ->pluck('discussion_id', 'id'); - - $relevantPostIds = []; - - foreach ($discussionIds as $postId => $discussionId) { - $relevantPostIds[$discussionId][] = $postId; - } - - return $relevantPostIds; - } -} diff --git a/src/Discussion/Search/Gambit/FulltextGambit.php b/src/Discussion/Search/Gambit/FulltextGambit.php index 3019f9f0f4..52307d60d1 100644 --- a/src/Discussion/Search/Gambit/FulltextGambit.php +++ b/src/Discussion/Search/Gambit/FulltextGambit.php @@ -12,26 +12,14 @@ namespace Flarum\Discussion\Search\Gambit; use Flarum\Discussion\Search\DiscussionSearch; -use Flarum\Discussion\Search\Fulltext\DriverInterface; +use Flarum\Event\ScopeModelVisibility; +use Flarum\Post\Post; use Flarum\Search\AbstractSearch; use Flarum\Search\GambitInterface; use LogicException; class FulltextGambit implements GambitInterface { - /** - * @var \Flarum\Discussion\Search\Fulltext\DriverInterface - */ - protected $fulltext; - - /** - * @param \Flarum\Discussion\Search\Fulltext\DriverInterface $fulltext - */ - public function __construct(DriverInterface $fulltext) - { - $this->fulltext = $fulltext; - } - /** * {@inheritdoc} */ @@ -41,14 +29,22 @@ public function apply(AbstractSearch $search, $bit) throw new LogicException('This gambit can only be applied on a DiscussionSearch'); } - $relevantPostIds = $this->fulltext->match($bit); - - $discussionIds = array_keys($relevantPostIds); - - $search->setRelevantPostIds($relevantPostIds); - - $search->getQuery()->whereIn('id', $discussionIds); - - $search->setDefaultSort(['id' => $discussionIds]); + $search->getQuery() + ->selectRaw('SUBSTRING_INDEX(GROUP_CONCAT(posts.id ORDER BY MATCH(posts.content) AGAINST (?) DESC), \',\', 1) as most_relevant_post_id', [$bit]) + ->leftJoin('posts', 'posts.discussion_id', '=', 'discussions.id') + ->where('posts.type', 'comment') + ->where(function ($query) use ($search) { + event(new ScopeModelVisibility(Post::query()->setQuery($query), $search->getActor(), 'view')); + }) + ->where(function ($query) use ($bit) { + $query->whereRaw('MATCH(discussions.title) AGAINST (? IN BOOLEAN MODE)', [$bit]) + ->orWhereRaw('MATCH(posts.content) AGAINST (? IN BOOLEAN MODE)', [$bit]); + }) + ->groupBy('posts.discussion_id'); + + $search->setDefaultSort(function ($query) use ($bit) { + $query->orderByRaw('MATCH(discussions.title) AGAINST (?) desc', [$bit]); + $query->orderByRaw('MATCH(posts.content) AGAINST (?) desc', [$bit]); + }); } } diff --git a/src/Search/AbstractSearch.php b/src/Search/AbstractSearch.php index 23e7c4b781..3c1583ba7d 100644 --- a/src/Search/AbstractSearch.php +++ b/src/Search/AbstractSearch.php @@ -85,12 +85,12 @@ public function getDefaultSort() * Set the default sort order for the search. This will only be applied if * a sort order has not been specified in the search criteria. * - * @param array $defaultSort An array of sort-order pairs, where the column + * @param mixed $defaultSort An array of sort-order pairs, where the column * is the key, and the order is the value. The order may be 'asc', * 'desc', or an array of IDs to order by. * @return mixed */ - public function setDefaultSort(array $defaultSort) + public function setDefaultSort($defaultSort) { $this->defaultSort = $defaultSort; } diff --git a/src/Search/ApplySearchParametersTrait.php b/src/Search/ApplySearchParametersTrait.php index 58691946c3..058c1b5dbb 100644 --- a/src/Search/ApplySearchParametersTrait.php +++ b/src/Search/ApplySearchParametersTrait.php @@ -23,13 +23,17 @@ protected function applySort(AbstractSearch $search, array $sort = null) { $sort = $sort ?: $search->getDefaultSort(); - foreach ($sort as $field => $order) { - if (is_array($order)) { - foreach ($order as $value) { - $search->getQuery()->orderByRaw(snake_case($field).' != ?', [$value]); + if (is_callable($sort)) { + $sort($search->getQuery()); + } else { + foreach ($sort as $field => $order) { + if (is_array($order)) { + foreach ($order as $value) { + $search->getQuery()->orderByRaw(snake_case($field).' != ?', [$value]); + } + } else { + $search->getQuery()->orderBy(snake_case($field), $order); } - } else { - $search->getQuery()->orderBy(snake_case($field), $order); } } }