mirror of
https://github.com/flarum/core.git
synced 2025-07-10 19:36:27 +02:00
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>
This commit is contained in:
committed by
GitHub
parent
1321b8cc28
commit
71f3379fcc
@ -8,7 +8,6 @@ import ItemList from '../../common/utils/ItemList';
|
||||
import abbreviateNumber from '../../common/utils/abbreviateNumber';
|
||||
import Dropdown from '../../common/components/Dropdown';
|
||||
import TerminalPost from './TerminalPost';
|
||||
import PostPreview from './PostPreview';
|
||||
import SubtreeRetainer from '../../common/utils/SubtreeRetainer';
|
||||
import DiscussionControls from '../utils/DiscussionControls';
|
||||
import slidable from '../utils/slidable';
|
||||
@ -20,13 +19,15 @@ import { escapeRegExp } from 'lodash-es';
|
||||
* The `DiscussionListItem` component shows a single discussion in the
|
||||
* discussion list.
|
||||
*
|
||||
* ### Props
|
||||
* ### Attrs
|
||||
*
|
||||
* - `discussion`
|
||||
* - `params`
|
||||
*/
|
||||
export default class DiscussionListItem extends Component {
|
||||
init() {
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
/**
|
||||
* Set up a subtree retainer so that the discussion will not be redrawn
|
||||
* unless new data comes in.
|
||||
@ -34,7 +35,7 @@ export default class DiscussionListItem extends Component {
|
||||
* @type {SubtreeRetainer}
|
||||
*/
|
||||
this.subtree = new SubtreeRetainer(
|
||||
() => this.props.discussion.freshness,
|
||||
() => this.attrs.discussion.freshness,
|
||||
() => {
|
||||
const time = app.session.user && app.session.user.markedAllAsReadAt();
|
||||
return time && time.getTime();
|
||||
@ -43,37 +44,33 @@ export default class DiscussionListItem extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
attrs() {
|
||||
elementAttrs() {
|
||||
return {
|
||||
className: classList([
|
||||
'DiscussionListItem',
|
||||
this.active() ? 'active' : '',
|
||||
this.props.discussion.isHidden() ? 'DiscussionListItem--hidden' : '',
|
||||
this.attrs.discussion.isHidden() ? 'DiscussionListItem--hidden' : '',
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
view() {
|
||||
const retain = this.subtree.retain();
|
||||
|
||||
if (retain) return retain;
|
||||
|
||||
const discussion = this.props.discussion;
|
||||
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.attrs();
|
||||
const attrs = this.elementAttrs();
|
||||
|
||||
if (this.props.params.q) {
|
||||
if (this.attrs.params.q) {
|
||||
const post = discussion.mostRelevantPost();
|
||||
if (post) {
|
||||
jumpTo = post.number();
|
||||
}
|
||||
|
||||
const phrase = escapeRegExp(this.props.params.q);
|
||||
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);
|
||||
@ -82,12 +79,14 @@ export default class DiscussionListItem extends Component {
|
||||
return (
|
||||
<div {...attrs}>
|
||||
{controls.length
|
||||
? Dropdown.component({
|
||||
icon: 'fas fa-ellipsis-v',
|
||||
children: controls,
|
||||
className: 'DiscussionListItem-controls',
|
||||
buttonClassName: 'Button Button--icon Button--flat Slidable-underneath Slidable-underneath--right',
|
||||
})
|
||||
? Dropdown.component(
|
||||
{
|
||||
icon: 'fas fa-ellipsis-v',
|
||||
className: 'DiscussionListItem-controls',
|
||||
buttonClassName: 'Button Button--icon Button--flat Slidable-underneath Slidable-underneath--right',
|
||||
},
|
||||
controls
|
||||
)
|
||||
: ''}
|
||||
|
||||
<a
|
||||
@ -99,14 +98,13 @@ export default class DiscussionListItem extends Component {
|
||||
|
||||
<div className={'DiscussionListItem-content Slidable-content' + (isUnread ? ' unread' : '') + (isRead ? ' read' : '')}>
|
||||
<a
|
||||
href={user ? app.route.user(user) : '#'}
|
||||
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()) })
|
||||
)}
|
||||
config={function (element) {
|
||||
$(element).tooltip({ placement: 'right' });
|
||||
m.route.apply(this, arguments);
|
||||
oncreate={function (vnode) {
|
||||
$(vnode.dom).tooltip({ placement: 'right' });
|
||||
}}
|
||||
>
|
||||
{avatar(user, { title: '' })}
|
||||
@ -114,7 +112,7 @@ export default class DiscussionListItem extends Component {
|
||||
|
||||
<ul className="DiscussionListItem-badges badges">{listItems(discussion.badges().toArray())}</ul>
|
||||
|
||||
<a href={app.route.discussion(discussion, jumpTo)} config={m.route} className="DiscussionListItem-main">
|
||||
<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>
|
||||
@ -131,8 +129,8 @@ export default class DiscussionListItem extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
config(isInitialized) {
|
||||
if (isInitialized) return;
|
||||
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
|
||||
@ -144,6 +142,12 @@ export default class DiscussionListItem extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
onbeforeupdate(vnode, old) {
|
||||
super.onbeforeupdate(vnode, old);
|
||||
|
||||
return this.subtree.needsRebuild();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether or not the discussion is currently being viewed.
|
||||
*
|
||||
@ -152,7 +156,7 @@ export default class DiscussionListItem extends Component {
|
||||
active() {
|
||||
const idParam = m.route.param('id');
|
||||
|
||||
return idParam && idParam.split('-')[0] === this.props.discussion.id();
|
||||
return idParam && idParam.split('-')[0] === this.attrs.discussion.id();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -163,7 +167,7 @@ export default class DiscussionListItem extends Component {
|
||||
* @return {Boolean}
|
||||
*/
|
||||
showFirstPost() {
|
||||
return ['newest', 'oldest'].indexOf(this.props.params.sort) !== -1;
|
||||
return ['newest', 'oldest'].indexOf(this.attrs.params.sort) !== -1;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -173,14 +177,14 @@ export default class DiscussionListItem extends Component {
|
||||
* @return {Boolean}
|
||||
*/
|
||||
showRepliesCount() {
|
||||
return this.props.params.sort === 'replies';
|
||||
return this.attrs.params.sort === 'replies';
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the discussion as read.
|
||||
*/
|
||||
markAsRead() {
|
||||
const discussion = this.props.discussion;
|
||||
const discussion = this.attrs.discussion;
|
||||
|
||||
if (discussion.isUnread()) {
|
||||
discussion.save({ lastReadPostNumber: discussion.lastPostNumber() });
|
||||
@ -197,8 +201,8 @@ export default class DiscussionListItem extends Component {
|
||||
infoItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
if (this.props.params.q) {
|
||||
const post = this.props.discussion.mostRelevantPost() || this.props.discussion.firstPost();
|
||||
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);
|
||||
@ -208,7 +212,7 @@ export default class DiscussionListItem extends Component {
|
||||
items.add(
|
||||
'terminalPost',
|
||||
TerminalPost.component({
|
||||
discussion: this.props.discussion,
|
||||
discussion: this.attrs.discussion,
|
||||
lastPost: !this.showFirstPost(),
|
||||
})
|
||||
);
|
||||
|
Reference in New Issue
Block a user