mirror of
https://github.com/flarum/core.git
synced 2025-08-14 12:24:33 +02:00
Compare commits
127 Commits
v0.1.0-bet
...
v0.1.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
53f7112248 | ||
|
a2def83045 | ||
|
cbcad27679 | ||
|
9bf485359a | ||
|
60323e0cf9 | ||
|
8cccaaaf6b | ||
|
b7d8afe6a4 | ||
|
ff8ec59310 | ||
|
8eda6c7d36 | ||
|
d5b58b3146 | ||
|
f00d2b1363 | ||
|
190aa925ac | ||
|
60b19efe0a | ||
|
b2fa6b1a2e | ||
|
e7d7df3b0c | ||
|
3b5a01e603 | ||
|
b05f83d25a | ||
|
902d01712b | ||
|
502a3787d5 | ||
|
b8ac49ffcc | ||
|
4b4cea4d87 | ||
|
c0e7ff5ea1 | ||
|
6a5427b600 | ||
|
e8621636c5 | ||
|
1aaff46f8e | ||
|
8c4e095f23 | ||
|
05c44ad2df | ||
|
84012ca2fd | ||
|
6393432d92 | ||
|
f6e21b75e1 | ||
|
6ee9412f35 | ||
|
478ca90c31 | ||
|
1f8f79d272 | ||
|
85fc0a3129 | ||
|
db8b9ed0c0 | ||
|
a3d59977b3 | ||
|
211d2d25cd | ||
|
0a992ee9f2 | ||
|
42f1abacaf | ||
|
b26c67dd3c | ||
|
fc7fc41383 | ||
|
a5d3aa9b36 | ||
|
b18909f1af | ||
|
695df18be0 | ||
|
ece23de750 | ||
|
4705600d47 | ||
|
8423de754c | ||
|
b597e6f8f6 | ||
|
276334ec52 | ||
|
9277fca0ec | ||
|
9ca67635fb | ||
|
7a6c48c30b | ||
|
f0186d7674 | ||
|
9bf6862c6d | ||
|
44f460cb11 | ||
|
7cce5b02ba | ||
|
722058f2fb | ||
|
70815b024a | ||
|
7269385786 | ||
|
2f8a449b74 | ||
|
b3aa0298d5 | ||
|
e192402a42 | ||
|
c81ceafb54 | ||
|
93b6f11484 | ||
|
0413daab74 | ||
|
f0c240f863 | ||
|
21dd516eaa | ||
|
3c9d851889 | ||
|
942db77416 | ||
|
04db806995 | ||
|
f3bc7d1c23 | ||
|
bd47653377 | ||
|
07ed4d10c0 | ||
|
25141c0f2f | ||
|
e35bb9e400 | ||
|
753a846e7a | ||
|
d3e57d77b4 | ||
|
6e0bffe395 | ||
|
eec4e97d65 | ||
|
bf83b36882 | ||
|
6aafe54ee7 | ||
|
c91f8de1f5 | ||
|
5783dbe77b | ||
|
ab496eb8f8 | ||
|
6f13a246db | ||
|
4c34d0867d | ||
|
f2a3a0cb10 | ||
|
5b7527144c | ||
|
6c169499b5 | ||
|
5e22458014 | ||
|
c72bdc8238 | ||
|
2438bbfd41 | ||
|
5af5f1fc77 | ||
|
e7f4e5060c | ||
|
bcc16a3329 | ||
|
283abb88c2 | ||
|
af2307868a | ||
|
f9d724738c | ||
|
42e722d824 | ||
|
f5517fbd88 | ||
|
6a0e3fcf2d | ||
|
0ae2d18f28 | ||
|
0474f410a4 | ||
|
9f28b4e8dc | ||
|
f44e9f5140 | ||
|
3e14ef0714 | ||
|
c999226449 | ||
|
ba097dc147 | ||
|
1d1cc9e443 | ||
|
f5d2d2ff79 | ||
|
a04acca92e | ||
|
4033319ed0 | ||
|
a4fe6f3ce3 | ||
|
ae06b45bc1 | ||
|
be33761950 | ||
|
015aaaa899 | ||
|
67f6b8599d | ||
|
12d5e48b95 | ||
|
a41e3e66ce | ||
|
874c023f8a | ||
|
bb3c57f9a4 | ||
|
98a79e957d | ||
|
cf68c95fb8 | ||
|
d5074c5286 | ||
|
41019597d0 | ||
|
b689c9de3b | ||
|
baed659668 |
@@ -12,21 +12,8 @@ insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.js]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{css,less}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.html]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{diff,md}]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.php]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
@@ -27,7 +27,12 @@
|
||||
"$": true,
|
||||
"moment": true
|
||||
},
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-uses-vars": 1,
|
||||
|
||||
/**
|
||||
* Strict mode
|
||||
*/
|
||||
|
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1,4 +1,5 @@
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
stubs/extension/.gitignore -export-ignore
|
||||
.gitmodules export-ignore
|
||||
.travis.yml export-ignore
|
||||
|
@@ -14,8 +14,8 @@ before_script:
|
||||
- php composer.phar install
|
||||
|
||||
script:
|
||||
- vendor/bin/phpcs --standard=PSR2 -np src
|
||||
- vendor/bin/phpspec run
|
||||
- php composer.phar style
|
||||
- php composer.phar test
|
||||
|
||||
notifications:
|
||||
email:
|
||||
|
44
CHANGELOG.md
Normal file
44
CHANGELOG.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Change Log
|
||||
All notable changes to Flarum and its bundled extensions will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [Unreleased][unreleased]
|
||||
*nothing yet*
|
||||
|
||||
## [0.1.0-beta.2] - 2015-09-15
|
||||
### Added
|
||||
- Check prerequisites (PHP version, extensions, etc.) before installation (#364)
|
||||
- Enforce maximum title and post length through validation (#53, #338)
|
||||
- Ctrl+Enter submits posts (#276)
|
||||
- Syntax highlighting for code blocks (#248)
|
||||
- All links open in new window, receive rel=nofollow attribute (#247)
|
||||
- Default build script for extensions (#438)
|
||||
- Input validation in installer
|
||||
|
||||
### Changed
|
||||
- Ask for admin password confirmation in installer (#405)
|
||||
- Increased some text contrasts for accessibility (#390)
|
||||
|
||||
### Fixed
|
||||
- Discussion list did not work with non-empty database prefix (#269, #380)
|
||||
- Non-admins could not reset their password (#229)
|
||||
- Requests ending with a slash resulted in a 404 (#334)
|
||||
- In rare cases, posts did not load correctly (#295)
|
||||
- Avatars did not show up when installed in a subfolder (#291)
|
||||
- Installer crashed when views directory was not writable (#376)
|
||||
- Table prefix could not be set in web installer (#269)
|
||||
- Enabling an extension disabled all other extensions (#402)
|
||||
- Invalid custom CSS could crash the application (#400)
|
||||
- First posts could not be restored or deleted
|
||||
- Several design bugs
|
||||
- Set cookies to be HTTP-only
|
||||
- Tags: Sometimes, tags could not be dragged for reordering in the admin panel (#341)
|
||||
- Suspend: Use correct column name in when migrating database
|
||||
- Lock: Check for correct permission when displaying lock control
|
||||
- Likes: Allow liking permissions to be configured
|
||||
|
||||
## 0.1.0-beta - 2015-08-27
|
||||
First Version
|
||||
|
||||
[unreleased]: https://github.com/flarum/core/compare/v0.1.0-beta.2...HEAD
|
||||
[0.1.0-beta.2]: https://github.com/flarum/core/compare/v0.1.0-beta...v0.1.0-beta.2
|
13
CONTRIBUTING.md
Normal file
13
CONTRIBUTING.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Contributing to Flarum
|
||||
|
||||
Thanks for your interest in contributing to Flarum! Please read the [Contributing docs](http://flarum.org/docs/contributing) to learn how you can help.
|
||||
|
||||
## Contributor License Agreement
|
||||
|
||||
By contributing your code to Flarum you grant Toby Zerner a non-exclusive, irrevocable, worldwide, royalty-free, sublicenseable, transferable license under all of Your relevant intellectual property rights (including copyright, patent, and any other rights), to use, copy, prepare derivative works of, distribute and publicly perform and display the Contributions on any licensing terms, including without limitation: (a) open source licenses like the MIT license; and (b) binary, proprietary, or commercial licenses. Except for the licenses granted herein, You reserve all right, title, and interest in and to the Contribution.
|
||||
|
||||
You confirm that you are able to grant us these rights. You represent that You are legally entitled to grant the above license. If Your employer has rights to intellectual property that You create, You represent that You have received permission to make the Contributions on behalf of that employer, or that Your employer has waived such rights for the Contributions.
|
||||
|
||||
You represent that the Contributions are Your original works of authorship, and to Your knowledge, no other person claims, or has the right to claim, any right in any invention or patent related to the Contributions. You also represent that You are not legally obligated, whether by entering into an agreement or otherwise, in any way that conflicts with the terms of this license.
|
||||
|
||||
Toby Zerner acknowledges that, except as explicitly described in this Agreement, any Contribution which you provide is on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
|
@@ -26,13 +26,14 @@
|
||||
"tobscure/json-api": "^0.1.1",
|
||||
"oyejorge/less.php": "~1.5",
|
||||
"intervention/image": "^2.3.0",
|
||||
"s9e/text-formatter": "^0.1.0",
|
||||
"s9e/text-formatter": "^0.3.2",
|
||||
"psr/http-message": "^1.0",
|
||||
"zendframework/zend-diactoros": "^1.1",
|
||||
"nikic/fast-route": "^0.6",
|
||||
"dflydev/fig-cookies": "^1.0",
|
||||
"symfony/console": "^2.7",
|
||||
"symfony/yaml": "^2.7"
|
||||
"symfony/yaml": "^2.7",
|
||||
"doctrine/dbal": "^2.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"squizlabs/php_codesniffer": "2.*",
|
||||
@@ -45,5 +46,9 @@
|
||||
"files": [
|
||||
"src/helpers.php"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"test": "phpspec run",
|
||||
"style": "phpcs --standard=PSR2 -np src"
|
||||
}
|
||||
}
|
||||
|
852
composer.lock
generated
852
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,66 +0,0 @@
|
||||
import Component from 'flarum/Component';
|
||||
import humanTime from 'flarum/helpers/humanTime';
|
||||
import avatar from 'flarum/helpers/avatar';
|
||||
|
||||
/**
|
||||
* The `Activity` component represents a piece of activity of a user's activity
|
||||
* feed. Subclasses should implement the `description` and `content` methods.
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - `activity`
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
export default class Activity extends Component {
|
||||
view() {
|
||||
const activity = this.props.activity;
|
||||
|
||||
return (
|
||||
<div className="Activity">
|
||||
{avatar(this.user(), {className: 'Activity-avatar'})}
|
||||
|
||||
<div className="Activity-header">
|
||||
<strong className="Activity-description">{this.description()}</strong>
|
||||
{humanTime(this.time())}
|
||||
</div>
|
||||
|
||||
{this.content()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user whose avatar should be displayed.
|
||||
*
|
||||
* @return {User}
|
||||
* @abstract
|
||||
*/
|
||||
user() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time of the activity.
|
||||
*
|
||||
* @return {Date}
|
||||
* @abstract
|
||||
*/
|
||||
time() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the description of the activity.
|
||||
*
|
||||
* @return {VirtualElement}
|
||||
*/
|
||||
description() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content to show below the activity description.
|
||||
*
|
||||
* @return {VirtualElement}
|
||||
*/
|
||||
content() {
|
||||
}
|
||||
}
|
@@ -39,7 +39,7 @@ export default class ChangeEmailModal extends Modal {
|
||||
return (
|
||||
<div className="Modal-body">
|
||||
<div className="Form Form--centered">
|
||||
<p className="helpText">{m.trust(app.trans('core.confirmation_email_sent', {email: <strong>{this.email()}</strong>}))}</p>
|
||||
<p className="helpText">{app.trans('core.confirmation_email_sent', {email: <strong>{this.email()}</strong>})}</p>
|
||||
<div className="Form-group">
|
||||
<a href={'http://' + emailProviderName} className="Button Button--primary Button--block">
|
||||
{app.trans('core.go_to', {location: emailProviderName})}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/*global s9e*/
|
||||
/*global s9e, hljs*/
|
||||
|
||||
import Post from 'flarum/components/Post';
|
||||
import classList from 'flarum/utils/classList';
|
||||
@@ -54,6 +54,49 @@ export default class CommentPost extends Post {
|
||||
];
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
super.config(...arguments);
|
||||
|
||||
const contentHtml = this.isEditing() ? '' : this.props.post.contentHtml();
|
||||
|
||||
if (context.contentHtml !== contentHtml) {
|
||||
if (typeof hljs === 'undefined') {
|
||||
this.loadHljs();
|
||||
} else {
|
||||
this.$('pre code').each(function(i, elm) {
|
||||
hljs.highlightBlock(elm);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
context.contentHtml = contentHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the highlight.js library and initialize highlighting when done.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
loadHljs() {
|
||||
const head = document.getElementsByTagName('head')[0];
|
||||
|
||||
const stylesheet = document.createElement('link');
|
||||
stylesheet.type = 'text/css';
|
||||
stylesheet.rel = 'stylesheet';
|
||||
stylesheet.href = '//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.7/styles/default.min.css';
|
||||
head.appendChild(stylesheet);
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.onload = () => {
|
||||
hljs._ = {};
|
||||
hljs.initHighlighting();
|
||||
};
|
||||
script.async = true;
|
||||
script.src = '//cdnjs.cloudflare.com/ajax/libs/highlight.js/8.7/highlight.min.js';
|
||||
head.appendChild(script);
|
||||
}
|
||||
|
||||
isEditing() {
|
||||
return app.composer.component instanceof EditPostComposer &&
|
||||
app.composer.component.props.post === this.props.post &&
|
||||
@@ -66,8 +109,8 @@ export default class CommentPost extends Post {
|
||||
return {
|
||||
className: classList({
|
||||
'CommentPost': true,
|
||||
'hidden': post.isHidden(),
|
||||
'edited': post.isEdited(),
|
||||
'Post--hidden': post.isHidden(),
|
||||
'Post--edited': post.isEdited(),
|
||||
'revealContent': this.revealContent,
|
||||
'editing': this.isEditing()
|
||||
})
|
||||
|
@@ -2,6 +2,7 @@ import Component from 'flarum/Component';
|
||||
import DiscussionListItem from 'flarum/components/DiscussionListItem';
|
||||
import Button from 'flarum/components/Button';
|
||||
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
||||
import Placeholder from 'flarum/components/Placeholder';
|
||||
|
||||
/**
|
||||
* The `DiscussionList` component displays a list of discussions.
|
||||
@@ -53,6 +54,15 @@ export default class DiscussionList extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.discussions.length === 0 && !this.loading) {
|
||||
const text = 'Looks like there are no discussions here. Why don\'t you create a new one?';
|
||||
return (
|
||||
<div className="DiscussionList">
|
||||
{Placeholder.component({text})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="DiscussionList">
|
||||
<ul className="DiscussionList-discussions">
|
||||
@@ -179,12 +189,7 @@ export default class DiscussionList extends Component {
|
||||
this.loading = false;
|
||||
this.moreResults = !!results.payload.links.next;
|
||||
|
||||
// Since this may be called during the component's constructor, i.e. in the
|
||||
// middle of a redraw, forcing another redraw would not bode well. Instead
|
||||
// we start/end a computation so Mithril will only redraw if it isn't
|
||||
// already doing so.
|
||||
m.startComputation();
|
||||
m.endComputation();
|
||||
m.lazyRedraw();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
@@ -51,6 +51,7 @@ export default class DiscussionListItem extends Component {
|
||||
const discussion = this.props.discussion;
|
||||
const startUser = discussion.startUser();
|
||||
const isUnread = discussion.isUnread();
|
||||
const isRead = discussion.isRead();
|
||||
const showUnread = !this.showRepliesCount() && isUnread;
|
||||
const jumpTo = Math.min(discussion.lastPostNumber(), (discussion.readNumber() || 0) + 1);
|
||||
const relevantPosts = this.props.params.q ? discussion.relevantPosts() : [];
|
||||
@@ -71,7 +72,7 @@ export default class DiscussionListItem extends Component {
|
||||
{icon('check')}
|
||||
</a>
|
||||
|
||||
<div className={'DiscussionListItem-content Slidable-content' + (isUnread ? ' unread' : '')}>
|
||||
<div className={'DiscussionListItem-content Slidable-content' + (isUnread ? ' unread' : '') + (isRead ? ' read' : '')}>
|
||||
<a href={startUser ? app.route.user(startUser) : '#'}
|
||||
className="DiscussionListItem-author"
|
||||
title={extractText(app.trans('core.discussion_started', {user: startUser, ago: humanTime(discussion.startTime())}))}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import Component from 'flarum/Component';
|
||||
import Page from 'flarum/components/Page';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import DiscussionHero from 'flarum/components/DiscussionHero';
|
||||
import PostStream from 'flarum/components/PostStream';
|
||||
@@ -6,15 +6,13 @@ import PostStreamScrubber from 'flarum/components/PostStreamScrubber';
|
||||
import LoadingIndicator from 'flarum/components/LoadingIndicator';
|
||||
import SplitDropdown from 'flarum/components/SplitDropdown';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
import mixin from 'flarum/utils/mixin';
|
||||
import evented from 'flarum/utils/evented';
|
||||
import DiscussionControls from 'flarum/utils/DiscussionControls';
|
||||
|
||||
/**
|
||||
* The `DiscussionPage` component displays a whole discussion page, including
|
||||
* the discussion list pane, the hero, the posts, and the sidebar.
|
||||
*/
|
||||
export default class DiscussionPage extends mixin(Component, evented) {
|
||||
export default class DiscussionPage extends Page {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
@@ -43,18 +41,14 @@ export default class DiscussionPage extends mixin(Component, evented) {
|
||||
app.pane.enable();
|
||||
app.pane.hide();
|
||||
|
||||
if (app.current instanceof DiscussionPage) {
|
||||
if (app.previous instanceof DiscussionPage) {
|
||||
m.redraw.strategy('diff');
|
||||
}
|
||||
}
|
||||
|
||||
// Push onto the history stack, but use a generalised key so that navigating
|
||||
// to a few different discussions won't override the behaviour of the back
|
||||
// button.
|
||||
app.history.push('discussion');
|
||||
app.current = this;
|
||||
app.drawer.hide();
|
||||
app.modal.close();
|
||||
|
||||
this.bodyClass = 'App--discussion';
|
||||
}
|
||||
|
||||
onunload(e) {
|
||||
@@ -67,9 +61,9 @@ export default class DiscussionPage extends mixin(Component, evented) {
|
||||
if (idParam && idParam.split('-')[0] === this.discussion.id()) {
|
||||
e.preventDefault();
|
||||
|
||||
const near = Number(m.route.param('near')) || 1;
|
||||
const near = m.route.param('near') || '1';
|
||||
|
||||
if (near !== Number(this.near)) {
|
||||
if (near !== String(this.near)) {
|
||||
this.stream.goToNumber(near);
|
||||
}
|
||||
|
||||
@@ -121,13 +115,6 @@ export default class DiscussionPage extends mixin(Component, evented) {
|
||||
);
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
if (isInitialized) return;
|
||||
|
||||
$('#app').addClass('App--discussion');
|
||||
context.onunload = () => $('#app').removeClass('App--discussion');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear and reload the discussion.
|
||||
*/
|
||||
@@ -141,20 +128,15 @@ export default class DiscussionPage extends mixin(Component, evented) {
|
||||
// component for the first time on page load, then any calls to m.redraw
|
||||
// will be ineffective and thus any configs (scroll code) will be run
|
||||
// before stuff is drawn to the page.
|
||||
setTimeout(this.init.bind(this, preloadedDiscussion));
|
||||
setTimeout(this.show.bind(this, preloadedDiscussion));
|
||||
} else {
|
||||
const params = this.requestParams();
|
||||
|
||||
app.store.find('discussions', m.route.param('id').split('-')[0], params)
|
||||
.then(this.init.bind(this));
|
||||
.then(this.show.bind(this));
|
||||
}
|
||||
|
||||
// Since this may be called during the component's constructor, i.e. in the
|
||||
// middle of a redraw, forcing another redraw would not bode well. Instead
|
||||
// we start/end a computation so Mithril will only redraw if it isn't
|
||||
// already doing so.
|
||||
m.startComputation();
|
||||
m.endComputation();
|
||||
m.lazyRedraw();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -174,7 +156,7 @@ export default class DiscussionPage extends mixin(Component, evented) {
|
||||
*
|
||||
* @param {Discussion} discussion
|
||||
*/
|
||||
init(discussion) {
|
||||
show(discussion) {
|
||||
this.discussion = discussion;
|
||||
|
||||
app.setTitle(discussion.title());
|
||||
@@ -202,8 +184,6 @@ export default class DiscussionPage extends mixin(Component, evented) {
|
||||
this.stream = new PostStream({discussion, includedPosts});
|
||||
this.stream.on('positionChanged', this.positionChanged.bind(this));
|
||||
this.stream.goToNumber(m.route.param('near') || includedPosts[0].number(), true);
|
||||
|
||||
this.trigger('loaded', discussion);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -23,8 +23,8 @@ export default class DiscussionRenamedPost extends EventPost {
|
||||
const newTitle = post.content()[1];
|
||||
|
||||
return {
|
||||
old: <strong className="DiscussionRenamedPost-old">{oldTitle}</strong>,
|
||||
new: <strong className="DiscussionRenamedPost-new">{newTitle}</strong>
|
||||
'old': <strong className="DiscussionRenamedPost-old">{oldTitle}</strong>,
|
||||
'new': <strong className="DiscussionRenamedPost-new">{newTitle}</strong>
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -30,7 +30,7 @@ export default class HeaderSecondary extends Component {
|
||||
items() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('search', app.search.render());
|
||||
items.add('search', app.search.render(), 30);
|
||||
|
||||
if (Object.keys(app.locales).length > 1) {
|
||||
const locales = [];
|
||||
@@ -54,12 +54,12 @@ export default class HeaderSecondary extends Component {
|
||||
items.add('locale', SelectDropdown.component({
|
||||
children: locales,
|
||||
buttonClassName: 'Button Button--link'
|
||||
}));
|
||||
}), 20);
|
||||
}
|
||||
|
||||
if (app.session.user) {
|
||||
items.add('notifications', NotificationsDropdown.component());
|
||||
items.add('session', SessionDropdown.component());
|
||||
items.add('notifications', NotificationsDropdown.component(), 10);
|
||||
items.add('session', SessionDropdown.component(), 0);
|
||||
} else {
|
||||
if (app.forum.attribute('allowSignUp')) {
|
||||
items.add('signUp',
|
||||
@@ -67,7 +67,7 @@ export default class HeaderSecondary extends Component {
|
||||
children: app.trans('core.sign_up'),
|
||||
className: 'Button Button--link',
|
||||
onclick: () => app.modal.show(new SignUpModal())
|
||||
})
|
||||
}), 10
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export default class HeaderSecondary extends Component {
|
||||
children: app.trans('core.log_in'),
|
||||
className: 'Button Button--link',
|
||||
onclick: () => app.modal.show(new LogInModal())
|
||||
})
|
||||
}), 0
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import Component from 'flarum/Component';
|
||||
import { extend } from 'flarum/extend';
|
||||
import Page from 'flarum/components/Page';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import affixSidebar from 'flarum/utils/affixSidebar';
|
||||
import listItems from 'flarum/helpers/listItems';
|
||||
import DiscussionList from 'flarum/components/DiscussionList';
|
||||
import WelcomeHero from 'flarum/components/WelcomeHero';
|
||||
@@ -16,22 +16,22 @@ import SelectDropdown from 'flarum/components/SelectDropdown';
|
||||
* The `IndexPage` component displays the index page, including the welcome
|
||||
* hero, the sidebar, and the discussion list.
|
||||
*/
|
||||
export default class IndexPage extends Component {
|
||||
export default class IndexPage extends Page {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
// If the user is returning from a discussion page, then take note of which
|
||||
// discussion they have just visited. After the view is rendered, we will
|
||||
// scroll down so that this discussion is in view.
|
||||
if (app.current instanceof DiscussionPage) {
|
||||
this.lastDiscussion = app.current.discussion;
|
||||
if (app.previous instanceof DiscussionPage) {
|
||||
this.lastDiscussion = app.previous.discussion;
|
||||
}
|
||||
|
||||
// If the user is coming from the discussion list, then they have either
|
||||
// just switched one of the parameters (filter, sort, search) or they
|
||||
// probably want to refresh the results. We will clear the discussion list
|
||||
// cache so that results are reloaded.
|
||||
if (app.current instanceof IndexPage) {
|
||||
if (app.previous instanceof IndexPage) {
|
||||
app.cache.discussionList = null;
|
||||
}
|
||||
|
||||
@@ -55,9 +55,8 @@ export default class IndexPage extends Component {
|
||||
}
|
||||
|
||||
app.history.push('index');
|
||||
app.current = this;
|
||||
app.drawer.hide();
|
||||
app.modal.close();
|
||||
|
||||
this.bodyClass = 'App--index';
|
||||
}
|
||||
|
||||
onunload() {
|
||||
@@ -71,7 +70,7 @@ export default class IndexPage extends Component {
|
||||
<div className="IndexPage">
|
||||
{this.hero()}
|
||||
<div className="container">
|
||||
<nav className="IndexPage-nav sideNav" config={affixSidebar}>
|
||||
<nav className="IndexPage-nav sideNav">
|
||||
<ul>{listItems(this.sidebarItems().toArray())}</ul>
|
||||
</nav>
|
||||
<div className="IndexPage-results sideNavOffset">
|
||||
@@ -87,13 +86,11 @@ export default class IndexPage extends Component {
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
super.config(...arguments);
|
||||
|
||||
if (isInitialized) return;
|
||||
|
||||
$('#app').addClass('App--index');
|
||||
context.onunload = () => {
|
||||
$('#app').removeClass('App--index')
|
||||
.css('min-height', '');
|
||||
};
|
||||
extend(context, 'onunload', () => $('#app').css('min-height', ''));
|
||||
|
||||
app.setTitle('');
|
||||
app.setTitleCount(0);
|
||||
|
@@ -19,18 +19,21 @@ export default class Notification extends Component {
|
||||
const href = this.href();
|
||||
|
||||
return (
|
||||
<div className={'Notification Notification--' + notification.contentType() + ' ' + (!notification.isRead() ? 'unread' : '')}
|
||||
onclick={this.markAsRead.bind(this)}>
|
||||
<a href={href} config={href.indexOf('://') === -1 ? m.route : undefined}>
|
||||
{avatar(notification.sender())}
|
||||
{icon(this.icon(), {className: 'Notification-icon'})}
|
||||
<span className="Notification-content">{this.content()}</span>
|
||||
{humanTime(notification.time())}
|
||||
<div className="Notification-excerpt">
|
||||
{this.excerpt()}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<a className={'Notification Notification--' + notification.contentType() + ' ' + (!notification.isRead() ? 'unread' : '')}
|
||||
href={href}
|
||||
config={function(element, isInitialized) {
|
||||
if (href.indexOf('://') === -1) m.route.apply(this, arguments);
|
||||
|
||||
if (!isInitialized) $(element).click(this.markAsRead.bind(this));
|
||||
}}>
|
||||
{avatar(notification.sender())}
|
||||
{icon(this.icon(), {className: 'Notification-icon'})}
|
||||
<span className="Notification-content">{this.content()}</span>
|
||||
{humanTime(notification.time())}
|
||||
<div className="Notification-excerpt">
|
||||
{this.excerpt()}
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -1,8 +1,18 @@
|
||||
import Component from 'flarum/Component';
|
||||
import Dropdown from 'flarum/components/Dropdown';
|
||||
import icon from 'flarum/helpers/icon';
|
||||
import NotificationList from 'flarum/components/NotificationList';
|
||||
|
||||
export default class NotificationsDropdown extends Component {
|
||||
export default class NotificationsDropdown extends Dropdown {
|
||||
static initProps(props) {
|
||||
props.className = props.className || 'NotificationsDropdown';
|
||||
props.buttonClassName = props.buttonClassName || 'Button Button--flat';
|
||||
props.menuClassName = props.menuClassName || 'Dropdown-menu--right';
|
||||
props.label = props.label || app.trans('core.notifications');
|
||||
props.icon = props.icon || 'bell';
|
||||
|
||||
super.initProps(props);
|
||||
}
|
||||
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
@@ -16,35 +26,51 @@ export default class NotificationsDropdown extends Component {
|
||||
this.list = new NotificationList();
|
||||
}
|
||||
|
||||
view() {
|
||||
const user = app.session.user;
|
||||
const unread = user.unreadNotificationsCount();
|
||||
getButton() {
|
||||
const unread = this.getUnreadCount();
|
||||
const vdom = super.getButton();
|
||||
|
||||
vdom.attrs.className += (unread ? ' unread' : '');
|
||||
vdom.attrs.onclick = this.onclick.bind(this);
|
||||
|
||||
return vdom;
|
||||
}
|
||||
|
||||
getButtonContent() {
|
||||
const unread = this.getUnreadCount();
|
||||
|
||||
return [
|
||||
icon(this.props.icon, {className: 'Button-icon'}),
|
||||
unread ? <span className="NotificationsDropdown-unread">{unread}</span> : '',
|
||||
<span className="Button-label">{this.props.label}</span>
|
||||
];
|
||||
}
|
||||
|
||||
getMenu() {
|
||||
return (
|
||||
<div className="Dropdown NotificationsDropdown">
|
||||
<a href="javascript:;"
|
||||
className={'Dropdown-toggle Button Button--flat NotificationsDropdown-button' + (unread ? ' unread' : '')}
|
||||
data-toggle="dropdown"
|
||||
onclick={this.onclick.bind(this)}>
|
||||
<span className="Button-icon">{unread || icon('bell')}</span>
|
||||
<span className="Button-label">{app.trans('core.notifications')}</span>
|
||||
</a>
|
||||
<div className="Dropdown-menu Dropdown-menu--right" onclick={this.menuClick.bind(this)}>
|
||||
{this.showing ? this.list.render() : ''}
|
||||
</div>
|
||||
<div className={'Dropdown-menu ' + this.props.menuClassName} onclick={this.menuClick.bind(this)}>
|
||||
{this.showing ? this.list.render() : ''}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onclick() {
|
||||
if (app.drawer.isOpen()) {
|
||||
m.route(app.route('notifications'));
|
||||
this.goToRoute();
|
||||
} else {
|
||||
this.showing = true;
|
||||
this.list.load();
|
||||
}
|
||||
}
|
||||
|
||||
goToRoute() {
|
||||
m.route(app.route('notifications'));
|
||||
}
|
||||
|
||||
getUnreadCount() {
|
||||
return app.session.user.unreadNotificationsCount();
|
||||
}
|
||||
|
||||
menuClick(e) {
|
||||
// Don't close the notifications dropdown if the user is opening a link in a
|
||||
// new tab or window.
|
||||
|
@@ -1,21 +1,20 @@
|
||||
import Component from 'flarum/Component';
|
||||
import Page from 'flarum/components/Page';
|
||||
import NotificationList from 'flarum/components/NotificationList';
|
||||
|
||||
/**
|
||||
* The `NotificationsPage` component shows the notifications list. It is only
|
||||
* used on mobile devices where the notifications dropdown is within the drawer.
|
||||
*/
|
||||
export default class NotificationsPage extends Component {
|
||||
export default class NotificationsPage extends Page {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
app.current = this;
|
||||
app.history.push('notifications');
|
||||
app.drawer.hide();
|
||||
app.modal.close();
|
||||
|
||||
this.list = new NotificationList();
|
||||
this.list.load();
|
||||
|
||||
this.bodyClass = 'App--notifications';
|
||||
}
|
||||
|
||||
view() {
|
||||
|
35
js/forum/src/components/Page.js
Normal file
35
js/forum/src/components/Page.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import Component from 'flarum/Component';
|
||||
|
||||
/**
|
||||
* The `Page` component
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
export default class Page extends Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
app.previous = app.current;
|
||||
app.current = this;
|
||||
|
||||
app.drawer.hide();
|
||||
app.modal.close();
|
||||
|
||||
/**
|
||||
* A class name to apply to the body while the route is active.
|
||||
*
|
||||
* @type {String}
|
||||
*/
|
||||
this.bodyClass = '';
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
if (isInitialized) return;
|
||||
|
||||
if (this.bodyClass) {
|
||||
$('#app').addClass(this.bodyClass);
|
||||
|
||||
context.onunload = () => $('#app').removeClass(this.bodyClass);
|
||||
}
|
||||
}
|
||||
}
|
@@ -38,7 +38,7 @@ class PostStream extends mixin(Component, evented) {
|
||||
this.loadPageTimeouts = {};
|
||||
this.pagesLoading = 0;
|
||||
|
||||
this.init(this.props.includedPosts);
|
||||
this.show(this.props.includedPosts);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,7 +153,7 @@ class PostStream extends mixin(Component, evented) {
|
||||
*
|
||||
* @param {Post[]} posts
|
||||
*/
|
||||
init(posts) {
|
||||
show(posts) {
|
||||
this.visibleStart = posts.length ? this.discussion.postIds().indexOf(posts[0].id()) : 0;
|
||||
this.visibleEnd = this.visibleStart + posts.length;
|
||||
}
|
||||
@@ -181,7 +181,7 @@ class PostStream extends mixin(Component, evented) {
|
||||
.map(id => {
|
||||
const post = app.store.getById('posts', id);
|
||||
|
||||
return post && post.discussion() ? post : null;
|
||||
return post && post.discussion() && post.user() !== false ? post : null;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -421,7 +421,7 @@ class PostStream extends mixin(Component, evented) {
|
||||
return app.store.find('posts', {
|
||||
filter: {discussion: this.discussion.id()},
|
||||
page: {near: number}
|
||||
}).then(this.init.bind(this));
|
||||
}).then(this.show.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -442,7 +442,7 @@ class PostStream extends mixin(Component, evented) {
|
||||
|
||||
this.reset(start, end);
|
||||
|
||||
return this.loadRange(start, end).then(this.init.bind(this));
|
||||
return this.loadRange(start, end).then(this.show.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -80,8 +80,8 @@ export default class PostsUserPage extends UserPage {
|
||||
* Initialize the component with a user, and trigger the loading of their
|
||||
* activity feed.
|
||||
*/
|
||||
init(user) {
|
||||
super.init(user);
|
||||
show(user) {
|
||||
super.show(user);
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
@@ -95,9 +95,7 @@ export default class PostsUserPage extends UserPage {
|
||||
this.loading = true;
|
||||
this.posts = [];
|
||||
|
||||
// Redraw, but only if we're not in the middle of a route change.
|
||||
m.startComputation();
|
||||
m.endComputation();
|
||||
m.lazyRedraw();
|
||||
|
||||
this.loadResults().then(this.parseResults.bind(this));
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ export default class SettingsPage extends UserPage {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.init(app.session.user);
|
||||
this.show(app.session.user);
|
||||
app.setTitle(app.trans('core.settings'));
|
||||
}
|
||||
|
||||
|
@@ -173,11 +173,7 @@ export default class SignUpModal extends Modal {
|
||||
|
||||
this.loading = true;
|
||||
|
||||
const data = {
|
||||
username: this.username(),
|
||||
email: this.email(),
|
||||
password: this.password()
|
||||
};
|
||||
const data = this.submitData();
|
||||
|
||||
app.store.createRecord('users').save(data).then(
|
||||
user => {
|
||||
@@ -191,4 +187,12 @@ export default class SignUpModal extends Modal {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
submitData() {
|
||||
return {
|
||||
username: this.username(),
|
||||
email: this.email(),
|
||||
password: this.password()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -52,10 +52,13 @@ export default class TextEditor extends Component {
|
||||
configTextarea(element, isInitialized) {
|
||||
if (isInitialized) return;
|
||||
|
||||
$(element).bind('keydown', 'meta+return', () => {
|
||||
var handler = () => {
|
||||
this.onsubmit();
|
||||
m.redraw();
|
||||
});
|
||||
};
|
||||
|
||||
$(element).bind('keydown', 'meta+return', handler);
|
||||
$(element).bind('keydown', 'ctrl+return', handler);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import Component from 'flarum/Component';
|
||||
import Page from 'flarum/components/Page';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import affixSidebar from 'flarum/utils/affixSidebar';
|
||||
import UserCard from 'flarum/components/UserCard';
|
||||
@@ -15,7 +15,7 @@ import listItems from 'flarum/helpers/listItems';
|
||||
*
|
||||
* @abstract
|
||||
*/
|
||||
export default class UserPage extends Component {
|
||||
export default class UserPage extends Page {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
@@ -27,9 +27,8 @@ export default class UserPage extends Component {
|
||||
this.user = null;
|
||||
|
||||
app.history.push('user');
|
||||
app.current = this;
|
||||
app.drawer.hide();
|
||||
app.modal.close();
|
||||
|
||||
this.bodyClass = 'App--user';
|
||||
}
|
||||
|
||||
view() {
|
||||
@@ -57,13 +56,6 @@ export default class UserPage extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
config(isInitialized, context) {
|
||||
if (isInitialized) return;
|
||||
|
||||
$('#app').addClass('App--user');
|
||||
context.onunload = () => $('#app').removeClass('App--user');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content to display in the user page.
|
||||
*
|
||||
@@ -79,7 +71,7 @@ export default class UserPage extends Component {
|
||||
* @param {User} user
|
||||
* @protected
|
||||
*/
|
||||
init(user) {
|
||||
show(user) {
|
||||
this.user = user;
|
||||
|
||||
app.setTitle(user.username());
|
||||
@@ -98,13 +90,13 @@ export default class UserPage extends Component {
|
||||
|
||||
app.store.all('users').some(user => {
|
||||
if (user.username().toLowerCase() === lowercaseUsername && user.joinTime()) {
|
||||
this.init(user);
|
||||
this.show(user);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!this.user) {
|
||||
app.store.find('users', username).then(this.init.bind(this));
|
||||
app.store.find('users', username).then(this.show.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -34,9 +34,9 @@ export default function(app) {
|
||||
* @return {String}
|
||||
*/
|
||||
app.route.discussion = (discussion, near) => {
|
||||
return app.route(near > 1 ? 'discussion.near' : 'discussion', {
|
||||
return app.route(near && near !== 1 ? 'discussion.near' : 'discussion', {
|
||||
id: discussion.id() + '-' + discussion.slug(),
|
||||
near: near > 1 ? near : undefined
|
||||
near: near && near !== 1 ? near : undefined
|
||||
});
|
||||
};
|
||||
|
||||
|
@@ -87,20 +87,18 @@ export default {
|
||||
destructiveControls(post) {
|
||||
const items = new ItemList();
|
||||
|
||||
if (post.number() !== 1) {
|
||||
if (post.contentType() === 'comment' && !post.isHidden() && post.canEdit()) {
|
||||
items.add('hide', Button.component({
|
||||
icon: 'times',
|
||||
children: app.trans('core.delete'),
|
||||
onclick: this.hideAction.bind(post)
|
||||
}));
|
||||
} else if ((post.contentType() !== 'comment' || post.isHidden()) && post.canDelete()) {
|
||||
items.add('delete', Button.component({
|
||||
icon: 'times',
|
||||
children: app.trans('core.delete_forever'),
|
||||
onclick: this.deleteAction.bind(post)
|
||||
}));
|
||||
}
|
||||
if (post.contentType() === 'comment' && !post.isHidden() && post.canEdit()) {
|
||||
items.add('hide', Button.component({
|
||||
icon: 'times',
|
||||
children: app.trans('core.delete'),
|
||||
onclick: this.hideAction.bind(post)
|
||||
}));
|
||||
} else if (post.number() !== 1 && (post.contentType() !== 'comment' || post.isHidden()) && post.canDelete()) {
|
||||
items.add('delete', Button.component({
|
||||
icon: 'times',
|
||||
children: app.trans('core.delete_forever'),
|
||||
onclick: this.deleteAction.bind(post)
|
||||
}));
|
||||
}
|
||||
|
||||
return items;
|
||||
|
@@ -53,6 +53,16 @@ export default class Component {
|
||||
* @public
|
||||
*/
|
||||
this.element = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the component is constructed.
|
||||
*
|
||||
* @protected
|
||||
*/
|
||||
init() {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,15 +166,17 @@ export default class Component {
|
||||
* @public
|
||||
*/
|
||||
static component(props = {}, children) {
|
||||
if (children) props.children = children;
|
||||
const componentProps = Object.assign({}, props);
|
||||
|
||||
this.initProps(props);
|
||||
if (children) componentProps.children = children;
|
||||
|
||||
this.initProps(componentProps);
|
||||
|
||||
// Set up a function for Mithril to get the component's view. It will accept
|
||||
// the component's controller (which happens to be the component itself, in
|
||||
// our case), update its props with the ones supplied, and then render the view.
|
||||
const view = (component) => {
|
||||
component.props = props;
|
||||
component.props = componentProps;
|
||||
return component.render();
|
||||
};
|
||||
|
||||
@@ -177,17 +189,17 @@ export default class Component {
|
||||
// attach a reference to the props that were passed through and the
|
||||
// component's class for reference.
|
||||
const output = {
|
||||
controller: this.bind(undefined, props),
|
||||
controller: this.bind(undefined, componentProps),
|
||||
view: view,
|
||||
props: props,
|
||||
props: componentProps,
|
||||
component: this
|
||||
};
|
||||
|
||||
// If a `key` prop was set, then we'll assume that we want that to actually
|
||||
// show up as an attribute on the component object so that Mithril's key
|
||||
// algorithm can be applied.
|
||||
if (props.key) {
|
||||
output.attrs = {key: props.key};
|
||||
if (componentProps.key) {
|
||||
output.attrs = {key: componentProps.key};
|
||||
}
|
||||
|
||||
return output;
|
||||
|
@@ -16,6 +16,7 @@ export default class Session {
|
||||
* The token that was used for authentication.
|
||||
*
|
||||
* @type {String|null}
|
||||
* @public
|
||||
*/
|
||||
this.token = token;
|
||||
}
|
||||
@@ -26,6 +27,7 @@ export default class Session {
|
||||
* @param {String} identification The username/email.
|
||||
* @param {String} password
|
||||
* @return {Promise}
|
||||
* @public
|
||||
*/
|
||||
login(identification, password) {
|
||||
return app.request({
|
||||
@@ -38,6 +40,8 @@ export default class Session {
|
||||
|
||||
/**
|
||||
* Log the user out.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
logout() {
|
||||
window.location = app.forum.attribute('baseUrl') + '/logout?token=' + this.token;
|
||||
@@ -48,8 +52,11 @@ export default class Session {
|
||||
* XMLHttpRequest object.
|
||||
*
|
||||
* @param {XMLHttpRequest} xhr
|
||||
* @public
|
||||
*/
|
||||
authorize(xhr) {
|
||||
xhr.setRequestHeader('Authorization', 'Token ' + this.token);
|
||||
if (this.token) {
|
||||
xhr.setRequestHeader('Authorization', 'Token ' + this.token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -51,7 +51,7 @@ export default class Translator {
|
||||
// If this translation has multiple options and a 'count' has been provided
|
||||
// in the input, we'll work out which option to choose using the `plural`
|
||||
// method.
|
||||
if (typeof translation === 'object' && typeof input.count !== 'undefined') {
|
||||
if (translation && typeof translation === 'object' && typeof input.count !== 'undefined') {
|
||||
translation = translation[this.plural(extractText(input.count))];
|
||||
}
|
||||
|
||||
|
@@ -23,20 +23,18 @@ export default class Dropdown extends Component {
|
||||
|
||||
props.className = props.className || '';
|
||||
props.buttonClassName = props.buttonClassName || '';
|
||||
props.contentClassName = props.contentClassName || '';
|
||||
props.menuClassName = props.menuClassName || '';
|
||||
props.label = props.label || app.trans('core.controls');
|
||||
props.caretIcon = typeof props.caretIcon !== 'undefined' ? props.caretIcon : 'caret-down';
|
||||
}
|
||||
|
||||
view() {
|
||||
const items = listItems(this.props.children);
|
||||
const items = this.props.children ? listItems(this.props.children) : [];
|
||||
|
||||
return (
|
||||
<div className={'ButtonGroup Dropdown dropdown ' + this.props.className + ' itemCount' + items.length}>
|
||||
{this.getButton()}
|
||||
<ul className={'Dropdown-menu dropdown-menu ' + this.props.menuClassName}>
|
||||
{items}
|
||||
</ul>
|
||||
{this.getMenu(items)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -94,4 +92,12 @@ export default class Dropdown extends Component {
|
||||
this.props.caretIcon ? icon(this.props.caretIcon, {className: 'Button-caret'}) : ''
|
||||
];
|
||||
}
|
||||
|
||||
getMenu(items) {
|
||||
return (
|
||||
<ul className={'Dropdown-menu dropdown-menu ' + this.props.menuClassName}>
|
||||
{items}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -129,7 +129,7 @@ export default class Modal extends Component {
|
||||
m.redraw();
|
||||
|
||||
if (errors) {
|
||||
this.$('form [name=' + errors[0].path + ']').select();
|
||||
this.$('form [name=' + errors[0].source.pointer.replace('/data/attributes/', '') + ']').select();
|
||||
} else {
|
||||
this.$('form :input:first').select();
|
||||
}
|
||||
|
@@ -81,7 +81,7 @@ export default class ModalManager extends Component {
|
||||
clear() {
|
||||
this.component = null;
|
||||
|
||||
m.redraw();
|
||||
m.lazyRedraw();
|
||||
}
|
||||
|
||||
/**
|
||||
|
19
js/lib/components/Placeholder.js
Normal file
19
js/lib/components/Placeholder.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import Component from 'flarum/Component';
|
||||
|
||||
/**
|
||||
* The `Placeholder` component displays a muted text with some call to action,
|
||||
* usually used as an empty state.
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - `text`
|
||||
*/
|
||||
export default class Placeholder extends Component {
|
||||
view() {
|
||||
return (
|
||||
<div className="Placeholder">
|
||||
<p>{this.props.text}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@@ -23,10 +23,12 @@ function withoutUnnecessarySeparators(items) {
|
||||
* The `listItems` helper wraps a collection of components in <li> tags,
|
||||
* stripping out any unnecessary `Separator` components.
|
||||
*
|
||||
* @param {Array} items
|
||||
* @param {*} items
|
||||
* @return {Array}
|
||||
*/
|
||||
export default function listItems(items) {
|
||||
if (!(items instanceof Array)) items = [items];
|
||||
|
||||
return withoutUnnecessarySeparators(items).map(item => {
|
||||
const isListItem = item.component && item.component.isListItem;
|
||||
const active = item.component && item.component.isActive && item.component.isActive(item.props);
|
||||
|
@@ -6,7 +6,7 @@
|
||||
* @return {Object}
|
||||
*/
|
||||
export default function username(user) {
|
||||
const name = (user && user.username()) || '[deleted]';
|
||||
const name = (user && user.username()) || app.trans('deleted');
|
||||
|
||||
return <span className="username">{name}</span>;
|
||||
}
|
||||
|
@@ -25,6 +25,7 @@ export default class Discussion extends mixin(Model, {
|
||||
readTime: Model.attribute('readTime', Model.transformDate),
|
||||
readNumber: Model.attribute('readNumber'),
|
||||
isUnread: computed('unreadCount', unreadCount => !!unreadCount),
|
||||
isRead: computed('unreadCount', unreadCount => app.session.user && !unreadCount),
|
||||
|
||||
canReply: Model.attribute('canReply'),
|
||||
canRename: Model.attribute('canRename'),
|
||||
@@ -61,7 +62,7 @@ export default class Discussion extends mixin(Model, {
|
||||
const user = app.session.user;
|
||||
|
||||
if (user && user.readTime() < this.lastTime()) {
|
||||
return Math.max(0, this.lastPostNumber() - (this.readNumber() || 0))
|
||||
return Math.max(0, this.lastPostNumber() - (this.readNumber() || 0));
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
@@ -5,7 +5,7 @@ import mixin from 'flarum/utils/mixin';
|
||||
import stringToColor from 'flarum/utils/stringToColor';
|
||||
import ItemList from 'flarum/utils/ItemList';
|
||||
import computed from 'flarum/utils/computed';
|
||||
import Badge from 'flarum/components/Badge';
|
||||
import GroupBadge from 'flarum/components/GroupBadge';
|
||||
|
||||
export default class User extends mixin(Model, {
|
||||
username: Model.attribute('username'),
|
||||
@@ -69,13 +69,7 @@ export default class User extends mixin(Model, {
|
||||
groups.forEach(group => {
|
||||
const name = group.nameSingular();
|
||||
|
||||
items.add('group' + group.id(),
|
||||
Badge.component({
|
||||
label: app.trans('core.group_' + name.toLowerCase(), undefined, name),
|
||||
icon: group.icon(),
|
||||
style: {backgroundColor: group.color()}
|
||||
})
|
||||
);
|
||||
items.add('group' + group.id(), GroupBadge.component({group}));
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -50,6 +50,8 @@ export default class ItemList {
|
||||
|
||||
for (const i in this) {
|
||||
if (this.hasOwnProperty(i) && this[i] instanceof Item) {
|
||||
this[i].content = Object(this[i].content);
|
||||
|
||||
this[i].content.itemName = i;
|
||||
items.push(this[i]);
|
||||
this[i].key = items.length;
|
||||
|
@@ -21,7 +21,6 @@ export default class SubtreeRetainer {
|
||||
* @param {...callbacks} callbacks Functions returning data to keep track of.
|
||||
*/
|
||||
constructor(...callbacks) {
|
||||
this.invalidate();
|
||||
this.callbacks = callbacks;
|
||||
this.data = {};
|
||||
}
|
||||
|
@@ -13,5 +13,15 @@ export default function patchMithril(global) {
|
||||
|
||||
Object.keys(mo).forEach(key => m[key] = mo[key]);
|
||||
|
||||
/**
|
||||
* Redraw only if not in the middle of a computation (e.g. a route change).
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
m.lazyRedraw = function() {
|
||||
m.startComputation();
|
||||
m.endComputation();
|
||||
};
|
||||
|
||||
global.m = m;
|
||||
}
|
||||
|
@@ -41,7 +41,12 @@ export function getPlainContent(string) {
|
||||
return dom.text();
|
||||
}
|
||||
|
||||
getPlainContent.removeSelectors = ['blockquote'];
|
||||
/**
|
||||
* An array of DOM selectors to remove when getting plain content.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
getPlainContent.removeSelectors = ['blockquote', 'script'];
|
||||
|
||||
/**
|
||||
* Make a string's first character uppercase.
|
||||
|
@@ -13,11 +13,15 @@
|
||||
> li {
|
||||
border-bottom: 1px solid @control-bg;
|
||||
}
|
||||
|
||||
.Post {
|
||||
padding-top: 10px;
|
||||
}
|
||||
}
|
||||
.PostsUserPage-discussion {
|
||||
font-weight: bold;
|
||||
margin-top: 15px;
|
||||
margin-bottom: -15px;
|
||||
margin-bottom: 10px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
|
@@ -44,8 +44,11 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.read & {
|
||||
color: mix(@heading-color, @body-bg, 55%);
|
||||
}
|
||||
.unread & {
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
.DiscussionListItem-info {
|
||||
@@ -56,6 +59,12 @@
|
||||
|
||||
> li {
|
||||
display: inline;
|
||||
opacity: 0.7;
|
||||
.transition(opacity 0.2s);
|
||||
|
||||
.DiscussionListItem:hover &, .DiscussionListItem.active & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.username {
|
||||
font-weight: bold;
|
||||
|
@@ -76,19 +76,17 @@
|
||||
padding: 0;
|
||||
}
|
||||
.Notification {
|
||||
> a {
|
||||
display: block;
|
||||
padding: 8px 15px 8px 70px;
|
||||
color: @muted-color;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
padding: 8px 15px 8px 70px;
|
||||
color: @muted-color !important; // required to override .light-contents applied to header
|
||||
overflow: hidden;
|
||||
|
||||
.unread& {
|
||||
background: @control-bg;
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background: @control-bg;
|
||||
}
|
||||
.unread& {
|
||||
background: @control-bg;
|
||||
}
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
background: @control-bg;
|
||||
}
|
||||
.Avatar {
|
||||
.Avatar--size(24px);
|
||||
|
@@ -10,7 +10,7 @@
|
||||
}
|
||||
}
|
||||
& .Dropdown-toggle .Button-label {
|
||||
margin-left: 10px;
|
||||
margin-left: 7px;
|
||||
}
|
||||
}
|
||||
@media @tablet-up {
|
||||
@@ -24,21 +24,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.NotificationsDropdown-button.unread .Button-icon {
|
||||
display: inline-block;
|
||||
border-radius: 12px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
padding: 2px 0;
|
||||
font-weight: bold;
|
||||
margin: -2px 0;
|
||||
background: @primary-color;
|
||||
color: @body-bg;
|
||||
font-size: 13px;
|
||||
vertical-align: 0;
|
||||
|
||||
& when (@config-colored-header = true) {
|
||||
background: #fff;
|
||||
}
|
||||
.NotificationsDropdown .Dropdown-toggle.unread .Button-icon {
|
||||
color: @header-color;
|
||||
}
|
||||
.NotificationsDropdown-unread {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 17px;
|
||||
background: @header-color;
|
||||
color: @header-bg;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
padding: 2px 5px 3px;
|
||||
line-height: 1em;
|
||||
border-radius: 10px;
|
||||
border: 1px solid @header-bg;
|
||||
}
|
||||
|
@@ -2,10 +2,12 @@
|
||||
// Posts
|
||||
|
||||
.Post {
|
||||
padding: 30px 0;
|
||||
padding: 30px 20px;
|
||||
margin: -1px -20px;
|
||||
transition: 0.2s box-shadow, top 0.2s, opacity 0.2s;
|
||||
position: relative;
|
||||
top: 0;
|
||||
border-radius: @border-radius;
|
||||
|
||||
&.editing {
|
||||
top: 5px;
|
||||
@@ -114,6 +116,11 @@
|
||||
color: #666;
|
||||
font-size: 90%;
|
||||
border-radius: @border-radius;
|
||||
|
||||
.hljs {
|
||||
padding: 0;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
h1 {
|
||||
font-size: 160%;
|
||||
@@ -131,13 +138,13 @@
|
||||
font-size: 100%;
|
||||
font-weight: bold;
|
||||
}
|
||||
img {
|
||||
img, iframe {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.Post.hidden {
|
||||
.Post-header, .Post-header a, .Post-user h3, .Post-user h3 a {
|
||||
.Post--hidden {
|
||||
.Post-header, .Post-header a, .PostUser h3, .PostUser h3 a {
|
||||
color: @muted-more-color;
|
||||
}
|
||||
.Post-body, .Post-footer, h3 .Avatar, .PostUser-badges {
|
||||
@@ -262,6 +269,10 @@
|
||||
padding-left: 50px;
|
||||
line-height: 1.7em;
|
||||
|
||||
.PostPreview-excerpt {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.Avatar {
|
||||
float: left;
|
||||
margin-left: -50px;
|
||||
@@ -319,7 +330,7 @@
|
||||
|
||||
@media @tablet-up {
|
||||
.Post {
|
||||
padding-left: 90px;
|
||||
padding-left: 20px + 90px;
|
||||
|
||||
.Post-controls {
|
||||
opacity: 0;
|
||||
|
@@ -126,6 +126,9 @@
|
||||
& .icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
&.online .fa-circle {
|
||||
color: @online-user-circle-color;
|
||||
}
|
||||
&.online .icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
@@ -27,8 +27,7 @@
|
||||
&[disabled],
|
||||
&[readonly],
|
||||
fieldset[disabled] & {
|
||||
// background-color: @input-bg-disabled;
|
||||
opacity: 1; // iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&[disabled],
|
||||
|
@@ -151,7 +151,7 @@
|
||||
}
|
||||
&:before {
|
||||
content: " ";
|
||||
// .App-header();
|
||||
.header-background();
|
||||
}
|
||||
}
|
||||
.Modal {
|
||||
|
9
less/lib/Placeholder.less
Normal file
9
less/lib/Placeholder.less
Normal file
@@ -0,0 +1,9 @@
|
||||
.Placeholder {
|
||||
margin-top: 40px;
|
||||
|
||||
p {
|
||||
font-size: 1.4em;
|
||||
color: @muted-more-color;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
@@ -9,6 +9,7 @@
|
||||
-moz-appearance: none;
|
||||
padding-right: 30px;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
.Select-caret {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
@import url(http://fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700,600);
|
||||
@import url(//fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700,600);
|
||||
|
||||
@import "font-awesome.less";
|
||||
@fa-font-path: "../../assets/fonts";
|
||||
@@ -22,9 +22,9 @@
|
||||
@import "LoadingIndicator.less";
|
||||
@import "Modal.less";
|
||||
@import "Navigation.less";
|
||||
@import "Placeholder.less";
|
||||
@import "Search.less";
|
||||
@import "Select.less";
|
||||
@import "Tooltip.less";
|
||||
|
||||
@import "variables.less";
|
||||
|
||||
|
@@ -24,11 +24,11 @@
|
||||
@secondary-color: @config-secondary-color;
|
||||
|
||||
@body-bg: #fff;
|
||||
@text-color: #333;
|
||||
@text-color: #111;
|
||||
@link-color: saturate(@primary-color, 10%);
|
||||
@heading-color: @text-color;
|
||||
@muted-color: hsl(@secondary-hue, min(25%, @secondary-sat), 63%);
|
||||
@muted-more-color: #bbb;
|
||||
@muted-color: hsl(@secondary-hue, min(25%, @secondary-sat), 55%);
|
||||
@muted-more-color: #aaa;
|
||||
@shadow-color: rgba(0, 0, 0, 0.35);
|
||||
|
||||
@control-bg: hsl(@secondary-hue, min(50%, @secondary-sat), 93%);
|
||||
@@ -130,3 +130,5 @@
|
||||
|
||||
@tooltip-bg: rgba(0, 0, 0, 0.9);
|
||||
@tooltip-color: #fff;
|
||||
|
||||
@online-user-circle-color: #7FBA00;
|
||||
|
@@ -1,6 +1,5 @@
|
||||
core:
|
||||
account: Account
|
||||
activity: Activity
|
||||
administration: Administration
|
||||
alert: Alert
|
||||
all_discussions: All Discussions
|
||||
@@ -50,7 +49,6 @@ core:
|
||||
group_mods: Mods
|
||||
invalid_login: Your login details were incorrect.
|
||||
joined: "Joined {ago}"
|
||||
joined_the_forum: Joined the forum
|
||||
load_more: Load More
|
||||
log_in: Log In
|
||||
log_in_to_reply: Log In to Reply
|
||||
@@ -71,7 +69,6 @@ core:
|
||||
post_edited: "{username} edited {ago}"
|
||||
post_number: "Post #{number}"
|
||||
post_reply: Post Reply
|
||||
posted_a_reply: Posted a reply
|
||||
posts: Posts
|
||||
powered_by_flarum: Powered by Flarum
|
||||
privacy: Privacy
|
||||
@@ -96,7 +93,6 @@ core:
|
||||
sort_relevance: Relevance
|
||||
sort_top: Top
|
||||
start_a_discussion: Start a Discussion
|
||||
started_a_discussion: Started a discussion
|
||||
unread_posts: "{count} unread"
|
||||
upload: Upload
|
||||
username: Username
|
||||
|
38
migrations/2015_02_24_000000_create_api_keys_table.php
Normal file
38
migrations/2015_02_24_000000_create_api_keys_table.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?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\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
|
||||
class CreateApiKeysTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$this->schema->create('api_keys', function (Blueprint $table) {
|
||||
$table->string('id', 100)->primary();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
$this->schema->drop('api_keys');
|
||||
}
|
||||
}
|
@@ -13,7 +13,7 @@ namespace Flarum\Api\Actions;
|
||||
use Flarum\Api\Request;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
|
||||
abstract class DeleteAction extends JsonApiAction
|
||||
abstract class DeleteAction implements Action
|
||||
{
|
||||
/**
|
||||
* Delegate deletion of the resource, and return a 204 No Content
|
||||
@@ -22,7 +22,7 @@ abstract class DeleteAction extends JsonApiAction
|
||||
* @param \Flarum\Api\Request $request
|
||||
* @return \Psr\Http\Message\ResponseInterface
|
||||
*/
|
||||
public function respond(Request $request)
|
||||
public function handle(Request $request)
|
||||
{
|
||||
$this->delete($request);
|
||||
|
||||
|
@@ -10,13 +10,13 @@
|
||||
|
||||
namespace Flarum\Api\Actions\Extensions;
|
||||
|
||||
use Flarum\Api\Actions\JsonApiAction;
|
||||
use Flarum\Api\Actions\Action;
|
||||
use Flarum\Api\Request;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Flarum\Core\Exceptions\PermissionDeniedException;
|
||||
use Flarum\Support\ExtensionManager;
|
||||
|
||||
class UpdateAction extends JsonApiAction
|
||||
class UpdateAction implements Action
|
||||
{
|
||||
protected $extensions;
|
||||
|
||||
@@ -25,7 +25,7 @@ class UpdateAction extends JsonApiAction
|
||||
$this->extensions = $extensions;
|
||||
}
|
||||
|
||||
protected function respond(Request $request)
|
||||
public function handle(Request $request)
|
||||
{
|
||||
if (! $request->actor->isAdmin()) {
|
||||
throw new PermissionDeniedException;
|
||||
|
@@ -16,7 +16,7 @@ use Flarum\Core\Users\Commands\RequestPasswordReset;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
|
||||
class ForgotAction extends JsonApiAction
|
||||
class ForgotAction implements Action
|
||||
{
|
||||
protected $users;
|
||||
|
||||
@@ -34,7 +34,7 @@ class ForgotAction extends JsonApiAction
|
||||
* @param \Flarum\Api\Request $request
|
||||
* @return \Psr\Http\Message\ResponseInterface
|
||||
*/
|
||||
public function respond(Request $request)
|
||||
public function handle(Request $request)
|
||||
{
|
||||
$email = $request->get('email');
|
||||
|
||||
|
@@ -1,63 +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\Api\Actions;
|
||||
|
||||
use Flarum\Api\Request;
|
||||
use Illuminate\Contracts\Validation\ValidationException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Flarum\Core\Exceptions\ValidationFailureException;
|
||||
use Flarum\Core\Exceptions\PermissionDeniedException;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
abstract class JsonApiAction implements Action
|
||||
{
|
||||
/**
|
||||
* Handle an API request and return an API response, handling any relevant
|
||||
* (API-related) exceptions that are thrown.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Psr\Http\Message\ResponseInterface
|
||||
*/
|
||||
public function handle(Request $request)
|
||||
{
|
||||
// TODO: This is gross. Move this error handling code to middleware?
|
||||
try {
|
||||
return $this->respond($request);
|
||||
} catch (ValidationException $e) {
|
||||
$errors = [];
|
||||
foreach ($e->errors()->toArray() as $field => $messages) {
|
||||
$errors[] = [
|
||||
'detail' => implode("\n", $messages),
|
||||
'path' => $field
|
||||
];
|
||||
}
|
||||
return new JsonResponse(['errors' => $errors], 422);
|
||||
} catch (\Flarum\Core\Exceptions\ValidationException $e) {
|
||||
$errors = [];
|
||||
foreach ($e->getMessages() as $path => $detail) {
|
||||
$errors[] = compact('path', 'detail');
|
||||
}
|
||||
return new JsonResponse(['errors' => $errors], 422);
|
||||
} catch (PermissionDeniedException $e) {
|
||||
return new JsonResponse(null, 401);
|
||||
} catch (ModelNotFoundException $e) {
|
||||
return new JsonResponse(null, 404);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an API request and return an API response.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Psr\Http\Message\ResponseInterface
|
||||
*/
|
||||
abstract protected function respond(Request $request);
|
||||
}
|
@@ -19,7 +19,7 @@ use Tobscure\JsonApi\Document;
|
||||
use Tobscure\JsonApi\SerializerInterface;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
abstract class SerializeAction extends JsonApiAction
|
||||
abstract class SerializeAction implements Action
|
||||
{
|
||||
/**
|
||||
* The name of the serializer class to output results with.
|
||||
@@ -77,7 +77,7 @@ abstract class SerializeAction extends JsonApiAction
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function respond(Request $request)
|
||||
public function handle(Request $request)
|
||||
{
|
||||
$request = $this->buildJsonApiRequest($request);
|
||||
$document = new Document();
|
||||
|
@@ -18,7 +18,7 @@ use Flarum\Events\UserEmailChangeWasRequested;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
class TokenAction extends JsonApiAction
|
||||
class TokenAction implements Action
|
||||
{
|
||||
protected $users;
|
||||
|
||||
@@ -37,7 +37,7 @@ class TokenAction extends JsonApiAction
|
||||
* @return \Psr\Http\Message\ResponseInterface
|
||||
* @throws PermissionDeniedException
|
||||
*/
|
||||
public function respond(Request $request)
|
||||
public function handle(Request $request)
|
||||
{
|
||||
$identification = $request->get('identification');
|
||||
$password = $request->get('password');
|
||||
|
57
src/Api/ApiKey.php
Normal file
57
src/Api/ApiKey.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?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\Api;
|
||||
|
||||
use Flarum\Core\Model;
|
||||
use DateTime;
|
||||
|
||||
/**
|
||||
* @todo document database columns with @property
|
||||
*/
|
||||
class ApiKey extends Model
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $table = 'api_keys';
|
||||
|
||||
/**
|
||||
* Use a custom primary key for this model.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $incrementing = false;
|
||||
|
||||
/**
|
||||
* Generate an API key.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function generate()
|
||||
{
|
||||
$key = new static;
|
||||
|
||||
$key->id = str_random(40);
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the given key only if it is valid.
|
||||
*
|
||||
* @param string $key
|
||||
* @return static|null
|
||||
*/
|
||||
public static function valid($key)
|
||||
{
|
||||
return static::where('id', $key)->first();
|
||||
}
|
||||
}
|
@@ -12,6 +12,8 @@ namespace Flarum\Api;
|
||||
|
||||
use Flarum\Core\Users\User;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Exception;
|
||||
use Flarum\Api\Middleware\JsonApiErrors;
|
||||
|
||||
class Client
|
||||
{
|
||||
@@ -38,10 +40,16 @@ class Client
|
||||
*/
|
||||
public function send(User $actor, $actionClass, array $input = [])
|
||||
{
|
||||
/** @var \Flarum\Api\Actions\JsonApiAction $action */
|
||||
/** @var \Flarum\Api\Actions\Action $action */
|
||||
$action = $this->container->make($actionClass);
|
||||
|
||||
$response = $action->handle(new Request($input, $actor));
|
||||
try {
|
||||
$response = $action->handle(new Request($input, $actor));
|
||||
} catch (Exception $e) {
|
||||
$middleware = new JsonApiErrors();
|
||||
|
||||
$response = $middleware->handle($e);
|
||||
}
|
||||
|
||||
return new Response($response);
|
||||
}
|
||||
|
@@ -10,35 +10,65 @@
|
||||
|
||||
namespace Flarum\Api\Middleware;
|
||||
|
||||
use Flarum\Core\Exceptions\JsonApiSerializable;
|
||||
use Illuminate\Contracts\Validation\ValidationException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
use Zend\Stratigility\ErrorMiddlewareInterface;
|
||||
use Flarum\Core;
|
||||
use Exception;
|
||||
|
||||
class JsonApiErrors implements ErrorMiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __invoke($error, Request $request, Response $response, callable $out = null)
|
||||
public function __invoke($e, Request $request, Response $response, callable $out = null)
|
||||
{
|
||||
$errorObject = [
|
||||
'title' => $error->getMessage(),
|
||||
];
|
||||
return $this->handle($e);
|
||||
}
|
||||
|
||||
$status = 500;
|
||||
public function handle(Exception $e)
|
||||
{
|
||||
if ($e instanceof JsonApiSerializable) {
|
||||
$status = $e->getStatusCode();
|
||||
|
||||
// If it seems to be a valid HTTP status code, we pass on the
|
||||
// exception's status.
|
||||
$errorCode = $error->getCode();
|
||||
if (is_int($errorCode) && $errorCode >= 400 && $errorCode < 600) {
|
||||
$status = $errorCode;
|
||||
$errors = $e->getErrors();
|
||||
} else if ($e instanceof ValidationException) {
|
||||
$status = 422;
|
||||
|
||||
$errors = $e->errors()->toArray();
|
||||
$errors = array_map(function ($field, $messages) {
|
||||
return [
|
||||
'detail' => implode("\n", $messages),
|
||||
'source' => ['pointer' => '/data/attributes/' . $field],
|
||||
];
|
||||
}, array_keys($errors), $errors);
|
||||
} else if ($e instanceof ModelNotFoundException) {
|
||||
$status = 404;
|
||||
|
||||
$errors = [];
|
||||
} else {
|
||||
$status = 500;
|
||||
|
||||
$error = [
|
||||
'code' => $status,
|
||||
'title' => 'Internal Server Error'
|
||||
];
|
||||
|
||||
if (Core::inDebugMode()) {
|
||||
$error['detail'] = (string) $e;
|
||||
}
|
||||
|
||||
$errors = [$error];
|
||||
}
|
||||
|
||||
// JSON API errors must be collected in an array under the
|
||||
// "errors" key in the top level of the document
|
||||
$data = [
|
||||
'errors' => [$errorObject]
|
||||
'errors' => $errors,
|
||||
];
|
||||
|
||||
return new JsonResponse($data, $status);
|
||||
|
@@ -11,6 +11,8 @@
|
||||
namespace Flarum\Api\Middleware;
|
||||
|
||||
use Flarum\Api\AccessToken;
|
||||
use Flarum\Api\ApiKey;
|
||||
use Flarum\Core\Users\User;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
@@ -42,13 +44,23 @@ class LoginWithHeader implements MiddlewareInterface
|
||||
public function __invoke(Request $request, Response $response, callable $out = null)
|
||||
{
|
||||
$header = $request->getHeaderLine('authorization');
|
||||
if (starts_with($header, $this->prefix) &&
|
||||
($token = substr($header, strlen($this->prefix))) &&
|
||||
($accessToken = AccessToken::valid($token))
|
||||
) {
|
||||
$this->app->instance('flarum.actor', $user = $accessToken->user);
|
||||
|
||||
$user->updateLastSeen()->save();
|
||||
$parts = explode(';', $header);
|
||||
|
||||
if (isset($parts[0]) && starts_with($parts[0], $this->prefix)) {
|
||||
$token = substr($parts[0], strlen($this->prefix));
|
||||
|
||||
if ($accessToken = AccessToken::valid($token)) {
|
||||
$this->app->instance('flarum.actor', $user = $accessToken->user);
|
||||
|
||||
$user->updateLastSeen()->save();
|
||||
} elseif (isset($parts[1]) && ($apiKey = ApiKey::valid($token))) {
|
||||
$userParts = explode('=', trim($parts[1]));
|
||||
|
||||
if (isset($userParts[0]) && $userParts[0] === 'userId') {
|
||||
$this->app->instance('flarum.actor', $user = User::find($userParts[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $out ? $out($request, $response) : $response;
|
||||
|
@@ -22,7 +22,7 @@ class Request
|
||||
public $input;
|
||||
|
||||
/**
|
||||
* @var Guest
|
||||
* @var User
|
||||
*/
|
||||
public $actor;
|
||||
|
||||
|
@@ -11,6 +11,7 @@
|
||||
namespace Flarum\Assets;
|
||||
|
||||
use Less_Parser;
|
||||
use Less_Exception_Parser;
|
||||
|
||||
class LessCompiler extends RevisionCompiler
|
||||
{
|
||||
@@ -28,7 +29,11 @@ class LessCompiler extends RevisionCompiler
|
||||
}
|
||||
|
||||
foreach ($this->strings as $callback) {
|
||||
$parser->parse($callback());
|
||||
try {
|
||||
$parser->parse($callback());
|
||||
} catch (Less_Exception_Parser $e) {
|
||||
// TODO: log an error somewhere?
|
||||
}
|
||||
}
|
||||
|
||||
return $parser->getCss();
|
||||
|
@@ -14,6 +14,7 @@ use Illuminate\Contracts\Container\Container;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Flarum\Core\Application;
|
||||
|
||||
class GenerateExtensionCommand extends Command
|
||||
{
|
||||
@@ -83,12 +84,17 @@ class GenerateExtensionCommand extends Command
|
||||
'version' => '0.1.0',
|
||||
'author' => [
|
||||
'name' => $authorName,
|
||||
'email' => $authorEmail
|
||||
'email' => $authorEmail,
|
||||
'homepage' => ''
|
||||
],
|
||||
'license' => $license,
|
||||
'require' => [
|
||||
'php' => '>=5.4.0',
|
||||
'flarum' => '>0.1.0'
|
||||
'flarum' => '>'.Application::VERSION
|
||||
],
|
||||
'icon' => [
|
||||
'name' => '',
|
||||
'backgroundColor' => '',
|
||||
'color' => ''
|
||||
]
|
||||
];
|
||||
|
||||
|
@@ -68,9 +68,13 @@ class UpgradeCommand extends Command
|
||||
$migrator = $extensions->getMigrator();
|
||||
|
||||
foreach ($extensions->getInfo() as $extension) {
|
||||
if (! $extensions->isEnabled($extension->name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->info('Upgrading extension: '.$extension->name);
|
||||
|
||||
$extensions->enable($extension->name);
|
||||
$extensions->migrate($extension->name);
|
||||
|
||||
foreach ($migrator->getNotes() as $note) {
|
||||
$this->info($note);
|
||||
|
@@ -27,7 +27,7 @@ class Application extends Container implements ApplicationContract
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const VERSION = '0.1.0-beta';
|
||||
const VERSION = '0.1.0-beta.2';
|
||||
|
||||
/**
|
||||
* The base path for the Laravel installation.
|
||||
|
2
src/Core/Discussions/Discussion.php
Executable file → Normal file
2
src/Core/Discussions/Discussion.php
Executable file → Normal file
@@ -46,7 +46,7 @@ class Discussion extends Model
|
||||
* @var array
|
||||
*/
|
||||
protected $rules = [
|
||||
'title' => 'required',
|
||||
'title' => 'required|max:80',
|
||||
'start_time' => 'required|date',
|
||||
'comments_count' => 'integer',
|
||||
'participants_count' => 'integer',
|
||||
|
@@ -13,7 +13,9 @@ namespace Flarum\Core\Discussions;
|
||||
use Flarum\Core\Search\GambitManager;
|
||||
use Flarum\Core\Users\User;
|
||||
use Flarum\Events\ModelAllow;
|
||||
use Flarum\Events\ScopeModelVisibility;
|
||||
use Flarum\Events\RegisterDiscussionGambits;
|
||||
use Flarum\Events\ScopeEmptyDiscussionVisibility;
|
||||
use Flarum\Support\ServiceProvider;
|
||||
use Flarum\Extend;
|
||||
use Illuminate\Contracts\Container\Container;
|
||||
@@ -53,6 +55,19 @@ class DiscussionsServiceProvider extends ServiceProvider
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$events->listen(ScopeModelVisibility::class, function (ScopeModelVisibility $event) {
|
||||
if ($event->model instanceof Discussion) {
|
||||
if (! $event->actor->hasPermission('discussion.editPosts')) {
|
||||
$event->query->where(function ($query) use ($event) {
|
||||
$query->where('comments_count', '>', '0')
|
||||
->orWhere('start_user_id', $event->actor->id);
|
||||
|
||||
event(new ScopeEmptyDiscussionVisibility($query, $event->actor));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -37,10 +37,12 @@ class DiscussionMetadataUpdater
|
||||
{
|
||||
$discussion = $event->post->discussion;
|
||||
|
||||
$discussion->comments_count++;
|
||||
$discussion->setLastPost($event->post);
|
||||
$discussion->refreshParticipantsCount();
|
||||
$discussion->save();
|
||||
if ($discussion && $discussion->exists) {
|
||||
$discussion->comments_count++;
|
||||
$discussion->setLastPost($event->post);
|
||||
$discussion->refreshParticipantsCount();
|
||||
$discussion->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,10 +68,12 @@ class DiscussionMetadataUpdater
|
||||
{
|
||||
$discussion = $event->post->discussion;
|
||||
|
||||
$discussion->refreshCommentsCount();
|
||||
$discussion->refreshParticipantsCount();
|
||||
$discussion->refreshLastPost();
|
||||
$discussion->save();
|
||||
if ($discussion && $discussion->exists) {
|
||||
$discussion->refreshCommentsCount();
|
||||
$discussion->refreshParticipantsCount();
|
||||
$discussion->refreshLastPost();
|
||||
$discussion->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,7 +83,7 @@ class DiscussionMetadataUpdater
|
||||
{
|
||||
$discussion = $post->discussion;
|
||||
|
||||
if ($discussion->exists) {
|
||||
if ($discussion && $discussion->exists) {
|
||||
$discussion->refreshCommentsCount();
|
||||
$discussion->refreshParticipantsCount();
|
||||
|
||||
|
@@ -12,6 +12,21 @@ namespace Flarum\Core\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class InvalidConfirmationTokenException extends Exception
|
||||
class InvalidConfirmationTokenException extends Exception implements JsonApiSerializable
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getStatusCode()
|
||||
{
|
||||
return 403;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getErrors()
|
||||
{
|
||||
return ['code' => 'invalid_confirmation_token'];
|
||||
}
|
||||
}
|
||||
|
29
src/Core/Exceptions/JsonApiSerializable.php
Normal file
29
src/Core/Exceptions/JsonApiSerializable.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?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\Core\Exceptions;
|
||||
|
||||
interface JsonApiSerializable
|
||||
{
|
||||
/**
|
||||
* Return the HTTP status code to be used for this exception.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getStatusCode();
|
||||
|
||||
/**
|
||||
* Return an array of errors, formatted as JSON-API error objects.
|
||||
*
|
||||
* @see http://jsonapi.org/format/#error-objects
|
||||
* @return array
|
||||
*/
|
||||
public function getErrors();
|
||||
}
|
@@ -12,6 +12,26 @@ namespace Flarum\Core\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class PermissionDeniedException extends Exception
|
||||
class PermissionDeniedException extends Exception implements JsonApiSerializable
|
||||
{
|
||||
/**
|
||||
* Return the HTTP status code to be used for this exception.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getStatusCode()
|
||||
{
|
||||
return 401;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of errors, formatted as JSON-API error objects.
|
||||
*
|
||||
* @see http://jsonapi.org/format/#error-objects
|
||||
* @return array
|
||||
*/
|
||||
public function getErrors()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@@ -12,17 +12,39 @@ namespace Flarum\Core\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ValidationException extends Exception
|
||||
class ValidationException extends Exception implements JsonApiSerializable
|
||||
{
|
||||
protected $messages;
|
||||
|
||||
public function __construct(array $messages)
|
||||
{
|
||||
$this->messages = $messages;
|
||||
|
||||
parent::__construct(implode("\n", $messages));
|
||||
}
|
||||
|
||||
public function getMessages()
|
||||
{
|
||||
return $this->messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getStatusCode()
|
||||
{
|
||||
return 422;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getErrors()
|
||||
{
|
||||
return array_map(function ($path, $detail) {
|
||||
$source = ['pointer' => '/data/attributes/' . $path];
|
||||
|
||||
return compact('source', 'detail');
|
||||
}, array_keys($this->messages), $this->messages);
|
||||
}
|
||||
}
|
||||
|
@@ -42,6 +42,15 @@ class Formatter
|
||||
|
||||
event(new FormatterConfigurator($configurator));
|
||||
|
||||
$dom = $configurator->tags['URL']->template->asDOM();
|
||||
|
||||
foreach ($dom->getElementsByTagName('a') as $a) {
|
||||
$a->setAttribute('target', '_blank');
|
||||
$a->setAttribute('rel', 'nofollow');
|
||||
}
|
||||
|
||||
$dom->saveChanges();
|
||||
|
||||
return $configurator;
|
||||
}
|
||||
|
||||
|
@@ -10,14 +10,8 @@
|
||||
|
||||
namespace Flarum\Core;
|
||||
|
||||
use Flarum\Core\Exceptions\PermissionDeniedException;
|
||||
use Flarum\Core\Users\User;
|
||||
use Flarum\Events\ModelAllow;
|
||||
use Flarum\Events\ModelDates;
|
||||
use Flarum\Events\ModelRelationship;
|
||||
use Flarum\Events\ScopeModelVisibility;
|
||||
use Illuminate\Contracts\Validation\Factory;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model as Eloquent;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use LogicException;
|
||||
@@ -77,7 +71,7 @@ abstract class Model extends Eloquent
|
||||
// If a custom relation with this key has been set up, then we will load
|
||||
// and return results from the query and hydrate the relationship's
|
||||
// value on the "relationships" array.
|
||||
if ($relation = $this->getCustomRelation($key)) {
|
||||
if (! $this->relationLoaded($key) && ($relation = $this->getCustomRelation($key))) {
|
||||
if (! $relation instanceof Relation) {
|
||||
throw new LogicException('Relationship method must return an object of type '
|
||||
. 'Illuminate\Database\Eloquent\Relations\Relation');
|
||||
|
@@ -100,7 +100,7 @@ class NotificationSyncer
|
||||
// removed from this collection by the above loop. Un-delete the
|
||||
// existing records that we want to keep.
|
||||
if (count($toDelete)) {
|
||||
$this->setDeleted($toDelete->lists('id'), true);
|
||||
$this->setDeleted($toDelete->lists('id')->all(), true);
|
||||
}
|
||||
|
||||
if (count($toUndelete)) {
|
||||
|
@@ -87,15 +87,11 @@ class CommentPost extends Post
|
||||
* @param User $actor
|
||||
* @return $this
|
||||
*/
|
||||
public function hide(User $actor)
|
||||
public function hide(User $actor = null)
|
||||
{
|
||||
if ($this->number == 1) {
|
||||
throw new DomainException('Cannot hide the first post of a discussion');
|
||||
}
|
||||
|
||||
if (! $this->hide_time) {
|
||||
$this->hide_time = time();
|
||||
$this->hide_user_id = $actor->id;
|
||||
$this->hide_user_id = $actor ? $actor->id : null;
|
||||
|
||||
$this->raise(new PostWasHidden($this));
|
||||
}
|
||||
@@ -110,10 +106,6 @@ class CommentPost extends Post
|
||||
*/
|
||||
public function restore()
|
||||
{
|
||||
if ($this->number == 1) {
|
||||
throw new DomainException('Cannot restore the first post of a discussion');
|
||||
}
|
||||
|
||||
if ($this->hide_time !== null) {
|
||||
$this->hide_time = null;
|
||||
$this->hide_user_id = null;
|
||||
|
@@ -43,7 +43,7 @@ class Post extends Model
|
||||
protected $rules = [
|
||||
'discussion_id' => 'required|integer',
|
||||
'time' => 'required|date',
|
||||
'content' => 'required',
|
||||
'content' => 'required|max:65535',
|
||||
'number' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'edit_time' => 'date',
|
||||
@@ -120,7 +120,7 @@ class Post extends Model
|
||||
if ($discussion) {
|
||||
$this->setRelation('discussion', $discussion);
|
||||
|
||||
return (bool) $discussion->postsVisibleTo($user)->find($this->id)->count();
|
||||
return (bool) $discussion->postsVisibleTo($user)->where('id', $this->id)->count();
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@@ -44,7 +44,7 @@ class PostsServiceProvider extends ServiceProvider
|
||||
$actor = $event->actor;
|
||||
|
||||
if ($action === 'view' &&
|
||||
(! $post->hide_user_id || $post->hide_user_id == $actor->id || $post->can($actor, 'edit'))) {
|
||||
(! $post->hide_time || $post->user_id == $actor->id || $post->can($actor, 'edit'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ class PostsServiceProvider extends ServiceProvider
|
||||
if ($post->discussion->can($actor, 'editPosts')) {
|
||||
return true;
|
||||
}
|
||||
if ($post->user_id == $actor->id && (! $post->hide_user_id || $post->hide_user_id == $actor->id)) {
|
||||
if ($post->user_id == $actor->id && (! $post->hide_time || $post->hide_user_id == $actor->id)) {
|
||||
$allowEditing = $settings->get('allow_post_editing');
|
||||
|
||||
if ($allowEditing === '-1' ||
|
||||
@@ -80,8 +80,8 @@ class PostsServiceProvider extends ServiceProvider
|
||||
|
||||
if (! $event->discussion->can($user, 'editPosts')) {
|
||||
$event->query->where(function ($query) use ($user) {
|
||||
$query->whereNull('hide_user_id')
|
||||
->orWhere('hide_user_id', $user->id);
|
||||
$query->whereNull('hide_time')
|
||||
->orWhere('user_id', $user->id);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@@ -30,6 +30,7 @@ class RegisterUserHandler
|
||||
/**
|
||||
* @param RegisterUser $command
|
||||
* @return User
|
||||
* @throws PermissionDeniedException
|
||||
*/
|
||||
public function handle(RegisterUser $command)
|
||||
{
|
||||
|
@@ -65,7 +65,7 @@ class UploadAvatarHandler
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'avatar');
|
||||
$command->file->moveTo($tmpFile);
|
||||
|
||||
$manager = new ImageManager(['driver' => 'imagick']);
|
||||
$manager = new ImageManager();
|
||||
$manager->make($tmpFile)->fit(100, 100)->save();
|
||||
|
||||
event(new AvatarWillBeSaved($user, $actor, $tmpFile));
|
||||
|
@@ -10,7 +10,8 @@
|
||||
|
||||
namespace Flarum\Core\Users\Listeners;
|
||||
|
||||
use Flarum\Core\Users\User;
|
||||
use Flarum\Core\Posts\Post;
|
||||
use Flarum\Core\Discussions\Discussion;
|
||||
use Flarum\Events\PostWasPosted;
|
||||
use Flarum\Events\PostWasDeleted;
|
||||
use Flarum\Events\PostWasHidden;
|
||||
@@ -39,7 +40,7 @@ class UserMetadataUpdater
|
||||
*/
|
||||
public function whenPostWasPosted(PostWasPosted $event)
|
||||
{
|
||||
$this->updateCommentsCount($event->post->user, 1);
|
||||
$this->updateCommentsCount($event->post, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,9 +48,7 @@ class UserMetadataUpdater
|
||||
*/
|
||||
public function whenPostWasDeleted(PostWasDeleted $event)
|
||||
{
|
||||
if ($event->post->user->exists) {
|
||||
$this->updateCommentsCount($event->post->user, -1);
|
||||
}
|
||||
$this->updateCommentsCount($event->post, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,7 +56,7 @@ class UserMetadataUpdater
|
||||
*/
|
||||
public function whenPostWasHidden(PostWasHidden $event)
|
||||
{
|
||||
$this->updateCommentsCount($event->post->user, -1);
|
||||
$this->updateCommentsCount($event->post, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,7 +64,7 @@ class UserMetadataUpdater
|
||||
*/
|
||||
public function whenPostWasRestored(PostWasRestored $event)
|
||||
{
|
||||
$this->updateCommentsCount($event->post->user, 1);
|
||||
$this->updateCommentsCount($event->post, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,7 +72,7 @@ class UserMetadataUpdater
|
||||
*/
|
||||
public function whenDiscussionWasStarted(DiscussionWasStarted $event)
|
||||
{
|
||||
$this->updateDiscussionsCount($event->discussion->startUser, 1);
|
||||
$this->updateDiscussionsCount($event->discussion, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,26 +80,34 @@ class UserMetadataUpdater
|
||||
*/
|
||||
public function whenDiscussionWasDeleted(DiscussionWasDeleted $event)
|
||||
{
|
||||
$this->updateDiscussionsCount($event->discussion->startUser, -1);
|
||||
$this->updateDiscussionsCount($event->discussion, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param Post $post
|
||||
* @param int $amount
|
||||
*/
|
||||
protected function updateCommentsCount(User $user, $amount)
|
||||
protected function updateCommentsCount(Post $post, $amount)
|
||||
{
|
||||
$user->comments_count += $amount;
|
||||
$user->save();
|
||||
$user = $post->user;
|
||||
|
||||
if ($user && $user->exists) {
|
||||
$user->comments_count += $amount;
|
||||
$user->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param Discussion $discussion
|
||||
* @param int $amount
|
||||
*/
|
||||
protected function updateDiscussionsCount(User $user, $amount)
|
||||
protected function updateDiscussionsCount(Discussion $discussion, $amount)
|
||||
{
|
||||
$user->discussions_count += $amount;
|
||||
$user->save();
|
||||
$user = $discussion->startUser;
|
||||
|
||||
if ($user && $user->exists) {
|
||||
$user->discussions_count += $amount;
|
||||
$user->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -32,6 +32,7 @@ use Flarum\Core\Support\Locked;
|
||||
use Flarum\Core\Support\VisibleScope;
|
||||
use Flarum\Core\Support\EventGenerator;
|
||||
use Flarum\Core\Support\ValidatesBeforeSave;
|
||||
use Flarum\Core\Exceptions\ValidationException;
|
||||
|
||||
/**
|
||||
* @todo document database columns with @property
|
||||
@@ -76,7 +77,7 @@ class User extends Model
|
||||
/**
|
||||
* An array of permissions that this user has.
|
||||
*
|
||||
* @var array|null
|
||||
* @var string[]|null
|
||||
*/
|
||||
protected $permissions = null;
|
||||
|
||||
@@ -149,6 +150,8 @@ class User extends Model
|
||||
{
|
||||
$user = new static;
|
||||
|
||||
$user->assertValidPassword($password);
|
||||
|
||||
$user->username = $username;
|
||||
$user->email = $email;
|
||||
$user->password = $password;
|
||||
@@ -225,6 +228,8 @@ class User extends Model
|
||||
*/
|
||||
public function changePassword($password)
|
||||
{
|
||||
$this->assertValidPassword($password);
|
||||
|
||||
$this->password = $password;
|
||||
|
||||
$this->raise(new UserPasswordWasChanged($this));
|
||||
@@ -232,6 +237,20 @@ class User extends Model
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate password input.
|
||||
*
|
||||
* @param string $password
|
||||
* @return void
|
||||
* @throws \Flarum\Core\Exceptions\ValidationException
|
||||
*/
|
||||
protected function assertValidPassword($password)
|
||||
{
|
||||
if (strlen($password) < 8) {
|
||||
throw new ValidationException(['password' => 'Password must be at least 8 characters']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the password attribute, storing it as a hash.
|
||||
*
|
||||
@@ -306,7 +325,7 @@ class User extends Model
|
||||
{
|
||||
$urlGenerator = app('Flarum\Http\UrlGeneratorInterface');
|
||||
|
||||
return $this->avatar_path ? $urlGenerator->toAsset('assets/avatars/'.$this->avatar_path) : null;
|
||||
return $this->avatar_path ? $urlGenerator->toAsset('avatars/'.$this->avatar_path) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -359,12 +378,38 @@ class User extends Model
|
||||
}
|
||||
|
||||
if (is_null($this->permissions)) {
|
||||
$this->permissions = $this->permissions()->lists('permission')->all();
|
||||
$this->permissions = $this->getPermissions();
|
||||
}
|
||||
|
||||
return in_array($permission, $this->permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the user has a permission that is like the given string,
|
||||
* based on their groups.
|
||||
*
|
||||
* @param string $match
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasPermissionLike($match)
|
||||
{
|
||||
if ($this->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_null($this->permissions)) {
|
||||
$this->permissions = $this->getPermissions();
|
||||
}
|
||||
|
||||
foreach ($this->permissions as $permission) {
|
||||
if (substr($permission, -strlen($match)) === $match) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification types that should be alerted to this user, according
|
||||
* to their preferences.
|
||||
@@ -576,6 +621,16 @@ class User extends Model
|
||||
return Permission::whereIn('group_id', $groupIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of permissions that the user has.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getPermissions()
|
||||
{
|
||||
return $this->permissions()->lists('permission')->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the relationship with the user's access tokens.
|
||||
*
|
||||
|
@@ -27,8 +27,23 @@ class RegisterLocales
|
||||
$this->manager = $manager;
|
||||
}
|
||||
|
||||
public function addLocale($locale, $name)
|
||||
{
|
||||
$this->manager->addLocale($locale, $name);
|
||||
}
|
||||
|
||||
public function addTranslations($locale, $file)
|
||||
{
|
||||
$this->manager->addTranslations($locale, $file);
|
||||
}
|
||||
|
||||
public function addJsFile($locale, $file)
|
||||
{
|
||||
$this->manager->addJsFile($locale, $file);
|
||||
}
|
||||
|
||||
public function addConfig($locale, $file)
|
||||
{
|
||||
$this->manager->addConfig($locale, $file);
|
||||
}
|
||||
}
|
||||
|
40
src/Events/ScopeEmptyDiscussionVisibility.php
Normal file
40
src/Events/ScopeEmptyDiscussionVisibility.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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\Events;
|
||||
|
||||
use Flarum\Core\Users\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* The `ScopeEmptyDiscussionVisibility` event
|
||||
*/
|
||||
class ScopeEmptyDiscussionVisibility
|
||||
{
|
||||
/**
|
||||
* @var Builder
|
||||
*/
|
||||
public $query;
|
||||
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
public $actor;
|
||||
|
||||
/**
|
||||
* @param Builder $query
|
||||
* @param User $actor
|
||||
*/
|
||||
public function __construct(Builder $query, User $actor)
|
||||
{
|
||||
$this->query = $query;
|
||||
$this->actor = $actor;
|
||||
}
|
||||
}
|
@@ -10,15 +10,27 @@
|
||||
|
||||
namespace Flarum\Events;
|
||||
|
||||
use Flarum\Api\Actions\Action;
|
||||
use Flarum\Api\JsonApiRequest;
|
||||
|
||||
class WillSerializeData
|
||||
{
|
||||
/**
|
||||
* @var Action
|
||||
*/
|
||||
public $action;
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
public $data;
|
||||
|
||||
/**
|
||||
* @var JsonApiRequest
|
||||
*/
|
||||
public $request;
|
||||
|
||||
public function __construct($action, &$data, $request)
|
||||
public function __construct(Action $action, &$data, JsonApiRequest $request)
|
||||
{
|
||||
$this->action = $action;
|
||||
$this->data = &$data;
|
||||
|
@@ -14,10 +14,24 @@ use Flarum\Core\Users\PasswordToken;
|
||||
use Flarum\Support\HtmlAction;
|
||||
use Flarum\Core\Exceptions\InvalidConfirmationTokenException;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use DateTime;
|
||||
|
||||
class ResetPasswordAction extends HtmlAction
|
||||
{
|
||||
/**
|
||||
* @var Factory
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
/**
|
||||
* @param Factory $view
|
||||
*/
|
||||
public function __construct(Factory $view)
|
||||
{
|
||||
$this->view = $view;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param array $routeParams
|
||||
@@ -33,6 +47,6 @@ class ResetPasswordAction extends HtmlAction
|
||||
throw new InvalidConfirmationTokenException;
|
||||
}
|
||||
|
||||
return view('flarum::reset')->with('token', $token->id);
|
||||
return $this->view->make('flarum::reset')->with('token', $token->id);
|
||||
}
|
||||
}
|
||||
|
@@ -13,24 +13,10 @@ namespace Flarum\Forum\Actions;
|
||||
use Flarum\Core\Users\PasswordToken;
|
||||
use Flarum\Core\Users\Commands\EditUser;
|
||||
use Flarum\Support\Action;
|
||||
use Illuminate\Contracts\Bus\Dispatcher;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
|
||||
class SavePasswordAction extends Action
|
||||
{
|
||||
/**
|
||||
* @var Dispatcher
|
||||
*/
|
||||
protected $bus;
|
||||
|
||||
/**
|
||||
* @param Dispatcher $bus
|
||||
*/
|
||||
public function __construct(Dispatcher $bus)
|
||||
{
|
||||
$this->bus = $bus;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param array $routeParams
|
||||
@@ -49,9 +35,8 @@ class SavePasswordAction extends Action
|
||||
return $this->redirectTo('/reset/'.$token->id); // TODO: Use UrlGenerator
|
||||
}
|
||||
|
||||
$this->bus->dispatch(
|
||||
new EditUser($token->user_id, $token->user, ['attributes' => ['password' => $password]])
|
||||
);
|
||||
$token->user->changePassword($password);
|
||||
$token->user->save();
|
||||
|
||||
$token->delete();
|
||||
|
||||
|
@@ -24,6 +24,7 @@ trait WritesRememberCookie
|
||||
SetCookie::create('flarum_remember', $token)
|
||||
->withMaxAge(14 * 24 * 60 * 60)
|
||||
->withPath('/')
|
||||
->withHttpOnly(true)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -35,6 +36,7 @@ trait WritesRememberCookie
|
||||
SetCookie::create('flarum_remember')
|
||||
->withMaxAge(-2628000)
|
||||
->withPath('/')
|
||||
->withHttpOnly(true)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -67,13 +67,13 @@ class ForumServiceProvider extends ServiceProvider
|
||||
);
|
||||
|
||||
$routes->get(
|
||||
'/d/{id:\d+(?:-[^/]*)?}[/{near}]',
|
||||
'/d/{id:\d+(?:-[^/]*)?}[/{near:[^/]*}]',
|
||||
'flarum.forum.discussion',
|
||||
$this->action('Flarum\Forum\Actions\DiscussionAction')
|
||||
);
|
||||
|
||||
$routes->get(
|
||||
'/u/{username}[/{filter}]',
|
||||
'/u/{username}[/{filter:[^/]*}]',
|
||||
'flarum.forum.user',
|
||||
$this->action('Flarum\Forum\Actions\ClientAction')
|
||||
);
|
||||
|
@@ -1,5 +1,4 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
@@ -80,18 +79,18 @@ class RouteCollection
|
||||
return $this->dataGenerator->getData();
|
||||
}
|
||||
|
||||
public function getPath($name, $parameters = [])
|
||||
protected function fixPathPart(&$part, $key, array $parameters)
|
||||
{
|
||||
if (is_array($part) && array_key_exists($part[0], $parameters)) {
|
||||
$part = $parameters[$part[0]];
|
||||
}
|
||||
}
|
||||
|
||||
public function getPath($name, array $parameters = [])
|
||||
{
|
||||
$parts = $this->reverse[$name][0];
|
||||
array_walk($parts, [$this, 'fixPathPart'], $parameters);
|
||||
|
||||
$path = implode('', array_map(function ($part) use ($parameters) {
|
||||
if (is_array($part)) {
|
||||
$part = $parameters[$part[0]];
|
||||
}
|
||||
return $part;
|
||||
}, $parts));
|
||||
|
||||
$path = '/' . ltrim($path, '/');
|
||||
return $path;
|
||||
return '/' . ltrim(implode('', $parts), '/');
|
||||
}
|
||||
}
|
||||
|
@@ -11,6 +11,8 @@
|
||||
|
||||
namespace Flarum\Http;
|
||||
|
||||
use Flarum\Core;
|
||||
|
||||
class UrlGenerator implements UrlGeneratorInterface
|
||||
{
|
||||
protected $routes;
|
||||
@@ -26,12 +28,11 @@ class UrlGenerator implements UrlGeneratorInterface
|
||||
$path = $this->routes->getPath($name, $parameters);
|
||||
$path = ltrim($path, '/');
|
||||
|
||||
// TODO: Prepend real base URL
|
||||
return "/$path";
|
||||
return Core::url() . "/$path";
|
||||
}
|
||||
|
||||
public function toAsset($path)
|
||||
{
|
||||
return "/$path";
|
||||
return Core::url() . "/assets/$path";
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user