mirror of
https://github.com/flarum/core.git
synced 2025-10-22 20:26:15 +02:00
Run prettier for all JS files
This commit is contained in:
@@ -22,7 +22,7 @@ export default class ForumApplication extends Application {
|
||||
* @type {Object}
|
||||
*/
|
||||
notificationComponents = {
|
||||
discussionRenamed: DiscussionRenamedNotification
|
||||
discussionRenamed: DiscussionRenamedNotification,
|
||||
};
|
||||
/**
|
||||
* A map of post types to their components.
|
||||
@@ -31,7 +31,7 @@ export default class ForumApplication extends Application {
|
||||
*/
|
||||
postComponents = {
|
||||
comment: CommentPost,
|
||||
discussionRenamed: DiscussionRenamedPost
|
||||
discussionRenamed: DiscussionRenamedPost,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -87,7 +87,7 @@ export default class ForumApplication extends Application {
|
||||
this.routes[defaultAction].path = '/';
|
||||
this.history.push(defaultAction, this.translator.trans('core.forum.header.back_to_index_tooltip'), '/');
|
||||
|
||||
m.mount(document.getElementById('app-navigation'), Navigation.component({className: 'App-backControl', drawer: true}));
|
||||
m.mount(document.getElementById('app-navigation'), Navigation.component({ className: 'App-backControl', drawer: true }));
|
||||
m.mount(document.getElementById('header-navigation'), Navigation.component());
|
||||
m.mount(document.getElementById('header-primary'), HeaderPrimary.component());
|
||||
m.mount(document.getElementById('header-secondary'), HeaderSecondary.component());
|
||||
@@ -102,7 +102,7 @@ export default class ForumApplication extends Application {
|
||||
|
||||
// Route the home link back home when clicked. We do not want it to register
|
||||
// if the user is opening it in a new tab, however.
|
||||
$('#home-link').click(e => {
|
||||
$('#home-link').click((e) => {
|
||||
if (e.ctrlKey || e.metaKey || e.which === 2) return;
|
||||
e.preventDefault();
|
||||
app.history.home();
|
||||
@@ -123,9 +123,11 @@ export default class ForumApplication extends Application {
|
||||
* @return {Boolean}
|
||||
*/
|
||||
composingReplyTo(discussion) {
|
||||
return this.composer.component instanceof ReplyComposer &&
|
||||
return (
|
||||
this.composer.component instanceof ReplyComposer &&
|
||||
this.composer.component.props.discussion === discussion &&
|
||||
this.composer.position !== Composer.PositionEnum.HIDDEN;
|
||||
this.composer.position !== Composer.PositionEnum.HIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,8 +137,7 @@ export default class ForumApplication extends Application {
|
||||
* @return {Boolean}
|
||||
*/
|
||||
viewingDiscussion(discussion) {
|
||||
return this.current instanceof DiscussionPage &&
|
||||
this.current.discussion === discussion;
|
||||
return this.current instanceof DiscussionPage && this.current.discussion === discussion;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -134,6 +134,6 @@ export default Object.assign(compat, {
|
||||
'components/DiscussionListItem': DiscussionListItem,
|
||||
'components/LoadingPost': LoadingPost,
|
||||
'components/PostsUserPage': PostsUserPage,
|
||||
'routes': routes,
|
||||
'ForumApplication': ForumApplication
|
||||
routes: routes,
|
||||
ForumApplication: ForumApplication,
|
||||
});
|
||||
|
@@ -44,7 +44,8 @@ export default class AvatarEditor extends Component {
|
||||
return (
|
||||
<div className={'AvatarEditor Dropdown ' + this.props.className + (this.loading ? ' loading' : '') + (this.isDraggedOver ? ' dragover' : '')}>
|
||||
{avatar(user)}
|
||||
<a className={ user.avatarUrl() ? "Dropdown-toggle" : "Dropdown-toggle AvatarEditor--noAvatar" }
|
||||
<a
|
||||
className={user.avatarUrl() ? 'Dropdown-toggle' : 'Dropdown-toggle AvatarEditor--noAvatar'}
|
||||
title={app.translator.trans('core.forum.user.avatar_upload_tooltip')}
|
||||
data-toggle="dropdown"
|
||||
onclick={this.quickUpload.bind(this)}
|
||||
@@ -52,12 +53,11 @@ export default class AvatarEditor extends Component {
|
||||
ondragenter={this.enableDragover.bind(this)}
|
||||
ondragleave={this.disableDragover.bind(this)}
|
||||
ondragend={this.disableDragover.bind(this)}
|
||||
ondrop={this.dropUpload.bind(this)}>
|
||||
{this.loading ? LoadingIndicator.component() : (user.avatarUrl() ? icon('fas fa-pencil-alt') : icon('fas fa-plus-circle'))}
|
||||
ondrop={this.dropUpload.bind(this)}
|
||||
>
|
||||
{this.loading ? LoadingIndicator.component() : user.avatarUrl() ? icon('fas fa-pencil-alt') : icon('fas fa-plus-circle')}
|
||||
</a>
|
||||
<ul className="Dropdown-menu Menu">
|
||||
{listItems(this.controlItems().toArray())}
|
||||
</ul>
|
||||
<ul className="Dropdown-menu Menu">{listItems(this.controlItems().toArray())}</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -70,19 +70,21 @@ export default class AvatarEditor extends Component {
|
||||
controlItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('upload',
|
||||
items.add(
|
||||
'upload',
|
||||
Button.component({
|
||||
icon: 'fas fa-upload',
|
||||
children: app.translator.trans('core.forum.user.avatar_upload_button'),
|
||||
onclick: this.openPicker.bind(this)
|
||||
onclick: this.openPicker.bind(this),
|
||||
})
|
||||
);
|
||||
|
||||
items.add('remove',
|
||||
items.add(
|
||||
'remove',
|
||||
Button.component({
|
||||
icon: 'fas fa-times',
|
||||
children: app.translator.trans('core.forum.user.avatar_remove_button'),
|
||||
onclick: this.remove.bind(this)
|
||||
onclick: this.remove.bind(this),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -150,9 +152,13 @@ export default class AvatarEditor extends Component {
|
||||
const user = this.props.user;
|
||||
const $input = $('<input type="file">');
|
||||
|
||||
$input.appendTo('body').hide().click().on('input', e => {
|
||||
this.upload($(e.target)[0].files[0]);
|
||||
});
|
||||
$input
|
||||
.appendTo('body')
|
||||
.hide()
|
||||
.click()
|
||||
.on('input', (e) => {
|
||||
this.upload($(e.target)[0].files[0]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,15 +176,14 @@ export default class AvatarEditor extends Component {
|
||||
this.loading = true;
|
||||
m.redraw();
|
||||
|
||||
app.request({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/avatar',
|
||||
serialize: raw => raw,
|
||||
data
|
||||
}).then(
|
||||
this.success.bind(this),
|
||||
this.failure.bind(this)
|
||||
);
|
||||
app
|
||||
.request({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/avatar',
|
||||
serialize: (raw) => raw,
|
||||
data,
|
||||
})
|
||||
.then(this.success.bind(this), this.failure.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,13 +195,12 @@ export default class AvatarEditor extends Component {
|
||||
this.loading = true;
|
||||
m.redraw();
|
||||
|
||||
app.request({
|
||||
method: 'DELETE',
|
||||
url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/avatar'
|
||||
}).then(
|
||||
this.success.bind(this),
|
||||
this.failure.bind(this)
|
||||
);
|
||||
app
|
||||
.request({
|
||||
method: 'DELETE',
|
||||
url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/avatar',
|
||||
})
|
||||
.then(this.success.bind(this), this.failure.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -44,7 +44,9 @@ export default class ChangeEmailModal extends Modal {
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<div className="Form Form--centered">
|
||||
<p className="helpText">{app.translator.trans('core.forum.change_email.confirmation_message', {email: <strong>{this.email()}</strong>})}</p>
|
||||
<p className="helpText">
|
||||
{app.translator.trans('core.forum.change_email.confirmation_message', { email: <strong>{this.email()}</strong> })}
|
||||
</p>
|
||||
<div className="Form-group">
|
||||
<Button className="Button Button--primary Button--block" onclick={this.hide.bind(this)}>
|
||||
{app.translator.trans('core.forum.change_email.dismiss_button')}
|
||||
@@ -59,23 +61,31 @@ export default class ChangeEmailModal extends Modal {
|
||||
<div className="Modal-body">
|
||||
<div className="Form Form--centered">
|
||||
<div className="Form-group">
|
||||
<input type="email" name="email" className="FormControl"
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
className="FormControl"
|
||||
placeholder={app.session.user.email()}
|
||||
bidi={this.email}
|
||||
disabled={this.loading}/>
|
||||
disabled={this.loading}
|
||||
/>
|
||||
</div>
|
||||
<div className="Form-group">
|
||||
<input type="password" name="password" className="FormControl"
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
className="FormControl"
|
||||
placeholder={app.translator.trans('core.forum.change_email.confirm_password_placeholder')}
|
||||
bidi={this.password}
|
||||
disabled={this.loading}/>
|
||||
disabled={this.loading}
|
||||
/>
|
||||
</div>
|
||||
<div className="Form-group">
|
||||
{Button.component({
|
||||
className: 'Button Button--primary Button--block',
|
||||
type: 'submit',
|
||||
loading: this.loading,
|
||||
children: app.translator.trans('core.forum.change_email.submit_button')
|
||||
children: app.translator.trans('core.forum.change_email.submit_button'),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,11 +107,15 @@ export default class ChangeEmailModal extends Modal {
|
||||
|
||||
this.loading = true;
|
||||
|
||||
app.session.user.save({email: this.email()}, {
|
||||
errorHandler: this.onerror.bind(this),
|
||||
meta: {password: this.password()}
|
||||
})
|
||||
.then(() => this.success = true)
|
||||
app.session.user
|
||||
.save(
|
||||
{ email: this.email() },
|
||||
{
|
||||
errorHandler: this.onerror.bind(this),
|
||||
meta: { password: this.password() },
|
||||
}
|
||||
)
|
||||
.then(() => (this.success = true))
|
||||
.catch(() => {})
|
||||
.then(this.loaded.bind(this));
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@ export default class ChangePasswordModal extends Modal {
|
||||
className: 'Button Button--primary Button--block',
|
||||
type: 'submit',
|
||||
loading: this.loading,
|
||||
children: app.translator.trans('core.forum.change_password.send_button')
|
||||
children: app.translator.trans('core.forum.change_password.send_button'),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,13 +37,12 @@ export default class ChangePasswordModal extends Modal {
|
||||
|
||||
this.loading = true;
|
||||
|
||||
app.request({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/forgot',
|
||||
data: {email: app.session.user.email()}
|
||||
}).then(
|
||||
this.hide.bind(this),
|
||||
this.loaded.bind(this)
|
||||
);
|
||||
app
|
||||
.request({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/forgot',
|
||||
data: { email: app.session.user.email() },
|
||||
})
|
||||
.then(this.hide.bind(this), this.loaded.bind(this));
|
||||
}
|
||||
}
|
||||
|
@@ -33,7 +33,7 @@ export default class CommentPost extends Post {
|
||||
|
||||
// Create an instance of the component that displays the post's author so
|
||||
// that we can force the post to rerender when the user card is shown.
|
||||
this.postUser = new PostUser({post: this.props.post});
|
||||
this.postUser = new PostUser({ post: this.props.post });
|
||||
this.subtree.check(
|
||||
() => this.postUser.cardVisible,
|
||||
() => this.isEditing()
|
||||
@@ -44,14 +44,14 @@ export default class CommentPost extends Post {
|
||||
// Note: we avoid using JSX for the <ul> below because it results in some
|
||||
// weirdness in Mithril.js 0.1.x (see flarum/core#975). This workaround can
|
||||
// be reverted when we upgrade to Mithril 1.0.
|
||||
return super.content().concat([
|
||||
<header className="Post-header">{m('ul', listItems(this.headerItems().toArray()))}</header>,
|
||||
<div className="Post-body">
|
||||
{this.isEditing()
|
||||
? <div className="Post-preview" config={this.configPreview.bind(this)}/>
|
||||
: m.trust(this.props.post.contentHtml())}
|
||||
</div>
|
||||
]);
|
||||
return super
|
||||
.content()
|
||||
.concat([
|
||||
<header className="Post-header">{m('ul', listItems(this.headerItems().toArray()))}</header>,
|
||||
<div className="Post-body">
|
||||
{this.isEditing() ? <div className="Post-preview" config={this.configPreview.bind(this)} /> : m.trust(this.props.post.contentHtml())}
|
||||
</div>,
|
||||
]);
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
@@ -63,7 +63,7 @@ export default class CommentPost extends Post {
|
||||
// all of the <script> tags in the content and evaluate them. This is
|
||||
// necessary because TextFormatter outputs them for e.g. syntax highlighting.
|
||||
if (context.contentHtml !== contentHtml) {
|
||||
this.$('.Post-body script').each(function() {
|
||||
this.$('.Post-body script').each(function () {
|
||||
eval.call(window, $(this).text());
|
||||
});
|
||||
}
|
||||
@@ -72,21 +72,23 @@ export default class CommentPost extends Post {
|
||||
}
|
||||
|
||||
isEditing() {
|
||||
return app.composer.component instanceof EditPostComposer &&
|
||||
app.composer.component.props.post === this.props.post;
|
||||
return app.composer.component instanceof EditPostComposer && app.composer.component.props.post === this.props.post;
|
||||
}
|
||||
|
||||
attrs() {
|
||||
const post = this.props.post;
|
||||
const attrs = super.attrs();
|
||||
|
||||
attrs.className = (attrs.className || '') + ' ' + classList({
|
||||
'CommentPost': true,
|
||||
'Post--hidden': post.isHidden(),
|
||||
'Post--edited': post.isEdited(),
|
||||
'revealContent': this.revealContent,
|
||||
'editing': this.isEditing()
|
||||
});
|
||||
attrs.className =
|
||||
(attrs.className || '') +
|
||||
' ' +
|
||||
classList({
|
||||
CommentPost: true,
|
||||
'Post--hidden': post.isHidden(),
|
||||
'Post--edited': post.isEdited(),
|
||||
revealContent: this.revealContent,
|
||||
editing: this.isEditing(),
|
||||
});
|
||||
|
||||
return attrs;
|
||||
}
|
||||
@@ -127,7 +129,7 @@ export default class CommentPost extends Post {
|
||||
headerItems() {
|
||||
const items = new ItemList();
|
||||
const post = this.props.post;
|
||||
const props = {post};
|
||||
const props = { post };
|
||||
|
||||
items.add('user', this.postUser.render(), 100);
|
||||
items.add('meta', PostMeta.component(props));
|
||||
@@ -139,13 +141,14 @@ export default class CommentPost extends Post {
|
||||
// If the post is hidden, add a button that allows toggling the visibility
|
||||
// of the post's content.
|
||||
if (post.isHidden()) {
|
||||
items.add('toggle', (
|
||||
items.add(
|
||||
'toggle',
|
||||
Button.component({
|
||||
className: 'Button Button--default Button--more',
|
||||
icon: 'fas fa-ellipsis-h',
|
||||
onclick: this.toggleContent.bind(this)
|
||||
onclick: this.toggleContent.bind(this),
|
||||
})
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
|
@@ -36,10 +36,10 @@ class Composer extends Component {
|
||||
|
||||
view() {
|
||||
const classes = {
|
||||
'normal': this.position === Composer.PositionEnum.NORMAL,
|
||||
'minimized': this.position === Composer.PositionEnum.MINIMIZED,
|
||||
'fullScreen': this.position === Composer.PositionEnum.FULLSCREEN,
|
||||
'active': this.active
|
||||
normal: this.position === Composer.PositionEnum.NORMAL,
|
||||
minimized: this.position === Composer.PositionEnum.MINIMIZED,
|
||||
fullScreen: this.position === Composer.PositionEnum.FULLSCREEN,
|
||||
active: this.active,
|
||||
};
|
||||
classes.visible = classes.normal || classes.minimized || classes.fullScreen;
|
||||
|
||||
@@ -52,7 +52,7 @@ class Composer extends Component {
|
||||
|
||||
return (
|
||||
<div className={'Composer ' + classList(classes)}>
|
||||
<div className="Composer-handle" config={this.configHandle.bind(this)}/>
|
||||
<div className="Composer-handle" config={this.configHandle.bind(this)} />
|
||||
<ul className="Composer-controls">{listItems(this.controlItems().toArray())}</ul>
|
||||
<div className="Composer-content" onclick={showIfMinimized}>
|
||||
{this.component ? this.component.render() : ''}
|
||||
@@ -77,7 +77,7 @@ class Composer extends Component {
|
||||
|
||||
// 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.
|
||||
this.$().on('focus blur', ':input', e => {
|
||||
this.$().on('focus blur', ':input', (e) => {
|
||||
this.active = e.type === 'focusin';
|
||||
m.redraw();
|
||||
});
|
||||
@@ -94,18 +94,18 @@ class Composer extends Component {
|
||||
|
||||
const handlers = {};
|
||||
|
||||
$(window).on('resize', handlers.onresize = this.updateHeight.bind(this)).resize();
|
||||
$(window)
|
||||
.on('resize', (handlers.onresize = this.updateHeight.bind(this)))
|
||||
.resize();
|
||||
|
||||
$(document)
|
||||
.on('mousemove', handlers.onmousemove = this.onmousemove.bind(this))
|
||||
.on('mouseup', handlers.onmouseup = this.onmouseup.bind(this));
|
||||
.on('mousemove', (handlers.onmousemove = this.onmousemove.bind(this)))
|
||||
.on('mouseup', (handlers.onmouseup = this.onmouseup.bind(this)));
|
||||
|
||||
context.onunload = () => {
|
||||
$(window).off('resize', handlers.onresize);
|
||||
|
||||
$(document)
|
||||
.off('mousemove', handlers.onmousemove)
|
||||
.off('mouseup', handlers.onmouseup);
|
||||
$(document).off('mousemove', handlers.onmousemove).off('mouseup', handlers.onmouseup);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -121,9 +121,10 @@ class Composer extends Component {
|
||||
|
||||
const composer = this;
|
||||
|
||||
$(element).css('cursor', 'row-resize')
|
||||
.bind('dragstart mousedown', e => e.preventDefault())
|
||||
.mousedown(function(e) {
|
||||
$(element)
|
||||
.css('cursor', 'row-resize')
|
||||
.bind('dragstart mousedown', (e) => e.preventDefault())
|
||||
.mousedown(function (e) {
|
||||
composer.mouseStart = e.clientY;
|
||||
composer.heightStart = composer.$().height();
|
||||
composer.handle = $(this);
|
||||
@@ -191,15 +192,12 @@ class Composer extends Component {
|
||||
* scrolled right to the bottom.
|
||||
*/
|
||||
updateBodyPadding() {
|
||||
const visible = this.position !== Composer.PositionEnum.HIDDEN &&
|
||||
this.position !== Composer.PositionEnum.MINIMIZED &&
|
||||
this.$().css('position') !== 'absolute';
|
||||
const visible =
|
||||
this.position !== Composer.PositionEnum.HIDDEN && this.position !== Composer.PositionEnum.MINIMIZED && this.$().css('position') !== 'absolute';
|
||||
|
||||
const paddingBottom = visible
|
||||
? this.computedHeight() - parseInt($('#app').css('padding-bottom'), 10)
|
||||
: 0;
|
||||
const paddingBottom = visible ? this.computedHeight() - parseInt($('#app').css('padding-bottom'), 10) : 0;
|
||||
|
||||
$('#content').css({paddingBottom});
|
||||
$('#content').css({ paddingBottom });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -289,12 +287,12 @@ class Composer extends Component {
|
||||
const newHeight = $composer.outerHeight();
|
||||
|
||||
if (oldPosition === Composer.PositionEnum.HIDDEN) {
|
||||
$composer.css({bottom: -newHeight, height: newHeight});
|
||||
$composer.css({ bottom: -newHeight, height: newHeight });
|
||||
} else {
|
||||
$composer.css({height: oldHeight});
|
||||
$composer.css({ height: oldHeight });
|
||||
}
|
||||
|
||||
$composer.animate({bottom: 0, height: newHeight}, 'fast', () => this.component.focus());
|
||||
$composer.animate({ bottom: 0, height: newHeight }, 'fast', () => this.component.focus());
|
||||
|
||||
this.updateBodyPadding();
|
||||
$(window).scrollTop(scrollTop);
|
||||
@@ -304,9 +302,7 @@ class Composer extends Component {
|
||||
* Show the Composer backdrop.
|
||||
*/
|
||||
showBackdrop() {
|
||||
this.$backdrop = $('<div/>')
|
||||
.addClass('composer-backdrop')
|
||||
.appendTo('body');
|
||||
this.$backdrop = $('<div/>').addClass('composer-backdrop').appendTo('body');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -346,7 +342,7 @@ class Composer extends Component {
|
||||
// Animate the composer sliding down off the bottom edge of the viewport.
|
||||
// Only when the animation is completed, update the Composer state flag and
|
||||
// other elements on the page.
|
||||
$composer.stop(true).animate({bottom: -$composer.height()}, 'fast', () => {
|
||||
$composer.stop(true).animate({ bottom: -$composer.height() }, 'fast', () => {
|
||||
this.position = Composer.PositionEnum.HIDDEN;
|
||||
this.clear();
|
||||
m.redraw();
|
||||
@@ -421,32 +417,44 @@ class Composer extends Component {
|
||||
const items = new ItemList();
|
||||
|
||||
if (this.position === Composer.PositionEnum.FULLSCREEN) {
|
||||
items.add('exitFullScreen', ComposerButton.component({
|
||||
icon: 'fas fa-compress',
|
||||
title: app.translator.trans('core.forum.composer.exit_full_screen_tooltip'),
|
||||
onclick: this.exitFullScreen.bind(this)
|
||||
}));
|
||||
items.add(
|
||||
'exitFullScreen',
|
||||
ComposerButton.component({
|
||||
icon: 'fas fa-compress',
|
||||
title: app.translator.trans('core.forum.composer.exit_full_screen_tooltip'),
|
||||
onclick: this.exitFullScreen.bind(this),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
if (this.position !== Composer.PositionEnum.MINIMIZED) {
|
||||
items.add('minimize', ComposerButton.component({
|
||||
icon: 'fas fa-minus minimize',
|
||||
title: app.translator.trans('core.forum.composer.minimize_tooltip'),
|
||||
onclick: this.minimize.bind(this),
|
||||
itemClassName: 'App-backControl'
|
||||
}));
|
||||
items.add(
|
||||
'minimize',
|
||||
ComposerButton.component({
|
||||
icon: 'fas fa-minus minimize',
|
||||
title: app.translator.trans('core.forum.composer.minimize_tooltip'),
|
||||
onclick: this.minimize.bind(this),
|
||||
itemClassName: 'App-backControl',
|
||||
})
|
||||
);
|
||||
|
||||
items.add('fullScreen', ComposerButton.component({
|
||||
icon: 'fas fa-expand',
|
||||
title: app.translator.trans('core.forum.composer.full_screen_tooltip'),
|
||||
onclick: this.fullScreen.bind(this)
|
||||
}));
|
||||
items.add(
|
||||
'fullScreen',
|
||||
ComposerButton.component({
|
||||
icon: 'fas fa-expand',
|
||||
title: app.translator.trans('core.forum.composer.full_screen_tooltip'),
|
||||
onclick: this.fullScreen.bind(this),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
items.add('close', ComposerButton.component({
|
||||
icon: 'fas fa-times',
|
||||
title: app.translator.trans('core.forum.composer.close_tooltip'),
|
||||
onclick: this.close.bind(this)
|
||||
}));
|
||||
items.add(
|
||||
'close',
|
||||
ComposerButton.component({
|
||||
icon: 'fas fa-times',
|
||||
title: app.translator.trans('core.forum.composer.close_tooltip'),
|
||||
onclick: this.close.bind(this),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
@@ -524,7 +532,7 @@ Composer.PositionEnum = {
|
||||
HIDDEN: 'hidden',
|
||||
NORMAL: 'normal',
|
||||
MINIMIZED: 'minimized',
|
||||
FULLSCREEN: 'fullScreen'
|
||||
FULLSCREEN: 'fullScreen',
|
||||
};
|
||||
|
||||
export default Composer;
|
||||
|
@@ -47,7 +47,7 @@ export default class ComposerBody extends Component {
|
||||
placeholder: this.props.placeholder,
|
||||
onchange: this.content,
|
||||
onsubmit: this.onsubmit.bind(this),
|
||||
value: this.content()
|
||||
value: this.content(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,12 +57,12 @@ export default class ComposerBody extends Component {
|
||||
|
||||
return (
|
||||
<div className={'ComposerBody ' + (this.props.className || '')}>
|
||||
{avatar(this.props.user, {className: 'ComposerBody-avatar'})}
|
||||
{avatar(this.props.user, { className: 'ComposerBody-avatar' })}
|
||||
<div className="ComposerBody-content">
|
||||
<ul className="ComposerBody-header">{listItems(this.headerItems().toArray())}</ul>
|
||||
<div className="ComposerBody-editor">{this.editor.render()}</div>
|
||||
</div>
|
||||
{LoadingIndicator.component({className: 'ComposerBody-loading' + (this.loading ? ' active' : '')})}
|
||||
{LoadingIndicator.component({ className: 'ComposerBody-loading' + (this.loading ? ' active' : '') })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -100,8 +100,7 @@ export default class ComposerBody extends Component {
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
onsubmit() {
|
||||
}
|
||||
onsubmit() {}
|
||||
|
||||
/**
|
||||
* Stop loading.
|
||||
|
@@ -39,16 +39,19 @@ export default class DiscussionComposer extends ComposerBody {
|
||||
|
||||
items.add('title', <h3>{app.translator.trans('core.forum.composer_discussion.title')}</h3>, 100);
|
||||
|
||||
items.add('discussionTitle', (
|
||||
items.add(
|
||||
'discussionTitle',
|
||||
<h3>
|
||||
<input className="FormControl"
|
||||
<input
|
||||
className="FormControl"
|
||||
value={this.title()}
|
||||
oninput={m.withAttr('value', this.title)}
|
||||
placeholder={this.props.titlePlaceholder}
|
||||
disabled={!!this.props.disabled}
|
||||
onkeydown={this.onkeydown.bind(this)}/>
|
||||
onkeydown={this.onkeydown.bind(this)}
|
||||
/>
|
||||
</h3>
|
||||
));
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -60,7 +63,8 @@ export default class DiscussionComposer extends ComposerBody {
|
||||
* @param {Event} e
|
||||
*/
|
||||
onkeydown(e) {
|
||||
if (e.which === 13) { // Return
|
||||
if (e.which === 13) {
|
||||
// Return
|
||||
e.preventDefault();
|
||||
this.editor.setSelectionRange(0, 0);
|
||||
}
|
||||
@@ -80,7 +84,7 @@ export default class DiscussionComposer extends ComposerBody {
|
||||
data() {
|
||||
return {
|
||||
title: this.title(),
|
||||
content: this.content()
|
||||
content: this.content(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -89,13 +93,13 @@ export default class DiscussionComposer extends ComposerBody {
|
||||
|
||||
const data = this.data();
|
||||
|
||||
app.store.createRecord('discussions').save(data).then(
|
||||
discussion => {
|
||||
app.store
|
||||
.createRecord('discussions')
|
||||
.save(data)
|
||||
.then((discussion) => {
|
||||
app.composer.hide();
|
||||
app.cache.discussionList.refresh();
|
||||
m.route(app.route.discussion(discussion));
|
||||
},
|
||||
this.loaded.bind(this)
|
||||
);
|
||||
}, this.loaded.bind(this));
|
||||
}
|
||||
}
|
||||
|
@@ -48,33 +48,27 @@ export default class DiscussionList extends Component {
|
||||
loading = Button.component({
|
||||
children: app.translator.trans('core.forum.discussion_list.load_more_button'),
|
||||
className: 'Button',
|
||||
onclick: this.loadMore.bind(this)
|
||||
onclick: this.loadMore.bind(this),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.discussions.length === 0 && !this.loading) {
|
||||
const text = app.translator.trans('core.forum.discussion_list.empty_text');
|
||||
return (
|
||||
<div className="DiscussionList">
|
||||
{Placeholder.component({text})}
|
||||
</div>
|
||||
);
|
||||
return <div className="DiscussionList">{Placeholder.component({ text })}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'DiscussionList'+(this.props.params.q ? ' DiscussionList--searchResults' : '')}>
|
||||
<div className={'DiscussionList' + (this.props.params.q ? ' DiscussionList--searchResults' : '')}>
|
||||
<ul className="DiscussionList-discussions">
|
||||
{this.discussions.map(discussion => {
|
||||
{this.discussions.map((discussion) => {
|
||||
return (
|
||||
<li key={discussion.id()} data-id={discussion.id()}>
|
||||
{DiscussionListItem.component({discussion, params})}
|
||||
{DiscussionListItem.component({ discussion, params })}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<div className="DiscussionList-loadMore">
|
||||
{loading}
|
||||
</div>
|
||||
<div className="DiscussionList-loadMore">{loading}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -87,7 +81,7 @@ export default class DiscussionList extends Component {
|
||||
* @api
|
||||
*/
|
||||
requestParams() {
|
||||
const params = {include: ['user', 'lastPostedUser'], filter: {}};
|
||||
const params = { include: ['user', 'lastPostedUser'], filter: {} };
|
||||
|
||||
params.sort = this.sortMap()[this.props.params.sort];
|
||||
|
||||
@@ -132,7 +126,7 @@ export default class DiscussionList extends Component {
|
||||
}
|
||||
|
||||
return this.loadResults().then(
|
||||
results => {
|
||||
(results) => {
|
||||
this.discussions = [];
|
||||
this.parseResults(results);
|
||||
},
|
||||
@@ -157,7 +151,7 @@ export default class DiscussionList extends Component {
|
||||
}
|
||||
|
||||
const params = this.requestParams();
|
||||
params.page = {offset};
|
||||
params.page = { offset };
|
||||
params.include = params.include.join(',');
|
||||
|
||||
return app.store.find('discussions', params);
|
||||
@@ -171,8 +165,7 @@ export default class DiscussionList extends Component {
|
||||
loadMore() {
|
||||
this.loading = true;
|
||||
|
||||
this.loadResults(this.discussions.length)
|
||||
.then(this.parseResults.bind(this));
|
||||
this.loadResults(this.discussions.length).then(this.parseResults.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -47,8 +47,8 @@ export default class DiscussionListItem extends Component {
|
||||
className: classList([
|
||||
'DiscussionListItem',
|
||||
this.active() ? 'active' : '',
|
||||
this.props.discussion.isHidden() ? 'DiscussionListItem--hidden' : ''
|
||||
])
|
||||
this.props.discussion.isHidden() ? 'DiscussionListItem--hidden' : '',
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -73,50 +73,56 @@ export default class DiscussionListItem extends Component {
|
||||
}
|
||||
|
||||
const phrase = this.props.params.q;
|
||||
this.highlightRegExp = new RegExp(phrase+'|'+phrase.trim().replace(/\s+/g, '|'), 'gi');
|
||||
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',
|
||||
children: controls,
|
||||
className: 'DiscussionListItem-controls',
|
||||
buttonClassName: 'Button Button--icon Button--flat Slidable-underneath Slidable-underneath--right'
|
||||
}) : ''}
|
||||
{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',
|
||||
})
|
||||
: ''}
|
||||
|
||||
<a className={'Slidable-underneath Slidable-underneath--left Slidable-underneath--elastic' + (isUnread ? '' : ' disabled')}
|
||||
onclick={this.markAsRead.bind(this)}>
|
||||
<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 href={user ? app.route.user(user) : '#'}
|
||||
<a
|
||||
href={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'});
|
||||
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);
|
||||
}}>
|
||||
{avatar(user, {title: ''})}
|
||||
}}
|
||||
>
|
||||
{avatar(user, { title: '' })}
|
||||
</a>
|
||||
|
||||
<ul className="DiscussionListItem-badges badges">
|
||||
{listItems(discussion.badges().toArray())}
|
||||
</ul>
|
||||
<ul className="DiscussionListItem-badges badges">{listItems(discussion.badges().toArray())}</ul>
|
||||
|
||||
<a href={app.route.discussion(discussion, jumpTo)}
|
||||
config={m.route}
|
||||
className="DiscussionListItem-main">
|
||||
<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"
|
||||
<span
|
||||
className="DiscussionListItem-count"
|
||||
onclick={this.markAsRead.bind(this)}
|
||||
title={showUnread ? app.translator.trans('core.forum.discussion_list.mark_as_read_tooltip') : ''}>
|
||||
title={showUnread ? app.translator.trans('core.forum.discussion_list.mark_as_read_tooltip') : ''}
|
||||
>
|
||||
{abbreviateNumber(discussion[showUnread ? 'unreadCount' : 'replyCount']())}
|
||||
</span>
|
||||
</div>
|
||||
@@ -133,8 +139,7 @@ export default class DiscussionListItem extends Component {
|
||||
if ('ontouchstart' in window) {
|
||||
const slidableInstance = slidable(this.$().addClass('Slidable'));
|
||||
|
||||
this.$('.DiscussionListItem-controls')
|
||||
.on('hidden.bs.dropdown', () => slidableInstance.reset());
|
||||
this.$('.DiscussionListItem-controls').on('hidden.bs.dropdown', () => slidableInstance.reset());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +182,7 @@ export default class DiscussionListItem extends Component {
|
||||
const discussion = this.props.discussion;
|
||||
|
||||
if (discussion.isUnread()) {
|
||||
discussion.save({lastReadPostNumber: discussion.lastPostNumber()});
|
||||
discussion.save({ lastReadPostNumber: discussion.lastPostNumber() });
|
||||
m.redraw();
|
||||
}
|
||||
}
|
||||
@@ -199,10 +204,11 @@ export default class DiscussionListItem extends Component {
|
||||
items.add('excerpt', excerpt, -100);
|
||||
}
|
||||
} else {
|
||||
items.add('terminalPost',
|
||||
items.add(
|
||||
'terminalPost',
|
||||
TerminalPost.component({
|
||||
discussion: this.props.discussion,
|
||||
lastPost: !this.showFirstPost()
|
||||
lastPost: !this.showFirstPost(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@@ -90,26 +90,26 @@ export default class DiscussionPage extends Page {
|
||||
|
||||
return (
|
||||
<div className="DiscussionPage">
|
||||
{app.cache.discussionList
|
||||
? <div className="DiscussionPage-list" config={this.configPane.bind(this)}>
|
||||
{!$('.App-navigation').is(':visible') ? app.cache.discussionList.render() : ''}
|
||||
</div>
|
||||
: ''}
|
||||
{app.cache.discussionList ? (
|
||||
<div className="DiscussionPage-list" config={this.configPane.bind(this)}>
|
||||
{!$('.App-navigation').is(':visible') ? app.cache.discussionList.render() : ''}
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
|
||||
<div className="DiscussionPage-discussion">
|
||||
{discussion
|
||||
? [
|
||||
DiscussionHero.component({discussion}),
|
||||
<div className="container">
|
||||
<nav className="DiscussionPage-nav">
|
||||
<ul>{listItems(this.sidebarItems().toArray())}</ul>
|
||||
</nav>
|
||||
<div className="DiscussionPage-stream">
|
||||
{this.stream.render()}
|
||||
</div>
|
||||
</div>
|
||||
]
|
||||
: LoadingIndicator.component({className: 'LoadingIndicator--block'})}
|
||||
DiscussionHero.component({ discussion }),
|
||||
<div className="container">
|
||||
<nav className="DiscussionPage-nav">
|
||||
<ul>{listItems(this.sidebarItems().toArray())}</ul>
|
||||
</nav>
|
||||
<div className="DiscussionPage-stream">{this.stream.render()}</div>
|
||||
</div>,
|
||||
]
|
||||
: LoadingIndicator.component({ className: 'LoadingIndicator--block' })}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -140,8 +140,7 @@ export default class DiscussionPage extends Page {
|
||||
} else {
|
||||
const params = this.requestParams();
|
||||
|
||||
app.store.find('discussions', m.route.param('id').split('-')[0], params)
|
||||
.then(this.show.bind(this));
|
||||
app.store.find('discussions', m.route.param('id').split('-')[0], params).then(this.show.bind(this));
|
||||
}
|
||||
|
||||
m.lazyRedraw();
|
||||
@@ -155,7 +154,7 @@ export default class DiscussionPage extends Page {
|
||||
*/
|
||||
requestParams() {
|
||||
return {
|
||||
page: {near: this.near}
|
||||
page: { near: this.near },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -182,11 +181,14 @@ export default class DiscussionPage extends Page {
|
||||
const discussionId = discussion.id();
|
||||
|
||||
includedPosts = discussion.payload.included
|
||||
.filter(record => record.type === 'posts'
|
||||
&& record.relationships
|
||||
&& record.relationships.discussion
|
||||
&& record.relationships.discussion.data.id === discussionId)
|
||||
.map(record => app.store.getById('posts', record.id))
|
||||
.filter(
|
||||
(record) =>
|
||||
record.type === 'posts' &&
|
||||
record.relationships &&
|
||||
record.relationships.discussion &&
|
||||
record.relationships.discussion.data.id === discussionId
|
||||
)
|
||||
.map((record) => app.store.getById('posts', record.id))
|
||||
.sort((a, b) => a.id() - b.id())
|
||||
.slice(0, 20);
|
||||
}
|
||||
@@ -194,7 +196,7 @@ export default class DiscussionPage extends Page {
|
||||
// Set up the post stream for this discussion, along with the first page of
|
||||
// posts we want to display. Tell the stream to scroll down and highlight
|
||||
// the specific post that was routed to.
|
||||
this.stream = new PostStream({discussion, includedPosts});
|
||||
this.stream = new PostStream({ discussion, includedPosts });
|
||||
this.stream.on('positionChanged', this.positionChanged.bind(this));
|
||||
this.stream.goToNumber(m.route.param('near') || (includedPosts[0] && includedPosts[0].number()), true);
|
||||
}
|
||||
@@ -219,7 +221,7 @@ export default class DiscussionPage extends Page {
|
||||
const pane = app.pane;
|
||||
$list.hover(pane.show.bind(pane), pane.onmouseleave.bind(pane));
|
||||
|
||||
const hotEdge = e => {
|
||||
const hotEdge = (e) => {
|
||||
if (e.pageX < 10) pane.show();
|
||||
};
|
||||
$(document).on('mousemove', hotEdge);
|
||||
@@ -249,19 +251,21 @@ export default class DiscussionPage extends Page {
|
||||
sidebarItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('controls',
|
||||
items.add(
|
||||
'controls',
|
||||
SplitDropdown.component({
|
||||
children: DiscussionControls.controls(this.discussion, this).toArray(),
|
||||
icon: 'fas fa-ellipsis-v',
|
||||
className: 'App-primaryControl',
|
||||
buttonClassName: 'Button--primary'
|
||||
buttonClassName: 'Button--primary',
|
||||
})
|
||||
);
|
||||
|
||||
items.add('scrubber',
|
||||
items.add(
|
||||
'scrubber',
|
||||
PostStreamScrubber.component({
|
||||
stream: this.stream,
|
||||
className: 'App-titleControl'
|
||||
className: 'App-titleControl',
|
||||
}),
|
||||
-100
|
||||
);
|
||||
@@ -281,7 +285,7 @@ export default class DiscussionPage extends Page {
|
||||
|
||||
// Construct a URL to this discussion with the updated position, then
|
||||
// replace it into the window's history and our own history stack.
|
||||
const url = app.route.discussion(discussion, this.near = startNumber);
|
||||
const url = app.route.discussion(discussion, (this.near = startNumber));
|
||||
|
||||
m.route(url, true);
|
||||
window.history.replaceState(null, document.title, url);
|
||||
@@ -291,7 +295,7 @@ export default class DiscussionPage extends Page {
|
||||
// If the user hasn't read past here before, then we'll update their read
|
||||
// state and redraw.
|
||||
if (app.session.user && endNumber > (discussion.lastReadPostNumber() || 0)) {
|
||||
discussion.save({lastReadPostNumber: endNumber});
|
||||
discussion.save({ lastReadPostNumber: endNumber });
|
||||
m.redraw();
|
||||
}
|
||||
}
|
||||
|
@@ -20,6 +20,6 @@ export default class DiscussionRenamedNotification extends Notification {
|
||||
}
|
||||
|
||||
content() {
|
||||
return app.translator.trans('core.forum.notifications.discussion_renamed_text', {user: this.props.notification.fromUser()});
|
||||
return app.translator.trans('core.forum.notifications.discussion_renamed_text', { user: this.props.notification.fromUser() });
|
||||
}
|
||||
}
|
||||
|
@@ -27,8 +27,8 @@ export default class DiscussionRenamedPost extends EventPost {
|
||||
const newTitle = post.content()[1];
|
||||
|
||||
return {
|
||||
'old': oldTitle,
|
||||
'new': <strong className="DiscussionRenamedPost-new">{newTitle}</strong>
|
||||
old: oldTitle,
|
||||
new: <strong className="DiscussionRenamedPost-new">{newTitle}</strong>,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -18,12 +18,12 @@ export default class DiscussionsSearchSource {
|
||||
this.results[query] = [];
|
||||
|
||||
const params = {
|
||||
filter: {q: query},
|
||||
page: {limit: 3},
|
||||
include: 'mostRelevantPost'
|
||||
filter: { q: query },
|
||||
page: { limit: 3 },
|
||||
include: 'mostRelevantPost',
|
||||
};
|
||||
|
||||
return app.store.find('discussions', params).then(results => this.results[query] = results);
|
||||
return app.store.find('discussions', params).then((results) => (this.results[query] = results));
|
||||
}
|
||||
|
||||
view(query) {
|
||||
@@ -36,11 +36,11 @@ export default class DiscussionsSearchSource {
|
||||
<li>
|
||||
{LinkButton.component({
|
||||
icon: 'fas fa-search',
|
||||
children: app.translator.trans('core.forum.search.all_discussions_button', {query}),
|
||||
href: app.route('index', {q: query})
|
||||
children: app.translator.trans('core.forum.search.all_discussions_button', { query }),
|
||||
href: app.route('index', { q: query }),
|
||||
})}
|
||||
</li>,
|
||||
results.map(discussion => {
|
||||
results.map((discussion) => {
|
||||
const mostRelevantPost = discussion.mostRelevantPost();
|
||||
|
||||
return (
|
||||
@@ -51,7 +51,7 @@ export default class DiscussionsSearchSource {
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -18,8 +18,8 @@ export default class DiscussionsUserPage extends UserPage {
|
||||
{DiscussionList.component({
|
||||
params: {
|
||||
q: 'author:' + this.user.username(),
|
||||
sort: 'newest'
|
||||
}
|
||||
sort: 'newest',
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
@@ -22,7 +22,7 @@ export default class EditPostComposer extends ComposerBody {
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
this.editor.props.preview = e => {
|
||||
this.editor.props.preview = (e) => {
|
||||
minimizeComposerIfFullScreen(e);
|
||||
|
||||
m.route(app.route.post(this.props.post));
|
||||
@@ -44,20 +44,21 @@ export default class EditPostComposer extends ComposerBody {
|
||||
const items = super.headerItems();
|
||||
const post = this.props.post;
|
||||
|
||||
const routeAndMinimize = function(element, isInitialized) {
|
||||
const routeAndMinimize = function (element, isInitialized) {
|
||||
if (isInitialized) return;
|
||||
$(element).on('click', minimizeComposerIfFullScreen);
|
||||
m.route.apply(this, arguments);
|
||||
};
|
||||
|
||||
items.add('title', (
|
||||
items.add(
|
||||
'title',
|
||||
<h3>
|
||||
{icon('fas fa-pencil-alt')} {' '}
|
||||
{icon('fas fa-pencil-alt')}{' '}
|
||||
<a href={app.route.discussion(post.discussion(), post.number())} config={routeAndMinimize}>
|
||||
{app.translator.trans('core.forum.composer_edit.post_link', {number: post.number(), discussion: post.discussion().title()})}
|
||||
{app.translator.trans('core.forum.composer_edit.post_link', { number: post.number(), discussion: post.discussion().title() })}
|
||||
</a>
|
||||
</h3>
|
||||
));
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -69,7 +70,7 @@ export default class EditPostComposer extends ComposerBody {
|
||||
*/
|
||||
data() {
|
||||
return {
|
||||
content: this.content()
|
||||
content: this.content(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -78,9 +79,6 @@ export default class EditPostComposer extends ComposerBody {
|
||||
|
||||
const data = this.data();
|
||||
|
||||
this.props.post.save(data).then(
|
||||
() => app.composer.hide(),
|
||||
this.loaded.bind(this)
|
||||
);
|
||||
this.props.post.save(data).then(() => app.composer.hide(), this.loaded.bind(this));
|
||||
}
|
||||
}
|
||||
|
@@ -21,9 +21,10 @@ export default class EditUserModal extends Modal {
|
||||
this.password = m.prop(user.password() || '');
|
||||
this.groups = {};
|
||||
|
||||
app.store.all('groups')
|
||||
.filter(group => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
|
||||
.forEach(group => this.groups[group.id()] = m.prop(user.groups().indexOf(group) !== -1));
|
||||
app.store
|
||||
.all('groups')
|
||||
.filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
|
||||
.forEach((group) => (this.groups[group.id()] = m.prop(user.groups().indexOf(group) !== -1)));
|
||||
}
|
||||
|
||||
className() {
|
||||
@@ -37,9 +38,7 @@ export default class EditUserModal extends Modal {
|
||||
content() {
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<div className="Form">
|
||||
{this.fields().toArray()}
|
||||
</div>
|
||||
<div className="Form">{this.fields().toArray()}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -47,78 +46,107 @@ export default class EditUserModal extends Modal {
|
||||
fields() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('username', <div className="Form-group">
|
||||
<label>{app.translator.trans('core.forum.edit_user.username_heading')}</label>
|
||||
<input className="FormControl" placeholder={extractText(app.translator.trans('core.forum.edit_user.username_label'))}
|
||||
bidi={this.username} />
|
||||
</div>, 40);
|
||||
items.add(
|
||||
'username',
|
||||
<div className="Form-group">
|
||||
<label>{app.translator.trans('core.forum.edit_user.username_heading')}</label>
|
||||
<input className="FormControl" placeholder={extractText(app.translator.trans('core.forum.edit_user.username_label'))} bidi={this.username} />
|
||||
</div>,
|
||||
40
|
||||
);
|
||||
|
||||
if (app.session.user !== this.props.user) {
|
||||
items.add('email', <div className="Form-group">
|
||||
<label>{app.translator.trans('core.forum.edit_user.email_heading')}</label>
|
||||
<div>
|
||||
<input className="FormControl"
|
||||
placeholder={extractText(app.translator.trans('core.forum.edit_user.email_label'))}
|
||||
bidi={this.email}/>
|
||||
</div>
|
||||
{!this.isEmailConfirmed() ? (
|
||||
items.add(
|
||||
'email',
|
||||
<div className="Form-group">
|
||||
<label>{app.translator.trans('core.forum.edit_user.email_heading')}</label>
|
||||
<div>
|
||||
{Button.component({
|
||||
className: 'Button Button--block',
|
||||
children: app.translator.trans('core.forum.edit_user.activate_button'),
|
||||
loading: this.loading,
|
||||
onclick: this.activate.bind(this)
|
||||
})}
|
||||
<input className="FormControl" placeholder={extractText(app.translator.trans('core.forum.edit_user.email_label'))} bidi={this.email} />
|
||||
</div>
|
||||
) : ''}
|
||||
</div>, 30);
|
||||
|
||||
items.add('password', <div className="Form-group">
|
||||
<label>{app.translator.trans('core.forum.edit_user.password_heading')}</label>
|
||||
<div>
|
||||
<label className="checkbox">
|
||||
<input type="checkbox" onchange={e => {
|
||||
this.setPassword(e.target.checked);
|
||||
m.redraw(true);
|
||||
if (e.target.checked) this.$('[name=password]').select();
|
||||
m.redraw.strategy('none');
|
||||
}}/>
|
||||
{app.translator.trans('core.forum.edit_user.set_password_label')}
|
||||
</label>
|
||||
{this.setPassword() ? (
|
||||
<input className="FormControl" type="password" name="password"
|
||||
placeholder={extractText(app.translator.trans('core.forum.edit_user.password_label'))}
|
||||
bidi={this.password}/>
|
||||
) : ''}
|
||||
</div>
|
||||
</div>, 20);
|
||||
{!this.isEmailConfirmed() ? (
|
||||
<div>
|
||||
{Button.component({
|
||||
className: 'Button Button--block',
|
||||
children: app.translator.trans('core.forum.edit_user.activate_button'),
|
||||
loading: this.loading,
|
||||
onclick: this.activate.bind(this),
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>,
|
||||
30
|
||||
);
|
||||
|
||||
items.add(
|
||||
'password',
|
||||
<div className="Form-group">
|
||||
<label>{app.translator.trans('core.forum.edit_user.password_heading')}</label>
|
||||
<div>
|
||||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
onchange={(e) => {
|
||||
this.setPassword(e.target.checked);
|
||||
m.redraw(true);
|
||||
if (e.target.checked) this.$('[name=password]').select();
|
||||
m.redraw.strategy('none');
|
||||
}}
|
||||
/>
|
||||
{app.translator.trans('core.forum.edit_user.set_password_label')}
|
||||
</label>
|
||||
{this.setPassword() ? (
|
||||
<input
|
||||
className="FormControl"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder={extractText(app.translator.trans('core.forum.edit_user.password_label'))}
|
||||
bidi={this.password}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
</div>,
|
||||
20
|
||||
);
|
||||
}
|
||||
|
||||
items.add('groups', <div className="Form-group EditUserModal-groups">
|
||||
<label>{app.translator.trans('core.forum.edit_user.groups_heading')}</label>
|
||||
<div>
|
||||
{Object.keys(this.groups)
|
||||
.map(id => app.store.getById('groups', id))
|
||||
.map(group => (
|
||||
<label className="checkbox">
|
||||
<input type="checkbox"
|
||||
bidi={this.groups[group.id()]}
|
||||
disabled={this.props.user.id() === '1' && group.id() === Group.ADMINISTRATOR_ID} />
|
||||
{GroupBadge.component({group, label: ''})} {group.nameSingular()}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>, 10);
|
||||
items.add(
|
||||
'groups',
|
||||
<div className="Form-group EditUserModal-groups">
|
||||
<label>{app.translator.trans('core.forum.edit_user.groups_heading')}</label>
|
||||
<div>
|
||||
{Object.keys(this.groups)
|
||||
.map((id) => app.store.getById('groups', id))
|
||||
.map((group) => (
|
||||
<label className="checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
bidi={this.groups[group.id()]}
|
||||
disabled={this.props.user.id() === '1' && group.id() === Group.ADMINISTRATOR_ID}
|
||||
/>
|
||||
{GroupBadge.component({ group, label: '' })} {group.nameSingular()}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>,
|
||||
10
|
||||
);
|
||||
|
||||
items.add('submit', <div className="Form-group">
|
||||
{Button.component({
|
||||
className: 'Button Button--primary',
|
||||
type: 'submit',
|
||||
loading: this.loading,
|
||||
children: app.translator.trans('core.forum.edit_user.submit_button')
|
||||
})}
|
||||
</div>, -10);
|
||||
items.add(
|
||||
'submit',
|
||||
<div className="Form-group">
|
||||
{Button.component({
|
||||
className: 'Button Button--primary',
|
||||
type: 'submit',
|
||||
loading: this.loading,
|
||||
children: app.translator.trans('core.forum.edit_user.submit_button'),
|
||||
})}
|
||||
</div>,
|
||||
-10
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -129,7 +157,8 @@ export default class EditUserModal extends Modal {
|
||||
username: this.username(),
|
||||
isEmailConfirmed: true,
|
||||
};
|
||||
this.props.user.save(data, {errorHandler: this.onerror.bind(this)})
|
||||
this.props.user
|
||||
.save(data, { errorHandler: this.onerror.bind(this) })
|
||||
.then(() => {
|
||||
this.isEmailConfirmed(true);
|
||||
this.loading = false;
|
||||
@@ -143,12 +172,12 @@ export default class EditUserModal extends Modal {
|
||||
|
||||
data() {
|
||||
const groups = Object.keys(this.groups)
|
||||
.filter(id => this.groups[id]())
|
||||
.map(id => app.store.getById('groups', id));
|
||||
.filter((id) => this.groups[id]())
|
||||
.map((id) => app.store.getById('groups', id));
|
||||
|
||||
const data = {
|
||||
username: this.username(),
|
||||
relationships: {groups}
|
||||
relationships: { groups },
|
||||
};
|
||||
|
||||
if (app.session.user !== this.props.user) {
|
||||
@@ -167,7 +196,8 @@ export default class EditUserModal extends Modal {
|
||||
|
||||
this.loading = true;
|
||||
|
||||
this.props.user.save(this.data(), {errorHandler: this.onerror.bind(this)})
|
||||
this.props.user
|
||||
.save(this.data(), { errorHandler: this.onerror.bind(this) })
|
||||
.then(this.hide.bind(this))
|
||||
.catch(() => {
|
||||
this.loading = false;
|
||||
|
@@ -28,17 +28,16 @@ export default class EventPost extends Post {
|
||||
const username = usernameHelper(user);
|
||||
const data = Object.assign(this.descriptionData(), {
|
||||
user,
|
||||
username: user
|
||||
? <a className="EventPost-user" href={app.route.user(user)} config={m.route}>{username}</a>
|
||||
: username
|
||||
username: user ? (
|
||||
<a className="EventPost-user" href={app.route.user(user)} config={m.route}>
|
||||
{username}
|
||||
</a>
|
||||
) : (
|
||||
username
|
||||
),
|
||||
});
|
||||
|
||||
return super.content().concat([
|
||||
icon(this.icon(), {className: 'EventPost-icon'}),
|
||||
<div class="EventPost-info">
|
||||
{this.description(data)}
|
||||
</div>
|
||||
]);
|
||||
return super.content().concat([icon(this.icon(), { className: 'EventPost-icon' }), <div class="EventPost-info">{this.description(data)}</div>]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -59,17 +59,22 @@ export default class ForgotPasswordModal extends Modal {
|
||||
<div className="Form Form--centered">
|
||||
<p className="helpText">{app.translator.trans('core.forum.forgot_password.text')}</p>
|
||||
<div className="Form-group">
|
||||
<input className="FormControl" name="email" type="email" placeholder={extractText(app.translator.trans('core.forum.forgot_password.email_placeholder'))}
|
||||
<input
|
||||
className="FormControl"
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder={extractText(app.translator.trans('core.forum.forgot_password.email_placeholder'))}
|
||||
value={this.email()}
|
||||
onchange={m.withAttr('value', this.email)}
|
||||
disabled={this.loading} />
|
||||
disabled={this.loading}
|
||||
/>
|
||||
</div>
|
||||
<div className="Form-group">
|
||||
{Button.component({
|
||||
className: 'Button Button--primary Button--block',
|
||||
type: 'submit',
|
||||
loading: this.loading,
|
||||
children: app.translator.trans('core.forum.forgot_password.submit_button')
|
||||
children: app.translator.trans('core.forum.forgot_password.submit_button'),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
@@ -82,12 +87,13 @@ export default class ForgotPasswordModal extends Modal {
|
||||
|
||||
this.loading = true;
|
||||
|
||||
app.request({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/forgot',
|
||||
data: {email: this.email()},
|
||||
errorHandler: this.onerror.bind(this)
|
||||
})
|
||||
app
|
||||
.request({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/forgot',
|
||||
data: { email: this.email() },
|
||||
errorHandler: this.onerror.bind(this),
|
||||
})
|
||||
.then(() => {
|
||||
this.success = true;
|
||||
this.alert = null;
|
||||
|
@@ -8,11 +8,7 @@ import listItems from '../../common/helpers/listItems';
|
||||
*/
|
||||
export default class HeaderPrimary extends Component {
|
||||
view() {
|
||||
return (
|
||||
<ul className="Header-controls">
|
||||
{listItems(this.items().toArray())}
|
||||
</ul>
|
||||
);
|
||||
return <ul className="Header-controls">{listItems(this.items().toArray())}</ul>;
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
|
@@ -15,11 +15,7 @@ import listItems from '../../common/helpers/listItems';
|
||||
*/
|
||||
export default class HeaderSecondary extends Component {
|
||||
view() {
|
||||
return (
|
||||
<ul className="Header-controls">
|
||||
{listItems(this.items().toArray())}
|
||||
</ul>
|
||||
);
|
||||
return <ul className="Header-controls">{listItems(this.items().toArray())}</ul>;
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
@@ -39,29 +35,35 @@ export default class HeaderSecondary extends Component {
|
||||
|
||||
items.add('search', app.search.render(), 30);
|
||||
|
||||
if (app.forum.attribute("showLanguageSelector") && Object.keys(app.data.locales).length > 1) {
|
||||
if (app.forum.attribute('showLanguageSelector') && Object.keys(app.data.locales).length > 1) {
|
||||
const locales = [];
|
||||
|
||||
for (const locale in app.data.locales) {
|
||||
locales.push(Button.component({
|
||||
active: app.data.locale === locale,
|
||||
children: app.data.locales[locale],
|
||||
icon: app.data.locale === locale ? 'fas fa-check' : true,
|
||||
onclick: () => {
|
||||
if (app.session.user) {
|
||||
app.session.user.savePreferences({locale}).then(() => window.location.reload());
|
||||
} else {
|
||||
document.cookie = `locale=${locale}; path=/; expires=Tue, 19 Jan 2038 03:14:07 GMT`;
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
}));
|
||||
locales.push(
|
||||
Button.component({
|
||||
active: app.data.locale === locale,
|
||||
children: app.data.locales[locale],
|
||||
icon: app.data.locale === locale ? 'fas fa-check' : true,
|
||||
onclick: () => {
|
||||
if (app.session.user) {
|
||||
app.session.user.savePreferences({ locale }).then(() => window.location.reload());
|
||||
} else {
|
||||
document.cookie = `locale=${locale}; path=/; expires=Tue, 19 Jan 2038 03:14:07 GMT`;
|
||||
window.location.reload();
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
items.add('locale', SelectDropdown.component({
|
||||
children: locales,
|
||||
buttonClassName: 'Button Button--link'
|
||||
}), 20);
|
||||
items.add(
|
||||
'locale',
|
||||
SelectDropdown.component({
|
||||
children: locales,
|
||||
buttonClassName: 'Button Button--link',
|
||||
}),
|
||||
20
|
||||
);
|
||||
}
|
||||
|
||||
if (app.session.user) {
|
||||
@@ -69,21 +71,25 @@ export default class HeaderSecondary extends Component {
|
||||
items.add('session', SessionDropdown.component(), 0);
|
||||
} else {
|
||||
if (app.forum.attribute('allowSignUp')) {
|
||||
items.add('signUp',
|
||||
items.add(
|
||||
'signUp',
|
||||
Button.component({
|
||||
children: app.translator.trans('core.forum.header.sign_up_link'),
|
||||
className: 'Button Button--link',
|
||||
onclick: () => app.modal.show(new SignUpModal())
|
||||
}), 10
|
||||
onclick: () => app.modal.show(new SignUpModal()),
|
||||
}),
|
||||
10
|
||||
);
|
||||
}
|
||||
|
||||
items.add('logIn',
|
||||
items.add(
|
||||
'logIn',
|
||||
Button.component({
|
||||
children: app.translator.trans('core.forum.header.log_in_link'),
|
||||
className: 'Button Button--link',
|
||||
onclick: () => app.modal.show(new LogInModal())
|
||||
}), 0
|
||||
onclick: () => app.modal.show(new LogInModal()),
|
||||
}),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -43,7 +43,7 @@ export default class IndexPage extends Page {
|
||||
// are currently present in the cached discussion list. If they differ, we
|
||||
// will clear the cache and set up a new discussion list component with
|
||||
// the new parameters.
|
||||
Object.keys(params).some(key => {
|
||||
Object.keys(params).some((key) => {
|
||||
if (app.cache.discussionList.props.params[key] !== params[key]) {
|
||||
app.cache.discussionList = null;
|
||||
return true;
|
||||
@@ -52,7 +52,7 @@ export default class IndexPage extends Page {
|
||||
}
|
||||
|
||||
if (!app.cache.discussionList) {
|
||||
app.cache.discussionList = new DiscussionList({params});
|
||||
app.cache.discussionList = new DiscussionList({ params });
|
||||
}
|
||||
|
||||
app.history.push('index', app.translator.trans('core.forum.header.back_to_index_tooltip'));
|
||||
@@ -102,7 +102,7 @@ export default class IndexPage extends Page {
|
||||
// previous hero. Maintain the same scroll position relative to the bottom
|
||||
// of the hero so that the sidebar doesn't jump around.
|
||||
const oldHeroHeight = app.cache.heroHeight;
|
||||
const heroHeight = app.cache.heroHeight = this.$('.Hero').outerHeight() || 0;
|
||||
const heroHeight = (app.cache.heroHeight = this.$('.Hero').outerHeight() || 0);
|
||||
const scrollTop = app.cache.scrollTop;
|
||||
|
||||
$('#app').css('min-height', $(window).height() + heroHeight);
|
||||
@@ -153,22 +153,26 @@ export default class IndexPage extends Page {
|
||||
const items = new ItemList();
|
||||
const canStartDiscussion = app.forum.attribute('canStartDiscussion') || !app.session.user;
|
||||
|
||||
items.add('newDiscussion',
|
||||
items.add(
|
||||
'newDiscussion',
|
||||
Button.component({
|
||||
children: app.translator.trans(canStartDiscussion ? 'core.forum.index.start_discussion_button' : 'core.forum.index.cannot_start_discussion_button'),
|
||||
children: app.translator.trans(
|
||||
canStartDiscussion ? 'core.forum.index.start_discussion_button' : 'core.forum.index.cannot_start_discussion_button'
|
||||
),
|
||||
icon: 'fas fa-edit',
|
||||
className: 'Button Button--primary IndexPage-newDiscussion',
|
||||
itemClassName: 'App-primaryControl',
|
||||
onclick: this.newDiscussionAction.bind(this),
|
||||
disabled: !canStartDiscussion
|
||||
disabled: !canStartDiscussion,
|
||||
})
|
||||
);
|
||||
|
||||
items.add('nav',
|
||||
items.add(
|
||||
'nav',
|
||||
SelectDropdown.component({
|
||||
children: this.navItems(this).toArray(),
|
||||
buttonClassName: 'Button',
|
||||
className: 'App-titleControl'
|
||||
className: 'App-titleControl',
|
||||
})
|
||||
);
|
||||
|
||||
@@ -185,11 +189,12 @@ export default class IndexPage extends Page {
|
||||
const items = new ItemList();
|
||||
const params = this.stickyParams();
|
||||
|
||||
items.add('allDiscussions',
|
||||
items.add(
|
||||
'allDiscussions',
|
||||
LinkButton.component({
|
||||
href: app.route('index', params),
|
||||
children: app.translator.trans('core.forum.index.all_discussions_link'),
|
||||
icon: 'far fa-comments'
|
||||
icon: 'far fa-comments',
|
||||
}),
|
||||
100
|
||||
);
|
||||
@@ -213,11 +218,12 @@ export default class IndexPage extends Page {
|
||||
sortOptions[i] = app.translator.trans('core.forum.index_sort.' + i + '_button');
|
||||
}
|
||||
|
||||
items.add('sort',
|
||||
items.add(
|
||||
'sort',
|
||||
Dropdown.component({
|
||||
buttonClassName: 'Button',
|
||||
label: sortOptions[this.params().sort] || Object.keys(sortMap).map(key => sortOptions[key])[0],
|
||||
children: Object.keys(sortOptions).map(value => {
|
||||
label: sortOptions[this.params().sort] || Object.keys(sortMap).map((key) => sortOptions[key])[0],
|
||||
children: Object.keys(sortOptions).map((value) => {
|
||||
const label = sortOptions[value];
|
||||
const active = (this.params().sort || Object.keys(sortMap)[0]) === value;
|
||||
|
||||
@@ -226,7 +232,7 @@ export default class IndexPage extends Page {
|
||||
icon: active ? 'fas fa-check' : true,
|
||||
onclick: this.changeSort.bind(this, value),
|
||||
active: active,
|
||||
})
|
||||
});
|
||||
}),
|
||||
})
|
||||
);
|
||||
@@ -243,7 +249,8 @@ export default class IndexPage extends Page {
|
||||
actionItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('refresh',
|
||||
items.add(
|
||||
'refresh',
|
||||
Button.component({
|
||||
title: app.translator.trans('core.forum.index.refresh_tooltip'),
|
||||
icon: 'fas fa-sync',
|
||||
@@ -254,17 +261,18 @@ export default class IndexPage extends Page {
|
||||
app.store.find('users', app.session.user.id());
|
||||
m.redraw();
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
if (app.session.user) {
|
||||
items.add('markAllAsRead',
|
||||
items.add(
|
||||
'markAllAsRead',
|
||||
Button.component({
|
||||
title: app.translator.trans('core.forum.index.mark_all_as_read_tooltip'),
|
||||
icon: 'fas fa-check',
|
||||
className: 'Button Button--icon',
|
||||
onclick: this.markAllAsRead.bind(this)
|
||||
onclick: this.markAllAsRead.bind(this),
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -321,7 +329,7 @@ export default class IndexPage extends Page {
|
||||
stickyParams() {
|
||||
return {
|
||||
sort: m.route.param('sort'),
|
||||
q: m.route.param('q')
|
||||
q: m.route.param('q'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -371,7 +379,7 @@ export default class IndexPage extends Page {
|
||||
const confirmation = confirm(app.translator.trans('core.forum.index.mark_all_as_read_confirmation'));
|
||||
|
||||
if (confirmation) {
|
||||
app.session.user.save({markedAllAsReadAt: new Date()});
|
||||
app.session.user.save({ markedAllAsReadAt: new Date() });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -10,14 +10,14 @@ export default class LoadingPost extends Component {
|
||||
return (
|
||||
<div className="Post CommentPost LoadingPost">
|
||||
<header className="Post-header">
|
||||
{avatar(null, {className: 'PostUser-avatar'})}
|
||||
<div className="fakeText"/>
|
||||
{avatar(null, { className: 'PostUser-avatar' })}
|
||||
<div className="fakeText" />
|
||||
</header>
|
||||
|
||||
<div className="Post-body">
|
||||
<div className="fakeText"/>
|
||||
<div className="fakeText"/>
|
||||
<div className="fakeText"/>
|
||||
<div className="fakeText" />
|
||||
<div className="fakeText" />
|
||||
<div className="fakeText" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@@ -6,23 +6,26 @@ import Button from '../../common/components/Button';
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - `path`
|
||||
* - `path`
|
||||
*/
|
||||
export default class LogInButton extends Button {
|
||||
static initProps(props) {
|
||||
props.className = (props.className || '') + ' LogInButton';
|
||||
|
||||
props.onclick = function() {
|
||||
props.onclick = function () {
|
||||
const width = 580;
|
||||
const height = 400;
|
||||
const $window = $(window);
|
||||
|
||||
window.open(app.forum.attribute('baseUrl') + props.path, 'logInPopup',
|
||||
window.open(
|
||||
app.forum.attribute('baseUrl') + props.path,
|
||||
'logInPopup',
|
||||
`width=${width},` +
|
||||
`height=${height},` +
|
||||
`top=${$window.height() / 2 - height / 2},` +
|
||||
`left=${$window.width() / 2 - width / 2},` +
|
||||
'status=no,scrollbars=yes,resizable=no');
|
||||
`height=${height},` +
|
||||
`top=${$window.height() / 2 - height / 2},` +
|
||||
`left=${$window.width() / 2 - width / 2},` +
|
||||
'status=no,scrollbars=yes,resizable=no'
|
||||
);
|
||||
};
|
||||
|
||||
super.initProps(props);
|
||||
|
@@ -6,11 +6,7 @@ import ItemList from '../../common/utils/ItemList';
|
||||
*/
|
||||
export default class LogInButtons extends Component {
|
||||
view() {
|
||||
return (
|
||||
<div className="LogInButtons">
|
||||
{this.items().toArray()}
|
||||
</div>
|
||||
);
|
||||
return <div className="LogInButtons">{this.items().toArray()}</div>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -49,58 +49,71 @@ export default class LogInModal extends Modal {
|
||||
}
|
||||
|
||||
content() {
|
||||
return [
|
||||
<div className="Modal-body">
|
||||
{this.body()}
|
||||
</div>,
|
||||
<div className="Modal-footer">
|
||||
{this.footer()}
|
||||
</div>
|
||||
];
|
||||
return [<div className="Modal-body">{this.body()}</div>, <div className="Modal-footer">{this.footer()}</div>];
|
||||
}
|
||||
|
||||
body() {
|
||||
return [
|
||||
<LogInButtons/>,
|
||||
|
||||
<div className="Form Form--centered">
|
||||
{this.fields().toArray()}
|
||||
</div>
|
||||
];
|
||||
return [<LogInButtons />, <div className="Form Form--centered">{this.fields().toArray()}</div>];
|
||||
}
|
||||
|
||||
fields() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('identification', <div className="Form-group">
|
||||
<input className="FormControl" name="identification" type="text" placeholder={extractText(app.translator.trans('core.forum.log_in.username_or_email_placeholder'))}
|
||||
bidi={this.identification}
|
||||
disabled={this.loading} />
|
||||
</div>, 30);
|
||||
items.add(
|
||||
'identification',
|
||||
<div className="Form-group">
|
||||
<input
|
||||
className="FormControl"
|
||||
name="identification"
|
||||
type="text"
|
||||
placeholder={extractText(app.translator.trans('core.forum.log_in.username_or_email_placeholder'))}
|
||||
bidi={this.identification}
|
||||
disabled={this.loading}
|
||||
/>
|
||||
</div>,
|
||||
30
|
||||
);
|
||||
|
||||
items.add('password', <div className="Form-group">
|
||||
<input className="FormControl" name="password" type="password" placeholder={extractText(app.translator.trans('core.forum.log_in.password_placeholder'))}
|
||||
bidi={this.password}
|
||||
disabled={this.loading} />
|
||||
</div>, 20);
|
||||
items.add(
|
||||
'password',
|
||||
<div className="Form-group">
|
||||
<input
|
||||
className="FormControl"
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder={extractText(app.translator.trans('core.forum.log_in.password_placeholder'))}
|
||||
bidi={this.password}
|
||||
disabled={this.loading}
|
||||
/>
|
||||
</div>,
|
||||
20
|
||||
);
|
||||
|
||||
items.add('remember', <div className="Form-group">
|
||||
<div>
|
||||
<label className="checkbox">
|
||||
<input type="checkbox" bidi={this.remember} disabled={this.loading} />
|
||||
{app.translator.trans('core.forum.log_in.remember_me_label')}
|
||||
</label>
|
||||
</div>
|
||||
</div>, 10);
|
||||
items.add(
|
||||
'remember',
|
||||
<div className="Form-group">
|
||||
<div>
|
||||
<label className="checkbox">
|
||||
<input type="checkbox" bidi={this.remember} disabled={this.loading} />
|
||||
{app.translator.trans('core.forum.log_in.remember_me_label')}
|
||||
</label>
|
||||
</div>
|
||||
</div>,
|
||||
10
|
||||
);
|
||||
|
||||
items.add('submit', <div className="Form-group">
|
||||
{Button.component({
|
||||
className: 'Button Button--primary Button--block',
|
||||
type: 'submit',
|
||||
loading: this.loading,
|
||||
children: app.translator.trans('core.forum.log_in.submit_button')
|
||||
})}
|
||||
</div>, -10);
|
||||
items.add(
|
||||
'submit',
|
||||
<div className="Form-group">
|
||||
{Button.component({
|
||||
className: 'Button Button--primary Button--block',
|
||||
type: 'submit',
|
||||
loading: this.loading,
|
||||
children: app.translator.trans('core.forum.log_in.submit_button'),
|
||||
})}
|
||||
</div>,
|
||||
-10
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -112,10 +125,10 @@ export default class LogInModal extends Modal {
|
||||
</p>,
|
||||
|
||||
app.forum.attribute('allowSignUp') ? (
|
||||
<p className="LogInModal-signUp">
|
||||
{app.translator.trans('core.forum.log_in.sign_up_text', {a: <a onclick={this.signUp.bind(this)}/>})}
|
||||
</p>
|
||||
) : ''
|
||||
<p className="LogInModal-signUp">{app.translator.trans('core.forum.log_in.sign_up_text', { a: <a onclick={this.signUp.bind(this)} /> })}</p>
|
||||
) : (
|
||||
''
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -127,7 +140,7 @@ export default class LogInModal extends Modal {
|
||||
*/
|
||||
forgotPassword() {
|
||||
const email = this.identification();
|
||||
const props = email.indexOf('@') !== -1 ? {email} : undefined;
|
||||
const props = email.indexOf('@') !== -1 ? { email } : undefined;
|
||||
|
||||
app.modal.show(new ForgotPasswordModal(props));
|
||||
}
|
||||
@@ -139,7 +152,7 @@ export default class LogInModal extends Modal {
|
||||
* @public
|
||||
*/
|
||||
signUp() {
|
||||
const props = {password: this.password()};
|
||||
const props = { password: this.password() };
|
||||
const identification = this.identification();
|
||||
props[identification.indexOf('@') !== -1 ? 'email' : 'username'] = identification;
|
||||
|
||||
@@ -159,11 +172,9 @@ export default class LogInModal extends Modal {
|
||||
const password = this.password();
|
||||
const remember = this.remember();
|
||||
|
||||
app.session.login({identification, password, remember}, {errorHandler: this.onerror.bind(this)})
|
||||
.then(
|
||||
() => window.location.reload(),
|
||||
this.loaded.bind(this)
|
||||
);
|
||||
app.session
|
||||
.login({ identification, password, remember }, { errorHandler: this.onerror.bind(this) })
|
||||
.then(() => window.location.reload(), this.loaded.bind(this));
|
||||
}
|
||||
|
||||
onerror(error) {
|
||||
|
@@ -20,31 +20,32 @@ export default class Notification extends Component {
|
||||
const href = this.href();
|
||||
|
||||
return (
|
||||
<a className={'Notification Notification--' + notification.contentType() + ' ' + (!notification.isRead() ? 'unread' : '')}
|
||||
<a
|
||||
className={'Notification Notification--' + notification.contentType() + ' ' + (!notification.isRead() ? 'unread' : '')}
|
||||
href={href}
|
||||
config={function(element, isInitialized) {
|
||||
config={function (element, isInitialized) {
|
||||
if (href.indexOf('://') === -1) m.route.apply(this, arguments);
|
||||
|
||||
if (!isInitialized) $(element).click(this.markAsRead.bind(this));
|
||||
}}>
|
||||
{!notification.isRead() && Button.component({
|
||||
className: 'Notification-action Button Button--icon Button--link',
|
||||
icon: 'fas fa-check',
|
||||
title: app.translator.trans('core.forum.notifications.mark_as_read_tooltip'),
|
||||
onclick: e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{!notification.isRead() &&
|
||||
Button.component({
|
||||
className: 'Notification-action Button Button--icon Button--link',
|
||||
icon: 'fas fa-check',
|
||||
title: app.translator.trans('core.forum.notifications.mark_as_read_tooltip'),
|
||||
onclick: (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.markAsRead();
|
||||
}
|
||||
})}
|
||||
this.markAsRead();
|
||||
},
|
||||
})}
|
||||
{avatar(notification.fromUser())}
|
||||
{icon(this.icon(), {className: 'Notification-icon'})}
|
||||
{icon(this.icon(), { className: 'Notification-icon' })}
|
||||
<span className="Notification-content">{this.content()}</span>
|
||||
{humanTime(notification.createdAt())}
|
||||
<div className="Notification-excerpt">
|
||||
{this.excerpt()}
|
||||
</div>
|
||||
<div className="Notification-excerpt">{this.excerpt()}</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -55,8 +56,7 @@ export default class Notification extends Component {
|
||||
* @return {String}
|
||||
* @abstract
|
||||
*/
|
||||
icon() {
|
||||
}
|
||||
icon() {}
|
||||
|
||||
/**
|
||||
* Get the URL that the notification should link to.
|
||||
@@ -64,8 +64,7 @@ export default class Notification extends Component {
|
||||
* @return {String}
|
||||
* @abstract
|
||||
*/
|
||||
href() {
|
||||
}
|
||||
href() {}
|
||||
|
||||
/**
|
||||
* Get the content of the notification.
|
||||
@@ -73,8 +72,7 @@ export default class Notification extends Component {
|
||||
* @return {VirtualElement}
|
||||
* @abstract
|
||||
*/
|
||||
content() {
|
||||
}
|
||||
content() {}
|
||||
|
||||
/**
|
||||
* Get the excerpt of the notification.
|
||||
@@ -82,8 +80,7 @@ export default class Notification extends Component {
|
||||
* @return {VirtualElement}
|
||||
* @abstract
|
||||
*/
|
||||
excerpt() {
|
||||
}
|
||||
excerpt() {}
|
||||
|
||||
/**
|
||||
* Mark the notification as read.
|
||||
@@ -91,8 +88,8 @@ export default class Notification extends Component {
|
||||
markAsRead() {
|
||||
if (this.props.notification.isRead()) return;
|
||||
|
||||
app.session.user.pushAttributes({unreadNotificationCount: app.session.user.unreadNotificationCount() - 1});
|
||||
app.session.user.pushAttributes({ unreadNotificationCount: app.session.user.unreadNotificationCount() - 1 });
|
||||
|
||||
this.props.notification.save({isRead: true});
|
||||
this.props.notification.save({ isRead: true });
|
||||
}
|
||||
}
|
||||
|
@@ -37,15 +37,15 @@ export default class NotificationGrid extends Component {
|
||||
|
||||
// For each of the notification type-method combinations, create and store a
|
||||
// new checkbox component instance, which we will render in the view.
|
||||
this.types.forEach(type => {
|
||||
this.methods.forEach(method => {
|
||||
this.types.forEach((type) => {
|
||||
this.methods.forEach((method) => {
|
||||
const key = this.preferenceKey(type.name, method.name);
|
||||
const preference = this.props.user.preferences()[key];
|
||||
|
||||
this.inputs[key] = new Checkbox({
|
||||
state: !!preference,
|
||||
disabled: typeof preference === 'undefined',
|
||||
onchange: () => this.toggle([key])
|
||||
onchange: () => this.toggle([key]),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -56,8 +56,8 @@ export default class NotificationGrid extends Component {
|
||||
<table className="NotificationGrid">
|
||||
<thead>
|
||||
<tr>
|
||||
<td/>
|
||||
{this.methods.map(method => (
|
||||
<td />
|
||||
{this.methods.map((method) => (
|
||||
<th className="NotificationGrid-groupToggle" onclick={this.toggleMethod.bind(this, method.name)}>
|
||||
{icon(method.icon)} {method.label}
|
||||
</th>
|
||||
@@ -66,15 +66,13 @@ export default class NotificationGrid extends Component {
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{this.types.map(type => (
|
||||
{this.types.map((type) => (
|
||||
<tr>
|
||||
<td className="NotificationGrid-groupToggle" onclick={this.toggleType.bind(this, type.name)}>
|
||||
{icon(type.icon)} {type.label}
|
||||
</td>
|
||||
{this.methods.map(method => (
|
||||
<td className="NotificationGrid-checkbox">
|
||||
{this.inputs[this.preferenceKey(type.name, method.name)].render()}
|
||||
</td>
|
||||
{this.methods.map((method) => (
|
||||
<td className="NotificationGrid-checkbox">{this.inputs[this.preferenceKey(type.name, method.name)].render()}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
@@ -86,13 +84,19 @@ export default class NotificationGrid extends Component {
|
||||
config(isInitialized) {
|
||||
if (isInitialized) return;
|
||||
|
||||
this.$('thead .NotificationGrid-groupToggle').bind('mouseenter mouseleave', function(e) {
|
||||
this.$('thead .NotificationGrid-groupToggle').bind('mouseenter mouseleave', function (e) {
|
||||
const i = parseInt($(this).index(), 10) + 1;
|
||||
$(this).parents('table').find('td:nth-child(' + i + ')').toggleClass('highlighted', e.type === 'mouseenter');
|
||||
$(this)
|
||||
.parents('table')
|
||||
.find('td:nth-child(' + i + ')')
|
||||
.toggleClass('highlighted', e.type === 'mouseenter');
|
||||
});
|
||||
|
||||
this.$('tbody .NotificationGrid-groupToggle').bind('mouseenter mouseleave', function(e) {
|
||||
$(this).parent().find('td').toggleClass('highlighted', e.type === 'mouseenter');
|
||||
this.$('tbody .NotificationGrid-groupToggle').bind('mouseenter mouseleave', function (e) {
|
||||
$(this)
|
||||
.parent()
|
||||
.find('td')
|
||||
.toggleClass('highlighted', e.type === 'mouseenter');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -107,7 +111,7 @@ export default class NotificationGrid extends Component {
|
||||
const preferences = user.preferences();
|
||||
const enabled = !preferences[keys[0]];
|
||||
|
||||
keys.forEach(key => {
|
||||
keys.forEach((key) => {
|
||||
const control = this.inputs[key];
|
||||
|
||||
control.loading = true;
|
||||
@@ -116,8 +120,8 @@ export default class NotificationGrid extends Component {
|
||||
|
||||
m.redraw();
|
||||
|
||||
user.save({preferences}).then(() => {
|
||||
keys.forEach(key => this.inputs[key].loading = false);
|
||||
user.save({ preferences }).then(() => {
|
||||
keys.forEach((key) => (this.inputs[key].loading = false));
|
||||
|
||||
m.redraw();
|
||||
});
|
||||
@@ -129,9 +133,7 @@ export default class NotificationGrid extends Component {
|
||||
* @param {String} method
|
||||
*/
|
||||
toggleMethod(method) {
|
||||
const keys = this.types
|
||||
.map(type => this.preferenceKey(type.name, method))
|
||||
.filter(key => !this.inputs[key].props.disabled);
|
||||
const keys = this.types.map((type) => this.preferenceKey(type.name, method)).filter((key) => !this.inputs[key].props.disabled);
|
||||
|
||||
this.toggle(keys);
|
||||
}
|
||||
@@ -142,9 +144,7 @@ export default class NotificationGrid extends Component {
|
||||
* @param {String} type
|
||||
*/
|
||||
toggleType(type) {
|
||||
const keys = this.methods
|
||||
.map(method => this.preferenceKey(type, method.name))
|
||||
.filter(key => !this.inputs[key].props.disabled);
|
||||
const keys = this.methods.map((method) => this.preferenceKey(type, method.name)).filter((key) => !this.inputs[key].props.disabled);
|
||||
|
||||
this.toggle(keys);
|
||||
}
|
||||
@@ -207,7 +207,7 @@ export default class NotificationGrid extends Component {
|
||||
items.add('discussionRenamed', {
|
||||
name: 'discussionRenamed',
|
||||
icon: 'fas fa-pencil-alt',
|
||||
label: app.translator.trans('core.forum.settings.notify_discussion_renamed_label')
|
||||
label: app.translator.trans('core.forum.settings.notify_discussion_renamed_label'),
|
||||
});
|
||||
|
||||
return items;
|
||||
|
@@ -36,7 +36,7 @@ export default class NotificationList extends Component {
|
||||
className: 'Button Button--icon Button--link',
|
||||
icon: 'fas fa-check',
|
||||
title: app.translator.trans('core.forum.notifications.mark_all_as_read_tooltip'),
|
||||
onclick: this.markAllAsRead.bind(this)
|
||||
onclick: this.markAllAsRead.bind(this),
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -44,65 +44,66 @@ export default class NotificationList extends Component {
|
||||
</div>
|
||||
|
||||
<div className="NotificationList-content">
|
||||
{pages.length ? pages.map(notifications => {
|
||||
const groups = [];
|
||||
const discussions = {};
|
||||
{pages.length
|
||||
? pages.map((notifications) => {
|
||||
const groups = [];
|
||||
const discussions = {};
|
||||
|
||||
notifications.forEach(notification => {
|
||||
const subject = notification.subject();
|
||||
notifications.forEach((notification) => {
|
||||
const subject = notification.subject();
|
||||
|
||||
if (typeof subject === 'undefined') return;
|
||||
if (typeof subject === 'undefined') return;
|
||||
|
||||
// Get the discussion that this notification is related to. If it's not
|
||||
// directly related to a discussion, it may be related to a post or
|
||||
// other entity which is related to a discussion.
|
||||
let discussion = false;
|
||||
if (subject instanceof Discussion) discussion = subject;
|
||||
else if (subject && subject.discussion) discussion = subject.discussion();
|
||||
// Get the discussion that this notification is related to. If it's not
|
||||
// directly related to a discussion, it may be related to a post or
|
||||
// other entity which is related to a discussion.
|
||||
let discussion = false;
|
||||
if (subject instanceof Discussion) discussion = subject;
|
||||
else if (subject && subject.discussion) discussion = subject.discussion();
|
||||
|
||||
// If the notification is not related to a discussion directly or
|
||||
// indirectly, then we will assign it to a neutral group.
|
||||
const key = discussion ? discussion.id() : 0;
|
||||
discussions[key] = discussions[key] || {discussion: discussion, notifications: []};
|
||||
discussions[key].notifications.push(notification);
|
||||
// If the notification is not related to a discussion directly or
|
||||
// indirectly, then we will assign it to a neutral group.
|
||||
const key = discussion ? discussion.id() : 0;
|
||||
discussions[key] = discussions[key] || { discussion: discussion, notifications: [] };
|
||||
discussions[key].notifications.push(notification);
|
||||
|
||||
if (groups.indexOf(discussions[key]) === -1) {
|
||||
groups.push(discussions[key]);
|
||||
}
|
||||
});
|
||||
if (groups.indexOf(discussions[key]) === -1) {
|
||||
groups.push(discussions[key]);
|
||||
}
|
||||
});
|
||||
|
||||
return groups.map(group => {
|
||||
const badges = group.discussion && group.discussion.badges().toArray();
|
||||
return groups.map((group) => {
|
||||
const badges = group.discussion && group.discussion.badges().toArray();
|
||||
|
||||
return (
|
||||
<div className="NotificationGroup">
|
||||
{group.discussion
|
||||
? (
|
||||
<a className="NotificationGroup-header"
|
||||
href={app.route.discussion(group.discussion)}
|
||||
config={m.route}>
|
||||
{badges && badges.length ? <ul className="NotificationGroup-badges badges">{listItems(badges)}</ul> : ''}
|
||||
{group.discussion.title()}
|
||||
</a>
|
||||
) : (
|
||||
<div className="NotificationGroup-header">
|
||||
{app.forum.attribute('title')}
|
||||
</div>
|
||||
)}
|
||||
return (
|
||||
<div className="NotificationGroup">
|
||||
{group.discussion ? (
|
||||
<a className="NotificationGroup-header" href={app.route.discussion(group.discussion)} config={m.route}>
|
||||
{badges && badges.length ? <ul className="NotificationGroup-badges badges">{listItems(badges)}</ul> : ''}
|
||||
{group.discussion.title()}
|
||||
</a>
|
||||
) : (
|
||||
<div className="NotificationGroup-header">{app.forum.attribute('title')}</div>
|
||||
)}
|
||||
|
||||
<ul className="NotificationGroup-content">
|
||||
{group.notifications.map(notification => {
|
||||
const NotificationComponent = app.notificationComponents[notification.contentType()];
|
||||
return NotificationComponent ? <li>{NotificationComponent.component({notification})}</li> : '';
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}) : ''}
|
||||
{this.loading
|
||||
? <LoadingIndicator className="LoadingIndicator--block" />
|
||||
: (pages.length ? '' : <div className="NotificationList-empty">{app.translator.trans('core.forum.notifications.empty_text')}</div>)}
|
||||
<ul className="NotificationGroup-content">
|
||||
{group.notifications.map((notification) => {
|
||||
const NotificationComponent = app.notificationComponents[notification.contentType()];
|
||||
return NotificationComponent ? <li>{NotificationComponent.component({ notification })}</li> : '';
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
})
|
||||
: ''}
|
||||
{this.loading ? (
|
||||
<LoadingIndicator className="LoadingIndicator--block" />
|
||||
) : pages.length ? (
|
||||
''
|
||||
) : (
|
||||
<div className="NotificationList-empty">{app.translator.trans('core.forum.notifications.empty_text')}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -145,7 +146,7 @@ export default class NotificationList extends Component {
|
||||
return;
|
||||
}
|
||||
|
||||
app.session.user.pushAttributes({newNotificationCount: 0});
|
||||
app.session.user.pushAttributes({ newNotificationCount: 0 });
|
||||
|
||||
this.loadMore();
|
||||
}
|
||||
@@ -159,9 +160,10 @@ export default class NotificationList extends Component {
|
||||
this.loading = true;
|
||||
m.redraw();
|
||||
|
||||
const params = app.cache.notifications ? {page: {offset: app.cache.notifications.length * 10}} : null;
|
||||
const params = app.cache.notifications ? { page: { offset: app.cache.notifications.length * 10 } } : null;
|
||||
|
||||
return app.store.find('notifications', params)
|
||||
return app.store
|
||||
.find('notifications', params)
|
||||
.then(this.parseResults.bind(this))
|
||||
.catch(() => {})
|
||||
.then(() => {
|
||||
@@ -192,15 +194,15 @@ export default class NotificationList extends Component {
|
||||
markAllAsRead() {
|
||||
if (!app.cache.notifications) return;
|
||||
|
||||
app.session.user.pushAttributes({unreadNotificationCount: 0});
|
||||
app.session.user.pushAttributes({ unreadNotificationCount: 0 });
|
||||
|
||||
app.cache.notifications.forEach(notifications => {
|
||||
notifications.forEach(notification => notification.pushAttributes({isRead: true}))
|
||||
app.cache.notifications.forEach((notifications) => {
|
||||
notifications.forEach((notification) => notification.pushAttributes({ isRead: true }));
|
||||
});
|
||||
|
||||
app.request({
|
||||
url: app.forum.attribute('apiUrl') + '/notifications/read',
|
||||
method: 'POST'
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@ export default class NotificationsDropdown extends Dropdown {
|
||||
|
||||
vdom.attrs.title = this.props.label;
|
||||
|
||||
vdom.attrs.className += (newNotifications ? ' new' : '');
|
||||
vdom.attrs.className += newNotifications ? ' new' : '';
|
||||
vdom.attrs.onclick = this.onclick.bind(this);
|
||||
|
||||
return vdom;
|
||||
@@ -35,9 +35,9 @@ export default class NotificationsDropdown extends Dropdown {
|
||||
const unread = this.getUnreadCount();
|
||||
|
||||
return [
|
||||
icon(this.props.icon, {className: 'Button-icon'}),
|
||||
icon(this.props.icon, { className: 'Button-icon' }),
|
||||
unread ? <span className="NotificationsDropdown-unread">{unread}</span> : '',
|
||||
<span className="Button-label">{this.props.label}</span>
|
||||
<span className="Button-label">{this.props.label}</span>,
|
||||
];
|
||||
}
|
||||
|
||||
|
@@ -43,32 +43,40 @@ export default class Post extends Component {
|
||||
|
||||
return (
|
||||
<article {...attrs}>
|
||||
{this.subtree.retain() || (() => {
|
||||
const controls = PostControls.controls(this.props.post, this).toArray();
|
||||
{this.subtree.retain() ||
|
||||
(() => {
|
||||
const controls = PostControls.controls(this.props.post, this).toArray();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{this.content()}
|
||||
<aside className="Post-actions">
|
||||
<ul>
|
||||
{listItems(this.actionItems().toArray())}
|
||||
{controls.length ? <li>
|
||||
<Dropdown
|
||||
className="Post-controls"
|
||||
buttonClassName="Button Button--icon Button--flat"
|
||||
menuClassName="Dropdown-menu--right"
|
||||
icon="fas fa-ellipsis-h"
|
||||
onshow={() => this.$('.Post-actions').addClass('open')}
|
||||
onhide={() => this.$('.Post-actions').removeClass('open')}>
|
||||
{controls}
|
||||
</Dropdown>
|
||||
</li> : ''}
|
||||
</ul>
|
||||
</aside>
|
||||
<footer className="Post-footer"><ul>{listItems(this.footerItems().toArray())}</ul></footer>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
return (
|
||||
<div>
|
||||
{this.content()}
|
||||
<aside className="Post-actions">
|
||||
<ul>
|
||||
{listItems(this.actionItems().toArray())}
|
||||
{controls.length ? (
|
||||
<li>
|
||||
<Dropdown
|
||||
className="Post-controls"
|
||||
buttonClassName="Button Button--icon Button--flat"
|
||||
menuClassName="Dropdown-menu--right"
|
||||
icon="fas fa-ellipsis-h"
|
||||
onshow={() => this.$('.Post-actions').addClass('open')}
|
||||
onhide={() => this.$('.Post-actions').removeClass('open')}
|
||||
>
|
||||
{controls}
|
||||
</Dropdown>
|
||||
</li>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</ul>
|
||||
</aside>
|
||||
<footer className="Post-footer">
|
||||
<ul>{listItems(this.footerItems().toArray())}</ul>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -118,7 +126,7 @@ export default class Post extends Component {
|
||||
}
|
||||
|
||||
if (user && app.current.discussion && app.current.discussion.attribute('startUserId') == user.id()) {
|
||||
classes.push('Post--by-start-user')
|
||||
classes.push('Post--by-start-user');
|
||||
}
|
||||
|
||||
return classes;
|
||||
|
@@ -19,10 +19,7 @@ export default class PostEdited extends Component {
|
||||
view() {
|
||||
const post = this.props.post;
|
||||
const editedUser = post.editedUser();
|
||||
const editedInfo = extractText(app.translator.trans(
|
||||
'core.forum.post.edited_tooltip',
|
||||
{user: editedUser, ago: humanTime(post.editedAt())}
|
||||
));
|
||||
const editedInfo = extractText(app.translator.trans('core.forum.post.edited_tooltip', { user: editedUser, ago: humanTime(post.editedAt()) }));
|
||||
if (editedInfo !== this.oldEditedInfo) {
|
||||
this.shouldUpdateTooltip = true;
|
||||
this.oldEditedInfo = editedInfo;
|
||||
|
@@ -20,7 +20,7 @@ export default class PostMeta extends Component {
|
||||
|
||||
// When the dropdown menu is shown, select the contents of the permalink
|
||||
// input so that the user can quickly copy the URL.
|
||||
const selectPermalink = function() {
|
||||
const selectPermalink = function () {
|
||||
setTimeout(() => $(this).parent().find('.PostMeta-permalink').select());
|
||||
|
||||
m.redraw.strategy('none');
|
||||
@@ -33,12 +33,15 @@ export default class PostMeta extends Component {
|
||||
</a>
|
||||
|
||||
<div className="Dropdown-menu dropdown-menu">
|
||||
<span className="PostMeta-number">{app.translator.trans('core.forum.post.number_tooltip', {number: post.number()})}</span>{' '}
|
||||
<span className="PostMeta-time">{fullTime(time)}</span>{' '}
|
||||
<span className="PostMeta-ip">{post.data.attributes.ipAddress}</span>
|
||||
{touch
|
||||
? <a className="Button PostMeta-permalink" href={permalink}>{permalink}</a>
|
||||
: <input className="FormControl PostMeta-permalink" value={permalink} onclick={e => e.stopPropagation()} />}
|
||||
<span className="PostMeta-number">{app.translator.trans('core.forum.post.number_tooltip', { number: post.number() })}</span>{' '}
|
||||
<span className="PostMeta-time">{fullTime(time)}</span> <span className="PostMeta-ip">{post.data.attributes.ipAddress}</span>
|
||||
{touch ? (
|
||||
<a className="Button PostMeta-permalink" href={permalink}>
|
||||
{permalink}
|
||||
</a>
|
||||
) : (
|
||||
<input className="FormControl PostMeta-permalink" value={permalink} onclick={(e) => e.stopPropagation()} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@@ -21,8 +21,7 @@ export default class PostPreview extends Component {
|
||||
<a className="PostPreview" href={app.route.post(post)} config={m.route} onclick={this.props.onclick}>
|
||||
<span className="PostPreview-content">
|
||||
{avatar(user)}
|
||||
{username(user)}{' '}
|
||||
<span className="PostPreview-excerpt">{excerpt}</span>
|
||||
{username(user)} <span className="PostPreview-excerpt">{excerpt}</span>
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
|
@@ -52,11 +52,17 @@ class PostStream extends Component {
|
||||
// discussion and then scroll to the very bottom of the page.
|
||||
if (number === 'reply') {
|
||||
return this.goToLast().then(() => {
|
||||
$('html,body').stop(true).animate({
|
||||
scrollTop: $(document).height() - $(window).height()
|
||||
}, 'fast', () => {
|
||||
this.flashItem(this.$('.PostStream-item:last-child'));
|
||||
});
|
||||
$('html,body')
|
||||
.stop(true)
|
||||
.animate(
|
||||
{
|
||||
scrollTop: $(document).height() - $(window).height(),
|
||||
},
|
||||
'fast',
|
||||
() => {
|
||||
this.flashItem(this.$('.PostStream-item:last-child'));
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -176,9 +182,10 @@ class PostStream extends Component {
|
||||
* @return {Post[]}
|
||||
*/
|
||||
posts() {
|
||||
return this.discussion.postIds()
|
||||
return this.discussion
|
||||
.postIds()
|
||||
.slice(this.visibleStart, this.visibleEnd)
|
||||
.map(id => {
|
||||
.map((id) => {
|
||||
const post = app.store.getById('posts', id);
|
||||
|
||||
return post && post.discussion() && typeof post.canEdit() !== 'undefined' ? post : null;
|
||||
@@ -201,12 +208,12 @@ class PostStream extends Component {
|
||||
|
||||
const items = posts.map((post, i) => {
|
||||
let content;
|
||||
const attrs = {'data-index': this.visibleStart + i};
|
||||
const attrs = { 'data-index': this.visibleStart + i };
|
||||
|
||||
if (post) {
|
||||
const time = post.createdAt();
|
||||
const PostComponent = app.postComponents[post.contentType()];
|
||||
content = PostComponent ? PostComponent.component({post}) : '';
|
||||
content = PostComponent ? PostComponent.component({ post }) : '';
|
||||
|
||||
attrs.key = 'post' + post.id();
|
||||
attrs.config = fadeIn;
|
||||
@@ -223,9 +230,9 @@ class PostStream extends Component {
|
||||
if (dt > 1000 * 60 * 60 * 24 * 4) {
|
||||
content = [
|
||||
<div className="PostStream-timeGap">
|
||||
<span>{app.translator.trans('core.forum.post_stream.time_lapsed_text', {period: moment.duration(dt).humanize()})}</span>
|
||||
<span>{app.translator.trans('core.forum.post_stream.time_lapsed_text', { period: moment.duration(dt).humanize() })}</span>
|
||||
</div>,
|
||||
content
|
||||
content,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -236,7 +243,11 @@ class PostStream extends Component {
|
||||
content = PostLoading.component();
|
||||
}
|
||||
|
||||
return <div className="PostStream-item" {...attrs}>{content}</div>;
|
||||
return (
|
||||
<div className="PostStream-item" {...attrs}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
if (!this.viewingEnd && posts[this.visibleEnd - this.visibleStart - 1]) {
|
||||
@@ -254,16 +265,12 @@ class PostStream extends Component {
|
||||
if (this.viewingEnd && (!app.session.user || this.discussion.canReply())) {
|
||||
items.push(
|
||||
<div className="PostStream-item" key="reply">
|
||||
{ReplyPlaceholder.component({discussion: this.discussion})}
|
||||
{ReplyPlaceholder.component({ discussion: this.discussion })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="PostStream">
|
||||
{items}
|
||||
</div>
|
||||
);
|
||||
return <div className="PostStream">{items}</div>;
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
@@ -320,7 +327,7 @@ class PostStream extends Component {
|
||||
*/
|
||||
loadNext() {
|
||||
const start = this.visibleEnd;
|
||||
const end = this.visibleEnd = this.sanitizeIndex(this.visibleEnd + this.constructor.loadCount);
|
||||
const end = (this.visibleEnd = this.sanitizeIndex(this.visibleEnd + this.constructor.loadCount));
|
||||
|
||||
// Unload the posts which are two pages back from the page we're currently
|
||||
// loading.
|
||||
@@ -343,7 +350,7 @@ class PostStream extends Component {
|
||||
*/
|
||||
loadPrevious() {
|
||||
const end = this.visibleStart;
|
||||
const start = this.visibleStart = this.sanitizeIndex(this.visibleStart - this.constructor.loadCount);
|
||||
const start = (this.visibleStart = this.sanitizeIndex(this.visibleStart - this.constructor.loadCount));
|
||||
|
||||
// Unload the posts which are two pages back from the page we're currently
|
||||
// loading.
|
||||
@@ -379,13 +386,16 @@ class PostStream extends Component {
|
||||
};
|
||||
redraw();
|
||||
|
||||
this.loadPageTimeouts[start] = setTimeout(() => {
|
||||
this.loadRange(start, end).then(() => {
|
||||
redraw();
|
||||
this.pagesLoading--;
|
||||
});
|
||||
this.loadPageTimeouts[start] = null;
|
||||
}, this.pagesLoading ? 1000 : 0);
|
||||
this.loadPageTimeouts[start] = setTimeout(
|
||||
() => {
|
||||
this.loadRange(start, end).then(() => {
|
||||
redraw();
|
||||
this.pagesLoading--;
|
||||
});
|
||||
this.loadPageTimeouts[start] = null;
|
||||
},
|
||||
this.pagesLoading ? 1000 : 0
|
||||
);
|
||||
|
||||
this.pagesLoading++;
|
||||
}
|
||||
@@ -402,19 +412,20 @@ class PostStream extends Component {
|
||||
const loadIds = [];
|
||||
const loaded = [];
|
||||
|
||||
this.discussion.postIds().slice(start, end).forEach(id => {
|
||||
const post = app.store.getById('posts', id);
|
||||
this.discussion
|
||||
.postIds()
|
||||
.slice(start, end)
|
||||
.forEach((id) => {
|
||||
const post = app.store.getById('posts', id);
|
||||
|
||||
if (post && post.discussion() && typeof post.canEdit() !== 'undefined') {
|
||||
loaded.push(post);
|
||||
} else {
|
||||
loadIds.push(id);
|
||||
}
|
||||
});
|
||||
if (post && post.discussion() && typeof post.canEdit() !== 'undefined') {
|
||||
loaded.push(post);
|
||||
} else {
|
||||
loadIds.push(id);
|
||||
}
|
||||
});
|
||||
|
||||
return loadIds.length
|
||||
? app.store.find('posts', loadIds)
|
||||
: m.deferred().resolve(loaded).promise;
|
||||
return loadIds.length ? app.store.find('posts', loadIds) : m.deferred().resolve(loaded).promise;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -426,16 +437,18 @@ class PostStream extends Component {
|
||||
* @return {Promise}
|
||||
*/
|
||||
loadNearNumber(number) {
|
||||
if (this.posts().some(post => post && Number(post.number()) === Number(number))) {
|
||||
if (this.posts().some((post) => post && Number(post.number()) === Number(number))) {
|
||||
return m.deferred().resolve().promise;
|
||||
}
|
||||
|
||||
this.reset();
|
||||
|
||||
return app.store.find('posts', {
|
||||
filter: {discussion: this.discussion.id()},
|
||||
page: {near: number}
|
||||
}).then(this.show.bind(this));
|
||||
return app.store
|
||||
.find('posts', {
|
||||
filter: { discussion: this.discussion.id() },
|
||||
page: { near: number },
|
||||
})
|
||||
.then(this.show.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -471,7 +484,7 @@ class PostStream extends Component {
|
||||
let startNumber;
|
||||
let endNumber;
|
||||
|
||||
this.$('.PostStream-item').each(function() {
|
||||
this.$('.PostStream-item').each(function () {
|
||||
const $item = $(this);
|
||||
const top = $item.offset().top;
|
||||
const height = $item.outerHeight(true);
|
||||
@@ -556,14 +569,12 @@ class PostStream extends Component {
|
||||
// If we're scrolling to the bottom of an item, then we'll make sure the
|
||||
// bottom will line up with the top of the composer.
|
||||
if (force || itemTop < scrollTop || itemBottom > scrollBottom) {
|
||||
const top = bottom
|
||||
? itemBottom - $(window).height() + app.composer.computedHeight()
|
||||
: ($item.is(':first-child') ? 0 : itemTop);
|
||||
const top = bottom ? itemBottom - $(window).height() + app.composer.computedHeight() : $item.is(':first-child') ? 0 : itemTop;
|
||||
|
||||
if (noAnimation) {
|
||||
$container.scrollTop(top);
|
||||
} else if (top !== scrollTop) {
|
||||
$container.animate({scrollTop: top}, 'fast');
|
||||
$container.animate({ scrollTop: top }, 'fast');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -40,7 +40,7 @@ export default class PostStreamScrubber extends Component {
|
||||
|
||||
// When the post stream begins loading posts at a certain index, we want our
|
||||
// scrubber scrollbar to jump to that position.
|
||||
this.props.stream.on('unpaused', this.handlers.streamWasUnpaused = this.streamWasUnpaused.bind(this));
|
||||
this.props.stream.on('unpaused', (this.handlers.streamWasUnpaused = this.streamWasUnpaused.bind(this)));
|
||||
|
||||
// Define a handler to update the state of the scrollbar to reflect the
|
||||
// current scroll position of the page.
|
||||
@@ -61,14 +61,14 @@ export default class PostStreamScrubber extends Component {
|
||||
|
||||
const viewing = app.translator.transChoice('core.forum.post_scrubber.viewing_text', count, {
|
||||
index: <span className="Scrubber-index">{retain || formatNumber(Math.min(Math.ceil(this.index + this.visible), count))}</span>,
|
||||
count: <span className="Scrubber-count">{formatNumber(count)}</span>
|
||||
count: <span className="Scrubber-count">{formatNumber(count)}</span>,
|
||||
});
|
||||
|
||||
function styleUnread(element, isInitialized, context) {
|
||||
const $element = $(element);
|
||||
const newStyle = {
|
||||
top: (100 - unreadPercent * 100) + '%',
|
||||
height: (unreadPercent * 100) + '%'
|
||||
top: 100 - unreadPercent * 100 + '%',
|
||||
height: unreadPercent * 100 + '%',
|
||||
};
|
||||
|
||||
if (context.oldStyle) {
|
||||
@@ -93,18 +93,18 @@ export default class PostStreamScrubber extends Component {
|
||||
</a>
|
||||
|
||||
<div className="Scrubber-scrollbar">
|
||||
<div className="Scrubber-before"/>
|
||||
<div className="Scrubber-before" />
|
||||
<div className="Scrubber-handle">
|
||||
<div className="Scrubber-bar"/>
|
||||
<div className="Scrubber-bar" />
|
||||
<div className="Scrubber-info">
|
||||
<strong>{viewing}</strong>
|
||||
<span className="Scrubber-description">{retain || this.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="Scrubber-after"/>
|
||||
<div className="Scrubber-after" />
|
||||
|
||||
<div className="Scrubber-unread" config={styleUnread}>
|
||||
{app.translator.trans('core.forum.post_scrubber.unread_text', {count: unreadCount})}
|
||||
{app.translator.trans('core.forum.post_scrubber.unread_text', { count: unreadCount })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -202,7 +202,7 @@ export default class PostStreamScrubber extends Component {
|
||||
// Now loop through each of the items in the discussion. An 'item' is
|
||||
// either a single post or a 'gap' of one or more posts that haven't
|
||||
// been loaded yet.
|
||||
$items.each(function() {
|
||||
$items.each(function () {
|
||||
const $this = $(this);
|
||||
const top = $this.offset().top;
|
||||
const height = $this.outerHeight(true);
|
||||
@@ -251,7 +251,9 @@ export default class PostStreamScrubber extends Component {
|
||||
|
||||
// Whenever the window is resized, adjust the height of the scrollbar
|
||||
// so that it fills the height of the sidebar.
|
||||
$(window).on('resize', this.handlers.onresize = this.onresize.bind(this)).resize();
|
||||
$(window)
|
||||
.on('resize', (this.handlers.onresize = this.onresize.bind(this)))
|
||||
.resize();
|
||||
|
||||
// When any part of the whole scrollbar is clicked, we want to jump to
|
||||
// that position.
|
||||
@@ -261,7 +263,7 @@ export default class PostStreamScrubber extends Component {
|
||||
// Now we want to make the scrollbar handle draggable. Let's start by
|
||||
// preventing default browser events from messing things up.
|
||||
.css({ cursor: 'pointer', 'user-select': 'none' })
|
||||
.bind('dragstart mousedown touchstart', e => e.preventDefault());
|
||||
.bind('dragstart mousedown touchstart', (e) => e.preventDefault());
|
||||
|
||||
// When the mouse is pressed on the scrollbar handle, we capture some
|
||||
// information about its current position. We will store this
|
||||
@@ -276,15 +278,15 @@ export default class PostStreamScrubber extends Component {
|
||||
.bind('mousedown touchstart', this.onmousedown.bind(this))
|
||||
|
||||
// Exempt the scrollbar handle from the 'jump to' click event.
|
||||
.click(e => e.stopPropagation());
|
||||
.click((e) => e.stopPropagation());
|
||||
|
||||
// When the mouse moves and when it is released, we pass the
|
||||
// information that we captured when the mouse was first pressed onto
|
||||
// some event handlers. These handlers will move the scrollbar/stream-
|
||||
// content as appropriate.
|
||||
$(document)
|
||||
.on('mousemove touchmove', this.handlers.onmousemove = this.onmousemove.bind(this))
|
||||
.on('mouseup touchend', this.handlers.onmouseup = this.onmouseup.bind(this));
|
||||
.on('mousemove touchmove', (this.handlers.onmousemove = this.onmousemove.bind(this)))
|
||||
.on('mouseup touchend', (this.handlers.onmouseup = this.onmouseup.bind(this)));
|
||||
}
|
||||
|
||||
ondestroy() {
|
||||
@@ -292,12 +294,9 @@ export default class PostStreamScrubber extends Component {
|
||||
|
||||
this.props.stream.off('unpaused', this.handlers.streamWasUnpaused);
|
||||
|
||||
$(window)
|
||||
.off('resize', this.handlers.onresize);
|
||||
$(window).off('resize', this.handlers.onresize);
|
||||
|
||||
$(document)
|
||||
.off('mousemove touchmove', this.handlers.onmousemove)
|
||||
.off('mouseup touchend', this.handlers.onmouseup);
|
||||
$(document).off('mousemove touchmove', this.handlers.onmousemove).off('mouseup touchend', this.handlers.onmouseup);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -325,7 +324,7 @@ export default class PostStreamScrubber extends Component {
|
||||
const func = animate ? 'animate' : 'css';
|
||||
for (const part in heights) {
|
||||
const $part = $scrubber.find(`.Scrubber-${part}`);
|
||||
$part.stop(true, true)[func]({height: heights[part] + '%'}, 'fast');
|
||||
$part.stop(true, true)[func]({ height: heights[part] + '%' }, 'fast');
|
||||
|
||||
// jQuery likes to put overflow:hidden, but because the scrollbar handle
|
||||
// has a negative margin-left, we need to override.
|
||||
@@ -353,13 +352,13 @@ export default class PostStreamScrubber extends Component {
|
||||
// minimum percentage per visible post. If this is greater than the actual
|
||||
// percentage per post, then we need to adjust the 'before' percentage to
|
||||
// account for it.
|
||||
const minPercentVisible = 50 / this.$('.Scrubber-scrollbar').outerHeight() * 100;
|
||||
const minPercentVisible = (50 / this.$('.Scrubber-scrollbar').outerHeight()) * 100;
|
||||
const percentPerVisiblePost = Math.max(100 / count, minPercentVisible / visible);
|
||||
const percentPerPost = count === visible ? 0 : (100 - percentPerVisiblePost * visible) / (count - visible);
|
||||
|
||||
return {
|
||||
index: percentPerPost,
|
||||
visible: percentPerVisiblePost
|
||||
visible: percentPerVisiblePost,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -371,10 +370,14 @@ export default class PostStreamScrubber extends Component {
|
||||
const scrubber = this.$();
|
||||
const scrollbar = this.$('.Scrubber-scrollbar');
|
||||
|
||||
scrollbar.css('max-height', $(window).height() -
|
||||
scrubber.offset().top + $(window).scrollTop() -
|
||||
parseInt($('#app').css('padding-bottom'), 10) -
|
||||
(scrubber.outerHeight() - scrollbar.outerHeight()));
|
||||
scrollbar.css(
|
||||
'max-height',
|
||||
$(window).height() -
|
||||
scrubber.offset().top +
|
||||
$(window).scrollTop() -
|
||||
parseInt($('#app').css('padding-bottom'), 10) -
|
||||
(scrubber.outerHeight() - scrollbar.outerHeight())
|
||||
);
|
||||
}
|
||||
|
||||
onmousedown(e) {
|
||||
@@ -393,8 +396,8 @@ export default class PostStreamScrubber extends Component {
|
||||
// finally convert it into an index. Add this delta index onto
|
||||
// the index at which the drag was started, and then scroll there.
|
||||
const deltaPixels = (e.clientY || e.originalEvent.touches[0].clientY) - this.mouseStart;
|
||||
const deltaPercent = deltaPixels / this.$('.Scrubber-scrollbar').outerHeight() * 100;
|
||||
const deltaIndex = (deltaPercent / this.percentPerPost().index) || 0;
|
||||
const deltaPercent = (deltaPixels / this.$('.Scrubber-scrollbar').outerHeight()) * 100;
|
||||
const deltaIndex = deltaPercent / this.percentPerPost().index || 0;
|
||||
const newIndex = Math.min(this.indexStart + deltaIndex, this.count() - 1);
|
||||
|
||||
this.index = Math.max(0, newIndex);
|
||||
@@ -425,7 +428,7 @@ export default class PostStreamScrubber extends Component {
|
||||
// percentage of the scrollbar's height.
|
||||
const $scrollbar = this.$('.Scrubber-scrollbar');
|
||||
const offsetPixels = (e.pageY || e.originalEvent.touches[0].pageY) - $scrollbar.offset().top + $('body').scrollTop();
|
||||
let offsetPercent = offsetPixels / $scrollbar.outerHeight() * 100;
|
||||
let offsetPercent = (offsetPixels / $scrollbar.outerHeight()) * 100;
|
||||
|
||||
// 2. We want the handle of the scrollbar to end up centered on the click
|
||||
// position. Thus, we calculate the height of the handle in percent and
|
||||
|
@@ -29,7 +29,9 @@ export default class PostUser extends Component {
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="PostUser">
|
||||
<h3>{avatar(user, {className: 'PostUser-avatar'})} {username(user)}</h3>
|
||||
<h3>
|
||||
{avatar(user, { className: 'PostUser-avatar' })} {username(user)}
|
||||
</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -40,7 +42,7 @@ export default class PostUser extends Component {
|
||||
card = UserCard.component({
|
||||
user,
|
||||
className: 'UserCard--popover',
|
||||
controlsButtonClassName: 'Button Button--icon Button--flat'
|
||||
controlsButtonClassName: 'Button Button--icon Button--flat',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,14 +50,12 @@ export default class PostUser extends Component {
|
||||
<div className="PostUser">
|
||||
<h3>
|
||||
<a href={app.route.user(user)} config={m.route}>
|
||||
{avatar(user, {className: 'PostUser-avatar'})}
|
||||
{avatar(user, { className: 'PostUser-avatar' })}
|
||||
{userOnline(user)}
|
||||
{username(user)}
|
||||
</a>
|
||||
</h3>
|
||||
<ul className="PostUser-badges badges">
|
||||
{listItems(user.badges().toArray())}
|
||||
</ul>
|
||||
<ul className="PostUser-badges badges">{listItems(user.badges().toArray())}</ul>
|
||||
{card}
|
||||
</div>
|
||||
);
|
||||
@@ -92,7 +92,8 @@ export default class PostUser extends Component {
|
||||
* Hide the user card.
|
||||
*/
|
||||
hideCard() {
|
||||
this.$('.UserCard').removeClass('in')
|
||||
this.$('.UserCard')
|
||||
.removeClass('in')
|
||||
.one('transitionend webkitTransitionEnd oTransitionEnd', () => {
|
||||
this.cardVisible = false;
|
||||
m.redraw();
|
||||
|
@@ -44,7 +44,7 @@ export default class PostsUserPage extends UserPage {
|
||||
}
|
||||
|
||||
content() {
|
||||
if (this.posts.length === 0 && ! this.loading) {
|
||||
if (this.posts.length === 0 && !this.loading) {
|
||||
return (
|
||||
<div className="PostsUserPage">
|
||||
<Placeholder text={app.translator.trans('core.forum.user.posts_empty_text')} />
|
||||
@@ -62,7 +62,7 @@ export default class PostsUserPage extends UserPage {
|
||||
{Button.component({
|
||||
children: app.translator.trans('core.forum.user.posts_load_more_button'),
|
||||
className: 'Button',
|
||||
onclick: this.loadMore.bind(this)
|
||||
onclick: this.loadMore.bind(this),
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
@@ -71,18 +71,22 @@ export default class PostsUserPage extends UserPage {
|
||||
return (
|
||||
<div className="PostsUserPage">
|
||||
<ul className="PostsUserPage-list">
|
||||
{this.posts.map(post => (
|
||||
{this.posts.map((post) => (
|
||||
<li>
|
||||
<div className="PostsUserPage-discussion">
|
||||
{app.translator.trans('core.forum.user.in_discussion_text', {discussion: <a href={app.route.post(post)} config={m.route}>{post.discussion().title()}</a>})}
|
||||
{app.translator.trans('core.forum.user.in_discussion_text', {
|
||||
discussion: (
|
||||
<a href={app.route.post(post)} config={m.route}>
|
||||
{post.discussion().title()}
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
</div>
|
||||
{CommentPost.component({post})}
|
||||
{CommentPost.component({ post })}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="PostsUserPage-loadMore">
|
||||
{footer}
|
||||
</div>
|
||||
<div className="PostsUserPage-loadMore">{footer}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -122,10 +126,10 @@ export default class PostsUserPage extends UserPage {
|
||||
return app.store.find('posts', {
|
||||
filter: {
|
||||
user: this.user.id(),
|
||||
type: 'comment'
|
||||
type: 'comment',
|
||||
},
|
||||
page: {offset, limit: this.loadLimit},
|
||||
sort: '-createdAt'
|
||||
page: { offset, limit: this.loadLimit },
|
||||
sort: '-createdAt',
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -33,12 +33,12 @@ export default class RenameDiscussionModal extends Modal {
|
||||
className: 'Button Button--primary Button--block',
|
||||
type: 'submit',
|
||||
loading: this.loading,
|
||||
children: app.translator.trans('core.forum.rename_discussion.submit_button')
|
||||
children: app.translator.trans('core.forum.rename_discussion.submit_button'),
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
@@ -53,16 +53,19 @@ export default class RenameDiscussionModal extends Modal {
|
||||
// save has completed, update the post stream as there will be a new post
|
||||
// indicating that the discussion was renamed.
|
||||
if (title && title !== currentTitle) {
|
||||
return this.discussion.save({title}).then(() => {
|
||||
if (app.viewingDiscussion(this.discussion)) {
|
||||
app.current.stream.update();
|
||||
}
|
||||
m.redraw();
|
||||
this.hide();
|
||||
}).catch(() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
return this.discussion
|
||||
.save({ title })
|
||||
.then(() => {
|
||||
if (app.viewingDiscussion(this.discussion)) {
|
||||
app.current.stream.update();
|
||||
}
|
||||
m.redraw();
|
||||
this.hide();
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@ export default class ReplyComposer extends ComposerBody {
|
||||
init() {
|
||||
super.init();
|
||||
|
||||
this.editor.props.preview = e => {
|
||||
this.editor.props.preview = (e) => {
|
||||
minimizeComposerIfFullScreen(e);
|
||||
|
||||
m.route(app.route.discussion(this.props.discussion, 'reply'));
|
||||
@@ -43,18 +43,21 @@ export default class ReplyComposer extends ComposerBody {
|
||||
const items = super.headerItems();
|
||||
const discussion = this.props.discussion;
|
||||
|
||||
const routeAndMinimize = function(element, isInitialized) {
|
||||
const routeAndMinimize = function (element, isInitialized) {
|
||||
if (isInitialized) return;
|
||||
$(element).on('click', minimizeComposerIfFullScreen);
|
||||
m.route.apply(this, arguments);
|
||||
};
|
||||
|
||||
items.add('title', (
|
||||
items.add(
|
||||
'title',
|
||||
<h3>
|
||||
{icon('fas fa-reply')} {' '}
|
||||
<a href={app.route.discussion(discussion)} config={routeAndMinimize}>{discussion.title()}</a>
|
||||
{icon('fas fa-reply')}{' '}
|
||||
<a href={app.route.discussion(discussion)} config={routeAndMinimize}>
|
||||
{discussion.title()}
|
||||
</a>
|
||||
</h3>
|
||||
));
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -67,7 +70,7 @@ export default class ReplyComposer extends ComposerBody {
|
||||
data() {
|
||||
return {
|
||||
content: this.content(),
|
||||
relationships: {discussion: this.props.discussion}
|
||||
relationships: { discussion: this.props.discussion },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,13 +82,14 @@ export default class ReplyComposer extends ComposerBody {
|
||||
|
||||
const data = this.data();
|
||||
|
||||
app.store.createRecord('posts').save(data).then(
|
||||
post => {
|
||||
app.store
|
||||
.createRecord('posts')
|
||||
.save(data)
|
||||
.then((post) => {
|
||||
// If we're currently viewing the discussion which this reply was made
|
||||
// in, then we can update the post stream and scroll to the post.
|
||||
if (app.viewingDiscussion(discussion)) {
|
||||
app.current.stream.update().then(() => app.current.stream.goToNumber(post.number()));
|
||||
|
||||
} else {
|
||||
// Otherwise, we'll create an alert message to inform the user that
|
||||
// their reply has been posted, containing a button which will
|
||||
@@ -97,20 +101,18 @@ export default class ReplyComposer extends ComposerBody {
|
||||
onclick: () => {
|
||||
m.route(app.route.post(post));
|
||||
app.alerts.dismiss(alert);
|
||||
}
|
||||
},
|
||||
});
|
||||
app.alerts.show(
|
||||
alert = new Alert({
|
||||
(alert = new Alert({
|
||||
type: 'success',
|
||||
children: app.translator.trans('core.forum.composer_reply.posted_message'),
|
||||
controls: [viewButton]
|
||||
})
|
||||
controls: [viewButton],
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
app.composer.hide();
|
||||
},
|
||||
this.loaded.bind(this)
|
||||
);
|
||||
}, this.loaded.bind(this));
|
||||
}
|
||||
}
|
||||
|
@@ -21,12 +21,12 @@ export default class ReplyPlaceholder extends Component {
|
||||
<header className="Post-header">
|
||||
<div className="PostUser">
|
||||
<h3>
|
||||
{avatar(app.session.user, {className: 'PostUser-avatar'})}
|
||||
{avatar(app.session.user, { className: 'PostUser-avatar' })}
|
||||
{username(app.session.user)}
|
||||
</h3>
|
||||
</div>
|
||||
</header>
|
||||
<div className="Post-body" config={this.configPreview.bind(this)}/>
|
||||
<div className="Post-body" config={this.configPreview.bind(this)} />
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -38,8 +38,7 @@ export default class ReplyPlaceholder extends Component {
|
||||
return (
|
||||
<article className="Post ReplyPlaceholder" onclick={reply}>
|
||||
<header className="Post-header">
|
||||
{avatar(app.session.user, {className: 'PostUser-avatar'})}{' '}
|
||||
{app.translator.trans('core.forum.post_stream.reply_placeholder')}
|
||||
{avatar(app.session.user, { className: 'PostUser-avatar' })} {app.translator.trans('core.forum.post_stream.reply_placeholder')}
|
||||
</header>
|
||||
</article>
|
||||
);
|
||||
|
@@ -84,30 +84,39 @@ export default class Search extends Component {
|
||||
if (!this.sources.length) return <div></div>;
|
||||
|
||||
return (
|
||||
<div className={'Search ' + classList({
|
||||
open: this.value() && this.hasFocus,
|
||||
focused: this.hasFocus,
|
||||
active: !!currentSearch,
|
||||
loading: !!this.loadingSources
|
||||
})}>
|
||||
<div
|
||||
className={
|
||||
'Search ' +
|
||||
classList({
|
||||
open: this.value() && this.hasFocus,
|
||||
focused: this.hasFocus,
|
||||
active: !!currentSearch,
|
||||
loading: !!this.loadingSources,
|
||||
})
|
||||
}
|
||||
>
|
||||
<div className="Search-input">
|
||||
<input className="FormControl"
|
||||
<input
|
||||
className="FormControl"
|
||||
type="search"
|
||||
placeholder={extractText(app.translator.trans('core.forum.header.search_placeholder'))}
|
||||
value={this.value()}
|
||||
oninput={m.withAttr('value', this.value)}
|
||||
onfocus={() => this.hasFocus = true}
|
||||
onblur={() => this.hasFocus = false}/>
|
||||
{this.loadingSources
|
||||
? LoadingIndicator.component({size: 'tiny', className: 'Button Button--icon Button--link'})
|
||||
: currentSearch
|
||||
? <button className="Search-clear Button Button--icon Button--link" onclick={this.clear.bind(this)}>{icon('fas fa-times-circle')}</button>
|
||||
: ''}
|
||||
onfocus={() => (this.hasFocus = true)}
|
||||
onblur={() => (this.hasFocus = false)}
|
||||
/>
|
||||
{this.loadingSources ? (
|
||||
LoadingIndicator.component({ size: 'tiny', className: 'Button Button--icon Button--link' })
|
||||
) : currentSearch ? (
|
||||
<button className="Search-clear Button Button--icon Button--link" onclick={this.clear.bind(this)}>
|
||||
{icon('fas fa-times-circle')}
|
||||
</button>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
<ul className="Dropdown-menu Search-results">
|
||||
{this.value() && this.hasFocus
|
||||
? this.sources.map(source => source.view(this.value()))
|
||||
: ''}
|
||||
{this.value() && this.hasFocus ? this.sources.map((source) => source.view(this.value())) : ''}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
@@ -122,14 +131,12 @@ export default class Search extends Component {
|
||||
const search = this;
|
||||
|
||||
this.$('.Search-results')
|
||||
.on('mousedown', e => e.preventDefault())
|
||||
.on('mousedown', (e) => e.preventDefault())
|
||||
.on('click', () => this.$('input').blur())
|
||||
|
||||
// Whenever the mouse is hovered over a search result, highlight it.
|
||||
.on('mouseenter', '> li:not(.Dropdown-header)', function() {
|
||||
search.setIndex(
|
||||
search.selectableItems().index(this)
|
||||
);
|
||||
.on('mouseenter', '> li:not(.Dropdown-header)', function () {
|
||||
search.setIndex(search.selectableItems().index(this));
|
||||
});
|
||||
|
||||
const $input = this.$('input');
|
||||
@@ -144,7 +151,7 @@ export default class Search extends Component {
|
||||
|
||||
// Handle input key events on the search input, triggering results to load.
|
||||
$input
|
||||
.on('input focus', function() {
|
||||
.on('input focus', function () {
|
||||
const query = this.value.toLowerCase();
|
||||
|
||||
if (!query) return;
|
||||
@@ -154,7 +161,7 @@ export default class Search extends Component {
|
||||
if (search.searched.indexOf(query) !== -1) return;
|
||||
|
||||
if (query.length >= 3) {
|
||||
search.sources.map(source => {
|
||||
search.sources.map((source) => {
|
||||
if (!source.search) return;
|
||||
|
||||
search.loadingSources++;
|
||||
@@ -171,8 +178,10 @@ export default class Search extends Component {
|
||||
}, 250);
|
||||
})
|
||||
|
||||
.on('focus', function() {
|
||||
$(this).one('mouseup', e => e.preventDefault()).select();
|
||||
.on('focus', function () {
|
||||
$(this)
|
||||
.one('mouseup', (e) => e.preventDefault())
|
||||
.select();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -243,9 +252,7 @@ export default class Search extends Component {
|
||||
* @return {Integer}
|
||||
*/
|
||||
getCurrentNumericIndex() {
|
||||
return this.selectableItems().index(
|
||||
this.getItem(this.index)
|
||||
);
|
||||
return this.selectableItems().index(this.getItem(this.index));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -303,7 +310,7 @@ export default class Search extends Component {
|
||||
}
|
||||
|
||||
if (typeof scrollTop !== 'undefined') {
|
||||
$dropdown.stop(true).animate({scrollTop}, 100);
|
||||
$dropdown.stop(true).animate({ scrollTop }, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -17,8 +17,7 @@ export default class SearchSource {
|
||||
* @param {String} query
|
||||
* @return {Promise}
|
||||
*/
|
||||
search() {
|
||||
}
|
||||
search() {}
|
||||
|
||||
/**
|
||||
* Get an array of virtual <li>s that list the search results for the given
|
||||
@@ -27,6 +26,5 @@ export default class SearchSource {
|
||||
* @param {String} query
|
||||
* @return {Object}
|
||||
*/
|
||||
view() {
|
||||
}
|
||||
view() {}
|
||||
}
|
||||
|
@@ -29,10 +29,7 @@ export default class SessionDropdown extends Dropdown {
|
||||
getButtonContent() {
|
||||
const user = app.session.user;
|
||||
|
||||
return [
|
||||
avatar(user), ' ',
|
||||
<span className="Button-label">{username(user)}</span>
|
||||
];
|
||||
return [avatar(user), ' ', <span className="Button-label">{username(user)}</span>];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,32 +41,35 @@ export default class SessionDropdown extends Dropdown {
|
||||
const items = new ItemList();
|
||||
const user = app.session.user;
|
||||
|
||||
items.add('profile',
|
||||
items.add(
|
||||
'profile',
|
||||
LinkButton.component({
|
||||
icon: 'fas fa-user',
|
||||
children: app.translator.trans('core.forum.header.profile_button'),
|
||||
href: app.route.user(user)
|
||||
href: app.route.user(user),
|
||||
}),
|
||||
100
|
||||
);
|
||||
|
||||
items.add('settings',
|
||||
items.add(
|
||||
'settings',
|
||||
LinkButton.component({
|
||||
icon: 'fas fa-cog',
|
||||
children: app.translator.trans('core.forum.header.settings_button'),
|
||||
href: app.route('settings')
|
||||
href: app.route('settings'),
|
||||
}),
|
||||
50
|
||||
);
|
||||
|
||||
if (app.forum.attribute('adminUrl')) {
|
||||
items.add('administration',
|
||||
items.add(
|
||||
'administration',
|
||||
LinkButton.component({
|
||||
icon: 'fas fa-wrench',
|
||||
children: app.translator.trans('core.forum.header.admin_button'),
|
||||
href: app.forum.attribute('adminUrl'),
|
||||
target: '_blank',
|
||||
config: () => {}
|
||||
config: () => {},
|
||||
}),
|
||||
0
|
||||
);
|
||||
@@ -77,11 +77,12 @@ export default class SessionDropdown extends Dropdown {
|
||||
|
||||
items.add('separator', Separator.component(), -90);
|
||||
|
||||
items.add('logOut',
|
||||
items.add(
|
||||
'logOut',
|
||||
Button.component({
|
||||
icon: 'fas fa-sign-out-alt',
|
||||
children: app.translator.trans('core.forum.header.log_out_button'),
|
||||
onclick: app.session.logout.bind(app.session)
|
||||
onclick: app.session.logout.bind(app.session),
|
||||
}),
|
||||
-100
|
||||
);
|
||||
|
@@ -36,27 +36,30 @@ export default class SettingsPage extends UserPage {
|
||||
settingsItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('account',
|
||||
items.add(
|
||||
'account',
|
||||
FieldSet.component({
|
||||
label: app.translator.trans('core.forum.settings.account_heading'),
|
||||
className: 'Settings-account',
|
||||
children: this.accountItems().toArray()
|
||||
children: this.accountItems().toArray(),
|
||||
})
|
||||
);
|
||||
|
||||
items.add('notifications',
|
||||
items.add(
|
||||
'notifications',
|
||||
FieldSet.component({
|
||||
label: app.translator.trans('core.forum.settings.notifications_heading'),
|
||||
className: 'Settings-notifications',
|
||||
children: this.notificationsItems().toArray()
|
||||
children: this.notificationsItems().toArray(),
|
||||
})
|
||||
);
|
||||
|
||||
items.add('privacy',
|
||||
items.add(
|
||||
'privacy',
|
||||
FieldSet.component({
|
||||
label: app.translator.trans('core.forum.settings.privacy_heading'),
|
||||
className: 'Settings-privacy',
|
||||
children: this.privacyItems().toArray()
|
||||
children: this.privacyItems().toArray(),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -71,19 +74,21 @@ export default class SettingsPage extends UserPage {
|
||||
accountItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('changePassword',
|
||||
items.add(
|
||||
'changePassword',
|
||||
Button.component({
|
||||
children: app.translator.trans('core.forum.settings.change_password_button'),
|
||||
className: 'Button',
|
||||
onclick: () => app.modal.show(new ChangePasswordModal())
|
||||
onclick: () => app.modal.show(new ChangePasswordModal()),
|
||||
})
|
||||
);
|
||||
|
||||
items.add('changeEmail',
|
||||
items.add(
|
||||
'changeEmail',
|
||||
Button.component({
|
||||
children: app.translator.trans('core.forum.settings.change_email_button'),
|
||||
className: 'Button',
|
||||
onclick: () => app.modal.show(new ChangeEmailModal())
|
||||
onclick: () => app.modal.show(new ChangeEmailModal()),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -98,7 +103,7 @@ export default class SettingsPage extends UserPage {
|
||||
notificationsItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('notificationGrid', NotificationGrid.component({user: this.user}));
|
||||
items.add('notificationGrid', NotificationGrid.component({ user: this.user }));
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -114,7 +119,7 @@ export default class SettingsPage extends UserPage {
|
||||
if (component) component.loading = true;
|
||||
m.redraw();
|
||||
|
||||
this.user.savePreferences({[key]: value}).then(() => {
|
||||
this.user.savePreferences({ [key]: value }).then(() => {
|
||||
if (component) component.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
@@ -129,14 +134,15 @@ export default class SettingsPage extends UserPage {
|
||||
privacyItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('discloseOnline',
|
||||
items.add(
|
||||
'discloseOnline',
|
||||
Switch.component({
|
||||
children: app.translator.trans('core.forum.settings.privacy_disclose_online_label'),
|
||||
state: this.user.preferences().discloseOnline,
|
||||
onchange: (value, component) => {
|
||||
this.user.pushAttributes({lastSeenAt: null});
|
||||
this.user.pushAttributes({ lastSeenAt: null });
|
||||
this.preferenceSaver('discloseOnline')(value, component);
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
|
@@ -50,14 +50,7 @@ export default class SignUpModal extends Modal {
|
||||
}
|
||||
|
||||
content() {
|
||||
return [
|
||||
<div className="Modal-body">
|
||||
{this.body()}
|
||||
</div>,
|
||||
<div className="Modal-footer">
|
||||
{this.footer()}
|
||||
</div>
|
||||
];
|
||||
return [<div className="Modal-body">{this.body()}</div>, <div className="Modal-footer">{this.footer()}</div>];
|
||||
}
|
||||
|
||||
isProvided(field) {
|
||||
@@ -65,58 +58,78 @@ export default class SignUpModal extends Modal {
|
||||
}
|
||||
|
||||
body() {
|
||||
return [
|
||||
this.props.token ? '' : <LogInButtons/>,
|
||||
|
||||
<div className="Form Form--centered">
|
||||
{this.fields().toArray()}
|
||||
</div>
|
||||
];
|
||||
return [this.props.token ? '' : <LogInButtons />, <div className="Form Form--centered">{this.fields().toArray()}</div>];
|
||||
}
|
||||
|
||||
fields() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('username', <div className="Form-group">
|
||||
<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 || this.isProvided('username')} />
|
||||
</div>, 30);
|
||||
items.add(
|
||||
'username',
|
||||
<div className="Form-group">
|
||||
<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 || this.isProvided('username')}
|
||||
/>
|
||||
</div>,
|
||||
30
|
||||
);
|
||||
|
||||
items.add('email', <div className="Form-group">
|
||||
<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.isProvided('email')} />
|
||||
</div>, 20);
|
||||
items.add(
|
||||
'email',
|
||||
<div className="Form-group">
|
||||
<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.isProvided('email')}
|
||||
/>
|
||||
</div>,
|
||||
20
|
||||
);
|
||||
|
||||
if (!this.props.token) {
|
||||
items.add('password', <div className="Form-group">
|
||||
<input className="FormControl" name="password" type="password" placeholder={extractText(app.translator.trans('core.forum.sign_up.password_placeholder'))}
|
||||
value={this.password()}
|
||||
onchange={m.withAttr('value', this.password)}
|
||||
disabled={this.loading} />
|
||||
</div>, 10);
|
||||
items.add(
|
||||
'password',
|
||||
<div className="Form-group">
|
||||
<input
|
||||
className="FormControl"
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder={extractText(app.translator.trans('core.forum.sign_up.password_placeholder'))}
|
||||
value={this.password()}
|
||||
onchange={m.withAttr('value', this.password)}
|
||||
disabled={this.loading}
|
||||
/>
|
||||
</div>,
|
||||
10
|
||||
);
|
||||
}
|
||||
|
||||
items.add('submit', <div className="Form-group">
|
||||
<Button
|
||||
className="Button Button--primary Button--block"
|
||||
type="submit"
|
||||
loading={this.loading}>
|
||||
{app.translator.trans('core.forum.sign_up.submit_button')}
|
||||
</Button>
|
||||
</div>, -10);
|
||||
items.add(
|
||||
'submit',
|
||||
<div className="Form-group">
|
||||
<Button className="Button Button--primary Button--block" type="submit" loading={this.loading}>
|
||||
{app.translator.trans('core.forum.sign_up.submit_button')}
|
||||
</Button>
|
||||
</div>,
|
||||
-10
|
||||
);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
footer() {
|
||||
return [
|
||||
<p className="SignUpModal-logIn">
|
||||
{app.translator.trans('core.forum.sign_up.log_in_text', {a: <a onclick={this.logIn.bind(this)}/>})}
|
||||
</p>
|
||||
<p className="SignUpModal-logIn">{app.translator.trans('core.forum.sign_up.log_in_text', { a: <a onclick={this.logIn.bind(this)} /> })}</p>,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -129,7 +142,7 @@ export default class SignUpModal extends Modal {
|
||||
logIn() {
|
||||
const props = {
|
||||
identification: this.email() || this.username(),
|
||||
password: this.password()
|
||||
password: this.password(),
|
||||
};
|
||||
|
||||
app.modal.show(new LogInModal(props));
|
||||
@@ -150,15 +163,14 @@ export default class SignUpModal extends Modal {
|
||||
|
||||
const data = this.submitData();
|
||||
|
||||
app.request({
|
||||
url: app.forum.attribute('baseUrl') + '/register',
|
||||
method: 'POST',
|
||||
data,
|
||||
errorHandler: this.onerror.bind(this)
|
||||
}).then(
|
||||
() => window.location.reload(),
|
||||
this.loaded.bind(this)
|
||||
);
|
||||
app
|
||||
.request({
|
||||
url: app.forum.attribute('baseUrl') + '/register',
|
||||
method: 'POST',
|
||||
data,
|
||||
errorHandler: this.onerror.bind(this),
|
||||
})
|
||||
.then(() => window.location.reload(), this.loaded.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,7 +182,7 @@ export default class SignUpModal extends Modal {
|
||||
submitData() {
|
||||
const data = {
|
||||
username: this.username(),
|
||||
email: this.email()
|
||||
email: this.email(),
|
||||
};
|
||||
|
||||
if (this.props.token) {
|
||||
|
@@ -23,7 +23,7 @@ export default class TerminalPost extends Component {
|
||||
{lastPost ? icon('fas fa-reply') : ''}{' '}
|
||||
{app.translator.trans('core.forum.discussion_list.' + (lastPost ? 'replied' : 'started') + '_text', {
|
||||
user,
|
||||
ago: humanTime(time)
|
||||
ago: humanTime(time),
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
|
@@ -27,18 +27,18 @@ export default class TextEditor extends Component {
|
||||
view() {
|
||||
return (
|
||||
<div className="TextEditor">
|
||||
<textarea className="FormControl Composer-flexible"
|
||||
<textarea
|
||||
className="FormControl Composer-flexible"
|
||||
config={this.configTextarea.bind(this)}
|
||||
oninput={m.withAttr('value', this.oninput.bind(this))}
|
||||
placeholder={this.props.placeholder || ''}
|
||||
disabled={!!this.props.disabled}
|
||||
value={this.value()}/>
|
||||
value={this.value()}
|
||||
/>
|
||||
|
||||
<ul className="TextEditor-controls Composer-footer">
|
||||
{listItems(this.controlItems().toArray())}
|
||||
<li className="TextEditor-toolbar">
|
||||
{this.toolbarItems().toArray()}
|
||||
</li>
|
||||
<li className="TextEditor-toolbar">{this.toolbarItems().toArray()}</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
@@ -70,24 +70,26 @@ export default class TextEditor extends Component {
|
||||
controlItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('submit',
|
||||
items.add(
|
||||
'submit',
|
||||
Button.component({
|
||||
children: this.props.submitLabel,
|
||||
icon: 'fas fa-paper-plane',
|
||||
className: 'Button Button--primary',
|
||||
itemClassName: 'App-primaryControl',
|
||||
onclick: this.onsubmit.bind(this)
|
||||
onclick: this.onsubmit.bind(this),
|
||||
})
|
||||
);
|
||||
|
||||
if (this.props.preview) {
|
||||
items.add('preview',
|
||||
items.add(
|
||||
'preview',
|
||||
Button.component({
|
||||
icon: 'far fa-eye',
|
||||
className: 'Button Button--icon',
|
||||
onclick: this.props.preview,
|
||||
title: app.translator.trans('core.forum.composer.preview_tooltip'),
|
||||
config: elm => $(elm).tooltip()
|
||||
config: (elm) => $(elm).tooltip(),
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -159,7 +161,7 @@ export default class TextEditor extends Component {
|
||||
this.setSelectionRange(pos, pos);
|
||||
}
|
||||
|
||||
textarea.dispatchEvent(new CustomEvent('input', {bubbles: true, cancelable: true}));
|
||||
textarea.dispatchEvent(new CustomEvent('input', { bubbles: true, cancelable: true }));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -29,41 +29,35 @@ export default class UserCard extends Component {
|
||||
const badges = user.badges().toArray();
|
||||
|
||||
return (
|
||||
<div className={'UserCard ' + (this.props.className || '')}
|
||||
style={color ? {backgroundColor: color} : ''}>
|
||||
<div className={'UserCard ' + (this.props.className || '')} style={color ? { backgroundColor: color } : ''}>
|
||||
<div className="darkenBackground">
|
||||
|
||||
<div className="container">
|
||||
{controls.length ? Dropdown.component({
|
||||
children: controls,
|
||||
className: 'UserCard-controls App-primaryControl',
|
||||
menuClassName: 'Dropdown-menu--right',
|
||||
buttonClassName: this.props.controlsButtonClassName,
|
||||
label: app.translator.trans('core.forum.user_controls.button'),
|
||||
icon: 'fas fa-ellipsis-v'
|
||||
}) : ''}
|
||||
{controls.length
|
||||
? Dropdown.component({
|
||||
children: controls,
|
||||
className: 'UserCard-controls App-primaryControl',
|
||||
menuClassName: 'Dropdown-menu--right',
|
||||
buttonClassName: this.props.controlsButtonClassName,
|
||||
label: app.translator.trans('core.forum.user_controls.button'),
|
||||
icon: 'fas fa-ellipsis-v',
|
||||
})
|
||||
: ''}
|
||||
|
||||
<div className="UserCard-profile">
|
||||
<h2 className="UserCard-identity">
|
||||
{this.props.editable
|
||||
? [AvatarEditor.component({user, className: 'UserCard-avatar'}), username(user)]
|
||||
: (
|
||||
<a href={app.route.user(user)} config={m.route}>
|
||||
<div className="UserCard-avatar">{avatar(user)}</div>
|
||||
{username(user)}
|
||||
</a>
|
||||
)}
|
||||
{this.props.editable ? (
|
||||
[AvatarEditor.component({ user, className: 'UserCard-avatar' }), username(user)]
|
||||
) : (
|
||||
<a href={app.route.user(user)} config={m.route}>
|
||||
<div className="UserCard-avatar">{avatar(user)}</div>
|
||||
{username(user)}
|
||||
</a>
|
||||
)}
|
||||
</h2>
|
||||
|
||||
{badges.length ? (
|
||||
<ul className="UserCard-badges badges">
|
||||
{listItems(badges)}
|
||||
</ul>
|
||||
) : ''}
|
||||
{badges.length ? <ul className="UserCard-badges badges">{listItems(badges)}</ul> : ''}
|
||||
|
||||
<ul className="UserCard-info">
|
||||
{listItems(this.infoItems().toArray())}
|
||||
</ul>
|
||||
<ul className="UserCard-info">{listItems(this.infoItems().toArray())}</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,16 +78,17 @@ export default class UserCard extends Component {
|
||||
if (lastSeenAt) {
|
||||
const online = user.isOnline();
|
||||
|
||||
items.add('lastSeen', (
|
||||
items.add(
|
||||
'lastSeen',
|
||||
<span className={'UserCard-lastSeen' + (online ? ' online' : '')}>
|
||||
{online
|
||||
? [icon('fas fa-circle'), ' ', app.translator.trans('core.forum.user.online_text')]
|
||||
: [icon('far fa-clock'), ' ', humanTime(lastSeenAt)]}
|
||||
</span>
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
items.add('joined', app.translator.trans('core.forum.user.joined_date_text', {ago: humanTime(user.joinTime())}));
|
||||
items.add('joined', app.translator.trans('core.forum.user.joined_date_text', { ago: humanTime(user.joinTime()) }));
|
||||
|
||||
return items;
|
||||
}
|
||||
|
@@ -32,26 +32,24 @@ export default class UserPage extends Page {
|
||||
view() {
|
||||
return (
|
||||
<div className="UserPage">
|
||||
{this.user ? [
|
||||
UserCard.component({
|
||||
user: this.user,
|
||||
className: 'Hero UserHero',
|
||||
editable: this.user.canEdit() || this.user === app.session.user,
|
||||
controlsButtonClassName: 'Button'
|
||||
}),
|
||||
<div className="container">
|
||||
<div className="sideNavContainer">
|
||||
<nav className="sideNav UserPage-nav" config={affixSidebar}>
|
||||
<ul>{listItems(this.sidebarItems().toArray())}</ul>
|
||||
</nav>
|
||||
<div className="sideNavOffset UserPage-content">
|
||||
{this.content()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
] : [
|
||||
LoadingIndicator.component({className: 'LoadingIndicator--block'})
|
||||
]}
|
||||
{this.user
|
||||
? [
|
||||
UserCard.component({
|
||||
user: this.user,
|
||||
className: 'Hero UserHero',
|
||||
editable: this.user.canEdit() || this.user === app.session.user,
|
||||
controlsButtonClassName: 'Button',
|
||||
}),
|
||||
<div className="container">
|
||||
<div className="sideNavContainer">
|
||||
<nav className="sideNav UserPage-nav" config={affixSidebar}>
|
||||
<ul>{listItems(this.sidebarItems().toArray())}</ul>
|
||||
</nav>
|
||||
<div className="sideNavOffset UserPage-content">{this.content()}</div>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
: [LoadingIndicator.component({ className: 'LoadingIndicator--block' })]}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -61,8 +59,7 @@ export default class UserPage extends Page {
|
||||
*
|
||||
* @return {VirtualElement}
|
||||
*/
|
||||
content() {
|
||||
}
|
||||
content() {}
|
||||
|
||||
/**
|
||||
* Initialize the component with a user, and trigger the loading of their
|
||||
@@ -93,7 +90,7 @@ export default class UserPage extends Page {
|
||||
// instead of the parsed models
|
||||
app.preloadedApiDocument();
|
||||
|
||||
app.store.all('users').some(user => {
|
||||
app.store.all('users').some((user) => {
|
||||
if ((user.username().toLowerCase() === lowercaseUsername || user.id() === username) && user.joinTime()) {
|
||||
this.show(user);
|
||||
return true;
|
||||
@@ -113,11 +110,12 @@ export default class UserPage extends Page {
|
||||
sidebarItems() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('nav',
|
||||
items.add(
|
||||
'nav',
|
||||
SelectDropdown.component({
|
||||
children: this.navItems().toArray(),
|
||||
className: 'App-titleControl',
|
||||
buttonClassName: 'Button'
|
||||
buttonClassName: 'Button',
|
||||
})
|
||||
);
|
||||
|
||||
@@ -133,31 +131,34 @@ export default class UserPage extends Page {
|
||||
const items = new ItemList();
|
||||
const user = this.user;
|
||||
|
||||
items.add('posts',
|
||||
items.add(
|
||||
'posts',
|
||||
LinkButton.component({
|
||||
href: app.route('user.posts', {username: user.username()}),
|
||||
href: app.route('user.posts', { username: user.username() }),
|
||||
children: [app.translator.trans('core.forum.user.posts_link'), <span className="Button-badge">{user.commentCount()}</span>],
|
||||
icon: 'far fa-comment'
|
||||
icon: 'far fa-comment',
|
||||
}),
|
||||
100
|
||||
);
|
||||
|
||||
items.add('discussions',
|
||||
items.add(
|
||||
'discussions',
|
||||
LinkButton.component({
|
||||
href: app.route('user.discussions', {username: user.username()}),
|
||||
href: app.route('user.discussions', { username: user.username() }),
|
||||
children: [app.translator.trans('core.forum.user.discussions_link'), <span className="Button-badge">{user.discussionCount()}</span>],
|
||||
icon: 'fas fa-bars'
|
||||
icon: 'fas fa-bars',
|
||||
}),
|
||||
90
|
||||
);
|
||||
|
||||
if (app.session.user === user) {
|
||||
items.add('separator', Separator.component(), -90);
|
||||
items.add('settings',
|
||||
items.add(
|
||||
'settings',
|
||||
LinkButton.component({
|
||||
href: app.route('settings'),
|
||||
children: app.translator.trans('core.forum.user.settings_link'),
|
||||
icon: 'fas fa-cog'
|
||||
icon: 'fas fa-cog',
|
||||
}),
|
||||
-100
|
||||
);
|
||||
|
@@ -14,20 +14,26 @@ export default class UsersSearchResults {
|
||||
}
|
||||
|
||||
search(query) {
|
||||
return app.store.find('users', {
|
||||
filter: {q: query},
|
||||
page: {limit: 5}
|
||||
}).then(results => {
|
||||
this.results[query] = results;
|
||||
m.redraw();
|
||||
});
|
||||
return app.store
|
||||
.find('users', {
|
||||
filter: { q: query },
|
||||
page: { limit: 5 },
|
||||
})
|
||||
.then((results) => {
|
||||
this.results[query] = results;
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
view(query) {
|
||||
query = query.toLowerCase();
|
||||
|
||||
const results = (this.results[query] || [])
|
||||
.concat(app.store.all('users').filter(user => [user.username(), user.displayName()].some(value => value.toLowerCase().substr(0, query.length) === query)))
|
||||
.concat(
|
||||
app.store
|
||||
.all('users')
|
||||
.filter((user) => [user.username(), user.displayName()].some((value) => value.toLowerCase().substr(0, query.length) === query))
|
||||
)
|
||||
.filter((e, i, arr) => arr.lastIndexOf(e) === i)
|
||||
.sort((a, b) => a.displayName().localeCompare(b.displayName()));
|
||||
|
||||
@@ -35,7 +41,7 @@ export default class UsersSearchResults {
|
||||
|
||||
return [
|
||||
<li className="Dropdown-header">{app.translator.trans('core.forum.search.users_heading')}</li>,
|
||||
results.map(user => {
|
||||
results.map((user) => {
|
||||
const name = username(user);
|
||||
name.children[0] = highlight(name.children[0], query);
|
||||
|
||||
@@ -47,7 +53,7 @@ export default class UsersSearchResults {
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@ export default class WelcomeHero extends Component {
|
||||
}
|
||||
|
||||
view() {
|
||||
if (this.hidden) return <div/>;
|
||||
if (this.hidden) return <div />;
|
||||
|
||||
const slideUp = () => {
|
||||
this.$().slideUp(this.hide.bind(this));
|
||||
@@ -23,7 +23,7 @@ export default class WelcomeHero extends Component {
|
||||
{Button.component({
|
||||
icon: 'fas fa-times',
|
||||
onclick: slideUp,
|
||||
className: 'Hero-close Button Button--icon Button--link'
|
||||
className: 'Hero-close Button Button--icon Button--link',
|
||||
})}
|
||||
|
||||
<div className="containerNarrow">
|
||||
|
@@ -10,20 +10,20 @@ import NotificationsPage from './components/NotificationsPage';
|
||||
*
|
||||
* @param {App} app
|
||||
*/
|
||||
export default function(app) {
|
||||
export default function (app) {
|
||||
app.routes = {
|
||||
'index': {path: '/all', component: IndexPage.component()},
|
||||
'index.filter': {path: '/:filter', component: IndexPage.component()},
|
||||
index: { path: '/all', component: IndexPage.component() },
|
||||
'index.filter': { path: '/:filter', component: IndexPage.component() },
|
||||
|
||||
'discussion': {path: '/d/:id', component: DiscussionPage.component()},
|
||||
'discussion.near': {path: '/d/:id/:near', component: DiscussionPage.component()},
|
||||
discussion: { path: '/d/:id', component: DiscussionPage.component() },
|
||||
'discussion.near': { path: '/d/:id/:near', component: DiscussionPage.component() },
|
||||
|
||||
'user': {path: '/u/:username', component: PostsUserPage.component()},
|
||||
'user.posts': {path: '/u/:username', component: PostsUserPage.component()},
|
||||
'user.discussions': {path: '/u/:username/discussions', component: DiscussionsUserPage.component()},
|
||||
user: { path: '/u/:username', component: PostsUserPage.component() },
|
||||
'user.posts': { path: '/u/:username', component: PostsUserPage.component() },
|
||||
'user.discussions': { path: '/u/:username/discussions', component: DiscussionsUserPage.component() },
|
||||
|
||||
'settings': {path: '/settings', component: SettingsPage.component()},
|
||||
'notifications': {path: '/notifications', component: NotificationsPage.component()}
|
||||
settings: { path: '/settings', component: SettingsPage.component() },
|
||||
notifications: { path: '/notifications', component: NotificationsPage.component() },
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -37,7 +37,7 @@ export default function(app) {
|
||||
const slug = discussion.slug();
|
||||
return app.route(near && near !== 1 ? 'discussion.near' : 'discussion', {
|
||||
id: discussion.id() + (slug.trim() ? '-' + slug : ''),
|
||||
near: near && near !== 1 ? near : undefined
|
||||
near: near && near !== 1 ? near : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function(app) {
|
||||
* @param {Post} post
|
||||
* @return {String}
|
||||
*/
|
||||
app.route.post = post => {
|
||||
app.route.post = (post) => {
|
||||
return app.route.discussion(post.discussion(), post.number());
|
||||
};
|
||||
|
||||
@@ -57,9 +57,9 @@ export default function(app) {
|
||||
* @param {User} user
|
||||
* @return {String}
|
||||
*/
|
||||
app.route.user = user => {
|
||||
app.route.user = (user) => {
|
||||
return app.route('user', {
|
||||
username: user.username()
|
||||
username: user.username(),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@@ -24,10 +24,10 @@ export default {
|
||||
controls(discussion, context) {
|
||||
const items = new ItemList();
|
||||
|
||||
['user', 'moderation', 'destructive'].forEach(section => {
|
||||
['user', 'moderation', 'destructive'].forEach((section) => {
|
||||
const controls = this[section + 'Controls'](discussion, context).toArray();
|
||||
if (controls.length) {
|
||||
controls.forEach(item => items.add(item.itemName, item));
|
||||
controls.forEach((item) => items.add(item.itemName, item));
|
||||
items.add(section + 'Separator', Separator.component());
|
||||
}
|
||||
});
|
||||
@@ -52,19 +52,22 @@ export default {
|
||||
// for the discussion page itself. We don't want it to show up for
|
||||
// discussions in the discussion list, etc.
|
||||
if (context instanceof DiscussionPage) {
|
||||
items.add('reply',
|
||||
items.add(
|
||||
'reply',
|
||||
!app.session.user || discussion.canReply()
|
||||
? Button.component({
|
||||
icon: 'fas fa-reply',
|
||||
children: app.translator.trans(app.session.user ? 'core.forum.discussion_controls.reply_button' : 'core.forum.discussion_controls.log_in_to_reply_button'),
|
||||
onclick: this.replyAction.bind(discussion, true, false)
|
||||
})
|
||||
icon: 'fas fa-reply',
|
||||
children: app.translator.trans(
|
||||
app.session.user ? 'core.forum.discussion_controls.reply_button' : 'core.forum.discussion_controls.log_in_to_reply_button'
|
||||
),
|
||||
onclick: this.replyAction.bind(discussion, true, false),
|
||||
})
|
||||
: Button.component({
|
||||
icon: 'fas fa-reply',
|
||||
children: app.translator.trans('core.forum.discussion_controls.cannot_reply_button'),
|
||||
className: 'disabled',
|
||||
title: app.translator.trans('core.forum.discussion_controls.cannot_reply_text')
|
||||
})
|
||||
icon: 'fas fa-reply',
|
||||
children: app.translator.trans('core.forum.discussion_controls.cannot_reply_button'),
|
||||
className: 'disabled',
|
||||
title: app.translator.trans('core.forum.discussion_controls.cannot_reply_text'),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -84,11 +87,14 @@ export default {
|
||||
const items = new ItemList();
|
||||
|
||||
if (discussion.canRename()) {
|
||||
items.add('rename', Button.component({
|
||||
icon: 'fas fa-pencil-alt',
|
||||
children: app.translator.trans('core.forum.discussion_controls.rename_button'),
|
||||
onclick: this.renameAction.bind(discussion)
|
||||
}));
|
||||
items.add(
|
||||
'rename',
|
||||
Button.component({
|
||||
icon: 'fas fa-pencil-alt',
|
||||
children: app.translator.trans('core.forum.discussion_controls.rename_button'),
|
||||
onclick: this.renameAction.bind(discussion),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
@@ -108,27 +114,36 @@ export default {
|
||||
|
||||
if (!discussion.isHidden()) {
|
||||
if (discussion.canHide()) {
|
||||
items.add('hide', Button.component({
|
||||
icon: 'far fa-trash-alt',
|
||||
children: app.translator.trans('core.forum.discussion_controls.delete_button'),
|
||||
onclick: this.hideAction.bind(discussion)
|
||||
}));
|
||||
items.add(
|
||||
'hide',
|
||||
Button.component({
|
||||
icon: 'far fa-trash-alt',
|
||||
children: app.translator.trans('core.forum.discussion_controls.delete_button'),
|
||||
onclick: this.hideAction.bind(discussion),
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (discussion.canHide()) {
|
||||
items.add('restore', Button.component({
|
||||
icon: 'fas fa-reply',
|
||||
children: app.translator.trans('core.forum.discussion_controls.restore_button'),
|
||||
onclick: this.restoreAction.bind(discussion)
|
||||
}));
|
||||
items.add(
|
||||
'restore',
|
||||
Button.component({
|
||||
icon: 'fas fa-reply',
|
||||
children: app.translator.trans('core.forum.discussion_controls.restore_button'),
|
||||
onclick: this.restoreAction.bind(discussion),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (discussion.canDelete()) {
|
||||
items.add('delete', Button.component({
|
||||
icon: 'fas fa-times',
|
||||
children: app.translator.trans('core.forum.discussion_controls.delete_forever_button'),
|
||||
onclick: this.deleteAction.bind(discussion)
|
||||
}));
|
||||
items.add(
|
||||
'delete',
|
||||
Button.component({
|
||||
icon: 'fas fa-times',
|
||||
children: app.translator.trans('core.forum.discussion_controls.delete_forever_button'),
|
||||
onclick: this.deleteAction.bind(discussion),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,13 +171,13 @@ export default {
|
||||
if (!app.composingReplyTo(this) || forceRefresh) {
|
||||
component = new ReplyComposer({
|
||||
user: app.session.user,
|
||||
discussion: this
|
||||
discussion: this,
|
||||
});
|
||||
app.composer.load(component);
|
||||
}
|
||||
app.composer.show();
|
||||
|
||||
if (goToLast && app.viewingDiscussion(this) && ! app.composer.isFullScreen()) {
|
||||
if (goToLast && app.viewingDiscussion(this) && !app.composer.isFullScreen()) {
|
||||
app.current.stream.goToNumber('reply');
|
||||
}
|
||||
|
||||
@@ -230,9 +245,11 @@ export default {
|
||||
* @return {Promise}
|
||||
*/
|
||||
renameAction() {
|
||||
return app.modal.show(new RenameDiscussionModal({
|
||||
currentTitle: this.title(),
|
||||
discussion: this
|
||||
}));
|
||||
}
|
||||
return app.modal.show(
|
||||
new RenameDiscussionModal({
|
||||
currentTitle: this.title(),
|
||||
discussion: this,
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
|
@@ -64,9 +64,9 @@ export default class History {
|
||||
// then we'll overwrite it with the new URL.
|
||||
const top = this.getCurrent();
|
||||
if (top && top.name === name) {
|
||||
Object.assign(top, {url, title});
|
||||
Object.assign(top, { url, title });
|
||||
} else {
|
||||
this.stack.push({name, url, title});
|
||||
this.stack.push({ name, url, title });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export default class History {
|
||||
* @public
|
||||
*/
|
||||
back() {
|
||||
if (! this.canGoBack()) {
|
||||
if (!this.canGoBack()) {
|
||||
return this.home();
|
||||
}
|
||||
|
||||
|
@@ -24,7 +24,7 @@ export default class KeyboardNavigatable {
|
||||
* @param {KeyboardEvent} event
|
||||
* @returns {boolean}
|
||||
*/
|
||||
this.whenCallback = event => true;
|
||||
this.whenCallback = (event) => true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,7 +37,7 @@ export default class KeyboardNavigatable {
|
||||
* @return {KeyboardNavigatable}
|
||||
*/
|
||||
onUp(callback) {
|
||||
this.callbacks[38] = e => {
|
||||
this.callbacks[38] = (e) => {
|
||||
e.preventDefault();
|
||||
callback(e);
|
||||
};
|
||||
@@ -55,7 +55,7 @@ export default class KeyboardNavigatable {
|
||||
* @return {KeyboardNavigatable}
|
||||
*/
|
||||
onDown(callback) {
|
||||
this.callbacks[40] = e => {
|
||||
this.callbacks[40] = (e) => {
|
||||
e.preventDefault();
|
||||
callback(e);
|
||||
};
|
||||
@@ -73,7 +73,7 @@ export default class KeyboardNavigatable {
|
||||
* @return {KeyboardNavigatable}
|
||||
*/
|
||||
onSelect(callback) {
|
||||
this.callbacks[9] = this.callbacks[13] = e => {
|
||||
this.callbacks[9] = this.callbacks[13] = (e) => {
|
||||
e.preventDefault();
|
||||
callback(e);
|
||||
};
|
||||
@@ -91,7 +91,7 @@ export default class KeyboardNavigatable {
|
||||
* @return {KeyboardNavigatable}
|
||||
*/
|
||||
onCancel(callback) {
|
||||
this.callbacks[27] = e => {
|
||||
this.callbacks[27] = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
callback(e);
|
||||
@@ -110,7 +110,7 @@ export default class KeyboardNavigatable {
|
||||
* @return {KeyboardNavigatable}
|
||||
*/
|
||||
onRemove(callback) {
|
||||
this.callbacks[8] = e => {
|
||||
this.callbacks[8] = (e) => {
|
||||
if (e.target.selectionStart === 0 && e.target.selectionEnd === 0) {
|
||||
callback(e);
|
||||
e.preventDefault();
|
||||
|
@@ -121,9 +121,6 @@ export default class Pane {
|
||||
* @protected
|
||||
*/
|
||||
render() {
|
||||
this.$element
|
||||
.toggleClass('panePinned', this.pinned)
|
||||
.toggleClass('hasPane', this.active)
|
||||
.toggleClass('paneShowing', this.showing);
|
||||
this.$element.toggleClass('panePinned', this.pinned).toggleClass('hasPane', this.active).toggleClass('paneShowing', this.showing);
|
||||
}
|
||||
}
|
||||
|
@@ -20,10 +20,10 @@ export default {
|
||||
controls(post, context) {
|
||||
const items = new ItemList();
|
||||
|
||||
['user', 'moderation', 'destructive'].forEach(section => {
|
||||
['user', 'moderation', 'destructive'].forEach((section) => {
|
||||
const controls = this[section + 'Controls'](post, context).toArray();
|
||||
if (controls.length) {
|
||||
controls.forEach(item => items.add(item.itemName, item));
|
||||
controls.forEach((item) => items.add(item.itemName, item));
|
||||
items.add(section + 'Separator', Separator.component());
|
||||
}
|
||||
});
|
||||
@@ -58,11 +58,14 @@ export default {
|
||||
|
||||
if (post.contentType() === 'comment' && post.canEdit()) {
|
||||
if (!post.isHidden()) {
|
||||
items.add('edit', Button.component({
|
||||
icon: 'fas fa-pencil-alt',
|
||||
children: app.translator.trans('core.forum.post_controls.edit_button'),
|
||||
onclick: this.editAction.bind(post)
|
||||
}));
|
||||
items.add(
|
||||
'edit',
|
||||
Button.component({
|
||||
icon: 'fas fa-pencil-alt',
|
||||
children: app.translator.trans('core.forum.post_controls.edit_button'),
|
||||
onclick: this.editAction.bind(post),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,26 +86,35 @@ export default {
|
||||
|
||||
if (post.contentType() === 'comment' && !post.isHidden()) {
|
||||
if (post.canHide()) {
|
||||
items.add('hide', Button.component({
|
||||
icon: 'far fa-trash-alt',
|
||||
children: app.translator.trans('core.forum.post_controls.delete_button'),
|
||||
onclick: this.hideAction.bind(post)
|
||||
}));
|
||||
items.add(
|
||||
'hide',
|
||||
Button.component({
|
||||
icon: 'far fa-trash-alt',
|
||||
children: app.translator.trans('core.forum.post_controls.delete_button'),
|
||||
onclick: this.hideAction.bind(post),
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (post.contentType() === 'comment' && post.canHide()) {
|
||||
items.add('restore', Button.component({
|
||||
icon: 'fas fa-reply',
|
||||
children: app.translator.trans('core.forum.post_controls.restore_button'),
|
||||
onclick: this.restoreAction.bind(post)
|
||||
}));
|
||||
items.add(
|
||||
'restore',
|
||||
Button.component({
|
||||
icon: 'fas fa-reply',
|
||||
children: app.translator.trans('core.forum.post_controls.restore_button'),
|
||||
onclick: this.restoreAction.bind(post),
|
||||
})
|
||||
);
|
||||
}
|
||||
if (post.canDelete()) {
|
||||
items.add('delete', Button.component({
|
||||
icon: 'fas fa-times',
|
||||
children: app.translator.trans('core.forum.post_controls.delete_forever_button'),
|
||||
onclick: this.deleteAction.bind(post, context)
|
||||
}));
|
||||
items.add(
|
||||
'delete',
|
||||
Button.component({
|
||||
icon: 'fas fa-times',
|
||||
children: app.translator.trans('core.forum.post_controls.delete_forever_button'),
|
||||
onclick: this.deleteAction.bind(post, context),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,5 +193,5 @@ export default {
|
||||
if (context) context.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@@ -22,10 +22,10 @@ export default {
|
||||
controls(user, context) {
|
||||
const items = new ItemList();
|
||||
|
||||
['user', 'moderation', 'destructive'].forEach(section => {
|
||||
['user', 'moderation', 'destructive'].forEach((section) => {
|
||||
const controls = this[section + 'Controls'](user, context).toArray();
|
||||
if (controls.length) {
|
||||
controls.forEach(item => items.add(item.itemName, item));
|
||||
controls.forEach((item) => items.add(item.itemName, item));
|
||||
items.add(section + 'Separator', Separator.component());
|
||||
}
|
||||
});
|
||||
@@ -59,11 +59,14 @@ export default {
|
||||
const items = new ItemList();
|
||||
|
||||
if (user.canEdit()) {
|
||||
items.add('edit', Button.component({
|
||||
icon: 'fas fa-pencil-alt',
|
||||
children: app.translator.trans('core.forum.user_controls.edit_button'),
|
||||
onclick: this.editAction.bind(this, user)
|
||||
}));
|
||||
items.add(
|
||||
'edit',
|
||||
Button.component({
|
||||
icon: 'fas fa-pencil-alt',
|
||||
children: app.translator.trans('core.forum.user_controls.edit_button'),
|
||||
onclick: this.editAction.bind(this, user),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
@@ -82,11 +85,14 @@ export default {
|
||||
const items = new ItemList();
|
||||
|
||||
if (user.id() !== '1' && user.canDelete()) {
|
||||
items.add('delete', Button.component({
|
||||
icon: 'fas fa-times',
|
||||
children: app.translator.trans('core.forum.user_controls.delete_button'),
|
||||
onclick: this.deleteAction.bind(this, user)
|
||||
}));
|
||||
items.add(
|
||||
'delete',
|
||||
Button.component({
|
||||
icon: 'fas fa-times',
|
||||
children: app.translator.trans('core.forum.user_controls.delete_button'),
|
||||
onclick: this.deleteAction.bind(this, user),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
@@ -102,14 +108,17 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
user.delete().then(() => {
|
||||
this.showDeletionAlert(user, 'success');
|
||||
if (app.current instanceof UserPage && app.current.user === user) {
|
||||
app.history.back();
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
}).catch(() => this.showDeletionAlert(user, 'error'));
|
||||
user
|
||||
.delete()
|
||||
.then(() => {
|
||||
this.showDeletionAlert(user, 'success');
|
||||
if (app.current instanceof UserPage && app.current.user === user) {
|
||||
app.history.back();
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
})
|
||||
.catch(() => this.showDeletionAlert(user, 'error'));
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -125,12 +134,12 @@ export default {
|
||||
error: 'core.forum.user_controls.delete_error_message',
|
||||
}[type];
|
||||
|
||||
app.alerts.show(new Alert({
|
||||
type,
|
||||
children: app.translator.trans(
|
||||
message, { username, email }
|
||||
)
|
||||
}));
|
||||
app.alerts.show(
|
||||
new Alert({
|
||||
type,
|
||||
children: app.translator.trans(message, { username, email }),
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -140,5 +149,5 @@ export default {
|
||||
*/
|
||||
editAction(user) {
|
||||
app.modal.show(new EditUserModal({ user }));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@@ -16,9 +16,7 @@ export default function affixSidebar(element, isInitialized, context) {
|
||||
const $affixElement = $sidebar.find('> ul');
|
||||
|
||||
$(window).off('.affix');
|
||||
$affixElement
|
||||
.removeClass('affix affix-top affix-bottom')
|
||||
.removeData('bs.affix');
|
||||
$affixElement.removeClass('affix affix-top affix-bottom').removeData('bs.affix');
|
||||
|
||||
// Don't affix the sidebar if it is taller than the viewport (otherwise
|
||||
// there would be no way to scroll through its content).
|
||||
@@ -27,8 +25,8 @@ export default function affixSidebar(element, isInitialized, context) {
|
||||
$affixElement.affix({
|
||||
offset: {
|
||||
top: () => $sidebar.offset().top - $header.outerHeight(true) - parseInt($sidebar.css('margin-top'), 10),
|
||||
bottom: () => this.bottom = $footer.outerHeight(true)
|
||||
}
|
||||
bottom: () => (this.bottom = $footer.outerHeight(true)),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -37,5 +35,5 @@ export default function affixSidebar(element, isInitialized, context) {
|
||||
|
||||
context.onunload = () => {
|
||||
$(window).off('resize', onresize);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -15,23 +15,26 @@ export default function alertEmailConfirmation(app) {
|
||||
const resendButton = Button.component({
|
||||
className: 'Button Button--link',
|
||||
children: app.translator.trans('core.forum.user_email_confirmation.resend_button'),
|
||||
onclick: function() {
|
||||
onclick: function () {
|
||||
resendButton.props.loading = true;
|
||||
m.redraw();
|
||||
|
||||
app.request({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/send-confirmation',
|
||||
}).then(() => {
|
||||
resendButton.props.loading = false;
|
||||
resendButton.props.children = [icon('fas fa-check'), ' ', app.translator.trans('core.forum.user_email_confirmation.sent_message')];
|
||||
resendButton.props.disabled = true;
|
||||
m.redraw();
|
||||
}).catch(() => {
|
||||
resendButton.props.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
app
|
||||
.request({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/send-confirmation',
|
||||
})
|
||||
.then(() => {
|
||||
resendButton.props.loading = false;
|
||||
resendButton.props.children = [icon('fas fa-check'), ' ', app.translator.trans('core.forum.user_email_confirmation.sent_message')];
|
||||
resendButton.props.disabled = true;
|
||||
m.redraw();
|
||||
})
|
||||
.catch(() => {
|
||||
resendButton.props.loading = false;
|
||||
m.redraw();
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
class ContainedAlert extends Alert {
|
||||
@@ -48,8 +51,8 @@ export default function alertEmailConfirmation(app) {
|
||||
$('<div/>').insertBefore('#content')[0],
|
||||
ContainedAlert.component({
|
||||
dismissible: false,
|
||||
children: app.translator.trans('core.forum.user_email_confirmation.alert_message', {email: <strong>{user.email()}</strong>}),
|
||||
controls: [resendButton]
|
||||
children: app.translator.trans('core.forum.user_email_confirmation.alert_message', { email: <strong>{user.email()}</strong> }),
|
||||
controls: [resendButton],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@@ -36,11 +36,11 @@ export default function slidable(element) {
|
||||
// will set the transform property, but then we animate an unused property
|
||||
// (background-position-x) with jQuery.
|
||||
options.duration = options.duration || 'fast';
|
||||
options.step = function(x) {
|
||||
options.step = function (x) {
|
||||
$(this).css('transform', 'translate(' + x + 'px, 0)');
|
||||
};
|
||||
|
||||
$element.find('.Slidable-content').animate({'background-position-x': newPos}, options);
|
||||
$element.find('.Slidable-content').animate({ 'background-position-x': newPos }, options);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -48,17 +48,18 @@ export default function slidable(element) {
|
||||
*/
|
||||
const reset = () => {
|
||||
animatePos(0, {
|
||||
complete: function() {
|
||||
complete: function () {
|
||||
$element.removeClass('sliding');
|
||||
$underneathLeft.hide();
|
||||
$underneathRight.hide();
|
||||
isSliding = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
$element.find('.Slidable-content')
|
||||
.on('touchstart', function(e) {
|
||||
$element
|
||||
.find('.Slidable-content')
|
||||
.on('touchstart', function (e) {
|
||||
// Update the references to the elements underneath the slider, provided
|
||||
// they're not disabled.
|
||||
$underneathLeft = $element.find('.Slidable-underneath--left:not(.disabled)');
|
||||
@@ -71,7 +72,7 @@ export default function slidable(element) {
|
||||
pos = 0;
|
||||
})
|
||||
|
||||
.on('touchmove', function(e) {
|
||||
.on('touchmove', function (e) {
|
||||
const newX = e.originalEvent.targetTouches[0].clientX;
|
||||
const newY = e.originalEvent.targetTouches[0].clientY;
|
||||
|
||||
@@ -118,13 +119,13 @@ export default function slidable(element) {
|
||||
}
|
||||
})
|
||||
|
||||
.on('touchend', function() {
|
||||
.on('touchend', function () {
|
||||
// If the user releases the touch and the slider is past the threshold
|
||||
// position on either side, then we will activate the control for that
|
||||
// side. We will also animate the slider's position all the way to the
|
||||
// other side, or back to its original position, depending on whether or
|
||||
// not the side is 'elastic'.
|
||||
const activate = $underneath => {
|
||||
const activate = ($underneath) => {
|
||||
$underneath.click();
|
||||
|
||||
if ($underneath.hasClass('Slidable-underneath--elastic')) {
|
||||
@@ -146,5 +147,5 @@ export default function slidable(element) {
|
||||
isSliding = false;
|
||||
});
|
||||
|
||||
return {reset};
|
||||
};
|
||||
return { reset };
|
||||
}
|
||||
|
Reference in New Issue
Block a user