1
0
mirror of https://github.com/flarum/core.git synced 2025-10-11 23:14:29 +02:00

Merge remote-tracking branch 'upstream/master' into signup-fields-locking

This commit is contained in:
Clark Winkelmann
2018-01-11 22:54:41 +01:00
442 changed files with 4713 additions and 4246 deletions

View File

@@ -1,9 +1,9 @@
language: php language: php
php: php:
- 5.6
- 7.0 - 7.0
- 7.1 - 7.1
- 7.2
- hhvm - hhvm
matrix: matrix:

View File

@@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2014-2017 Toby Zerner Copyright (c) 2014-2018 Toby Zerner
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -20,24 +20,24 @@
"docs": "http://flarum.org/docs" "docs": "http://flarum.org/docs"
}, },
"require": { "require": {
"php": ">=5.6.0", "php": ">=7.0",
"dflydev/fig-cookies": "^1.0.2", "dflydev/fig-cookies": "^1.0.2",
"doctrine/dbal": "^2.5", "doctrine/dbal": "^2.5",
"components/font-awesome": "^4.6", "components/font-awesome": "^4.6",
"franzl/whoops-middleware": "^0.4.0", "franzl/whoops-middleware": "^0.4.0",
"illuminate/bus": "5.1.*", "illuminate/bus": "5.5.*",
"illuminate/cache": "5.1.*", "illuminate/cache": "5.5.*",
"illuminate/config": "5.1.*", "illuminate/config": "5.5.*",
"illuminate/container": "5.1.*", "illuminate/container": "5.5.*",
"illuminate/contracts": "5.1.*", "illuminate/contracts": "5.5.*",
"illuminate/database": "^5.1.31", "illuminate/database": "5.5.*",
"illuminate/events": "5.1.*", "illuminate/events": "5.5.*",
"illuminate/filesystem": "5.1.*", "illuminate/filesystem": "5.5.*",
"illuminate/hashing": "5.1.*", "illuminate/hashing": "5.5.*",
"illuminate/mail": "5.1.*", "illuminate/mail": "5.5.*",
"illuminate/support": "5.1.*", "illuminate/support": "5.5.*",
"illuminate/validation": "5.1.*", "illuminate/validation": "5.5.*",
"illuminate/view": "5.1.*", "illuminate/view": "5.5.*",
"intervention/image": "^2.3.0", "intervention/image": "^2.3.0",
"league/flysystem": "^1.0.11", "league/flysystem": "^1.0.11",
"league/oauth2-client": "~1.0", "league/oauth2-client": "~1.0",
@@ -46,10 +46,11 @@
"nikic/fast-route": "^0.6", "nikic/fast-route": "^0.6",
"oyejorge/less.php": "~1.5", "oyejorge/less.php": "~1.5",
"psr/http-message": "^1.0", "psr/http-message": "^1.0",
"symfony/console": "^2.7", "symfony/config": "^3.3",
"symfony/http-foundation": "^2.7", "symfony/console": "^3.3",
"symfony/translation": "^2.7", "symfony/http-foundation": "^3.3",
"symfony/yaml": "^2.7", "symfony/translation": "^3.3",
"symfony/yaml": "^3.3",
"s9e/text-formatter": "^0.8.1", "s9e/text-formatter": "^0.8.1",
"tobscure/json-api": "^0.3.0", "tobscure/json-api": "^0.3.0",
"zendframework/zend-diactoros": "^1.6", "zendframework/zend-diactoros": "^1.6",
@@ -57,7 +58,7 @@
}, },
"require-dev": { "require-dev": {
"mockery/mockery": "^0.9.4", "mockery/mockery": "^0.9.4",
"phpunit/phpunit": "^4.8" "phpunit/phpunit": "^6.0"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

View File

@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>403 Forbidden</h1>
<p>You do not have permissions to access this page.</p>
</body>
</html>

View File

@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>404 Not Found</h1>
<p>Looks like this page could not be found.</p>
</body>
</html>

View File

@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>500 Internal Server Error</h1>
<p>Something went wrong on our server.</p>
</body>
</html>

View File

@@ -1,13 +0,0 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
</head>
<body>
<h1>503 Service Unavailable</h1>
<p>This forum is down for maintenance.</p>
</body>
</html>

277
js/admin/dist/app.js vendored
View File

@@ -17536,11 +17536,13 @@ System.register('flarum/components/AdminNav', ['flarum/Component', 'flarum/compo
babelHelpers.createClass(AdminNav, [{ babelHelpers.createClass(AdminNav, [{
key: 'view', key: 'view',
value: function view() { value: function view() {
return m(SelectDropdown, { return m(
className: 'AdminNav App-titleControl', SelectDropdown,
buttonClassName: 'Button', {
children: this.items().toArray() className: 'AdminNav App-titleControl',
}); buttonClassName: 'Button' },
this.items().toArray()
);
} }
}, { }, {
key: 'items', key: 'items',
@@ -17832,8 +17834,8 @@ System.register('flarum/components/AppearancePage', ['flarum/components/Page', '
m( m(
'div', 'div',
{ className: 'AppearancePage-colors-input' }, { className: 'AppearancePage-colors-input' },
m('input', { className: 'FormControl', type: 'color', placeholder: '#aaaaaa', value: this.primaryColor(), onchange: m.withAttr('value', this.primaryColor) }), m('input', { className: 'FormControl', type: 'text', placeholder: '#aaaaaa', value: this.primaryColor(), onchange: m.withAttr('value', this.primaryColor) }),
m('input', { className: 'FormControl', type: 'color', placeholder: '#aaaaaa', value: this.secondaryColor(), onchange: m.withAttr('value', this.secondaryColor) }) m('input', { className: 'FormControl', type: 'text', placeholder: '#aaaaaa', value: this.secondaryColor(), onchange: m.withAttr('value', this.secondaryColor) })
), ),
Switch.component({ Switch.component({
state: this.darkMode(), state: this.darkMode(),
@@ -18356,15 +18358,17 @@ System.register('flarum/components/Checkbox', ['flarum/Component', 'flarum/compo
} }
}; };
});; });;
"use strict"; 'use strict';
System.register("flarum/components/DashboardPage", ["flarum/components/Page"], function (_export, _context) { System.register('flarum/components/DashboardPage', ['flarum/components/Page', 'flarum/components/StatusWidget'], function (_export, _context) {
"use strict"; "use strict";
var Page, DashboardPage; var Page, StatusWidget, DashboardPage;
return { return {
setters: [function (_flarumComponentsPage) { setters: [function (_flarumComponentsPage) {
Page = _flarumComponentsPage.default; Page = _flarumComponentsPage.default;
}, function (_flarumComponentsStatusWidget) {
StatusWidget = _flarumComponentsStatusWidget.default;
}], }],
execute: function () { execute: function () {
DashboardPage = function (_Page) { DashboardPage = function (_Page) {
@@ -18376,70 +18380,74 @@ System.register("flarum/components/DashboardPage", ["flarum/components/Page"], f
} }
babelHelpers.createClass(DashboardPage, [{ babelHelpers.createClass(DashboardPage, [{
key: "view", key: 'view',
value: function view() { value: function view() {
return m( return m(
"div", 'div',
{ className: "DashboardPage" }, { className: 'DashboardPage' },
m( m(
"div", 'div',
{ className: "container" }, { className: 'container' },
m( this.availableWidgets()
"h2",
null,
app.translator.trans('core.admin.dashboard.welcome_text')
),
m(
"p",
null,
app.translator.trans('core.admin.dashboard.version_text', { version: m(
"strong",
null,
app.forum.attribute('version')
) })
),
m(
"p",
null,
app.translator.trans('core.admin.dashboard.beta_warning_text', { strong: m("strong", null) })
),
m(
"ul",
null,
m(
"li",
null,
app.translator.trans('core.admin.dashboard.contributing_text', { a: m("a", { href: "http://flarum.org/docs/contributing", target: "_blank" }) })
),
m(
"li",
null,
app.translator.trans('core.admin.dashboard.troubleshooting_text', { a: m("a", { href: "http://flarum.org/docs/troubleshooting", target: "_blank" }) })
),
m(
"li",
null,
app.translator.trans('core.admin.dashboard.support_text', { a: m("a", { href: "http://discuss.flarum.org/t/support", target: "_blank" }) })
),
m(
"li",
null,
app.translator.trans('core.admin.dashboard.features_text', { a: m("a", { href: "http://discuss.flarum.org/t/features", target: "_blank" }) })
),
m(
"li",
null,
app.translator.trans('core.admin.dashboard.extension_text', { a: m("a", { href: "http://flarum.org/docs/extend", target: "_blank" }) })
)
)
) )
); );
} }
}, {
key: 'availableWidgets',
value: function availableWidgets() {
return [m(StatusWidget, null)];
}
}]); }]);
return DashboardPage; return DashboardPage;
}(Page); }(Page);
_export("default", DashboardPage); _export('default', DashboardPage);
}
};
});;
'use strict';
System.register('flarum/components/DashboardWidget', ['flarum/Component'], function (_export, _context) {
"use strict";
var Component, Widget;
return {
setters: [function (_flarumComponent) {
Component = _flarumComponent.default;
}],
execute: function () {
Widget = function (_Component) {
babelHelpers.inherits(Widget, _Component);
function Widget() {
babelHelpers.classCallCheck(this, Widget);
return babelHelpers.possibleConstructorReturn(this, (Widget.__proto__ || Object.getPrototypeOf(Widget)).apply(this, arguments));
}
babelHelpers.createClass(Widget, [{
key: 'view',
value: function view() {
return m(
'div',
{ className: "Widget " + this.className() },
this.content()
);
}
}, {
key: 'className',
value: function className() {
return '';
}
}, {
key: 'content',
value: function content() {
return [];
}
}]);
return Widget;
}(Component);
_export('default', Widget);
} }
}; };
});; });;
@@ -18509,6 +18517,10 @@ System.register('flarum/components/Dropdown', ['flarum/Component', 'flarum/helpe
$menu.toggleClass('Dropdown-menu--top', $menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height()); $menu.toggleClass('Dropdown-menu--top', $menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height());
if ($menu.offset().top < 0) {
$menu.removeClass('Dropdown-menu--top');
}
$menu.toggleClass('Dropdown-menu--right', isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width()); $menu.toggleClass('Dropdown-menu--right', isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width());
}); });
@@ -21102,6 +21114,84 @@ System.register('flarum/components/SplitDropdown', ['flarum/components/Dropdown'
});; });;
'use strict'; 'use strict';
System.register('flarum/components/StatusWidget', ['flarum/components/DashboardWidget', 'flarum/helpers/icon', 'flarum/helpers/listItems', 'flarum/utils/ItemList'], function (_export, _context) {
"use strict";
var DashboardWidget, icon, listItems, ItemList, StatusWidget;
return {
setters: [function (_flarumComponentsDashboardWidget) {
DashboardWidget = _flarumComponentsDashboardWidget.default;
}, function (_flarumHelpersIcon) {
icon = _flarumHelpersIcon.default;
}, function (_flarumHelpersListItems) {
listItems = _flarumHelpersListItems.default;
}, function (_flarumUtilsItemList) {
ItemList = _flarumUtilsItemList.default;
}],
execute: function () {
StatusWidget = function (_DashboardWidget) {
babelHelpers.inherits(StatusWidget, _DashboardWidget);
function StatusWidget() {
babelHelpers.classCallCheck(this, StatusWidget);
return babelHelpers.possibleConstructorReturn(this, (StatusWidget.__proto__ || Object.getPrototypeOf(StatusWidget)).apply(this, arguments));
}
babelHelpers.createClass(StatusWidget, [{
key: 'className',
value: function className() {
return 'StatusWidget';
}
}, {
key: 'content',
value: function content() {
return m(
'ul',
null,
listItems(this.items().toArray())
);
}
}, {
key: 'items',
value: function items() {
var items = new ItemList();
items.add('help', m(
'a',
{ href: 'http://flarum.org/docs/troubleshooting', target: '_blank' },
icon('question-circle'),
' ',
app.translator.trans('core.admin.dashboard.help_link')
));
items.add('version-flarum', [m(
'strong',
null,
'Flarum'
), m('br', null), app.forum.attribute('version')]);
items.add('version-php', [m(
'strong',
null,
'PHP'
), m('br', null), app.data.phpVersion]);
items.add('version-mysql', [m(
'strong',
null,
'MySQL'
), m('br', null), app.data.mysqlVersion]);
return items;
}
}]);
return StatusWidget;
}(DashboardWidget);
_export('default', StatusWidget);
}
};
});;
'use strict';
System.register('flarum/components/Switch', ['flarum/components/Checkbox'], function (_export, _context) { System.register('flarum/components/Switch', ['flarum/components/Checkbox'], function (_export, _context) {
"use strict"; "use strict";
@@ -21255,6 +21345,52 @@ System.register('flarum/components/UploadImageButton', ['flarum/components/Butto
} }
}; };
});; });;
'use strict';
System.register('flarum/components/Widget', ['flarum/Component'], function (_export, _context) {
"use strict";
var Component, DashboardWidget;
return {
setters: [function (_flarumComponent) {
Component = _flarumComponent.default;
}],
execute: function () {
DashboardWidget = function (_Component) {
babelHelpers.inherits(DashboardWidget, _Component);
function DashboardWidget() {
babelHelpers.classCallCheck(this, DashboardWidget);
return babelHelpers.possibleConstructorReturn(this, (DashboardWidget.__proto__ || Object.getPrototypeOf(DashboardWidget)).apply(this, arguments));
}
babelHelpers.createClass(DashboardWidget, [{
key: 'view',
value: function view() {
return m(
'div',
{ className: "DashboardWidget " + this.className() },
this.content()
);
}
}, {
key: 'className',
value: function className() {
return '';
}
}, {
key: 'content',
value: function content() {
return [];
}
}]);
return DashboardWidget;
}(Component);
_export('default', DashboardWidget);
}
};
});;
"use strict"; "use strict";
System.register("flarum/extend", [], function (_export, _context) { System.register("flarum/extend", [], function (_export, _context) {
@@ -22451,10 +22587,6 @@ System.register('flarum/models/User', ['flarum/Model', 'flarum/utils/stringToCol
password: Model.attribute('password'), password: Model.attribute('password'),
avatarUrl: Model.attribute('avatarUrl'), avatarUrl: Model.attribute('avatarUrl'),
bio: Model.attribute('bio'),
bioHtml: computed('bio', function (bio) {
return bio ? '<p>' + $('<div/>').text(bio).html().replace(/\n/g, '<br>').autoLink({ rel: 'nofollow' }) + '</p>' : '';
}),
preferences: Model.attribute('preferences'), preferences: Model.attribute('preferences'),
groups: Model.hasMany('groups'), groups: Model.hasMany('groups'),
@@ -23317,7 +23449,7 @@ System.register('flarum/utils/extractText', [], function (_export, _context) {
return vdom.map(function (element) { return vdom.map(function (element) {
return extractText(element); return extractText(element);
}).join(''); }).join('');
} else if ((typeof vdom === 'undefined' ? 'undefined' : babelHelpers.typeof(vdom)) === 'object') { } else if ((typeof vdom === 'undefined' ? 'undefined' : babelHelpers.typeof(vdom)) === 'object' && vdom !== null) {
return extractText(vdom.children); return extractText(vdom.children);
} else { } else {
return vdom; return vdom;
@@ -23603,7 +23735,12 @@ System.register('flarum/utils/patchMithril', ['../Component'], function (_export
} }
if (comp.prototype && comp.prototype instanceof Component) { if (comp.prototype && comp.prototype instanceof Component) {
return comp.component.apply(comp, args); var children = args.slice(1);
if (children.length === 1 && Array.isArray(children[0])) {
children = children[0];
}
return comp.component(args[0], children);
} }
var node = mo.apply(this, arguments); var node = mo.apply(this, arguments);

View File

@@ -18,9 +18,9 @@ export default class AdminNav extends Component {
return ( return (
<SelectDropdown <SelectDropdown
className="AdminNav App-titleControl" className="AdminNav App-titleControl"
buttonClassName="Button" buttonClassName="Button">
children={this.items().toArray()} {this.items().toArray()}
/> </SelectDropdown>
); );
} }

View File

@@ -3,6 +3,7 @@ import Button from 'flarum/components/Button';
import Switch from 'flarum/components/Switch'; import Switch from 'flarum/components/Switch';
import EditCustomCssModal from 'flarum/components/EditCustomCssModal'; import EditCustomCssModal from 'flarum/components/EditCustomCssModal';
import EditCustomHeaderModal from 'flarum/components/EditCustomHeaderModal'; import EditCustomHeaderModal from 'flarum/components/EditCustomHeaderModal';
import EditCustomFooterModal from 'flarum/components/EditCustomFooterModal';
import UploadImageButton from 'flarum/components/UploadImageButton'; import UploadImageButton from 'flarum/components/UploadImageButton';
import saveSettings from 'flarum/utils/saveSettings'; import saveSettings from 'flarum/utils/saveSettings';
@@ -28,8 +29,8 @@ export default class AppearancePage extends Page {
</div> </div>
<div className="AppearancePage-colors-input"> <div className="AppearancePage-colors-input">
<input className="FormControl" type="color" placeholder="#aaaaaa" value={this.primaryColor()} onchange={m.withAttr('value', this.primaryColor)}/> <input className="FormControl" type="text" placeholder="#aaaaaa" value={this.primaryColor()} onchange={m.withAttr('value', this.primaryColor)}/>
<input className="FormControl" type="color" placeholder="#aaaaaa" value={this.secondaryColor()} onchange={m.withAttr('value', this.secondaryColor)}/> <input className="FormControl" type="text" placeholder="#aaaaaa" value={this.secondaryColor()} onchange={m.withAttr('value', this.secondaryColor)}/>
</div> </div>
{Switch.component({ {Switch.component({
@@ -81,6 +82,18 @@ export default class AppearancePage extends Page {
})} })}
</fieldset> </fieldset>
<fieldset>
<legend>{app.translator.trans('core.admin.appearance.custom_footer_heading')}</legend>
<div className="helpText">
{app.translator.trans('core.admin.appearance.custom_footer_text')}
</div>
{Button.component({
className: 'Button',
children: app.translator.trans('core.admin.appearance.edit_footer_button'),
onclick: () => app.modal.show(new EditCustomFooterModal())
})}
</fieldset>
<fieldset> <fieldset>
<legend>{app.translator.trans('core.admin.appearance.custom_styles_heading')}</legend> <legend>{app.translator.trans('core.admin.appearance.custom_styles_heading')}</legend>
<div className="helpText"> <div className="helpText">

View File

@@ -1,22 +1,18 @@
import Page from 'flarum/components/Page'; import Page from 'flarum/components/Page';
import StatusWidget from 'flarum/components/StatusWidget';
export default class DashboardPage extends Page { export default class DashboardPage extends Page {
view() { view() {
return ( return (
<div className="DashboardPage"> <div className="DashboardPage">
<div className="container"> <div className="container">
<h2>{app.translator.trans('core.admin.dashboard.welcome_text')}</h2> {this.availableWidgets()}
<p>{app.translator.trans('core.admin.dashboard.version_text', {version: <strong>{app.forum.attribute('version')}</strong>})}</p>
<p>{app.translator.trans('core.admin.dashboard.beta_warning_text', {strong: <strong/>})}</p>
<ul>
<li>{app.translator.trans('core.admin.dashboard.contributing_text', {a: <a href="http://flarum.org/docs/contributing" target="_blank"/>})}</li>
<li>{app.translator.trans('core.admin.dashboard.troubleshooting_text', {a: <a href="http://flarum.org/docs/troubleshooting" target="_blank"/>})}</li>
<li>{app.translator.trans('core.admin.dashboard.support_text', {a: <a href="http://discuss.flarum.org/t/support" target="_blank"/>})}</li>
<li>{app.translator.trans('core.admin.dashboard.features_text', {a: <a href="http://discuss.flarum.org/t/features" target="_blank"/>})}</li>
<li>{app.translator.trans('core.admin.dashboard.extension_text', {a: <a href="http://flarum.org/docs/extend" target="_blank"/>})}</li>
</ul>
</div> </div>
</div> </div>
); );
} }
availableWidgets() {
return [<StatusWidget/>];
}
} }

View File

@@ -0,0 +1,38 @@
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import Component from 'flarum/Component';
export default class Widget extends Component {
view() {
return (
<div className={"Widget "+this.className()}>
{this.content()}
</div>
);
}
/**
* Get the class name to apply to the widget.
*
* @return {String}
*/
className() {
return '';
}
/**
* Get the content of the widget.
*
* @return {VirtualElement}
*/
content() {
return [];
}
}

View File

@@ -0,0 +1,24 @@
import SettingsModal from 'flarum/components/SettingsModal';
export default class EditCustomFooterModal extends SettingsModal {
className() {
return 'EditCustomFooterModal Modal--large';
}
title() {
return app.translator.trans('core.admin.edit_footer.title');
}
form() {
return [
<p>{app.translator.trans('core.admin.edit_footer.customize_text')}</p>,
<div className="Form-group">
<textarea className="FormControl" rows="30" bidi={this.setting('custom_footer')}/>
</div>
];
}
onsaved() {
window.location.reload();
}
}

View File

@@ -0,0 +1,41 @@
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import DashboardWidget from 'flarum/components/DashboardWidget';
import icon from 'flarum/helpers/icon';
import listItems from 'flarum/helpers/listItems';
import ItemList from 'flarum/utils/ItemList';
export default class StatusWidget extends DashboardWidget {
className() {
return 'StatusWidget';
}
content() {
return (
<ul>{listItems(this.items().toArray())}</ul>
);
}
items() {
const items = new ItemList();
items.add('help', (
<a href="http://flarum.org/docs/troubleshooting" target="_blank">
{icon('question-circle')} {app.translator.trans('core.admin.dashboard.help_link')}
</a>
));
items.add('version-flarum', [<strong>Flarum</strong>, <br/>, app.forum.attribute('version')]);
items.add('version-php', [<strong>PHP</strong>, <br/>, app.data.phpVersion]);
items.add('version-mysql', [<strong>MySQL</strong>, <br/>, app.data.mysqlVersion]);
return items;
}
}

View File

@@ -0,0 +1,38 @@
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import Component from 'flarum/Component';
export default class DashboardWidget extends Component {
view() {
return (
<div className={"DashboardWidget "+this.className()}>
{this.content()}
</div>
);
}
/**
* Get the class name to apply to the widget.
*
* @return {String}
*/
className() {
return '';
}
/**
* Get the content of the widget.
*
* @return {VirtualElement}
*/
content() {
return [];
}
}

View File

@@ -9,8 +9,6 @@
"color-thief": "v2.0", "color-thief": "v2.0",
"mithril": "lhorie/mithril.js#v0.2.5", "mithril": "lhorie/mithril.js#v0.2.5",
"es6-micro-loader": "caridy/es6-micro-loader#v0.2.1", "es6-micro-loader": "caridy/es6-micro-loader#v0.2.1",
"fastclick": "~1.0.6",
"autolink": "~1.0.0",
"m.attrs.bidi": "tobscure/m.attrs.bidi", "m.attrs.bidi": "tobscure/m.attrs.bidi",
"punycode": "http://cdnjs.cloudflare.com/ajax/libs/punycode/1.4.1/punycode.js" "punycode": "http://cdnjs.cloudflare.com/ajax/libs/punycode/1.4.1/punycode.js"
} }

View File

@@ -13,7 +13,6 @@ gulp({
bowerDir + '/jquery.hotkeys/jquery.hotkeys.js', bowerDir + '/jquery.hotkeys/jquery.hotkeys.js',
bowerDir + '/color-thief/src/color-thief.js', bowerDir + '/color-thief/src/color-thief.js',
bowerDir + '/moment/moment.js', bowerDir + '/moment/moment.js',
bowerDir + '/autolink/autolink-min.js',
bowerDir + '/bootstrap/js/affix.js', bowerDir + '/bootstrap/js/affix.js',
bowerDir + '/bootstrap/js/dropdown.js', bowerDir + '/bootstrap/js/dropdown.js',
@@ -23,7 +22,6 @@ gulp({
bowerDir + '/spin.js/spin.js', bowerDir + '/spin.js/spin.js',
bowerDir + '/spin.js/jquery.spin.js', bowerDir + '/spin.js/jquery.spin.js',
bowerDir + '/fastclick/lib/fastclick.js',
bowerDir + '/punycode/index.js' bowerDir + '/punycode/index.js'
], ],
modules: { modules: {

1328
js/forum/dist/app.js vendored

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,13 @@ export default class AvatarEditor extends Component {
* @type {Boolean} * @type {Boolean}
*/ */
this.loading = false; this.loading = false;
/**
* Whether or not an image has been dragged over the dropzone.
*
* @type {Boolean}
*/
this.isDraggedOver = false;
} }
static initProps(props) { static initProps(props) {
@@ -35,12 +42,17 @@ export default class AvatarEditor extends Component {
const user = this.props.user; const user = this.props.user;
return ( return (
<div className={'AvatarEditor Dropdown ' + this.props.className + (this.loading ? ' loading' : '')}> <div className={'AvatarEditor Dropdown ' + this.props.className + (this.loading ? ' loading' : '') + (this.isDraggedOver ? ' dragover' : '')}>
{avatar(user)} {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')} title={app.translator.trans('core.forum.user.avatar_upload_tooltip')}
data-toggle="dropdown" data-toggle="dropdown"
onclick={this.quickUpload.bind(this)}> onclick={this.quickUpload.bind(this)}
ondragover={this.enableDragover.bind(this)}
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('pencil') : icon('plus-circle'))} {this.loading ? LoadingIndicator.component() : (user.avatarUrl() ? icon('pencil') : icon('plus-circle'))}
</a> </a>
<ul className="Dropdown-menu Menu"> <ul className="Dropdown-menu Menu">
@@ -62,7 +74,7 @@ export default class AvatarEditor extends Component {
Button.component({ Button.component({
icon: 'upload', icon: 'upload',
children: app.translator.trans('core.forum.user.avatar_upload_button'), children: app.translator.trans('core.forum.user.avatar_upload_button'),
onclick: this.upload.bind(this) onclick: this.openPicker.bind(this)
}) })
); );
@@ -77,6 +89,40 @@ export default class AvatarEditor extends Component {
return items; return items;
} }
/**
* Enable dragover style
*
* @param {Event} e
*/
enableDragover(e) {
e.preventDefault();
e.stopPropagation();
this.isDraggedOver = true;
}
/**
* Disable dragover style
*
* @param {Event} e
*/
disableDragover(e) {
e.preventDefault();
e.stopPropagation();
this.isDraggedOver = false;
}
/**
* Upload avatar when file is dropped into dropzone.
*
* @param {Event} e
*/
dropUpload(e) {
e.preventDefault();
e.stopPropagation();
this.isDraggedOver = false;
this.upload(e.dataTransfer.files[0]);
}
/** /**
* If the user doesn't have an avatar, there's no point in showing the * If the user doesn't have an avatar, there's no point in showing the
* controls dropdown, because only one option would be viable: uploading. * controls dropdown, because only one option would be viable: uploading.
@@ -89,14 +135,14 @@ export default class AvatarEditor extends Component {
if (!this.props.user.avatarUrl()) { if (!this.props.user.avatarUrl()) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.upload(); this.openPicker();
} }
} }
/** /**
* Prompt the user to upload a new avatar. * Upload avatar using file picker
*/ */
upload() { openPicker() {
if (this.loading) return; if (this.loading) return;
// Create a hidden HTML input element and click on it so the user can select // Create a hidden HTML input element and click on it so the user can select
@@ -105,24 +151,36 @@ export default class AvatarEditor extends Component {
const $input = $('<input type="file">'); const $input = $('<input type="file">');
$input.appendTo('body').hide().click().on('change', e => { $input.appendTo('body').hide().click().on('change', e => {
const data = new FormData(); this.upload($(e.target)[0].files[0]);
data.append('avatar', $(e.target)[0].files[0]);
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)
);
}); });
} }
/**
* Upload avatar
*
* @param {File} file
*/
upload(file) {
if (this.loading) return;
const user = this.props.user;
const data = new FormData();
data.append('avatar', file);
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)
);
}
/** /**
* Remove the user's avatar. * Remove the user's avatar.
*/ */

View File

@@ -3,7 +3,6 @@ import ItemList from 'flarum/utils/ItemList';
import ComposerButton from 'flarum/components/ComposerButton'; import ComposerButton from 'flarum/components/ComposerButton';
import listItems from 'flarum/helpers/listItems'; import listItems from 'flarum/helpers/listItems';
import classList from 'flarum/utils/classList'; import classList from 'flarum/utils/classList';
import computed from 'flarum/utils/computed';
/** /**
* The `Composer` component displays the composer. It can be loaded with a * The `Composer` component displays the composer. It can be loaded with a
@@ -33,28 +32,6 @@ class Composer extends Component {
* @type {Boolean} * @type {Boolean}
*/ */
this.active = false; this.active = false;
/**
* Computed the composer's current height, based on the intended height, and
* the composer's current state. This will be applied to the composer's
* content's DOM element.
*
* @return {Integer}
*/
this.computedHeight = computed('height', 'position', (height, position) => {
// If the composer is minimized, then we don't want to set a height; we'll
// let the CSS decide how high it is. If it's fullscreen, then we need to
// make it as high as the window.
if (position === Composer.PositionEnum.MINIMIZED) {
return '';
} else if (position === Composer.PositionEnum.FULLSCREEN) {
return $(window).height();
}
// Otherwise, if it's normal or hidden, then we use the intended height.
// We don't let the composer get too small or too big, though.
return Math.max(200, Math.min(height, $(window).height() - $('#header').outerHeight()));
});
} }
view() { view() {
@@ -85,11 +62,9 @@ class Composer extends Component {
} }
config(isInitialized, context) { config(isInitialized, context) {
let defaultHeight; // Set the height of the Composer element and its contents on each redraw,
// so that they do not lose it if their DOM elements are recreated.
if (!isInitialized) { this.updateHeight();
defaultHeight = this.$().height();
}
if (isInitialized) return; if (isInitialized) return;
@@ -97,11 +72,8 @@ class Composer extends Component {
// routes, we will flag the DOM to be retained across route changes. // routes, we will flag the DOM to be retained across route changes.
context.retain = true; context.retain = true;
// Initialize the composer's intended height based on what the user has set this.initializeHeight();
// it at previously, or otherwise the composer's default height. After that, this.$().hide().css('bottom', -this.computedHeight());
// we'll hide the composer.
this.height = localStorage.getItem('composerHeight') || defaultHeight;
this.$().hide().css('bottom', -this.height);
// Whenever any of the inputs inside the composer are have focus, we want to // 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. // add a class to the composer to draw attention to it.
@@ -172,8 +144,7 @@ class Composer extends Component {
// height so that it fills the height of the composer, and update the // height so that it fills the height of the composer, and update the
// body's padding. // body's padding.
const deltaPixels = this.mouseStart - e.clientY; const deltaPixels = this.mouseStart - e.clientY;
this.height = this.heightStart + deltaPixels; this.changeHeight(this.heightStart + deltaPixels);
this.updateHeight();
// Update the body's padding-bottom so that no content on the page will ever // Update the body's padding-bottom so that no content on the page will ever
// get permanently hidden behind the composer. If the user is already // get permanently hidden behind the composer. If the user is already
@@ -182,8 +153,6 @@ class Composer extends Component {
const scrollTop = $(window).scrollTop(); const scrollTop = $(window).scrollTop();
const anchorToBottom = scrollTop > 0 && scrollTop + $(window).height() >= $(document).height(); const anchorToBottom = scrollTop > 0 && scrollTop + $(window).height() >= $(document).height();
this.updateBodyPadding(anchorToBottom); this.updateBodyPadding(anchorToBottom);
localStorage.setItem('composerHeight', this.height);
} }
/** /**
@@ -482,6 +451,73 @@ class Composer extends Component {
return items; return items;
} }
/**
* Initialize default Composer height.
*/
initializeHeight() {
this.height = localStorage.getItem('composerHeight');
if (!this.height) {
this.height = this.defaultHeight();
}
}
/**
* Default height of the Composer in case none is saved.
* @returns {Integer}
*/
defaultHeight() {
return this.$().height();
}
/**
* Minimum height of the Composer.
* @returns {Integer}
*/
minimumHeight() {
return 200;
}
/**
* Maxmimum height of the Composer.
* @returns {Integer}
*/
maximumHeight() {
return $(window).height() - $('#header').outerHeight();
}
/**
* Computed the composer's current height, based on the intended height, and
* the composer's current state. This will be applied to the composer's
* content's DOM element.
* @returns {Integer|String}
*/
computedHeight() {
// If the composer is minimized, then we don't want to set a height; we'll
// let the CSS decide how high it is. If it's fullscreen, then we need to
// make it as high as the window.
if (this.position === Composer.PositionEnum.MINIMIZED) {
return '';
} else if (this.position === Composer.PositionEnum.FULLSCREEN) {
return $(window).height();
}
// Otherwise, if it's normal or hidden, then we use the intended height.
// We don't let the composer get too small or too big, though.
return Math.max(this.minimumHeight(), Math.min(this.height, this.maximumHeight()));
}
/**
* Save a new Composer height and update the DOM.
* @param {Integer} height
*/
changeHeight(height) {
this.height = height;
this.updateHeight();
localStorage.setItem('composerHeight', this.height);
}
} }
Composer.PositionEnum = { Composer.PositionEnum = {

View File

@@ -16,39 +16,17 @@ export default class NotificationList extends Component {
* @type {Boolean} * @type {Boolean}
*/ */
this.loading = false; this.loading = false;
/**
* Whether or not there are more results that can be loaded.
*
* @type {Boolean}
*/
this.moreResults = false;
} }
view() { view() {
const groups = []; const pages = app.cache.notifications || [];
if (app.cache.notifications) {
const discussions = {};
// Build an array of discussions which the notifications are related to,
// and add the notifications as children.
app.cache.notifications.forEach(notification => {
const subject = notification.subject();
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();
// 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]);
}
});
}
return ( return (
<div className="NotificationList"> <div className="NotificationList">
@@ -66,8 +44,34 @@ export default class NotificationList extends Component {
</div> </div>
<div className="NotificationList-content"> <div className="NotificationList-content">
{groups.length {pages.length ? pages.map(notifications => {
? groups.map(group => { const groups = [];
const discussions = {};
notifications.forEach(notification => {
const subject = notification.subject();
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();
// 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]);
}
});
return groups.map(group => {
const badges = group.discussion && group.discussion.badges().toArray(); const badges = group.discussion && group.discussion.badges().toArray();
return ( return (
@@ -94,32 +98,71 @@ export default class NotificationList extends Component {
</ul> </ul>
</div> </div>
); );
}) });
: !this.loading }) : ''}
? <div className="NotificationList-empty">{app.translator.trans('core.forum.notifications.empty_text')}</div> {this.loading
: LoadingIndicator.component({className: 'LoadingIndicator--block'})} ? <LoadingIndicator className="LoadingIndicator--block" />
: (pages.length ? '' : <div className="NotificationList-empty">{app.translator.trans('core.forum.notifications.empty_text')}</div>)}
</div> </div>
</div> </div>
); );
} }
config(isInitialized, context) {
if (isInitialized) return;
const $notifications = this.$('.NotificationList-content');
const $scrollParent = $notifications.css('overflow') === 'auto' ? $notifications : $(window);
const scrollHandler = () => {
const scrollTop = $scrollParent.scrollTop();
const viewportHeight = $scrollParent.height();
const contentTop = $scrollParent === $notifications ? 0 : $notifications.offset().top;
const contentHeight = $notifications[0].scrollHeight;
if (this.moreResults && !this.loading && scrollTop + viewportHeight >= contentTop + contentHeight) {
this.loadMore();
}
};
$scrollParent.on('scroll', scrollHandler);
context.onunload = () => {
$scrollParent.off('scroll', scrollHandler);
};
}
/** /**
* Load notifications into the application's cache if they haven't already * Load notifications into the application's cache if they haven't already
* been loaded. * been loaded.
*/ */
load() { load() {
if (app.cache.notifications && !app.session.user.newNotificationsCount()) { if (app.session.user.newNotificationsCount()) {
delete app.cache.notifications;
}
if (app.cache.notifications) {
return; return;
} }
app.session.user.pushAttributes({newNotificationsCount: 0});
this.loadMore();
}
/**
* Load the next page of notification results.
*
* @public
*/
loadMore() {
this.loading = true; this.loading = true;
m.redraw(); m.redraw();
app.store.find('notifications') const params = app.cache.notifications ? {page: {offset: app.cache.notifications.length * 10}} : null;
.then(notifications => {
app.session.user.pushAttributes({newNotificationsCount: 0}); return app.store.find('notifications', params)
app.cache.notifications = notifications.sort((a, b) => b.time() - a.time()); .then(this.parseResults.bind(this))
})
.catch(() => {}) .catch(() => {})
.then(() => { .then(() => {
this.loading = false; this.loading = false;
@@ -127,6 +170,21 @@ export default class NotificationList extends Component {
}); });
} }
/**
* Parse results and append them to the notification list.
*
* @param {Notification[]} results
* @return {Notification[]}
*/
parseResults(results) {
app.cache.notifications = app.cache.notifications || [];
app.cache.notifications.push(results);
this.moreResults = !!results.payload.links.next;
return results;
}
/** /**
* Mark all of the notifications as read. * Mark all of the notifications as read.
*/ */
@@ -135,7 +193,9 @@ export default class NotificationList extends Component {
app.session.user.pushAttributes({unreadNotificationsCount: 0}); app.session.user.pushAttributes({unreadNotificationsCount: 0});
app.cache.notifications.forEach(notification => notification.pushAttributes({isRead: true})); app.cache.notifications.forEach(notifications => {
notifications.forEach(notification => notification.pushAttributes({isRead: true}))
});
app.request({ app.request({
url: app.forum.attribute('apiUrl') + '/notifications/read', url: app.forum.attribute('apiUrl') + '/notifications/read',

View File

@@ -126,7 +126,7 @@ class PostStream extends Component {
this.visibleEnd = this.count(); this.visibleEnd = this.count();
this.loadRange(this.visibleStart, this.visibleEnd).then(() => m.redraw()); return this.loadRange(this.visibleStart, this.visibleEnd).then(() => m.redraw());
} }
/** /**

View File

@@ -82,9 +82,10 @@ export default class ReplyComposer extends ComposerBody {
app.store.createRecord('posts').save(data).then( app.store.createRecord('posts').save(data).then(
post => { post => {
// If we're currently viewing the discussion which this reply was made // If we're currently viewing the discussion which this reply was made
// in, then we can update the post stream. // in, then we can update the post stream and scroll to the post.
if (app.viewingDiscussion(discussion)) { if (app.viewingDiscussion(discussion)) {
app.current.stream.update(); app.current.stream.update().then(() => app.current.stream.goToNumber(post.number()));
} else { } else {
// Otherwise, we'll create an alert message to inform the user that // Otherwise, we'll create an alert message to inform the user that
// their reply has been posted, containing a button which will // their reply has been posted, containing a button which will

View File

@@ -82,7 +82,8 @@ export default class TextEditor extends Component {
Button.component({ Button.component({
icon: 'eye', icon: 'eye',
className: 'Button Button--icon', className: 'Button Button--icon',
onclick: this.props.preview onclick: this.props.preview,
title: app.translator.trans('core.forum.composer.preview_tooltip')
}) })
); );
} }

View File

@@ -1,104 +0,0 @@
import Component from 'flarum/Component';
import LoadingIndicator from 'flarum/components/LoadingIndicator';
import classList from 'flarum/utils/classList';
import extractText from 'flarum/utils/extractText';
/**
* The `UserBio` component displays a user's bio, optionally letting the user
* edit it.
*/
export default class UserBio extends Component {
init() {
/**
* Whether or not the bio is currently being edited.
*
* @type {Boolean}
*/
this.editing = false;
/**
* Whether or not the bio is currently being saved.
*
* @type {Boolean}
*/
this.loading = false;
}
view() {
const user = this.props.user;
let content;
if (this.editing) {
content = <textarea className="FormControl" placeholder={extractText(app.translator.trans('core.forum.user.bio_placeholder'))} rows="3" value={user.bio()}/>;
} else {
let subContent;
if (this.loading) {
subContent = <p className="UserBio-placeholder">{LoadingIndicator.component({size: 'tiny'})}</p>;
} else {
const bioHtml = user.bioHtml();
if (bioHtml) {
subContent = m.trust(bioHtml);
} else if (this.props.editable) {
subContent = <p className="UserBio-placeholder">{app.translator.trans('core.forum.user.bio_placeholder')}</p>;
}
}
content = <div className="UserBio-content" onclick={this.edit.bind(this)}>{subContent}</div>;
}
return (
<div className={'UserBio ' + classList({
editable: this.props.editable,
editing: this.editing
})}>
{content}
</div>
);
}
/**
* Edit the bio.
*/
edit() {
if (!this.props.editable) return;
this.editing = true;
m.redraw();
const bio = this;
const save = function(e) {
if (e.shiftKey) return;
e.preventDefault();
bio.save($(this).val());
};
this.$('textarea').focus()
.bind('blur', save)
.bind('keydown', 'return', save);
}
/**
* Save the bio.
*
* @param {String} value
*/
save(value) {
const user = this.props.user;
if (user.bio() !== value) {
this.loading = true;
user.save({bio: value})
.catch(() => {})
.then(() => {
this.loading = false;
m.redraw();
});
}
this.editing = false;
m.redraw();
}
}

View File

@@ -6,7 +6,6 @@ import avatar from 'flarum/helpers/avatar';
import username from 'flarum/helpers/username'; import username from 'flarum/helpers/username';
import icon from 'flarum/helpers/icon'; import icon from 'flarum/helpers/icon';
import Dropdown from 'flarum/components/Dropdown'; import Dropdown from 'flarum/components/Dropdown';
import UserBio from 'flarum/components/UserBio';
import AvatarEditor from 'flarum/components/AvatarEditor'; import AvatarEditor from 'flarum/components/AvatarEditor';
import listItems from 'flarum/helpers/listItems'; import listItems from 'flarum/helpers/listItems';
@@ -82,13 +81,6 @@ export default class UserCard extends Component {
const user = this.props.user; const user = this.props.user;
const lastSeenTime = user.lastSeenTime(); const lastSeenTime = user.lastSeenTime();
items.add('bio',
UserBio.component({
user,
editable: this.props.editable
})
);
if (lastSeenTime) { if (lastSeenTime) {
const online = user.isOnline(); const online = user.isOnline();

View File

@@ -9,18 +9,27 @@ import username from 'flarum/helpers/username';
* @implements SearchSource * @implements SearchSource
*/ */
export default class UsersSearchResults { export default class UsersSearchResults {
constructor() {
this.results = {};
}
search(query) { search(query) {
return app.store.find('users', { return app.store.find('users', {
filter: {q: query}, filter: {q: query},
page: {limit: 5} page: {limit: 5}
}).then(results => {
this.results[query] = results;
m.redraw();
}); });
} }
view(query) { view(query) {
query = query.toLowerCase(); query = query.toLowerCase();
const results = app.store.all('users') const results = (this.results[query] || [])
.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()));
if (!results.length) return ''; if (!results.length) return '';

View File

@@ -78,11 +78,7 @@ export default function boot(app) {
.toggleClass('scrolled', top > offset); .toggleClass('scrolled', top > offset);
}).start(); }).start();
// Initialize FastClick, which makes links and buttons much more responsive on
// touch devices.
$(() => { $(() => {
FastClick.attach(document.body);
$('body').addClass('ontouchstart' in window ? 'touch' : 'no-touch'); $('body').addClass('ontouchstart' in window ? 'touch' : 'no-touch');
}); });

View File

@@ -162,7 +162,7 @@ export default {
} }
app.composer.show(); app.composer.show();
if (goToLast && app.viewingDiscussion(this)) { if (goToLast && app.viewingDiscussion(this) && ! app.composer.isFullScreen()) {
app.current.stream.goToNumber('reply'); app.current.stream.goToNumber('reply');
} }

View File

@@ -69,6 +69,10 @@ export default class Dropdown extends Component {
$menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height() $menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height()
); );
if ($menu.offset().top < 0) {
$menu.removeClass('Dropdown-menu--top');
}
$menu.toggleClass( $menu.toggleClass(
'Dropdown-menu--right', 'Dropdown-menu--right',
isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width() isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width()

View File

@@ -16,8 +16,6 @@ Object.assign(User.prototype, {
password: Model.attribute('password'), password: Model.attribute('password'),
avatarUrl: Model.attribute('avatarUrl'), avatarUrl: Model.attribute('avatarUrl'),
bio: Model.attribute('bio'),
bioHtml: computed('bio', bio => bio ? '<p>' + $('<div/>').text(bio).html().replace(/\n/g, '<br>').autoLink({rel: 'nofollow'}) + '</p>' : ''),
preferences: Model.attribute('preferences'), preferences: Model.attribute('preferences'),
groups: Model.hasMany('groups'), groups: Model.hasMany('groups'),

View File

@@ -5,7 +5,12 @@ export default function patchMithril(global) {
const m = function(comp, ...args) { const m = function(comp, ...args) {
if (comp.prototype && comp.prototype instanceof Component) { if (comp.prototype && comp.prototype instanceof Component) {
return comp.component(args[0], args.slice(1)); let children = args.slice(1);
if (children.length === 1 && Array.isArray(children[0])) {
children = children[0]
}
return comp.component(args[0], children);
} }
const node = mo.apply(this, arguments); const node = mo.apply(this, arguments);

View File

@@ -2,20 +2,46 @@
background: @control-bg; background: @control-bg;
color: @control-color; color: @control-color;
min-height: 100vh; min-height: 100vh;
font-size: 14px;
line-height: 1.7;
@media @desktop-up { @media @desktop-up {
.container { .container {
max-width: 600px;
padding: 30px; padding: 30px;
margin: 0; margin: 0;
} }
} }
}
h2 { .Widget {
font-size: 26px; background: @body-bg;
font-weight: 300; color: @text-color;
margin-top: 0; border-radius: @border-radius;
padding: 20px;
margin-bottom: 20px;
}
.StatusWidget {
color: @muted-color;
> ul {
margin: 0;
padding: 0;
list-style-type: none;
> li {
display: inline-block;
margin-right: 30px;
vertical-align: middle;
&[class^="item-version-"] {
max-width: 100px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&.item-help {
float: right;
margin-right: 0;
}
}
} }
} }

View File

@@ -17,7 +17,10 @@
.AvatarEditor--noAvatar { .AvatarEditor--noAvatar {
opacity: 0.7; opacity: 0.7;
} }
&:hover .Dropdown-toggle, &.open .Dropdown-toggle, &.loading .Dropdown-toggle { &:hover .Dropdown-toggle,
&.open .Dropdown-toggle,
&.loading .Dropdown-toggle,
&.dragover .Dropdown-toggle {
opacity: 1; opacity: 1;
} }
.LoadingIndicator { .LoadingIndicator {

View File

@@ -88,14 +88,11 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
background: fade(@body-bg, 90%); background: fade(@body-bg, 90%);
opacity: 0; display: none;
pointer-events: none;
border-radius: @border-radius @border-radius 0 0; border-radius: @border-radius @border-radius 0 0;
.transition(opacity 0.2s);
&.active { &.active {
opacity: 1; display: block;
pointer-events: auto;
} }
} }
.ComposerBody-editor { .ComposerBody-editor {
@@ -119,7 +116,7 @@
&:not(.minimized) { &:not(.minimized) {
position: absolute; position: absolute;
top: 0; top: 0;
height: 100vh !important; height: 350px !important;
padding-top: @header-height-phone; padding-top: @header-height-phone;
&:before { &:before {

View File

@@ -6,7 +6,6 @@
.NotificationList-content { .NotificationList-content {
max-height: 70vh; max-height: 70vh;
overflow: auto; overflow: auto;
padding-bottom: 10px;
} }
} }
& .Dropdown-toggle .Button-label { & .Dropdown-toggle .Button-label {

View File

@@ -90,37 +90,6 @@
display: inline-block; display: inline-block;
margin-right: 15px; margin-right: 15px;
} }
.item-bio {
display: block;
margin: 0;
}
}
.UserBio {
margin: -10px -10px 10px;
border: 1px dashed transparent;
border-radius: @border-radius;
&.editable:not(.editing) {
cursor: text;
&:hover {
border-color: rgba(255, 255, 255, 0.2);
}
}
&, textarea {
font-size: 14px;
}
textarea {
padding: 10px;
font-size: 14px;
resize: none;
}
}
.UserBio-content {
padding: 10px 10px 1px;
}
.UserBio-placeholder {
opacity: 0.3;
} }
.UserCard-lastSeen { .UserCard-lastSeen {
& .icon { & .icon {

0
less/lib/Alert.less Executable file → Normal file
View File

0
less/lib/AlertManager.less Executable file → Normal file
View File

2
less/lib/App.less Executable file → Normal file
View File

@@ -171,6 +171,8 @@
.Header-logo { .Header-logo {
max-height: 30px; max-height: 30px;
vertical-align: middle; vertical-align: middle;
// Prevent blurriness in Chrome
image-rendering: -webkit-optimize-contrast;
} }
// On phones, the header is displayed inside of the drawer. We lay its // On phones, the header is displayed inside of the drawer. We lay its

2
less/lib/Avatar.less Executable file → Normal file
View File

@@ -14,6 +14,8 @@
height: 100%; height: 100%;
border-radius: 100%; border-radius: 100%;
vertical-align: top; vertical-align: top;
// Prevent blurriness in Chrome
image-rendering: -webkit-optimize-contrast;
} }
} }

0
less/lib/Badge.less Executable file → Normal file
View File

0
less/lib/Button.less Executable file → Normal file
View File

0
less/lib/Checkbox.less Executable file → Normal file
View File

0
less/lib/Dropdown.less Executable file → Normal file
View File

0
less/lib/Form.less Executable file → Normal file
View File

0
less/lib/FormControl.less Executable file → Normal file
View File

0
less/lib/LoadingIndicator.less Executable file → Normal file
View File

0
less/lib/Modal.less Executable file → Normal file
View File

0
less/lib/Navigation.less Executable file → Normal file
View File

0
less/lib/Search.less Executable file → Normal file
View File

0
less/lib/Select.less Executable file → Normal file
View File

0
less/lib/Tooltip.less Executable file → Normal file
View File

0
less/lib/lib.less Executable file → Normal file
View File

0
less/lib/mixins.less Executable file → Normal file
View File

0
less/lib/mixins/border-radius.less Executable file → Normal file
View File

0
less/lib/mixins/clearfix.less Executable file → Normal file
View File

0
less/lib/mixins/vendor-prefixes.less Executable file → Normal file
View File

0
less/lib/normalize.less vendored Executable file → Normal file
View File

0
less/lib/print.less Executable file → Normal file
View File

0
less/lib/scaffolding.less Executable file → Normal file
View File

0
less/lib/sideNav.less Executable file → Normal file
View File

0
less/lib/variables.less Executable file → Normal file
View File

View File

@@ -20,7 +20,7 @@ return [
}); });
// Store slugs for existing discussions // Store slugs for existing discussions
$schema->getConnection()->table('discussions')->chunk(100, function ($discussions) use ($schema) { $schema->getConnection()->table('discussions')->chunkById(100, function ($discussions) use ($schema) {
foreach ($discussions as $discussion) { foreach ($discussions as $discussion) {
$schema->getConnection()->table('discussions')->where('id', $discussion->id)->update([ $schema->getConnection()->table('discussions')->where('id', $discussion->id)->update([
'slug' => Str::slug($discussion->title) 'slug' => Str::slug($discussion->title)

0
scripts/compile.sh Executable file → Normal file
View File

View File

@@ -11,12 +11,23 @@
namespace Flarum\Admin; namespace Flarum\Admin;
use Flarum\Event\ExtensionWasDisabled; use Flarum\Admin\Middleware\RequireAdministrateAbility;
use Flarum\Event\ExtensionWasEnabled; use Flarum\Event\ConfigureMiddleware;
use Flarum\Event\SettingWasSet; use Flarum\Extension\Event\Disabled;
use Flarum\Extension\Event\Enabled;
use Flarum\Foundation\AbstractServiceProvider; use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Http\Handler\RouteHandlerFactory; use Flarum\Http\Middleware\AuthenticateWithSession;
use Flarum\Http\Middleware\DispatchRoute;
use Flarum\Http\Middleware\HandleErrors;
use Flarum\Http\Middleware\ParseJsonBody;
use Flarum\Http\Middleware\RememberFromCookie;
use Flarum\Http\Middleware\SetLocale;
use Flarum\Http\Middleware\StartSession;
use Flarum\Http\RouteCollection; use Flarum\Http\RouteCollection;
use Flarum\Http\RouteHandlerFactory;
use Flarum\Http\UrlGenerator;
use Flarum\Settings\Event\Saved;
use Zend\Stratigility\MiddlewarePipe;
class AdminServiceProvider extends AbstractServiceProvider class AdminServiceProvider extends AbstractServiceProvider
{ {
@@ -25,13 +36,35 @@ class AdminServiceProvider extends AbstractServiceProvider
*/ */
public function register() public function register()
{ {
$this->app->singleton(UrlGenerator::class, function () { $this->app->extend(UrlGenerator::class, function (UrlGenerator $url) {
return new UrlGenerator($this->app, $this->app->make('flarum.admin.routes')); return $url->addCollection('admin', $this->app->make('flarum.admin.routes'), 'admin');
}); });
$this->app->singleton('flarum.admin.routes', function () { $this->app->singleton('flarum.admin.routes', function () {
return new RouteCollection; return new RouteCollection;
}); });
$this->app->singleton('flarum.admin.middleware', function ($app) {
$pipe = new MiddlewarePipe;
$pipe->raiseThrowables();
// All requests should first be piped through our global error handler
$debugMode = ! $app->isUpToDate() || $app->inDebugMode();
$pipe->pipe($app->make(HandleErrors::class, ['debug' => $debugMode]));
$pipe->pipe($app->make(ParseJsonBody::class));
$pipe->pipe($app->make(StartSession::class));
$pipe->pipe($app->make(RememberFromCookie::class));
$pipe->pipe($app->make(AuthenticateWithSession::class));
$pipe->pipe($app->make(SetLocale::class));
$pipe->pipe($app->make(RequireAdministrateAbility::class));
event(new ConfigureMiddleware($pipe, 'admin'));
$pipe->pipe($app->make(DispatchRoute::class, ['routes' => $app->make('flarum.admin.routes')]));
return $pipe;
});
} }
/** /**
@@ -43,9 +76,7 @@ class AdminServiceProvider extends AbstractServiceProvider
$this->loadViewsFrom(__DIR__.'/../../views', 'flarum.admin'); $this->loadViewsFrom(__DIR__.'/../../views', 'flarum.admin');
$this->flushWebAppAssetsWhenThemeChanged(); $this->registerListeners();
$this->flushWebAppAssetsWhenExtensionsChanged();
} }
/** /**
@@ -55,30 +86,29 @@ class AdminServiceProvider extends AbstractServiceProvider
*/ */
protected function populateRoutes(RouteCollection $routes) protected function populateRoutes(RouteCollection $routes)
{ {
$route = $this->app->make(RouteHandlerFactory::class); $factory = $this->app->make(RouteHandlerFactory::class);
$routes->get( $callback = include __DIR__.'/routes.php';
'/', $callback($routes, $factory);
'index',
$route->toController(Controller\WebAppController::class)
);
} }
protected function flushWebAppAssetsWhenThemeChanged() protected function registerListeners()
{ {
$this->app->make('events')->listen(SettingWasSet::class, function (SettingWasSet $event) { $dispatcher = $this->app->make('events');
// Flush web app assets when the theme is changed
$dispatcher->listen(Saved::class, function (Saved $event) {
if (preg_match('/^theme_|^custom_less$/i', $event->key)) { if (preg_match('/^theme_|^custom_less$/i', $event->key)) {
$this->getWebAppAssets()->flushCss(); $this->getWebAppAssets()->flushCss();
} }
}); });
}
protected function flushWebAppAssetsWhenExtensionsChanged() // Flush web app assets when extensions are changed
{ $dispatcher->listen(Enabled::class, [$this, 'flushWebAppAssets']);
$events = $this->app->make('events'); $dispatcher->listen(Disabled::class, [$this, 'flushWebAppAssets']);
$events->listen(ExtensionWasEnabled::class, [$this, 'flushWebAppAssets']); // Check the format of custom LESS code
$events->listen(ExtensionWasDisabled::class, [$this, 'flushWebAppAssets']); $dispatcher->subscribe(CheckCustomLessFormat::class);
} }
public function flushWebAppAssets() public function flushWebAppAssets()
@@ -87,10 +117,10 @@ class AdminServiceProvider extends AbstractServiceProvider
} }
/** /**
* @return \Flarum\Http\WebApp\WebAppAssets * @return \Flarum\Frontend\FrontendAssets
*/ */
protected function getWebAppAssets() protected function getWebAppAssets()
{ {
return $this->app->make(WebApp::class)->getAssets(); return $this->app->make(Frontend::class)->getAssets();
} }
} }

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Admin;
use Flarum\Foundation\ValidationException;
use Flarum\Settings\Event\Serializing;
use Illuminate\Contracts\Events\Dispatcher;
use Less_Exception_Parser;
use Less_Parser;
class CheckCustomLessFormat
{
public function subscribe(Dispatcher $events)
{
$events->listen(Serializing::class, [$this, 'check']);
}
public function check(Serializing $event)
{
if ($event->key === 'custom_less') {
$parser = new Less_Parser();
try {
// Check the custom less format before saving
// Variables names are not checked, we would have to set them and call getCss() to check them
$parser->parse($event->value);
} catch (Less_Exception_Parser $e) {
throw new ValidationException([
'custom_less' => $e->getMessage(),
]);
}
}
}
}

View File

@@ -11,16 +11,17 @@
namespace Flarum\Admin\Controller; namespace Flarum\Admin\Controller;
use Flarum\Admin\WebApp; use Flarum\Admin\Frontend;
use Flarum\Core\Permission;
use Flarum\Event\PrepareUnserializedSettings;
use Flarum\Extension\ExtensionManager; use Flarum\Extension\ExtensionManager;
use Flarum\Http\Controller\AbstractWebAppController; use Flarum\Frontend\AbstractFrontendController;
use Flarum\Group\Permission;
use Flarum\Settings\Event\Deserializing;
use Flarum\Settings\SettingsRepositoryInterface; use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\ConnectionInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
class WebAppController extends AbstractWebAppController class FrontendController extends AbstractFrontendController
{ {
/** /**
* @var SettingsRepositoryInterface * @var SettingsRepositoryInterface
@@ -33,17 +34,24 @@ class WebAppController extends AbstractWebAppController
protected $extensions; protected $extensions;
/** /**
* @param WebApp $webApp * @var ConnectionInterface
*/
protected $db;
/**
* @param Frontend $webApp
* @param Dispatcher $events * @param Dispatcher $events
* @param SettingsRepositoryInterface $settings * @param SettingsRepositoryInterface $settings
* @param ExtensionManager $extensions * @param ExtensionManager $extensions
* @param ConnectionInterface $db
*/ */
public function __construct(WebApp $webApp, Dispatcher $events, SettingsRepositoryInterface $settings, ExtensionManager $extensions) public function __construct(Frontend $webApp, Dispatcher $events, SettingsRepositoryInterface $settings, ExtensionManager $extensions, ConnectionInterface $db)
{ {
$this->webApp = $webApp; $this->webApp = $webApp;
$this->events = $events; $this->events = $events;
$this->settings = $settings; $this->settings = $settings;
$this->extensions = $extensions; $this->extensions = $extensions;
$this->db = $db;
} }
/** /**
@@ -56,13 +64,16 @@ class WebAppController extends AbstractWebAppController
$settings = $this->settings->all(); $settings = $this->settings->all();
$this->events->fire( $this->events->fire(
new PrepareUnserializedSettings($settings) new Deserializing($settings)
); );
$view->setVariable('settings', $settings); $view->setVariable('settings', $settings);
$view->setVariable('permissions', Permission::map()); $view->setVariable('permissions', Permission::map());
$view->setVariable('extensions', $this->extensions->getExtensions()->toArray()); $view->setVariable('extensions', $this->extensions->getExtensions()->toArray());
$view->setVariable('phpVersion', PHP_VERSION);
$view->setVariable('mysqlVersion', $this->db->selectOne('select version() as version')->version);
return $view; return $view;
} }
} }

View File

@@ -11,9 +11,9 @@
namespace Flarum\Admin; namespace Flarum\Admin;
use Flarum\Http\WebApp\AbstractWebApp; use Flarum\Frontend\AbstractFrontend;
class WebApp extends AbstractWebApp class Frontend extends AbstractFrontend
{ {
/** /**
* {@inheritdoc} * {@inheritdoc}

View File

@@ -11,7 +11,7 @@
namespace Flarum\Admin\Middleware; namespace Flarum\Admin\Middleware;
use Flarum\Core\Access\AssertPermissionTrait; use Flarum\User\AssertPermissionTrait;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Zend\Stratigility\MiddlewareInterface; use Zend\Stratigility\MiddlewareInterface;

View File

@@ -1,58 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Admin;
use Flarum\Event\ConfigureMiddleware;
use Flarum\Foundation\Application;
use Flarum\Http\AbstractServer;
use Flarum\Http\Middleware\HandleErrors;
use Zend\Stratigility\MiddlewarePipe;
class Server extends AbstractServer
{
/**
* {@inheritdoc}
*/
protected function getMiddleware(Application $app)
{
$pipe = new MiddlewarePipe;
$pipe->raiseThrowables();
if ($app->isInstalled()) {
$path = parse_url($app->url('admin'), PHP_URL_PATH);
$errorDir = __DIR__.'/../../error';
// All requests should first be piped through our global error handler
$debugMode = ! $app->isUpToDate() || $app->inDebugMode();
$pipe->pipe($path, new HandleErrors($errorDir, $app->make('log'), $debugMode));
if ($app->isUpToDate()) {
$pipe->pipe($path, $app->make('Flarum\Http\Middleware\ParseJsonBody'));
$pipe->pipe($path, $app->make('Flarum\Http\Middleware\StartSession'));
$pipe->pipe($path, $app->make('Flarum\Http\Middleware\RememberFromCookie'));
$pipe->pipe($path, $app->make('Flarum\Http\Middleware\AuthenticateWithSession'));
$pipe->pipe($path, $app->make('Flarum\Http\Middleware\SetLocale'));
$pipe->pipe($path, $app->make('Flarum\Admin\Middleware\RequireAdministrateAbility'));
event(new ConfigureMiddleware($pipe, $path, $this));
$pipe->pipe($path, $app->make('Flarum\Http\Middleware\DispatchRoute', ['routes' => $app->make('flarum.admin.routes')]));
} else {
$app->register('Flarum\Update\UpdateServiceProvider');
$pipe->pipe($path, $app->make('Flarum\Http\Middleware\DispatchRoute', ['routes' => $app->make('flarum.update.routes')]));
}
}
return $pipe;
}
}

22
src/Admin/routes.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
use Flarum\Admin\Controller;
use Flarum\Http\RouteCollection;
use Flarum\Http\RouteHandlerFactory;
return function (RouteCollection $map, RouteHandlerFactory $route) {
$map->get(
'/',
'index',
$route->toController(Controller\FrontendController::class)
);
};

View File

@@ -37,10 +37,8 @@ class ApiKey extends AbstractModel
*/ */
public static function generate() public static function generate()
{ {
$key = new static; return new static([
'id' => str_random(40)
$key->id = str_random(40); ]);
return $key;
} }
} }

View File

@@ -12,16 +12,29 @@
namespace Flarum\Api; namespace Flarum\Api;
use Flarum\Api\Controller\AbstractSerializeController; use Flarum\Api\Controller\AbstractSerializeController;
use Flarum\Api\Middleware\FakeHttpMethods;
use Flarum\Api\Middleware\HandleErrors;
use Flarum\Api\Serializer\AbstractSerializer; use Flarum\Api\Serializer\AbstractSerializer;
use Flarum\Api\Serializer\BasicDiscussionSerializer;
use Flarum\Api\Serializer\NotificationSerializer; use Flarum\Api\Serializer\NotificationSerializer;
use Flarum\Event\ConfigureApiRoutes; use Flarum\Event\ConfigureApiRoutes;
use Flarum\Event\ConfigureMiddleware;
use Flarum\Event\ConfigureNotificationTypes; use Flarum\Event\ConfigureNotificationTypes;
use Flarum\Foundation\AbstractServiceProvider; use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Http\Handler\RouteHandlerFactory; use Flarum\Http\Middleware\AuthenticateWithHeader;
use Flarum\Http\Middleware\AuthenticateWithSession;
use Flarum\Http\Middleware\DispatchRoute;
use Flarum\Http\Middleware\ParseJsonBody;
use Flarum\Http\Middleware\RememberFromCookie;
use Flarum\Http\Middleware\SetLocale;
use Flarum\Http\Middleware\StartSession;
use Flarum\Http\RouteCollection; use Flarum\Http\RouteCollection;
use Flarum\Http\RouteHandlerFactory;
use Flarum\Http\UrlGenerator;
use Tobscure\JsonApi\ErrorHandler; use Tobscure\JsonApi\ErrorHandler;
use Tobscure\JsonApi\Exception\Handler\FallbackExceptionHandler; use Tobscure\JsonApi\Exception\Handler\FallbackExceptionHandler;
use Tobscure\JsonApi\Exception\Handler\InvalidParameterExceptionHandler; use Tobscure\JsonApi\Exception\Handler\InvalidParameterExceptionHandler;
use Zend\Stratigility\MiddlewarePipe;
class ApiServiceProvider extends AbstractServiceProvider class ApiServiceProvider extends AbstractServiceProvider
{ {
@@ -30,27 +43,48 @@ class ApiServiceProvider extends AbstractServiceProvider
*/ */
public function register() public function register()
{ {
$this->app->singleton(UrlGenerator::class, function () { $this->app->extend(UrlGenerator::class, function (UrlGenerator $url) {
return new UrlGenerator($this->app, $this->app->make('flarum.api.routes')); return $url->addCollection('api', $this->app->make('flarum.api.routes'), 'api');
}); });
$this->app->singleton('flarum.api.routes', function () { $this->app->singleton('flarum.api.routes', function () {
return new RouteCollection; return new RouteCollection;
}); });
$this->app->singleton('flarum.api.middleware', function ($app) {
$pipe = new MiddlewarePipe;
$pipe->raiseThrowables();
$pipe->pipe($app->make(HandleErrors::class));
$pipe->pipe($app->make(ParseJsonBody::class));
$pipe->pipe($app->make(FakeHttpMethods::class));
$pipe->pipe($app->make(StartSession::class));
$pipe->pipe($app->make(RememberFromCookie::class));
$pipe->pipe($app->make(AuthenticateWithSession::class));
$pipe->pipe($app->make(AuthenticateWithHeader::class));
$pipe->pipe($app->make(SetLocale::class));
event(new ConfigureMiddleware($pipe, 'api'));
$pipe->pipe($app->make(DispatchRoute::class, ['routes' => $app->make('flarum.api.routes')]));
return $pipe;
});
$this->app->singleton(ErrorHandler::class, function () { $this->app->singleton(ErrorHandler::class, function () {
$handler = new ErrorHandler; $handler = new ErrorHandler;
$handler->registerHandler(new Handler\FloodingExceptionHandler); $handler->registerHandler(new ExceptionHandler\FloodingExceptionHandler);
$handler->registerHandler(new Handler\IlluminateValidationExceptionHandler); $handler->registerHandler(new ExceptionHandler\IlluminateValidationExceptionHandler);
$handler->registerHandler(new Handler\InvalidAccessTokenExceptionHandler); $handler->registerHandler(new ExceptionHandler\InvalidAccessTokenExceptionHandler);
$handler->registerHandler(new Handler\InvalidConfirmationTokenExceptionHandler); $handler->registerHandler(new ExceptionHandler\InvalidConfirmationTokenExceptionHandler);
$handler->registerHandler(new Handler\MethodNotAllowedExceptionHandler); $handler->registerHandler(new ExceptionHandler\MethodNotAllowedExceptionHandler);
$handler->registerHandler(new Handler\ModelNotFoundExceptionHandler); $handler->registerHandler(new ExceptionHandler\ModelNotFoundExceptionHandler);
$handler->registerHandler(new Handler\PermissionDeniedExceptionHandler); $handler->registerHandler(new ExceptionHandler\PermissionDeniedExceptionHandler);
$handler->registerHandler(new Handler\RouteNotFoundExceptionHandler); $handler->registerHandler(new ExceptionHandler\RouteNotFoundExceptionHandler);
$handler->registerHandler(new Handler\TokenMismatchExceptionHandler); $handler->registerHandler(new ExceptionHandler\TokenMismatchExceptionHandler);
$handler->registerHandler(new Handler\ValidationExceptionHandler); $handler->registerHandler(new ExceptionHandler\ValidationExceptionHandler);
$handler->registerHandler(new InvalidParameterExceptionHandler); $handler->registerHandler(new InvalidParameterExceptionHandler);
$handler->registerHandler(new FallbackExceptionHandler($this->app->inDebugMode())); $handler->registerHandler(new FallbackExceptionHandler($this->app->inDebugMode()));
@@ -81,7 +115,7 @@ class ApiServiceProvider extends AbstractServiceProvider
{ {
$blueprints = []; $blueprints = [];
$serializers = [ $serializers = [
'discussionRenamed' => 'Flarum\Api\Serializer\DiscussionBasicSerializer' 'discussionRenamed' => BasicDiscussionSerializer::class
]; ];
$this->app->make('events')->fire( $this->app->make('events')->fire(
@@ -100,298 +134,13 @@ class ApiServiceProvider extends AbstractServiceProvider
*/ */
protected function populateRoutes(RouteCollection $routes) protected function populateRoutes(RouteCollection $routes)
{ {
$route = $this->app->make(RouteHandlerFactory::class); $factory = $this->app->make(RouteHandlerFactory::class);
// Get forum information $callback = include __DIR__.'/routes.php';
$routes->get( $callback($routes, $factory);
'/forum',
'forum.show',
$route->toController(Controller\ShowForumController::class)
);
// Retrieve authentication token
$routes->post(
'/token',
'token',
$route->toController(Controller\TokenController::class)
);
// Send forgot password email
$routes->post(
'/forgot',
'forgot',
$route->toController(Controller\ForgotPasswordController::class)
);
/*
|--------------------------------------------------------------------------
| Users
|--------------------------------------------------------------------------
*/
// List users
$routes->get(
'/users',
'users.index',
$route->toController(Controller\ListUsersController::class)
);
// Register a user
$routes->post(
'/users',
'users.create',
$route->toController(Controller\CreateUserController::class)
);
// Get a single user
$routes->get(
'/users/{id}',
'users.show',
$route->toController(Controller\ShowUserController::class)
);
// Edit a user
$routes->patch(
'/users/{id}',
'users.update',
$route->toController(Controller\UpdateUserController::class)
);
// Delete a user
$routes->delete(
'/users/{id}',
'users.delete',
$route->toController(Controller\DeleteUserController::class)
);
// Upload avatar
$routes->post(
'/users/{id}/avatar',
'users.avatar.upload',
$route->toController(Controller\UploadAvatarController::class)
);
// Remove avatar
$routes->delete(
'/users/{id}/avatar',
'users.avatar.delete',
$route->toController(Controller\DeleteAvatarController::class)
);
// send confirmation email
$routes->post(
'/users/{id}/send-confirmation',
'users.confirmation.send',
$route->toController(Controller\SendConfirmationEmailController::class)
);
/*
|--------------------------------------------------------------------------
| Notifications
|--------------------------------------------------------------------------
*/
// List notifications for the current user
$routes->get(
'/notifications',
'notifications.index',
$route->toController(Controller\ListNotificationsController::class)
);
// Mark all notifications as read
$routes->post(
'/notifications/read',
'notifications.readAll',
$route->toController(Controller\ReadAllNotificationsController::class)
);
// Mark a single notification as read
$routes->patch(
'/notifications/{id}',
'notifications.update',
$route->toController(Controller\UpdateNotificationController::class)
);
/*
|--------------------------------------------------------------------------
| Discussions
|--------------------------------------------------------------------------
*/
// List discussions
$routes->get(
'/discussions',
'discussions.index',
$route->toController(Controller\ListDiscussionsController::class)
);
// Create a discussion
$routes->post(
'/discussions',
'discussions.create',
$route->toController(Controller\CreateDiscussionController::class)
);
// Show a single discussion
$routes->get(
'/discussions/{id}',
'discussions.show',
$route->toController(Controller\ShowDiscussionController::class)
);
// Edit a discussion
$routes->patch(
'/discussions/{id}',
'discussions.update',
$route->toController(Controller\UpdateDiscussionController::class)
);
// Delete a discussion
$routes->delete(
'/discussions/{id}',
'discussions.delete',
$route->toController(Controller\DeleteDiscussionController::class)
);
/*
|--------------------------------------------------------------------------
| Posts
|--------------------------------------------------------------------------
*/
// List posts, usually for a discussion
$routes->get(
'/posts',
'posts.index',
$route->toController(Controller\ListPostsController::class)
);
// Create a post
$routes->post(
'/posts',
'posts.create',
$route->toController(Controller\CreatePostController::class)
);
// Show a single or multiple posts by ID
$routes->get(
'/posts/{id}',
'posts.show',
$route->toController(Controller\ShowPostController::class)
);
// Edit a post
$routes->patch(
'/posts/{id}',
'posts.update',
$route->toController(Controller\UpdatePostController::class)
);
// Delete a post
$routes->delete(
'/posts/{id}',
'posts.delete',
$route->toController(Controller\DeletePostController::class)
);
/*
|--------------------------------------------------------------------------
| Groups
|--------------------------------------------------------------------------
*/
// List groups
$routes->get(
'/groups',
'groups.index',
$route->toController(Controller\ListGroupsController::class)
);
// Create a group
$routes->post(
'/groups',
'groups.create',
$route->toController(Controller\CreateGroupController::class)
);
// Edit a group
$routes->patch(
'/groups/{id}',
'groups.update',
$route->toController(Controller\UpdateGroupController::class)
);
// Delete a group
$routes->delete(
'/groups/{id}',
'groups.delete',
$route->toController(Controller\DeleteGroupController::class)
);
/*
|--------------------------------------------------------------------------
| Administration
|--------------------------------------------------------------------------
*/
// Toggle an extension
$routes->patch(
'/extensions/{name}',
'extensions.update',
$route->toController(Controller\UpdateExtensionController::class)
);
// Uninstall an extension
$routes->delete(
'/extensions/{name}',
'extensions.delete',
$route->toController(Controller\UninstallExtensionController::class)
);
// Update settings
$routes->post(
'/settings',
'settings',
$route->toController(Controller\SetSettingsController::class)
);
// Update a permission
$routes->post(
'/permission',
'permission',
$route->toController(Controller\SetPermissionController::class)
);
// Upload a logo
$routes->post(
'/logo',
'logo',
$route->toController(Controller\UploadLogoController::class)
);
// Remove the logo
$routes->delete(
'/logo',
'logo.delete',
$route->toController(Controller\DeleteLogoController::class)
);
// Upload a favicon
$routes->post(
'/favicon',
'favicon',
$route->toController(Controller\UploadFaviconController::class)
);
// Remove the favicon
$routes->delete(
'/favicon',
'favicon.delete',
$route->toController(Controller\DeleteFaviconController::class)
);
$this->app->make('events')->fire( $this->app->make('events')->fire(
new ConfigureApiRoutes($routes, $route) new ConfigureApiRoutes($routes, $factory)
); );
} }
} }

View File

@@ -12,9 +12,9 @@
namespace Flarum\Api; namespace Flarum\Api;
use Exception; use Exception;
use Flarum\Core\User;
use Flarum\Foundation\Application; use Flarum\Foundation\Application;
use Flarum\Http\Controller\ControllerInterface; use Flarum\Http\Controller\ControllerInterface;
use Flarum\User\User;
use InvalidArgumentException; use InvalidArgumentException;
use Zend\Diactoros\ServerRequestFactory; use Zend\Diactoros\ServerRequestFactory;

View File

@@ -13,7 +13,7 @@ namespace Flarum\Api\Controller;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
abstract class AbstractCreateController extends AbstractResourceController abstract class AbstractCreateController extends AbstractShowController
{ {
/** /**
* {@inheritdoc} * {@inheritdoc}

View File

@@ -14,7 +14,7 @@ namespace Flarum\Api\Controller;
use Tobscure\JsonApi\Collection; use Tobscure\JsonApi\Collection;
use Tobscure\JsonApi\SerializerInterface; use Tobscure\JsonApi\SerializerInterface;
abstract class AbstractCollectionController extends AbstractSerializeController abstract class AbstractListController extends AbstractSerializeController
{ {
/** /**
* {@inheritdoc} * {@inheritdoc}

View File

@@ -11,9 +11,9 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Api\Event\WillGetData;
use Flarum\Api\Event\WillSerializeData;
use Flarum\Api\JsonApiResponse; use Flarum\Api\JsonApiResponse;
use Flarum\Event\ConfigureApiController;
use Flarum\Event\PrepareApiData;
use Flarum\Http\Controller\ControllerInterface; use Flarum\Http\Controller\ControllerInterface;
use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Events\Dispatcher;
@@ -91,13 +91,13 @@ abstract class AbstractSerializeController implements ControllerInterface
$document = new Document; $document = new Document;
static::$events->fire( static::$events->fire(
new ConfigureApiController($this) new WillGetData($this)
); );
$data = $this->data($request, $document); $data = $this->data($request, $document);
static::$events->fire( static::$events->fire(
new PrepareApiData($this, $data, $request, $document) new WillSerializeData($this, $data, $request, $document)
); );
$serializer = static::$container->make($this->serializer); $serializer = static::$container->make($this->serializer);

View File

@@ -14,7 +14,7 @@ namespace Flarum\Api\Controller;
use Tobscure\JsonApi\Resource; use Tobscure\JsonApi\Resource;
use Tobscure\JsonApi\SerializerInterface; use Tobscure\JsonApi\SerializerInterface;
abstract class AbstractResourceController extends AbstractSerializeController abstract class AbstractShowController extends AbstractSerializeController
{ {
/** /**
* {@inheritdoc} * {@inheritdoc}

View File

@@ -11,9 +11,10 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Command\ReadDiscussion; use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Core\Command\StartDiscussion; use Flarum\Discussion\Command\ReadDiscussion;
use Flarum\Core\Post\Floodgate; use Flarum\Discussion\Command\StartDiscussion;
use Flarum\Post\Floodgate;
use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document; use Tobscure\JsonApi\Document;
@@ -23,7 +24,7 @@ class CreateDiscussionController extends AbstractCreateController
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public $serializer = 'Flarum\Api\Serializer\DiscussionSerializer'; public $serializer = DiscussionSerializer::class;
/** /**
* {@inheritdoc} * {@inheritdoc}

View File

@@ -11,7 +11,8 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Command\CreateGroup; use Flarum\Api\Serializer\GroupSerializer;
use Flarum\Group\Command\CreateGroup;
use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document; use Tobscure\JsonApi\Document;
@@ -21,7 +22,7 @@ class CreateGroupController extends AbstractCreateController
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public $serializer = 'Flarum\Api\Serializer\GroupSerializer'; public $serializer = GroupSerializer::class;
/** /**
* @var Dispatcher * @var Dispatcher

View File

@@ -11,9 +11,10 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Command\PostReply; use Flarum\Api\Serializer\PostSerializer;
use Flarum\Core\Command\ReadDiscussion; use Flarum\Discussion\Command\ReadDiscussion;
use Flarum\Core\Post\Floodgate; use Flarum\Post\Command\PostReply;
use Flarum\Post\Floodgate;
use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document; use Tobscure\JsonApi\Document;
@@ -23,7 +24,7 @@ class CreatePostController extends AbstractCreateController
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public $serializer = 'Flarum\Api\Serializer\PostSerializer'; public $serializer = PostSerializer::class;
/** /**
* {@inheritdoc} * {@inheritdoc}
@@ -41,13 +42,13 @@ class CreatePostController extends AbstractCreateController
protected $bus; protected $bus;
/** /**
* @var Floodgate * @var \Flarum\Post\Floodgate
*/ */
protected $floodgate; protected $floodgate;
/** /**
* @param Dispatcher $bus * @param Dispatcher $bus
* @param Floodgate $floodgate * @param \Flarum\Post\Floodgate $floodgate
*/ */
public function __construct(Dispatcher $bus, Floodgate $floodgate) public function __construct(Dispatcher $bus, Floodgate $floodgate)
{ {
@@ -83,7 +84,7 @@ class CreatePostController extends AbstractCreateController
} }
$discussion = $post->discussion; $discussion = $post->discussion;
$discussion->posts = $discussion->postsVisibleTo($actor)->orderBy('time')->lists('id'); $discussion->posts = $discussion->postsVisibleTo($actor)->orderBy('time')->pluck('id');
return $post; return $post;
} }

View File

@@ -11,7 +11,8 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Command\RegisterUser; use Flarum\Api\Serializer\CurrentUserSerializer;
use Flarum\User\Command\RegisterUser;
use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document; use Tobscure\JsonApi\Document;
@@ -21,7 +22,7 @@ class CreateUserController extends AbstractCreateController
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public $serializer = 'Flarum\Api\Serializer\CurrentUserSerializer'; public $serializer = CurrentUserSerializer::class;
/** /**
* @var Dispatcher * @var Dispatcher

View File

@@ -11,17 +11,18 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Command\DeleteAvatar; use Flarum\Api\Serializer\UserSerializer;
use Flarum\User\Command\DeleteAvatar;
use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document; use Tobscure\JsonApi\Document;
class DeleteAvatarController extends AbstractResourceController class DeleteAvatarController extends AbstractShowController
{ {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public $serializer = 'Flarum\Api\Serializer\UserSerializer'; public $serializer = UserSerializer::class;
/** /**
* @var Dispatcher * @var Dispatcher

View File

@@ -11,7 +11,7 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Command\DeleteDiscussion; use Flarum\Discussion\Command\DeleteDiscussion;
use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;

View File

@@ -11,9 +11,9 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Access\AssertPermissionTrait;
use Flarum\Foundation\Application; use Flarum\Foundation\Application;
use Flarum\Settings\SettingsRepositoryInterface; use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\AssertPermissionTrait;
use League\Flysystem\Adapter\Local; use League\Flysystem\Adapter\Local;
use League\Flysystem\Filesystem; use League\Flysystem\Filesystem;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;

View File

@@ -11,7 +11,7 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Command\DeleteGroup; use Flarum\Group\Command\DeleteGroup;
use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;

View File

@@ -11,9 +11,9 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Access\AssertPermissionTrait;
use Flarum\Foundation\Application; use Flarum\Foundation\Application;
use Flarum\Settings\SettingsRepositoryInterface; use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\AssertPermissionTrait;
use League\Flysystem\Adapter\Local; use League\Flysystem\Adapter\Local;
use League\Flysystem\Filesystem; use League\Flysystem\Filesystem;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;

View File

@@ -11,7 +11,7 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Command\DeletePost; use Flarum\Post\Command\DeletePost;
use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;

View File

@@ -11,7 +11,7 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Command\DeleteUser; use Flarum\User\Command\DeleteUser;
use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;

View File

@@ -11,9 +11,9 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Command\RequestPasswordReset;
use Flarum\Core\Repository\UserRepository;
use Flarum\Http\Controller\ControllerInterface; use Flarum\Http\Controller\ControllerInterface;
use Flarum\User\Command\RequestPasswordReset;
use Flarum\User\UserRepository;
use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\EmptyResponse; use Zend\Diactoros\Response\EmptyResponse;
@@ -21,7 +21,7 @@ use Zend\Diactoros\Response\EmptyResponse;
class ForgotPasswordController implements ControllerInterface class ForgotPasswordController implements ControllerInterface
{ {
/** /**
* @var \Flarum\Core\Repository\UserRepository * @var \Flarum\User\UserRepository
*/ */
protected $users; protected $users;
@@ -31,7 +31,7 @@ class ForgotPasswordController implements ControllerInterface
protected $bus; protected $bus;
/** /**
* @param \Flarum\Core\Repository\UserRepository $users * @param \Flarum\User\UserRepository $users
* @param Dispatcher $bus * @param Dispatcher $bus
*/ */
public function __construct(UserRepository $users, Dispatcher $bus) public function __construct(UserRepository $users, Dispatcher $bus)

View File

@@ -11,18 +11,19 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Api\UrlGenerator; use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Core\Search\Discussion\DiscussionSearcher; use Flarum\Discussion\Search\DiscussionSearcher;
use Flarum\Core\Search\SearchCriteria; use Flarum\Http\UrlGenerator;
use Flarum\Search\SearchCriteria;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document; use Tobscure\JsonApi\Document;
class ListDiscussionsController extends AbstractCollectionController class ListDiscussionsController extends AbstractListController
{ {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public $serializer = 'Flarum\Api\Serializer\DiscussionSerializer'; public $serializer = DiscussionSerializer::class;
/** /**
* {@inheritdoc} * {@inheritdoc}
@@ -86,13 +87,25 @@ class ListDiscussionsController extends AbstractCollectionController
$results = $this->searcher->search($criteria, $limit, $offset, $load); $results = $this->searcher->search($criteria, $limit, $offset, $load);
$document->addPaginationLinks( $document->addPaginationLinks(
$this->url->toRoute('discussions.index'), $this->url->to('api')->route('discussions.index'),
$request->getQueryParams(), $request->getQueryParams(),
$offset, $offset,
$limit, $limit,
$results->areMoreResults() ? null : 0 $results->areMoreResults() ? null : 0
); );
return $results->getResults(); $results = $results->getResults();
if ($relations = array_intersect($load, ['startPost', 'lastPost'])) {
foreach ($results as $discussion) {
foreach ($relations as $relation) {
if ($discussion->$relation) {
$discussion->$relation->discussion = $discussion;
}
}
}
}
return $results;
} }
} }

View File

@@ -11,16 +11,17 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Group; use Flarum\Api\Serializer\GroupSerializer;
use Flarum\Group\Group;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document; use Tobscure\JsonApi\Document;
class ListGroupsController extends AbstractCollectionController class ListGroupsController extends AbstractListController
{ {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public $serializer = 'Flarum\Api\Serializer\GroupSerializer'; public $serializer = GroupSerializer::class;
/** /**
* {@inheritdoc} * {@inheritdoc}

View File

@@ -11,18 +11,20 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Discussion; use Flarum\Api\Serializer\NotificationSerializer;
use Flarum\Core\Exception\PermissionDeniedException; use Flarum\Discussion\Discussion;
use Flarum\Core\Repository\NotificationRepository; use Flarum\Http\UrlGenerator;
use Flarum\Notification\NotificationRepository;
use Flarum\User\Exception\PermissionDeniedException;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document; use Tobscure\JsonApi\Document;
class ListNotificationsController extends AbstractCollectionController class ListNotificationsController extends AbstractListController
{ {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public $serializer = 'Flarum\Api\Serializer\NotificationSerializer'; public $serializer = NotificationSerializer::class;
/** /**
* {@inheritdoc} * {@inheritdoc}
@@ -39,16 +41,23 @@ class ListNotificationsController extends AbstractCollectionController
public $limit = 10; public $limit = 10;
/** /**
* @var \Flarum\Core\Repository\NotificationRepository * @var NotificationRepository
*/ */
protected $notifications; protected $notifications;
/** /**
* @param \Flarum\Core\Repository\NotificationRepository $notifications * @var UrlGenerator
*/ */
public function __construct(NotificationRepository $notifications) protected $url;
/**
* @param NotificationRepository $notifications
* @param UrlGenerator $url
*/
public function __construct(NotificationRepository $notifications, UrlGenerator $url)
{ {
$this->notifications = $notifications; $this->notifications = $notifications;
$this->url = $url;
} }
/** /**
@@ -68,10 +77,33 @@ class ListNotificationsController extends AbstractCollectionController
$offset = $this->extractOffset($request); $offset = $this->extractOffset($request);
$include = $this->extractInclude($request); $include = $this->extractInclude($request);
$notifications = $this->notifications->findByUser($actor, $limit, $offset) if (! in_array('subject', $include)) {
$include[] = 'subject';
}
$notifications = $this->notifications->findByUser($actor, $limit + 1, $offset)
->load(array_diff($include, ['subject.discussion'])) ->load(array_diff($include, ['subject.discussion']))
->all(); ->all();
$areMoreResults = false;
if (count($notifications) > $limit) {
array_pop($notifications);
$areMoreResults = true;
}
$document->addPaginationLinks(
$this->url->to('api')->route('notifications.index'),
$request->getQueryParams(),
$offset,
$limit,
$areMoreResults ? null : 0
);
$notifications = array_filter($notifications, function ($notification) {
return ! $notification->subjectModel || $notification->subject;
});
if (in_array('subject.discussion', $include)) { if (in_array('subject.discussion', $include)) {
$this->loadSubjectDiscussions($notifications); $this->loadSubjectDiscussions($notifications);
} }
@@ -80,7 +112,7 @@ class ListNotificationsController extends AbstractCollectionController
} }
/** /**
* @param \Flarum\Core\Notification[] $notifications * @param \Flarum\Notification\Notification[] $notifications
*/ */
private function loadSubjectDiscussions(array $notifications) private function loadSubjectDiscussions(array $notifications)
{ {

View File

@@ -11,19 +11,20 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Repository\PostRepository; use Flarum\Api\Serializer\PostSerializer;
use Flarum\Event\ConfigurePostsQuery; use Flarum\Event\ConfigurePostsQuery;
use Flarum\Post\PostRepository;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document; use Tobscure\JsonApi\Document;
use Tobscure\JsonApi\Exception\InvalidParameterException; use Tobscure\JsonApi\Exception\InvalidParameterException;
class ListPostsController extends AbstractCollectionController class ListPostsController extends AbstractListController
{ {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public $serializer = 'Flarum\Api\Serializer\PostSerializer'; public $serializer = PostSerializer::class;
/** /**
* {@inheritdoc} * {@inheritdoc}
@@ -42,12 +43,12 @@ class ListPostsController extends AbstractCollectionController
public $sortFields = ['time']; public $sortFields = ['time'];
/** /**
* @var \Flarum\Core\Repository\PostRepository * @var \Flarum\Post\PostRepository
*/ */
protected $posts; protected $posts;
/** /**
* @param \Flarum\Core\Repository\PostRepository $posts * @param \Flarum\Post\PostRepository $posts
*/ */
public function __construct(PostRepository $posts) public function __construct(PostRepository $posts)
{ {
@@ -122,7 +123,7 @@ class ListPostsController extends AbstractCollectionController
$query->orderBy($field, $order); $query->orderBy($field, $order);
} }
return $query->lists('id')->all(); return $query->pluck('id')->all();
} }
/** /**

View File

@@ -11,19 +11,20 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Api\UrlGenerator; use Flarum\Api\Serializer\UserSerializer;
use Flarum\Core\Exception\PermissionDeniedException; use Flarum\Http\UrlGenerator;
use Flarum\Core\Search\SearchCriteria; use Flarum\Search\SearchCriteria;
use Flarum\Core\Search\User\UserSearcher; use Flarum\User\Exception\PermissionDeniedException;
use Flarum\User\Search\UserSearcher;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document; use Tobscure\JsonApi\Document;
class ListUsersController extends AbstractCollectionController class ListUsersController extends AbstractListController
{ {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public $serializer = 'Flarum\Api\Serializer\UserSerializer'; public $serializer = UserSerializer::class;
/** /**
* {@inheritdoc} * {@inheritdoc}
@@ -84,7 +85,7 @@ class ListUsersController extends AbstractCollectionController
$results = $this->searcher->search($criteria, $limit, $offset, $load); $results = $this->searcher->search($criteria, $limit, $offset, $load);
$document->addPaginationLinks( $document->addPaginationLinks(
$this->url->toRoute('users.index'), $this->url->to('api')->route('users.index'),
$request->getQueryParams(), $request->getQueryParams(),
$offset, $offset,
$limit, $limit,

View File

@@ -11,7 +11,7 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Command\ReadAllNotifications; use Flarum\Notification\Command\ReadAllNotifications;
use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Bus\Dispatcher;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;

View File

@@ -11,12 +11,12 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Access\AssertPermissionTrait;
use Flarum\Core\EmailToken;
use Flarum\Core\Exception\PermissionDeniedException;
use Flarum\Forum\UrlGenerator;
use Flarum\Http\Controller\ControllerInterface; use Flarum\Http\Controller\ControllerInterface;
use Flarum\Http\UrlGenerator;
use Flarum\Settings\SettingsRepositoryInterface; use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\AssertPermissionTrait;
use Flarum\User\EmailToken;
use Flarum\User\Exception\PermissionDeniedException;
use Illuminate\Contracts\Mail\Mailer; use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Mail\Message; use Illuminate\Mail\Message;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
@@ -80,7 +80,7 @@ class SendConfirmationEmailController implements ControllerInterface
$data = [ $data = [
'{username}' => $actor->username, '{username}' => $actor->username,
'{url}' => $this->url->toRoute('confirmEmail', ['token' => $token->id]), '{url}' => $this->url->to('forum')->route('confirmEmail', ['token' => $token->id]),
'{forum}' => $this->settings->get('forum_title') '{forum}' => $this->settings->get('forum_title')
]; ];

View File

@@ -11,9 +11,9 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Access\AssertPermissionTrait; use Flarum\Group\Permission;
use Flarum\Core\Permission;
use Flarum\Http\Controller\ControllerInterface; use Flarum\Http\Controller\ControllerInterface;
use Flarum\User\AssertPermissionTrait;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\EmptyResponse; use Zend\Diactoros\Response\EmptyResponse;

View File

@@ -11,11 +11,11 @@
namespace Flarum\Api\Controller; namespace Flarum\Api\Controller;
use Flarum\Core\Access\AssertPermissionTrait;
use Flarum\Event\PrepareSerializedSetting;
use Flarum\Event\SettingWasSet;
use Flarum\Http\Controller\ControllerInterface; use Flarum\Http\Controller\ControllerInterface;
use Flarum\Settings\Event\Saved;
use Flarum\Settings\Event\Serializing;
use Flarum\Settings\SettingsRepositoryInterface; use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\AssertPermissionTrait;
use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Events\Dispatcher;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\EmptyResponse; use Zend\Diactoros\Response\EmptyResponse;
@@ -53,11 +53,11 @@ class SetSettingsController implements ControllerInterface
$settings = $request->getParsedBody(); $settings = $request->getParsedBody();
foreach ($settings as $k => $v) { foreach ($settings as $k => $v) {
$this->dispatcher->fire(new PrepareSerializedSetting($k, $v)); $this->dispatcher->fire(new Serializing($k, $v));
$this->settings->set($k, $v); $this->settings->set($k, $v);
$this->dispatcher->fire(new SettingWasSet($k, $v)); $this->dispatcher->fire(new Saved($k, $v));
} }
return new EmptyResponse(204); return new EmptyResponse(204);

Some files were not shown because too many files have changed in this diff Show More