mirror of
https://github.com/flarum/core.git
synced 2025-07-14 13:26:23 +02:00
* Improve fulltext gambit * Only search in visible posts This change relies on the `visibility-scoping` branch to be merged. * Change posts table to use InnoDB engine Doing a JOIN between an InnoDB table (discussions) and a MyISAM table (posts) is very very (very) bad for performance. FULLTEXT indexes are fully supported in InnoDB now, and it is a superior engine in every other way, so there is no longer any reason to be using MyISAM. * Use ::class * Only search for comment posts * Add fulltext index to discussions.title * Fix migration not working if there is a table prefix * Update frontend appearance * Apply fixes from StyleCI [ci skip] [skip ci] * Show search result excerpts on mobile
213 lines
6.6 KiB
JavaScript
213 lines
6.6 KiB
JavaScript
import Component from 'flarum/Component';
|
|
import avatar from 'flarum/helpers/avatar';
|
|
import listItems from 'flarum/helpers/listItems';
|
|
import highlight from 'flarum/helpers/highlight';
|
|
import icon from 'flarum/helpers/icon';
|
|
import humanTime from 'flarum/utils/humanTime';
|
|
import ItemList from 'flarum/utils/ItemList';
|
|
import abbreviateNumber from 'flarum/utils/abbreviateNumber';
|
|
import Dropdown from 'flarum/components/Dropdown';
|
|
import TerminalPost from 'flarum/components/TerminalPost';
|
|
import PostPreview from 'flarum/components/PostPreview';
|
|
import SubtreeRetainer from 'flarum/utils/SubtreeRetainer';
|
|
import DiscussionControls from 'flarum/utils/DiscussionControls';
|
|
import slidable from 'flarum/utils/slidable';
|
|
import extractText from 'flarum/utils/extractText';
|
|
import classList from 'flarum/utils/classList';
|
|
|
|
/**
|
|
* The `DiscussionListItem` component shows a single discussion in the
|
|
* discussion list.
|
|
*
|
|
* ### Props
|
|
*
|
|
* - `discussion`
|
|
* - `params`
|
|
*/
|
|
export default class DiscussionListItem extends Component {
|
|
init() {
|
|
/**
|
|
* Set up a subtree retainer so that the discussion will not be redrawn
|
|
* unless new data comes in.
|
|
*
|
|
* @type {SubtreeRetainer}
|
|
*/
|
|
this.subtree = new SubtreeRetainer(
|
|
() => this.props.discussion.freshness,
|
|
() => {
|
|
const time = app.session.user && app.session.user.readTime();
|
|
return time && time.getTime();
|
|
},
|
|
() => this.active()
|
|
);
|
|
}
|
|
|
|
attrs() {
|
|
return {
|
|
className: classList([
|
|
'DiscussionListItem',
|
|
this.active() ? 'active' : '',
|
|
this.props.discussion.isHidden() ? 'DiscussionListItem--hidden' : ''
|
|
])
|
|
};
|
|
}
|
|
|
|
view() {
|
|
const retain = this.subtree.retain();
|
|
|
|
if (retain) return retain;
|
|
|
|
const discussion = this.props.discussion;
|
|
const startUser = discussion.startUser();
|
|
const isUnread = discussion.isUnread();
|
|
const isRead = discussion.isRead();
|
|
const showUnread = !this.showRepliesCount() && isUnread;
|
|
let jumpTo = 0;
|
|
const controls = DiscussionControls.controls(discussion, this).toArray();
|
|
const attrs = this.attrs();
|
|
|
|
if (this.props.params.q) {
|
|
const post = discussion.mostRelevantPost();
|
|
if (post) {
|
|
jumpTo = post.number();
|
|
}
|
|
|
|
const 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 (
|
|
<div {...attrs}>
|
|
{controls.length ? Dropdown.component({
|
|
icon: 'ellipsis-v',
|
|
children: controls,
|
|
className: 'DiscussionListItem-controls',
|
|
buttonClassName: 'Button Button--icon Button--flat Slidable-underneath Slidable-underneath--right'
|
|
}) : ''}
|
|
|
|
<a className={'Slidable-underneath Slidable-underneath--left Slidable-underneath--elastic' + (isUnread ? '' : ' disabled')}
|
|
onclick={this.markAsRead.bind(this)}>
|
|
{icon('check')}
|
|
</a>
|
|
|
|
<div className={'DiscussionListItem-content Slidable-content' + (isUnread ? ' unread' : '') + (isRead ? ' read' : '')}>
|
|
<a href={startUser ? app.route.user(startUser) : '#'}
|
|
className="DiscussionListItem-author"
|
|
title={extractText(app.translator.trans('core.forum.discussion_list.started_text', {user: startUser, ago: humanTime(discussion.startTime())}))}
|
|
config={function(element) {
|
|
$(element).tooltip({placement: 'right'});
|
|
m.route.apply(this, arguments);
|
|
}}>
|
|
{avatar(startUser, {title: ''})}
|
|
</a>
|
|
|
|
<ul className="DiscussionListItem-badges badges">
|
|
{listItems(discussion.badges().toArray())}
|
|
</ul>
|
|
|
|
<a href={app.route.discussion(discussion, jumpTo)}
|
|
config={m.route}
|
|
className="DiscussionListItem-main">
|
|
<h3 className="DiscussionListItem-title">{highlight(discussion.title(), this.highlightRegExp)}</h3>
|
|
<ul className="DiscussionListItem-info">{listItems(this.infoItems().toArray())}</ul>
|
|
</a>
|
|
|
|
<span className="DiscussionListItem-count"
|
|
onclick={this.markAsRead.bind(this)}
|
|
title={showUnread ? app.translator.trans('core.forum.discussion_list.mark_as_read_tooltip') : ''}>
|
|
{abbreviateNumber(discussion[showUnread ? 'unreadCount' : 'repliesCount']())}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
config(isInitialized) {
|
|
if (isInitialized) return;
|
|
|
|
// If we're on a touch device, set up the discussion row to be slidable.
|
|
// This allows the user to drag the row to either side of the screen to
|
|
// reveal controls.
|
|
if ('ontouchstart' in window) {
|
|
const slidableInstance = slidable(this.$().addClass('Slidable'));
|
|
|
|
this.$('.DiscussionListItem-controls')
|
|
.on('hidden.bs.dropdown', () => slidableInstance.reset());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine whether or not the discussion is currently being viewed.
|
|
*
|
|
* @return {Boolean}
|
|
*/
|
|
active() {
|
|
const idParam = m.route.param('id');
|
|
|
|
return idParam && idParam.split('-')[0] === this.props.discussion.id();
|
|
}
|
|
|
|
/**
|
|
* Determine whether or not information about who started the discussion
|
|
* should be displayed instead of information about the most recent reply to
|
|
* the discussion.
|
|
*
|
|
* @return {Boolean}
|
|
*/
|
|
showStartPost() {
|
|
return ['newest', 'oldest'].indexOf(this.props.params.sort) !== -1;
|
|
}
|
|
|
|
/**
|
|
* Determine whether or not the number of replies should be shown instead of
|
|
* the number of unread posts.
|
|
*
|
|
* @return {Boolean}
|
|
*/
|
|
showRepliesCount() {
|
|
return this.props.params.sort === 'replies';
|
|
}
|
|
|
|
/**
|
|
* Mark the discussion as read.
|
|
*/
|
|
markAsRead() {
|
|
const discussion = this.props.discussion;
|
|
|
|
if (discussion.isUnread()) {
|
|
discussion.save({readNumber: discussion.lastPostNumber()});
|
|
m.redraw();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build an item list of info for a discussion listing. By default this is
|
|
* just the first/last post indicator.
|
|
*
|
|
* @return {ItemList}
|
|
*/
|
|
infoItems() {
|
|
const items = new ItemList();
|
|
|
|
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;
|
|
}
|
|
}
|