1
0
mirror of https://github.com/flarum/core.git synced 2025-07-10 19:36:27 +02:00
Files
php-flarum/js/src/forum/components/DiscussionListItem.js
David Sevilla Martín 71f3379fcc Mithril 2 update (#2255)
* Update frontend to Mithril 2

- Update Mithril version to v2.0.4
- Add Typescript typings for Mithril
- Rename "props" to "attrs"; "initProps" to "initAttrs"; "m.prop" to "m.stream"; "m.withAttr" to "utils/withAttr".
- Use Mithril 2's new lifecycle hooks
- SubtreeRetainer has been rewritten to be more useful for the new system
- Utils for forcing page re-initializations have been added (force attr in links, setRouteWithForcedRefresh util)
- Other mechanical changes, following the upgrade guide
- Remove some of the custom stuff in our Component base class
- Introduce "fragments" for non-components that control their own DOM
- Remove Mithril patches, introduce a few new ones (route attrs in <a>; 
- Redesign AlertManagerState `show` with 3 overloads: `show(children)`, `show(attrs, children)`, `show(componentClass, attrs, children)`
- The `affixedSidebar` util has been replaced with an `AffixedSidebar` component

Challenges:
- `children` and `tag` are now reserved, and can not be used as attr names
- Behavior of links to current page changed in Mithril. If moving to a page that is handled by the same component, the page component WILL NOT be re-initialized by default. Additional code to keep track of the current url is needed (See IndexPage, DiscussionPage, and UserPage for examples)
- Native Promise rejections are shown on console when not handled
- Instances of components can no longer be stored. The state pattern should be used instead.

Refs #1821.

Co-authored-by: Alexander Skvortsov <sasha.skvortsov109@gmail.com>
Co-authored-by: Matthew Kilgore <tankerkiller125@gmail.com>
Co-authored-by: Franz Liedke <franz@develophp.org>
2020-09-23 22:40:37 -04:00

224 lines
6.8 KiB
JavaScript

import Component from '../../common/Component';
import avatar from '../../common/helpers/avatar';
import listItems from '../../common/helpers/listItems';
import highlight from '../../common/helpers/highlight';
import icon from '../../common/helpers/icon';
import humanTime from '../../common/utils/humanTime';
import ItemList from '../../common/utils/ItemList';
import abbreviateNumber from '../../common/utils/abbreviateNumber';
import Dropdown from '../../common/components/Dropdown';
import TerminalPost from './TerminalPost';
import SubtreeRetainer from '../../common/utils/SubtreeRetainer';
import DiscussionControls from '../utils/DiscussionControls';
import slidable from '../utils/slidable';
import extractText from '../../common/utils/extractText';
import classList from '../../common/utils/classList';
import { escapeRegExp } from 'lodash-es';
/**
* The `DiscussionListItem` component shows a single discussion in the
* discussion list.
*
* ### Attrs
*
* - `discussion`
* - `params`
*/
export default class DiscussionListItem extends Component {
oninit(vnode) {
super.oninit(vnode);
/**
* 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.attrs.discussion.freshness,
() => {
const time = app.session.user && app.session.user.markedAllAsReadAt();
return time && time.getTime();
},
() => this.active()
);
}
elementAttrs() {
return {
className: classList([
'DiscussionListItem',
this.active() ? 'active' : '',
this.attrs.discussion.isHidden() ? 'DiscussionListItem--hidden' : '',
]),
};
}
view() {
const discussion = this.attrs.discussion;
const user = discussion.user();
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.elementAttrs();
if (this.attrs.params.q) {
const post = discussion.mostRelevantPost();
if (post) {
jumpTo = post.number();
}
const phrase = escapeRegExp(this.attrs.params.q);
this.highlightRegExp = new RegExp(phrase + '|' + phrase.trim().replace(/\s+/g, '|'), 'gi');
} else {
jumpTo = Math.min(discussion.lastPostNumber(), (discussion.lastReadPostNumber() || 0) + 1);
}
return (
<div {...attrs}>
{controls.length
? Dropdown.component(
{
icon: 'fas fa-ellipsis-v',
className: 'DiscussionListItem-controls',
buttonClassName: 'Button Button--icon Button--flat Slidable-underneath Slidable-underneath--right',
},
controls
)
: ''}
<a
className={'Slidable-underneath Slidable-underneath--left Slidable-underneath--elastic' + (isUnread ? '' : ' disabled')}
onclick={this.markAsRead.bind(this)}
>
{icon('fas fa-check')}
</a>
<div className={'DiscussionListItem-content Slidable-content' + (isUnread ? ' unread' : '') + (isRead ? ' read' : '')}>
<a
route={user ? app.route.user(user) : '#'}
className="DiscussionListItem-author"
title={extractText(
app.translator.trans('core.forum.discussion_list.started_text', { user: user, ago: humanTime(discussion.createdAt()) })
)}
oncreate={function (vnode) {
$(vnode.dom).tooltip({ placement: 'right' });
}}
>
{avatar(user, { title: '' })}
</a>
<ul className="DiscussionListItem-badges badges">{listItems(discussion.badges().toArray())}</ul>
<a route={app.route.discussion(discussion, jumpTo)} 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' : 'replyCount']())}
</span>
</div>
</div>
);
}
oncreate(vnode) {
super.oncreate(vnode);
// 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());
}
}
onbeforeupdate(vnode, old) {
super.onbeforeupdate(vnode, old);
return this.subtree.needsRebuild();
}
/**
* 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.attrs.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}
*/
showFirstPost() {
return ['newest', 'oldest'].indexOf(this.attrs.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.attrs.params.sort === 'replies';
}
/**
* Mark the discussion as read.
*/
markAsRead() {
const discussion = this.attrs.discussion;
if (discussion.isUnread()) {
discussion.save({ lastReadPostNumber: 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.attrs.params.q) {
const post = this.attrs.discussion.mostRelevantPost() || this.attrs.discussion.firstPost();
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.attrs.discussion,
lastPost: !this.showFirstPost(),
})
);
}
return items;
}
}