1
0
mirror of https://github.com/flarum/core.git synced 2025-07-27 19:50:20 +02:00
Files
php-flarum/js/forum/src/components/discussion-page.js
2015-04-25 22:28:39 +09:30

281 lines
8.3 KiB
JavaScript

import Component from 'flarum/component';
import ItemList from 'flarum/utils/item-list';
import IndexPage from 'flarum/components/index-page';
import PostStream from 'flarum/utils/post-stream';
import DiscussionList from 'flarum/components/discussion-list';
import StreamContent from 'flarum/components/stream-content';
import StreamScrubber from 'flarum/components/stream-scrubber';
import ComposerReply from 'flarum/components/composer-reply';
import ActionButton from 'flarum/components/action-button';
import LoadingIndicator from 'flarum/components/loading-indicator';
import DropdownSplit from 'flarum/components/dropdown-split';
import Separator from 'flarum/components/separator';
import listItems from 'flarum/helpers/list-items';
export default class DiscussionPage extends Component {
/**
*/
constructor(props) {
super(props);
this.discussion = m.prop();
// Set up the stream. The stream is an object that represents the posts in
// a discussion, as they're displayed on the screen (i.e. missing posts
// are condensed into "load more" gaps).
this.stream = m.prop();
// Get the discussion. We may already have a copy of it in our store, so
// we'll start off with that. If we do have a copy of the discussion, and
// its posts relationship has been loaded (i.e. we've viewed this
// discussion before), then we can proceed with displaying it immediately.
// If not, we'll make an API request first.
app.store.find('discussions', m.route.param('id'), {
near: this.currentNear = m.route.param('near'),
include: 'posts'
}).then(this.setupDiscussion.bind(this));
if (app.cache.discussionList) {
app.pane.enable();
app.pane.hide();
m.redraw.strategy('diff'); // otherwise pane redraws and mouseenter even is triggered so it doesn't hide
}
app.history.push('discussion');
app.current = this;
app.composer.minimize();
}
/*
*/
setupDiscussion(discussion) {
this.discussion(discussion);
var includedPosts = [];
discussion.payload.included.forEach(record => {
if (record.type === 'posts') {
includedPosts.push(record.id);
}
});
// Set up the post stream for this discussion, and add all of the posts we
// have loaded so far.
this.stream(new PostStream(discussion));
this.stream().addPosts(discussion.posts().filter(value => value && includedPosts.indexOf(value.id()) !== -1));
this.streamContent = new StreamContent({
stream: this.stream(),
className: 'discussion-posts posts',
positionChanged: this.positionChanged.bind(this)
});
// Hold up there skippy! If the slug in the URL doesn't match up, we'll
// redirect so we have the correct one.
if (m.route.param('id') === discussion.id() && m.route.param('slug') !== discussion.slug()) {
var params = m.route.param();
params.slug = discussion.slug();
params.near = params.near || '';
m.route(app.route('discussion.near', params), null, true);
return;
}
this.streamContent.goToNumber(this.currentNear, true);
}
onload(element, isInitialized, context) {
if (isInitialized) { return; }
context.retain = true;
$('body').addClass('discussion-page');
context.onunload = function() {
$('body').removeClass('discussion-page');
}
}
/**
*/
onunload(e) {
// If we have routed to the same discussion as we were viewing previously,
// cancel the unloading of this controller and instead prompt the post
// stream to jump to the new 'near' param.
var discussion = this.discussion();
if (discussion) {
var discussionRoute = app.route('discussion', discussion);
if (m.route().substr(0, discussionRoute.length) === discussionRoute) {
e.preventDefault();
if (m.route.param('near') != this.currentNear) {
this.streamContent.goToNumber(m.route.param('near'));
}
return;
}
}
app.pane.disable();
}
/**
*/
view() {
var discussion = this.discussion();
return m('div', {config: this.onload.bind(this)}, [
app.cache.discussionList ? m('div.index-area.paned', {config: this.configIndex.bind(this)}, app.cache.discussionList.view()) : '',
m('div.discussion-area', discussion ? [
m('header.hero.discussion-hero', [
m('div.container', [
m('ul.badges', listItems(discussion.badges().toArray())), ' ',
m('h2.discussion-title', discussion.title())
])
]),
m('div.container', [
m('nav.discussion-nav', [
m('ul', listItems(this.sidebarItems().toArray()))
]),
this.streamContent.view()
])
] : LoadingIndicator.component({className: 'loading-indicator-block'}))
]);
}
/**
*/
configIndex(element, isInitialized, context) {
if (isInitialized) { return; }
context.retain = true;
// When viewing a discussion (for which the discussions route is the
// parent,) the discussion list is still rendered but it becomes a
// pane hidden on the side of the screen. When the mouse enters and
// leaves the discussions pane, we want to show and hide the pane
// respectively. We also create a 10px 'hot edge' on the left of the
// screen to activate the pane.
var pane = app.pane;
$(element).hover(pane.show.bind(pane), pane.onmouseleave.bind(pane));
var hotEdge = function(e) {
if (e.pageX < 10) { pane.show(); }
};
$(document).on('mousemove', hotEdge);
context.onunload = function() {
$(document).off('mousemove', hotEdge);
};
}
/**
*/
sidebarItems() {
var items = new ItemList();
items.add('controls',
DropdownSplit.component({
items: this.controlItems().toArray(),
icon: 'reply',
buttonClass: 'btn btn-primary',
wrapperClass: 'primary-control'
})
);
items.add('scrubber',
StreamScrubber.component({
streamContent: this.streamContent,
wrapperClass: 'title-control'
})
);
return items;
}
/**
*/
controlItems() {
var items = new ItemList();
var discussion = this.discussion();
items.add('reply', ActionButton.component({ icon: 'reply', label: 'Reply', onclick: this.reply.bind(this) }));
items.add('separator', Separator.component());
if (discussion.canEdit()) {
items.add('rename', ActionButton.component({ icon: 'pencil', label: 'Rename', onclick: this.rename.bind(this) }));
}
if (discussion.canDelete()) {
items.add('delete', ActionButton.component({ icon: 'times', label: 'Delete', onclick: this.delete.bind(this) }));
}
return items;
}
reply() {
if (app.session.user()) {
this.streamContent.goToLast();
if (!this.composer || app.composer.component !== this.composer) {
this.composer = new ComposerReply({
user: app.session.user(),
discussion: this.discussion()
});
app.composer.load(this.composer);
}
app.composer.show();
} else {
// signup
}
}
delete() {
if (confirm('Are you sure you want to delete this discussion?')) {
var discussion = this.discussion();
discussion.delete();
if (app.cache.discussionList) {
app.cache.discussionList.removeDiscussion(discussion);
}
app.history.back();
}
}
rename() {
var discussion = this.discussion();
var currentTitle = discussion.title();
var title = prompt('Enter a new title for this discussion:', currentTitle);
if (title && title !== currentTitle) {
discussion.save({title}).then(discussion => {
discussion.addedPosts().forEach(post => this.stream().addPostToEnd(post));
m.redraw();
});
}
}
/**
*/
positionChanged(startNumber, endNumber) {
var discussion = this.discussion();
var url = app.route('discussion.near', {
id: discussion.id(),
slug: discussion.slug(),
near: this.currentNear = startNumber
});
// https://github.com/lhorie/mithril.js/issues/559
m.route(url, true);
window.history.replaceState(null, document.title, (m.route.mode === 'hash' ? '#' : '')+url);
app.history.push('discussion');
if (app.session.user() && endNumber > discussion.readNumber()) {
discussion.save({readNumber: endNumber});
m.redraw();
}
}
}