1
0
mirror of https://github.com/flarum/core.git synced 2025-08-21 23:56:33 +02:00

Compare commits

..

265 Commits

Author SHA1 Message Date
Toby Zerner
d1c25a4bad Fix regression with full-screen composer being obscured by header/side pane
This is not ideal as dropdowns appear above the header, but it will probably be resolved when we redo the composer's full screen mode soon enough.
2016-03-29 18:24:23 +10:30
Toby Zerner
4b2f0c2d1a v0.1.0-beta.5 2016-03-29 18:02:12 +10:30
Toby Zerner
48be5ac2eb Prevent unapproved discussions from dropping to the bottom of the discussion list 2016-03-29 17:53:07 +10:30
Toby Zerner
0b3a4264a3 Use more precise regex to prevent some translations being compiled unnecessarily 2016-03-29 17:31:13 +10:30
Toby Zerner
76ea6f3695 Clean up unused code 2016-03-28 15:46:52 +10:30
Toby Zerner
7120ba2050 Add specific error message when an email address is not found in forgot password modal 2016-03-28 15:46:20 +10:30
Toby Zerner
ff77912dc6 Reconfigure z-index hierarchy: show dropdowns above post composer 2016-03-28 13:32:23 +10:30
Toby Zerner
53b32eda12 Tweak badge shadow radius 2016-03-28 10:25:47 +10:30
Toby Zerner
6d69e90662 Prevent long forum title in mobile drawer from entering viewport 2016-03-28 10:23:58 +10:30
Toby Zerner
589e903c71 Fix search box overlapping forum title in some cases. closes #697
- Fix jank in shrinking animation when search box loses focus after overlapping forum title.
- Use solid colors instead of transparent whites/blacks for colored header controls so that search box isn't transparent when it does overlap forum title.
- This also simplifies colored header variables, making them more analogous to the non-colored header variables, and allowing for the removal of some conditional CSS in the notifications dropdown button.

Some more radical changes to header layout (flexbox?) may be made when we implement the new mobile design (#867), but for now this is an acceptable fix.
2016-03-28 10:23:49 +10:30
Franz Liedke
4fe7acfddf Revert "Add a middleware for authentication with CGI wrap"
This reverts commit 685d5f1517.

This will now be dealt with at the Stratigility level.
2016-03-26 18:56:31 +09:00
Franz Liedke
685d5f1517 Add a middleware for authentication with CGI wrap
If the authorization header is stripped by CGI wrap,
the server can be configured to send the value along
in an environment variable. If the server admin sticks
to this convention, Flarum can now use this variable.

This is supposed to take care of #384.
2016-03-24 21:53:11 +09:00
Toby Zerner
a5c8ef0566 Tweak user email confirmation alert
- Make sure is_activated is serialized to a bool (otherwise "0" will evaluate to true)
- Remove "error" class from message so it's more friendly
- Make the alert more prominent by mounting it into a new div at the top of the page
- Add loading UX to the resend button
2016-03-23 22:17:42 +10:30
Franz Liedke
cb428f1e4a Make StyleCI happy 2016-03-23 19:54:04 +09:00
Toby Zerner
3d11309b35 Merge pull request #862 from sijad/confirm-msg
Show alert for unverified User
2016-03-23 21:19:02 +10:30
Sajjad Hasehmian
b13adfec84 Show alert for unverified User 2016-03-22 18:52:32 +04:30
Franz Liedke
b2b5789c25 info: Show commit hashes for Flarum core and extensions 2016-03-22 00:55:10 +09:00
Franz Liedke
673a78a203 info: Show loaded PHP extensions 2016-03-22 00:33:39 +09:00
Franz Liedke
31caced04c info: Show installation path 2016-03-22 00:29:58 +09:00
Franz Liedke
5d88ad2431 info: Show base URL 2016-03-22 00:28:02 +09:00
Franz Liedke
96a40fd6ea info: Print PHP version, too 2016-03-22 00:22:40 +09:00
Franz Liedke
77086c9be6 Travis: Do not run PhpUnit through Composer
We need to run PhpUnit with xDebug enabled in order to collect
code coverage information. It is disabled for performance reasons,
though: https://github.com/travis-ci/travis-ci/issues/5780.
2016-03-21 23:12:09 +09:00
Franz Liedke
3c629f091d Travis: Generate code coverage report when running tests 2016-03-21 20:21:51 +09:00
Toby Zerner
820752f61c Oops, back to Mithril 0.2.3! 2016-03-21 21:25:00 +10:30
Toby Zerner
67f3a4a5bf Merge pull request #844 from Luceos/codecov
added integration with codecov to track coverage of tests
2016-03-21 20:19:15 +10:30
Franz Liedke
cd4d669127 Make console command descriptions consistent 2016-03-20 23:16:08 +09:00
Franz Liedke
238f2fca73 Get rid of some repetition 2016-03-20 23:15:26 +09:00
Franz Liedke
7e33690660 Add first, basic version of info command
This will hopefully help in debugging some problems.
2016-03-20 23:12:20 +09:00
Franz Liedke
eef895c16f Composer: Sort dependencies alphabetically 2016-03-20 22:27:43 +09:00
Franz Liedke
2be964f8e2 Update to latest version of text-formatter 2016-03-20 22:25:45 +09:00
Toby Zerner
2f05a2d80b Merge pull request #882 from flarum/analysis-8PxnZR
Applied fixes from StyleCI
2016-03-20 20:37:03 +10:30
Toby Zerner
e6a001335d Applied fixes from StyleCI 2016-03-20 06:06:43 -04:00
Franz Liedke
4c03f13fef AbstractOAuth2Controller: Store provider and token in class properties
This way, they are available for subclasses to access them in one of
the template methods.

Refs #673.
2016-03-18 22:22:35 +09:00
Franz Liedke
588dd7b213 Fix JSON serialization error on PHP 7
Closes #685.

Thanks to @sijad.
2016-03-18 21:11:54 +09:00
Toby Zerner
1ca1639139 Extract sortMap variable
Also revert previous uncommitted change in dist file
2016-03-18 10:06:58 +10:30
Toby Zerner
476c1a5691 Prevent users from being incorrectly able to delete their own discussions 2016-03-18 09:39:41 +10:30
Toby Zerner
3b19fe3a33 Lighten discussion list hover color
When the list is shown in the side-pane, the background of the currently-selected discussion is the @control-bg. The hover color shouldn't be quite as strong as that.
2016-03-18 09:38:37 +10:30
Toby Zerner
65f2d84d55 Fix "sort by" dropdown being empty
Must be something in the latest version of Chrome that caused this to start being a problem, because @franzliedke started experiencing it a few days ago, and I only just experienced it for the first time yesterday.
2016-03-18 09:37:25 +10:30
Toby Zerner
cf63e063ba Fix regression with maintenance of scroll position when jumping between discussion list filters 2016-03-18 09:36:09 +10:30
Toby Zerner
cd6e6addf7 Remove unmaintained changelog
We will look at reintroducing once out of beta.
2016-03-18 09:34:48 +10:30
Toby Zerner
1395ce6c30 Upgrade to flarum-gulp 0.2.0 / Babel 6 2016-03-18 09:31:01 +10:30
Franz Liedke
05732be929 Merge pull request #858 from sijad/update-mithril
Update Mithril
2016-03-16 10:11:05 +09:00
Sajjad Hasehmian
5097d7f9a4 Update Mithril 2016-03-16 00:48:01 +03:30
Toby Zerner
0b3bc9f2ba Increase avatar upload max file size 2016-03-14 09:25:02 +10:30
Toby Zerner
8087d9ea47 Add missing super.init calls 2016-03-11 13:45:38 +10:30
Toby Zerner
d1c436c4d5 Dramatically improve performance when typing in a modal
Since Mithril doesn't really offer granular redraw control, typing in a text input on a modal would trigger a redraw for the whole page (including the page content behind the modal) on every keystroke. This commit allows components to be "paused" so that their vdom subtree will be retained instead of reconstructed on subsequent redraws. When a modal is opened, we pause the main page component, and when it's closed, we unpause it. This means that while a modal is visible, only the content inside of the modal will be redrawn, dramatically improving performance.
2016-03-11 13:18:16 +10:30
Toby Zerner
e37c7a9b06 Remove sudo mode and add password confirmation when changing email address
closes #674
2016-03-11 12:44:18 +10:30
Toby Zerner
dc757fae5f Remove white border from badges, decrease overlap 2016-03-11 12:01:47 +10:30
Toby Zerner
3b236dd66e Add padding between items in fieldsets on the settings page 2016-03-10 17:56:18 +10:30
Toby Zerner
e2e5ac8c0c Fix browser back button losing scroll position. ref #835 2016-03-10 17:55:35 +10:30
Toby Zerner
beb2f91fef Fix posts being incorrectly visible on user page. closes #680
- When no discussions are visible, the query that filters posts by discussion visibility was incorrectly making all posts visible.
- Also hide user profiles altogether if discussions are not visible.
2016-03-10 17:50:29 +10:30
Toby Zerner
2391471937 Clean up linting stuff. closes #852 2016-03-10 17:13:30 +10:30
Franz Liedke
f631b98df6 Whoopsie, fix syntax error 2016-03-08 00:05:53 +09:00
Franz Liedke
01cb5c4478 Add another migration shortcut for defining default settings 2016-03-08 00:02:33 +09:00
Toby Zerner
fc517ca94d Merge pull request #846 from sijad/extension-path
Remove 'extensions' path for writable check
2016-03-04 17:04:01 +10:30
Sajjad Hasehmian
393fa67d2d Remove 'extensions' path for writable check 2016-03-04 09:55:40 +03:30
Daniel Klabbers
cb6ac9e9e2 added integration with codecov to track coverage of tests 2016-03-03 11:50:09 +01:00
Toby Zerner
7d2f24bb47 Merge pull request #843 from Luceos/add-tests
adding new tests to cover api handlers
2016-03-03 20:53:15 +10:30
Daniel Klabbers
5a7b57df96 adding new tests to cover api handlers, part 1 of #245 and #74 2016-03-03 11:00:11 +01:00
Toby Zerner
a75a76e95b Fix fatal error when deleting a discussion forever. closes #842 2016-03-03 12:52:53 +10:30
Toby Zerner
639f5c0114 Merge pull request #841 from Luceos/drop-ext-dir
Refactoring to drop extensions dir, see #774
2016-03-02 18:46:08 +10:30
Daniel Klabbers
15c0a8c2db Refactoring to drop extensions dir, see #774
satisfy nitpick
2016-03-02 09:04:10 +01:00
Toby Zerner
1b5b91c85b Merge pull request #840 from flarum/analysis-z4xEVE
Applied fixes from StyleCI
2016-03-01 14:21:22 +10:30
Toby Zerner
5d5f47aab2 Applied fixes from StyleCI 2016-02-29 22:51:13 -05:00
Toby Zerner
24713733fc Don't require a previous Post when saving event posts
A bit of an edge-case since it shouldn't really be possible to have a discussion with zero posts anymore, but when renaming an empty discussion (or taking any action that will create an "event post"), Flarum would crash. This is due to the MergeableInterface requiring these posts to be saved after a previous post.
2016-02-29 18:50:27 +10:30
Toby Zerner
56b39f9fba Fix crash when sending notification to non-existent user
When renaming a discussion, an attempt is made to send a notification to the discussion's author. However, there is no check to see if the user account still exists - this can lead to a crash. While the check should technically be in the initiating code, it will probably slip through the cracks in other scenarios/extensions, so it's probably best that we safe-guard against this in the NotificationSyncer itself.
2016-02-29 18:48:02 +10:30
Toby Zerner
cdbc4b9717 Fix regressions related to deleting posts
- On the front-end, correct the check to see if the discussion has no more posts
- On the back-end, run a query to count the posts instead of using the comments_count, because the comments_count does not include other deleted posts
2016-02-29 18:41:59 +10:30
Franz Liedke
594a2ba8cc More indentation cleanup 2016-02-26 13:10:32 +09:00
Toby Zerner
445517ee84 Use regex for username validation
Laravel's alpha_dash rule allows unicode letters including those with inflections, leading to issues like #832. As per discussion in #557, we are sticking with ASCII-only usernames for now.
2016-02-26 13:59:05 +10:30
Franz Liedke
b4cf197cc6 Improve alignment of string 2016-02-26 12:20:37 +09:00
Toby Zerner
102db3c913 Simplify StyleCI config 2016-02-26 13:47:17 +10:30
Toby Zerner
0ccfad3931 Merge pull request #831 from flarum/analysis-qvQMPx
Applied fixes from StyleCI
2016-02-26 13:40:39 +10:30
Toby Zerner
a6cf10f854 Applied fixes from StyleCI 2016-02-25 22:09:39 -05:00
Toby Zerner
83c22d73a4 Fix StyleCI misconfiguration error
> The provided fixer 'unalign_double_arrow' cannot be disabled unless it was already enabled by your preset.
2016-02-26 13:36:19 +10:30
Toby Zerner
952b4693da Add StyleCI config 2016-02-26 13:35:09 +10:30
Toby Zerner
c7b6426fd4 Delete a discussion when its last post is deleted. fixes #823 2016-02-26 13:26:09 +10:30
Toby Zerner
acdb1ff749 Revert #687 + #197. fixes #785
Unfortunately we have no way to calculate the number of comment posts that are previous to the current viewing position of the discussion, without loading all of the posts which is going to be too expensive (even if we do it selectively somehow).
2016-02-26 13:11:52 +10:30
Toby Zerner
50e56ac0a1 Recompile admin JS 2016-02-26 12:50:03 +10:30
Toby Zerner
82fc4dd483 Refactor Composer rendering for smoother animations
Also fixes a couple of miscellaneous bugs:
- Minimise the Composer when clicking the preview button in full-screen mode on desktop.
- Minimise the Composer when clicking the link to the discussion/post in the header on mobile/full-screen mode.
2016-02-26 12:49:49 +10:30
Franz Liedke
5390187a4f Just a tad of cleanup 2016-02-25 23:29:55 +09:00
Daniel Klabbers
e4412178b1 refactoring to support array closures migrations and fixed issues with previous pr for extension rewriting 2016-02-25 23:26:10 +09:00
Franz Liedke
2b5dab73f9 Use the new migration shortcuts in most of core's migrations 2016-02-25 00:50:54 +09:00
Franz Liedke
db7a03fbe5 Add some handy shortcuts for typical migration tasks
This will make it much easier for extension developers (and also less
error-prone) to create migrations for things like creating tables,
renaming columns and so on...
2016-02-25 00:50:03 +09:00
Franz Liedke
ad95a44e7d Remove obsolete AbstractMigration class 2016-02-24 23:22:52 +09:00
Franz Liedke
59613910b1 Update generate:migration command to deal with new migration structure 2016-02-24 23:20:33 +09:00
Franz Liedke
13fe162db3 Add two missing copyright headers 2016-02-24 22:25:09 +09:00
Franz Liedke
51955504aa Revamp migration structure
They are now simply files that return an array of closures, for
running the named "up" and "down" actions, respectively.

Related to #732.
2016-02-24 22:23:49 +09:00
Toby Zerner
05fe4446bf Fix crash when displaying a discussion with no posts. closes #823 2016-02-22 22:22:49 +10:30
Toby Zerner
71d2e71908 Condense into value/oninput into bidi 2016-02-22 21:22:18 +10:30
Toby Zerner
93f3f22623 Merge pull request #811 from sijad/firefox-fix
Fix login box autocomplete in firefox
2016-02-22 21:09:18 +10:30
Franz Liedke
ff69dade15 Merge pull request #817 from flarum/revert-813-typehint
Revert "typehint fix, opening for implementation"
2016-02-18 17:35:17 +01:00
Franz Liedke
17851c4dfe Revert "typehint fix, opening for implementation" 2016-02-18 17:33:34 +01:00
Franz Liedke
46dfdf2deb Merge pull request #813 from Luceos/typehint
typehint fix, opening for implementation
2016-02-17 16:12:15 +01:00
Daniel Klabbers
d944a9e618 typehint fix, opening for implementation 2016-02-17 13:34:13 +01:00
Sajjad Hasehmian
2143a96c19 Fix login box autocomplete 2016-02-16 21:08:45 +03:30
Toby Zerner
d7fe3ca35b Merge pull request #787 from sijad/401-page
401 for unauthorised request to settings, notifications page
2016-02-15 21:04:39 +10:30
Daniël Klabbers
48e29ed168 Merge pull request #801 from Luceos/extension_fix
Extension fix
2016-02-14 22:18:08 +01:00
Daniel Klabbers
0ad4c0ac61 fixes #800, forgot these controllers 2016-02-13 20:33:33 +01:00
Daniel Klabbers
458f4f811c fixes #799, now properly assigning a id 2016-02-13 20:32:46 +01:00
Sajjad Hasehmian
e90dfe04fd 401 for unauthorised request to settings, notifications page fixes #714 2016-02-11 09:59:01 +03:30
Daniel Klabbers
191589e2b1 Implemented extensions as an object, usable by backend and frontend. 2016-02-10 15:13:51 +01:00
Franz Liedke
96c4e6b147 Merge pull request #786 from Luceos/imports
reordering and removing unused imports
2016-02-10 15:02:37 +01:00
Franz Liedke
d15a9dc0f0 Avoid use of model class in migration
See commit 0831256
2016-02-10 14:17:38 +01:00
Franz Liedke
08312568ba Installer: Fix models not being ready for use when running migrations
This was a regression after the recent introduction of a new migration that actually uses models.
Maybe we should change this.

See https://discuss.flarum.org/d/2023-can-t-manage-to-install-the-development-version-503-service-unavailable/8
2016-02-10 14:07:29 +01:00
Daniel Klabbers
31be2f8f86 reordering and removing unused imports 2016-02-10 11:00:37 +01:00
Toby Zerner
89598646c1 Merge pull request #784 from sijad/no-confirm
Remove "Mark as Read" confirmation fixes #782
2016-02-10 18:13:38 +10:30
Sajjad Hasehmian
b3035c18b6 Remove "Mark as Read" confirmation fixes #782 2016-02-10 10:50:24 +03:30
Toby Zerner
235c265c06 Merge pull request #779 from sijad/uri-fix
Correct redirect uri in OAuth2 Controller
2016-02-10 07:06:14 +10:30
Sajjad Hasehmian
f1a1a7a806 Correct redirect uri in OAuth2 Controller (fixes #778) 2016-02-09 18:01:59 +03:30
Toby Zerner
dfef3c1ff1 Slightly widen index sidebar, overflow buttons properly
First half of #349 fix. Supersedes #734 (190px wide instead of 200px, correctly modify margin-left of .sideNavOffset, more descriptive commit message)
2016-02-07 12:10:02 +10:30
Toby Zerner
fb09cef540 Merge pull request #748 from JoshyPHP/Minifiers
Added support for new minifiers
2016-02-07 11:37:15 +10:30
Toby Zerner
24ed2c0d8f Update Mithril 2016-02-06 18:58:34 +10:30
Toby Zerner
173f88da92 Better post scrubber size calculations. fixes #109 2016-02-06 18:47:09 +10:30
Franz Liedke
9ecb5f437a Use stored slug for generating server-rendered link to discussion
Fixes #646.
2016-02-04 11:47:03 +01:00
Franz Liedke
97979b2189 Store discussion slug in database table
In preparation for #646.
2016-02-04 11:46:30 +01:00
Toby Zerner
efff4c1801 Add priorities to user page sidebar items 2016-01-31 17:11:13 +10:30
Toby Zerner
2018e424ec Refactor ListPostsController, make filtering extensible
It became apparent in https://github.com/flarum/core/issues/319#issuecomment-170558573 that there was no way for extensions to add filter parameters to the /api/posts endpoint (e.g. /api/posts?filter[mentioned]=1). Simply adding an event to modify the `$where` array severely limits how much can be done with the query. This commit refactors the controller so that filters are applied directly to the query Builder, and exposes the Builder in a new `ConfigurePostsQuery` event.
2016-01-31 17:06:38 +10:30
Toby Zerner
36ad4a8554 Fix fatal error
"PHP Fatal error:  Cannot use Symfony\Component\Translation\Translator as Translator because the name is already in use"
2016-01-31 15:35:53 +10:30
Franz Liedke
3581fe8d1e No sudo 2016-01-28 08:06:33 +01:00
Franz Liedke
90ce0fa521 Travis: Make sure Composer is up-to-date. 2016-01-28 08:06:20 +01:00
Franz Liedke
63b5cd0812 Travis: Update Xdebug removal code 2016-01-28 07:59:04 +01:00
Franz Liedke
2a3240b9d1 Travis: Use pre-installed Composer
I also disabled the XDebug extension for the PHP runtime, which should
improve Composer runtime considerably. This is what Composer itself does.
2016-01-20 22:22:13 +01:00
Franz Liedke
e0790de2e5 Update extension skeleton
Closes #743.
2016-01-20 22:14:08 +01:00
Franz Liedke
c99c83435b Fix path to extension stub directory
Refs #743.
2016-01-20 22:01:01 +01:00
Franz Liedke
c8f2d94558 Fix obsolete import 2016-01-20 21:38:14 +01:00
Franz Liedke
c842fa0184 Hardcode primary keys during installation
This avoids misleading assumptions about automatically generated primary keys
in some cases.

Fixes #566.
2016-01-20 21:36:50 +01:00
Toby Zerner
ad2bbdd115 Tweak padding on user dropdown button so avatar is flush with border radius 2016-01-19 19:19:16 +10:30
Toby Zerner
db06b8c71a Fix mistake in previous commit 2016-01-19 19:07:06 +10:30
Toby Zerner
3cec7e8b46 Patch Mithril bug causing redraws to fail
Turns out there's a little more to the regression in e5a7013. First, we need to give the spaces in between list items a key too. Second, there's a bug in the latest Mithril code where using string keys can break the diffing algorithm. I've patched it manually in our dist JS files for now, and reported the issue: https://github.com/lhorie/mithril.js/issues/934
2016-01-19 18:55:57 +10:30
Toby Zerner
60d78cedef Update bower dependencies, fix redraw regression
- In Mithril, `finally` has been removed from promise objects as it is not part of the ES spec. See https://gist.github.com/jish/e9bcd75e391a2b21206b for info on the substitute.
- Fix a regression introduced in e5a7013 which broke some redraws
2016-01-19 17:59:19 +10:30
Toby Zerner
2980c94247 Add Composer branch-alias
This allows installations to require version 0.1.0 with minimum-stability=dev, and they will get the latest from master.

See #727
2016-01-19 17:00:10 +10:30
Toby Zerner
9b5ec9d7ba Commit latest dist files
See https://github.com/flarum/core/issues/727#issuecomment-172384020
2016-01-19 16:52:01 +10:30
Toby Zerner
f17f0b5278 Merge pull request #752 from dcsjapan/ext-instructions
Extract translations for the Add Extension modal
2016-01-19 12:20:54 +10:30
dcsjapan
be924c4fa0 Extract translations for the Add Extension modal
- Extracts three translations for this placeholder dialog.
- Adds a forum link to one of the translations.
2016-01-19 10:16:07 +09:00
Toby Zerner
285e397d05 Remove hack to make tag permissions work
Since we now grant these global permissions if the user has the respective permission for any individual tags.
2016-01-16 14:07:13 +10:30
Toby Zerner
2e27d5938a Merge branch 'master' of https://github.com/flarum/core 2016-01-16 13:57:17 +10:30
Toby Zerner
be013c6db0 Check permission through the gate rather than directly on the actor 2016-01-16 13:57:05 +10:30
Toby Zerner
dfc0cf53b0 Give GetPermission event priority when determining permissions 2016-01-16 13:56:37 +10:30
JoshyPHP
09ad4a180b Added support for new minifiers 2016-01-15 16:59:56 +01:00
Franz Liedke
194f304752 Merge pull request #720 from Albert221/permission-denied-fix
#719 Fixed PermissionDeniedException
2016-01-13 12:31:38 +01:00
Toby Zerner
aaab2cc86e Clear search when input is empty and enter is pressed. fixes #650 2016-01-13 10:06:04 +10:30
Toby Zerner
ba7fba9015 Fix/clean up created gambit
$matches indices were incorrect.
2016-01-13 10:03:26 +10:30
Toby Zerner
4ec108f28a Merge branch 'created-gambit' of https://github.com/Albert221/core 2016-01-13 09:53:24 +10:30
Toby Zerner
e5a7013c2c Key item lists to maintain identity across redraws
Fixes #667. This issue was due to the fact that Mithril would change the "Lock" badge into a "Sticky" badge, but the tooltip initialization would not be triggered because it was using the same element. By maintaining element identity, the "Lock" badge will remain untouched, and a new element for the "Sticky" badge will be inserted before it. See https://lhorie.github.io/mithril/mithril.html#dealing-with-focus for more information.
2016-01-13 09:34:12 +10:30
Toby Zerner
df2a199b48 Merge pull request #741 from Albert221/prefix-fix
UrlGenerator prefix fix.
2016-01-13 08:17:10 +10:30
Albert221
b123e435ff Unified two URL prefix variables into one 2016-01-12 22:07:47 +01:00
Albert
17da649d0a Merge pull request #2 from flarum/master
Update
2016-01-12 22:04:03 +01:00
Toby Zerner
1e33ca4111 Merge branch 'replay-animation' of https://github.com/sijad/core 2016-01-12 19:14:07 +10:30
Toby Zerner
8506d095db Use correct directory in loadLanguagePackFrom API 2016-01-12 18:35:37 +10:30
Toby Zerner
94a62293eb Extract Google font import to a head string, make overideable
Allowing headStrings to be named is a bit of a stopgap solution. Really ClientView needs to be given much more power with headStrings and footStrings as separate objects, similar to the ItemList in the JS app.
2016-01-12 18:29:21 +10:30
Sajjad Hasehmian
02bcb0f898 Add flash animation when scrolling to post preview fixes #666 🤘 2016-01-12 10:58:19 +03:30
Toby Zerner
98ea4d1e71 Merge pull request #735 from bogdanteodoru/master
#679 Ask for confirmation before "Mark all as Read"
2016-01-12 17:09:26 +10:30
Bogdan Teodoru
5120d9577e #679 Ask for confirmation before "Mark all as Read" 2016-01-12 08:23:02 +02:00
Franz Liedke
23eaee6b16 Merge pull request #731 from sijad/bio-nofollow
Add rel="nofollow" to bio links (fixes #449)
2016-01-11 11:09:52 +01:00
Sajjad Hasehmian
15398fcc6d Add rel="nofollow" to bio links (fixes #449) 2016-01-11 13:29:01 +03:30
Franz Liedke
bd1d05ee2c #717: Implement helper for registering a language pack 2016-01-11 08:46:20 +01:00
Franz Liedke
4a6137fdb1 Remove Studio hack 2016-01-11 08:38:30 +01:00
Franz Liedke
537ab6e41f Remove empty line 2016-01-11 08:15:14 +01:00
Franz Liedke
ace4bcf7d8 Merge pull request #730 from Luceos/remove_path_forum
removed patch from api routes, fixes #725
2016-01-11 08:14:18 +01:00
Daniel Klabbers
159810c335 removed patch from api routes, fixes #725 2016-01-11 08:09:01 +01:00
Franz Liedke
b7120fb176 Merge pull request #729 from bogdanteodoru/master
#679 Ask for confirmation before "Mark all as Read"
2016-01-10 20:58:54 +01:00
Bogdan Teodoru
1f5219f2a2 #679 Ask for confirmation before "Mark all as Read" 2016-01-10 17:20:01 +02:00
Albert221
e8a6fe2f7b #719 Fixed PermissionDeniedException
...causing Whoops on debug and 500 HTTP error
instead of 403 Forbidden error page.
2016-01-07 19:09:57 +01:00
Albert
1a2cc6a603 Merge pull request #1 from flarum/master
Update
2016-01-07 19:05:41 +01:00
Franz Liedke
417b7f7972 Clarify console option 2016-01-07 16:32:01 +01:00
Franz Liedke
9e3771cac3 Clean up code in FileDataProvider 2016-01-07 16:31:21 +01:00
Franz Liedke
819728d8dd Merge pull request #718 from opi/install-from-config-file
Add configuration file installation method.
2016-01-07 16:29:34 +01:00
opi
e3c7f5379b Add configuration file installation method. 2016-01-07 15:20:41 +01:00
Toby Zerner
41ccade385 Merge pull request #706 from Albert221/prefixes
#696 Added support for prefixes in AbstractUrlGenerator.
2016-01-07 12:43:47 +10:30
Albert221
6d42bcb5ce 256 Added created gambit 2016-01-05 17:04:41 +01:00
Albert221
096aae7919 #696 Added support for prefixes in AbstractUrlGenerator. 2016-01-04 15:28:55 +01:00
Toby Zerner
5bbcba6332 Allow existing user to be activated via API 2016-01-04 15:43:23 +10:30
Toby Zerner
b671c3ccfa Merge pull request #703 from Albert221/master
#256 Added multiple author search gambit
2016-01-04 11:40:16 +10:30
Albert221
9d89d8a127 Fixed code style 2016-01-03 14:30:35 +01:00
Albert221
6dfe455fd6 #256 Added multiple author search gambit 2016-01-03 14:26:41 +01:00
Toby Zerner
1f2eaea960 Merge pull request #701 from maelsoucaze/patch-1
Update year range in LICENSE
2016-01-02 17:04:54 +10:30
Maël Soucaze
b2ec380d4c Update year range in LICENSE
Because some changes have been done on that year.
2016-01-02 07:08:58 +01:00
Toby Zerner
08dbc246dd Clean up 2016-01-02 15:26:05 +10:30
Toby Zerner
3767ee4bf6 Allow admins to set a time when creating a post via the API
Again, the use-case for this is to allow the API to be used to import data from an old forum.
2016-01-02 15:25:48 +10:30
Toby Zerner
248de34242 Don't automatically activate users created by admins - require an attribute to be set 2016-01-02 15:24:35 +10:30
Toby Zerner
8d671f4de4 Make sure GetPermission event arguments array is empty if there is no model 2016-01-02 15:23:48 +10:30
Toby Zerner
6de7038f83 Allow setting the token lifetime 2016-01-02 15:22:53 +10:30
Toby Zerner
07a20a10fd Move flood control from core to API layer
This means that flood control can be disabled depending on the nature of the request (i.e. when authenticated using a master API key). The particular use case for this is to allow using the API to migrate data from an old forum.
2016-01-02 15:22:16 +10:30
Toby Zerner
c8027d344a Add admin-only email: gambit to look up users by email 2016-01-02 15:09:56 +10:30
Toby Zerner
f7709aff95 Allow custom redirection after logging out 2016-01-02 15:08:50 +10:30
Toby Zerner
46818ccd94 Extend access token lifetime when remembering a login 2016-01-02 15:08:28 +10:30
Toby Zerner
f6f9e45085 Disable session (and thus enable sudo mode) when authenticating with API token 2016-01-02 15:07:33 +10:30
Toby Zerner
ff0ce09620 Ensure routes are only populated after extensions have registered listeners
Because extensions can have dependencies injected, a RouteCollection could potentially be instantiated, and thus the ConfigureRoutes event would be called before extensions have had a chance to subscribe to it. Instead, we instantiate the RouteCollection on demand, but only populate it when the application boots.
2016-01-02 15:03:11 +10:30
Toby Zerner
e86cc39f5b API: Add an event to configure server middleware 2016-01-02 15:00:07 +10:30
Toby Zerner
a719d4109f Ensure a new asset revision identifier is generated if there is none 2016-01-02 14:59:09 +10:30
Toby Zerner
1aaf588341 Merge branch 'scrubber-display-only-comments' of https://github.com/ahsanity/core 2016-01-02 12:04:04 +10:30
Toby Zerner
0fcc8dca46 Merge pull request #676 from petermein/user-online-indicators
User online indicators
2016-01-02 09:34:11 +10:30
Toby Zerner
5a4e3b09cf Allow extensions to modify text/XML prior to formatting 2015-12-30 15:27:34 +10:30
Toby Zerner
bf87518161 Use username helper when displaying user search results 2015-12-30 15:26:54 +10:30
Toby Zerner
08dae7b530 Add getters 2015-12-30 15:26:24 +10:30
Toby Zerner
aa516fb5c3 Extract method 2015-12-30 15:26:11 +10:30
Toby Zerner
1cac48f90a Always grant master API keys sudo mode 2015-12-30 15:26:07 +10:30
Toby Zerner
5e476fae16 Merge branch 'oauth2-controller' 2015-12-29 11:13:00 +10:30
Toby Zerner
341ffaced5 Bypass email activation when admin creates user via API 2015-12-29 11:02:07 +10:30
Franz Liedke
595d715b1d Installer: Loosen restrictions on MySQL connection details
Closes #602.
2015-12-27 17:31:42 +01:00
Peter Mein
8c8de8eb22 Fixed name to camel case 2015-12-26 13:06:58 +01:00
Peter Mein
5431a90dbd Changed case on helper function
Stub for renaming case of file
2015-12-26 13:06:31 +01:00
Ahsanul Bari
7a8c7518bd Issue #197: Make PostStreamScrubber display numbers relating to only comment posts 2015-12-25 13:01:42 +06:00
Toby Zerner
08f0425c43 Merge pull request #690 from Luceos/phpdoc
fixes flarum/core#678 phpdoc for ip_address on Post model
2015-12-24 10:11:23 +10:30
Daniel Klabbers
ffb76715f6 fixes flarum/core#678 phpdoc for ip_address on Post model 2015-12-23 13:54:58 +01:00
Toby Zerner
9cb45c98d8 Extract notification settings into an item list 2015-12-21 10:38:15 +10:30
Franz Liedke
e0db5823ee Merge pull request #684 from ahsanity/settings-migration
Converted 'settings' table 'value' column from BLOB to TEXT
2015-12-18 13:45:20 +01:00
Ahsanul Bari
46f7f6b3fe Issue#669: Convert 'settings' table 'value' column to TEXT instead of BLOB 2015-12-18 02:25:50 +06:00
Peter Mein
fbcd2cf88c Added missing import 2015-12-16 13:48:38 +01:00
Peter Mein
e55b7a14e5 Added user online indicator to post 2015-12-16 13:43:46 +01:00
Franz Liedke
32601d2c98 Don't return from inside a finally block
This is not supported in HHVM:
https://github.com/facebook/hhvm/issues/5162

Reported on the forum:
https://discuss.flarum.org/d/1390-migrating-from-php-5-6-x-to-php-7-0-x/7
2015-12-10 11:35:51 +01:00
Toby Zerner
d9d52dab3c Fix admin login 2015-12-06 08:47:51 +10:30
Toby Zerner
d743e56bc1 Fix tests and CS 2015-12-05 22:31:33 +10:30
Toby Zerner
0cf000122f Allow username capitalisation to be changed
See https://discuss.flarum.org/d/1573-uppercase-lowercase-username-flagged-as-taken
2015-12-05 15:43:40 +10:30
Toby Zerner
973ca16eee Add base OAuth2 controller 2015-12-05 15:25:10 +10:30
Toby Zerner
262dc70fe1 Garbage-collect email/password/auth tokens. closes #217 2015-12-05 15:24:05 +10:30
Toby Zerner
3efd5fbcb0 Clean up some method arguments 2015-12-05 15:22:42 +10:30
Toby Zerner
c97b01a445 Log in immediately after registration
Newly-created accounts are allowed to log in straight away, but they still have the permissions of a guest until they've confirmed their email address. Instead of showing a success message after registration, we reload the page since they're already logged in.

Still todo: show a message explaining that they need to verify their email address to do anything, and allow it to be resent.
2015-12-05 15:22:25 +10:30
Toby Zerner
b0b3af0305 Improve LoginButton styles, make popup window smaller 2015-12-05 15:19:24 +10:30
Toby Zerner
387109002e Rework sessions, remember cookies, and auth again
- Use Symfony's Session component to work with sessions, instead of a custom database model. Separate the concept of access tokens from sessions once again.
- Extract common session/remember cookie logic into SessionAuthenticator and Rememberer classes.
- Extract AuthenticateUserTrait into a new AuthenticationResponseFactory class.
- Fix forgot password process.
2015-12-05 15:11:25 +10:30
Toby Zerner
1d9e7b0262 Fix case-sensitive class names 2015-12-03 18:29:00 +10:30
Toby Zerner
094ad74abc Allow forum to be taken offline via config 2015-12-03 17:56:27 +10:30
Toby Zerner
67e9e23df1 Fix previous commit 2015-12-03 17:56:04 +10:30
Toby Zerner
1cfae4ad14 Merge branch 'sudo-mode'
# Conflicts:
#	CHANGELOG.md
2015-12-03 15:12:51 +10:30
Toby Zerner
9896378b59 Overhaul sessions, tokens, and authentication
- Use cookies + CSRF token for API authentication in the default client. This mitigates potential XSS attacks by making the token unavailable to JavaScript. The Authorization header is still supported, but not used by default.
- Make sensitive/destructive actions (editing a user, permanently deleting anything, visiting the admin CP) require the user to re-enter their password if they haven't entered it in the last 30 minutes.
- Refactor and clean up the authentication middleware.
- Add an `onhide` hook to the Modal component. (+1 squashed commit)
2015-12-03 15:11:57 +10:30
Toby Zerner
287ce2fddd Fix crash when loading notifications in some instances
Specifically, the crash would occur when the first notification had a subject without a discussion relationship (e.g. the Subscriptions extension's newPost notification, where the subject itself was a discussion). Instead of simply eager loading the nested subject.discussion relationship, we load discussions manually instead.
2015-12-03 15:10:05 +10:30
Toby Zerner
cea1cbc2d6 Fuzzy-match global forum permissions
This means that the "Start a Discussion" button will still be enabled if the user is not allowed to start globally, but only in certain tags.

Also add some other stuff to the changelog.

closes #640
2015-12-03 15:08:28 +10:30
Toby Zerner
b9148364fa Various user interface tweaks 2015-12-03 15:02:52 +10:30
Toby Zerner
2ba890c239 Fix notifications icon/badge color for dark header 2015-12-03 15:02:29 +10:30
Toby Zerner
55e80f135d Tweak admin side-pane styles
Position the side-pane absolutely when scrolled to the top so that it does not disjoin from the header in Safari.
2015-12-03 15:02:07 +10:30
Toby Zerner
81a1c0955b Fix some issues with dropdown positioning 2015-12-03 14:51:55 +10:30
Toby Zerner
05386b1259 Clean up 2015-12-03 14:51:35 +10:30
Toby Zerner
d96e57eabb Truncate long title controls on mobile 2015-12-01 11:48:54 +10:30
Toby Zerner
173de809b8 Merge pull request #648 from dcsjapan/adjust-key
Add third-level namespacing to deleted_user_text
2015-11-30 15:28:39 +10:30
dcsjapan
c432ed7d5c Add third-level namespacing to deleted_user_text 2015-11-30 11:17:11 +09:00
Toby Zerner
172fffd1ed Merge pull request #645 from dcsjapan/leftover-translations
Extract leftover strings
2015-11-28 18:54:27 +10:30
dcsjapan
4bfbf68bca Extract leftover strings
Extracts strings that were missed previously in:
- Dashboard page of admin interface.
- Edit Custom CSS modal of admin interface.
- Settings modal of admin interface.
- Post activity list on user page of forum UI.
Hopefully there aren't any more!
2015-11-28 17:14:22 +09:00
Toby Zerner
cd411a0c6b Merge pull request #644 from dcsjapan/update-locale-template
Update locale file template
2015-11-28 17:33:53 +10:30
dcsjapan
7f05d9dce3 Update locale file template
Adjusts comments to match current english locale files.
2015-11-28 15:55:21 +09:00
Franz Liedke
b3a5822ddb Rename HTTP method override header
This is the name recommended by the JSON-API spec:
http://jsonapi.org/recommendations/#patchless-clients
2015-11-26 17:43:32 +01:00
Toby Zerner
a1e1635019 Update changelog 2015-11-26 10:43:48 +10:30
Toby Zerner
1cc5e1cb26 Merge pull request #642 from binaryoung/master
Fixed #627
2015-11-26 10:32:36 +10:30
young
a80d72d165 Fix #627 2015-11-26 02:03:00 +08:00
Toby Zerner
153a82e937 cs fix 2015-11-23 14:18:56 +10:30
Toby Zerner
262a934747 Prevent error if no input is given in create actions 2015-11-23 14:15:30 +10:30
Toby Zerner
a61929730e Validate avatar URL
Still needs refactor
2015-11-23 14:14:53 +10:30
Toby Zerner
ce02387ee4 Prevent crash if logged in user has been deleted 2015-11-23 11:54:30 +10:30
Toby Zerner
2c4fae60bc Allow provision of an avatar URL to upload during sign up
This can be used by authentication extensions (i.e. mirror Facebook/Twitter profile picture). Rough implementation, needs refactoring.
2015-11-23 11:53:57 +10:30
Toby Zerner
7eab206f91 Don't pad the body when the composer is positioned absolutely (on mobile) 2015-11-23 10:07:23 +10:30
Toby Zerner
599958354c Refactor composer preview logic 2015-11-23 08:47:16 +10:30
Toby Zerner
2088fceb8b Truncate long dropdown menu items (e.g. tags in the sidebar)
ref #391
2015-11-21 14:01:07 +10:30
Toby Zerner
5b25a77e82 Improve spacing of drawer elements 2015-11-21 13:21:27 +10:30
Toby Zerner
59c534a882 Tweak mobile drawer appearance 2015-11-21 13:16:46 +10:30
Toby Zerner
c79bda6279 Fix composer preview button on mobile. closes #196 2015-11-21 13:16:25 +10:30
Toby Zerner
6374f92676 Improve composer appearance/usability on mobile
On mobile:
- Move submit button to right side of toolbar
- Move first header item to toolbar
- Size textarea correctly
2015-11-21 13:16:05 +10:30
Toby Zerner
1f4e03d1fa Make sure dropdowns stay within the viewport horizontally too 2015-11-20 12:35:07 +10:30
Toby Zerner
acf67ca416 Add a "load more" button to the end of the post stream
This is necessary if the page is viewed in a context with no scrolling, i.e. an auto-resizing iframe
2015-11-20 12:35:07 +10:30
Toby Zerner
bd750ca154 Show "reply" action in discussion menu on mobile 2015-11-20 12:35:07 +10:30
Franz Liedke
61b09ac982 Update text-formatter dependency 2015-11-19 13:00:32 +01:00
Franz Liedke
6d895e6d77 Inject hardcoded prerequisite parameters
This affects version numbers, extensions and paths, which might be
skeleton-specific. This commit moves those hardcoded values out of
the classes and instead injects them through the constructor. This
way, all prerequisites can be configured in the service provider.
2015-11-11 19:30:35 +01:00
Franz Liedke
e199997231 Merge pull request #628 from binaryoung/patch-1
[beta4]Fixed login input fields have different style
2015-11-09 10:37:02 +01:00
young
095e8164e8 Update LogInModal.js 2015-11-06 15:54:06 +08:00
Franz Liedke
0bdf873e65 Fix another error handling regression 2015-11-05 14:17:48 +01:00
Franz Liedke
439b867dde Update version number 2015-11-05 09:58:05 +01:00
Toby Zerner
63d00e8b34 WIP sudo mode, better error responses 2015-11-05 16:17:00 +10:30
340 changed files with 11797 additions and 12266 deletions

View File

@@ -1,5 +0,0 @@
**/bower_components/**/*
**/node_modules/**/*
vendor/**/*
**/Gulpfile.js
**/dist/**/*

175
.eslintrc
View File

@@ -1,175 +0,0 @@
{
"parser": "babel-eslint", // https://github.com/babel/babel-eslint
"env": { // http://eslint.org/docs/user-guide/configuring.html#specifying-environments
"browser": true // browser global variables
},
"ecmaFeatures": {
"arrowFunctions": true,
"blockBindings": true,
"classes": true,
"defaultParams": true,
"destructuring": true,
"forOf": true,
"generators": false,
"modules": true,
"objectLiteralComputedProperties": true,
"objectLiteralDuplicateProperties": false,
"objectLiteralShorthandMethods": true,
"objectLiteralShorthandProperties": true,
"spread": true,
"superInFunctions": true,
"templateStrings": true,
"jsx": true
},
"globals": {
"m": true,
"app": true,
"$": true,
"moment": true
},
"plugins": [
"react"
],
"rules": {
"react/jsx-uses-vars": 1,
/**
* Strict mode
*/
// babel inserts "use strict"; for us
"strict": [2, "never"], // http://eslint.org/docs/rules/strict
/**
* ES6
*/
"no-var": 2, // http://eslint.org/docs/rules/no-var
"prefer-const": 2, // http://eslint.org/docs/rules/prefer-const
/**
* Variables
*/
"no-shadow": 2, // http://eslint.org/docs/rules/no-shadow
"no-shadow-restricted-names": 2, // http://eslint.org/docs/rules/no-shadow-restricted-names
"no-unused-vars": [2, { // http://eslint.org/docs/rules/no-unused-vars
"vars": "local",
"args": "after-used"
}],
"no-use-before-define": 2, // http://eslint.org/docs/rules/no-use-before-define
/**
* Possible errors
*/
"comma-dangle": [2, "never"], // http://eslint.org/docs/rules/comma-dangle
"no-cond-assign": [2, "always"], // http://eslint.org/docs/rules/no-cond-assign
"no-console": 1, // http://eslint.org/docs/rules/no-console
"no-debugger": 1, // http://eslint.org/docs/rules/no-debugger
"no-alert": 1, // http://eslint.org/docs/rules/no-alert
"no-constant-condition": 1, // http://eslint.org/docs/rules/no-constant-condition
"no-dupe-keys": 2, // http://eslint.org/docs/rules/no-dupe-keys
"no-duplicate-case": 2, // http://eslint.org/docs/rules/no-duplicate-case
"no-empty": 2, // http://eslint.org/docs/rules/no-empty
"no-ex-assign": 2, // http://eslint.org/docs/rules/no-ex-assign
"no-extra-boolean-cast": 0, // http://eslint.org/docs/rules/no-extra-boolean-cast
"no-extra-semi": 2, // http://eslint.org/docs/rules/no-extra-semi
"no-func-assign": 2, // http://eslint.org/docs/rules/no-func-assign
"no-inner-declarations": 2, // http://eslint.org/docs/rules/no-inner-declarations
"no-invalid-regexp": 2, // http://eslint.org/docs/rules/no-invalid-regexp
"no-irregular-whitespace": 2, // http://eslint.org/docs/rules/no-irregular-whitespace
"no-obj-calls": 2, // http://eslint.org/docs/rules/no-obj-calls
"no-reserved-keys": 2, // http://eslint.org/docs/rules/no-reserved-keys
"no-sparse-arrays": 2, // http://eslint.org/docs/rules/no-sparse-arrays
"no-unreachable": 2, // http://eslint.org/docs/rules/no-unreachable
"use-isnan": 2, // http://eslint.org/docs/rules/use-isnan
"block-scoped-var": 2, // http://eslint.org/docs/rules/block-scoped-var
/**
* Best practices
*/
"consistent-return": 2, // http://eslint.org/docs/rules/consistent-return
"curly": [2, "multi-line"], // http://eslint.org/docs/rules/curly
"default-case": 2, // http://eslint.org/docs/rules/default-case
"dot-notation": [2, { // http://eslint.org/docs/rules/dot-notation
"allowKeywords": true
}],
"eqeqeq": 2, // http://eslint.org/docs/rules/eqeqeq
"no-caller": 2, // http://eslint.org/docs/rules/no-caller
"no-else-return": 2, // http://eslint.org/docs/rules/no-else-return
"no-eq-null": 2, // http://eslint.org/docs/rules/no-eq-null
"no-eval": 2, // http://eslint.org/docs/rules/no-eval
"no-extend-native": 2, // http://eslint.org/docs/rules/no-extend-native
"no-extra-bind": 2, // http://eslint.org/docs/rules/no-extra-bind
"no-fallthrough": 2, // http://eslint.org/docs/rules/no-fallthrough
"no-floating-decimal": 2, // http://eslint.org/docs/rules/no-floating-decimal
"no-implied-eval": 2, // http://eslint.org/docs/rules/no-implied-eval
"no-lone-blocks": 2, // http://eslint.org/docs/rules/no-lone-blocks
"no-loop-func": 2, // http://eslint.org/docs/rules/no-loop-func
"no-multi-str": 2, // http://eslint.org/docs/rules/no-multi-str
"no-native-reassign": 2, // http://eslint.org/docs/rules/no-native-reassign
"no-new": 2, // http://eslint.org/docs/rules/no-new
"no-new-func": 2, // http://eslint.org/docs/rules/no-new-func
"no-new-wrappers": 2, // http://eslint.org/docs/rules/no-new-wrappers
"no-octal": 2, // http://eslint.org/docs/rules/no-octal
"no-octal-escape": 2, // http://eslint.org/docs/rules/no-octal-escape
"no-param-reassign": 2, // http://eslint.org/docs/rules/no-param-reassign
"no-proto": 2, // http://eslint.org/docs/rules/no-proto
"no-redeclare": 2, // http://eslint.org/docs/rules/no-redeclare
"no-return-assign": 2, // http://eslint.org/docs/rules/no-return-assign
"no-self-compare": 2, // http://eslint.org/docs/rules/no-self-compare
"no-sequences": 2, // http://eslint.org/docs/rules/no-sequences
"no-throw-literal": 2, // http://eslint.org/docs/rules/no-throw-literal
"no-with": 2, // http://eslint.org/docs/rules/no-with
"radix": 2, // http://eslint.org/docs/rules/radix
"vars-on-top": 2, // http://eslint.org/docs/rules/vars-on-top
"wrap-iife": [2, "any"], // http://eslint.org/docs/rules/wrap-iife
"yoda": 2, // http://eslint.org/docs/rules/yoda
/**
* Style
*/
"indent": [2, 2], // http://eslint.org/docs/rules/indent
"brace-style": [2, // http://eslint.org/docs/rules/brace-style
"1tbs", {
"allowSingleLine": true
}],
"quotes": [
2, "single", "avoid-escape" // http://eslint.org/docs/rules/quotes
],
"camelcase": [2, { // http://eslint.org/docs/rules/camelcase
"properties": "never"
}],
"comma-spacing": [2, { // http://eslint.org/docs/rules/comma-spacing
"before": false,
"after": true
}],
"comma-style": [2, "last"], // http://eslint.org/docs/rules/comma-style
"eol-last": 2, // http://eslint.org/docs/rules/eol-last
"key-spacing": [2, { // http://eslint.org/docs/rules/key-spacing
"beforeColon": false,
"afterColon": true
}],
"new-cap": [2, { // http://eslint.org/docs/rules/new-cap
"newIsCap": true
}],
"no-multiple-empty-lines": [2, { // http://eslint.org/docs/rules/no-multiple-empty-lines
"max": 2
}],
"no-new-object": 2, // http://eslint.org/docs/rules/no-new-object
"no-spaced-func": 2, // http://eslint.org/docs/rules/no-spaced-func
"no-trailing-spaces": 2, // http://eslint.org/docs/rules/no-trailing-spaces
"no-wrap-func": 2, // http://eslint.org/docs/rules/no-wrap-func
"no-underscore-dangle": 0, // http://eslint.org/docs/rules/no-underscore-dangle
"one-var": [2, "never"], // http://eslint.org/docs/rules/one-var
"padded-blocks": [2, "never"], // http://eslint.org/docs/rules/padded-blocks
"semi": [2, "always"], // http://eslint.org/docs/rules/semi
"semi-spacing": [2, { // http://eslint.org/docs/rules/semi-spacing
"before": false,
"after": true
}],
"space-after-keywords": 2, // http://eslint.org/docs/rules/space-after-keywords
"space-before-blocks": 2, // http://eslint.org/docs/rules/space-before-blocks
"space-before-function-paren": [2, "never"], // http://eslint.org/docs/rules/space-before-function-paren
"space-infix-ops": 2, // http://eslint.org/docs/rules/space-infix-ops
"space-return-throw-case": 2, // http://eslint.org/docs/rules/space-return-throw-case
"spaced-line-comment": 2, // http://eslint.org/docs/rules/spaced-line-comment
}
}

26
.php_cs
View File

@@ -1,26 +0,0 @@
<?php
$header = <<<EOF
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.
EOF;
Symfony\CS\Fixer\Contrib\HeaderCommentFixer::setHeader($header);
$finder = Symfony\CS\Finder\DefaultFinder::create()
->exclude('stubs')
->in(__DIR__);
return Symfony\CS\Config\Config::create()
->setUsingCache(true)
->level(Symfony\CS\FixerInterface::PSR2_LEVEL)
->fixers([
'short_array_syntax',
'header_comment',
'-psr0'
])
->finder($finder);

17
.styleci.yml Normal file
View File

@@ -0,0 +1,17 @@
preset: recommended
enabled:
- logical_not_operators_with_successor_space
disabled:
- align_double_arrow
- multiline_array_trailing_comma
- new_with_braces
- phpdoc_align
- phpdoc_order
- phpdoc_separation
- phpdoc_types
finder:
exclude:
- "stubs"

View File

@@ -12,12 +12,12 @@ matrix:
fast_finish: true
before_script:
- curl -s http://getcomposer.org/installer | php
- php composer.phar install
- if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then phpenv config-rm xdebug.ini; fi;
- composer self-update
- composer install
script:
- php composer.phar style
- php composer.phar test
- vendor/bin/phpunit -c tests/phpunit.xml --coverage-clover=coverage.xml
notifications:
email:
@@ -29,4 +29,7 @@ notifications:
on_failure: always
on_start: false
after_success:
- bash <(curl -s https://codecov.io/bash)
sudo: false

View File

@@ -1,108 +0,0 @@
# 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/).
## [0.1.0-beta.4] - 2015-11-05
### Added
- Add an icon/label to the back button to indicate where it leads
- Add "Loading..." text while the JavaScript payload is loading
### Fixed
- Fix some admin actions resulting in "You do not have permission to do that"
- Fix translation keys persisting after enabling an initial language pack
- Fix translation `=>` references not being parsed in some cases
## [0.1.0-beta.3] - 2015-11-03
### Architecture improvements
- **Composer-driven extension architecture.** All extensions are Composer packages installable via Packagist.
- **Backend codebase & API refactoring.** Classes, namespaces, and events systematically tidied up.
### Improved internationalization
> A huge thanks to @dcsjapan for the countless hours he put in to make this stuff happen. You're amazing!
- New systematic translation key naming scheme.
- Make many hardcoded strings translatable, including administration UI and validation messages.
- More powerful pluralization via use of Symfony's Translation component instead of a proprietary one.
### New moderation tools
- **Hide/restore discussions.** Discussions can be soft-deleted by moderators or by the OP if no one has replied.
- **Flags.** New bundled extension that allows posts to be flagged for moderator review.
- **Approval.** New bundled extension that hides/flags new posts to be approved by the moderation team.
- **Akismet.** New bundled extension that checks new posts for spam with Akismet.
- **IP address logging.** IP addresses are stored with posts for use by extensions (e.g. Akismet).
- **Flood control.** Users must wait at least ten seconds between consecutive posts.
### Other features
- **Social login.** New bundled extensions that allow users to log in with Facebook, Twitter, and GitHub.
- **More compact post layout.** All controls are grouped over to the right.
- **Improved permissions.** The admin Permissions page has been improved with icons and other tweaks.
- **Improved extension management.** The admin Extensions page has a new look and is easier to use.
- **Easier debugging.** The "oops" error message has a Debug button to inspect a failed AJAX request.
- **Improved JavaScript minification.** Minification is done by ClosureCompiler only when debug mode is off, resulting in easier debugging and smaller production assets.
### Added
- Allow HTML tag syntax in translations (#574)
- Add gzip/caching directives to webserver configuration (#514)
- API to set the asset compiler's filename
- Migration generator, available via generate:migration console command
- Tags: Ability to set the tags page as the home page
- `bidi` attribute for Mithril elements as a shortcut to set up bidirectional bindings
- `route` attribute for Mithril elements as a shortcut to link to a route
- Abstract SettingsModal component for quickly building admin config modals
- `Model::afterSave()` API to run callback after a model instance is saved
- Sticky: Allow permission to be configured
- Lock: Allow permission to be configured
- Add a third state to header icons (#500)
- Allow faking of PATCH/DELETE methods (#502)
- More reliable form validation and error handling
### Changed
- Rename `notification_read_time` column in discussions table to `notifications_read_time`.
- Update to FontAwesome 4.4.0.
### Fixed
- Output forum description in meta description tag (#506)
- Allow users to edit their last post in a discussion even if it's hidden
- Allow users to rename their discussion even if their first post is hidden
- API links correctly include the `/api` path (#579)
- Tags: Fix sub-tag ordering algorithm in Chrome (#325)
- Fix several design bugs
## [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
[0.1.0-beta.4]: https://github.com/flarum/core/compare/v0.1.0-beta.3...v0.1.0-beta.4
[0.1.0-beta.3]: https://github.com/flarum/core/compare/v0.1.0-beta.2...v0.1.0-beta.3
[0.1.0-beta.2]: https://github.com/flarum/core/compare/v0.1.0-beta...v0.1.0-beta.2

View File

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

View File

@@ -21,12 +21,15 @@
},
"require": {
"php": ">=5.5.9",
"dflydev/fig-cookies": "^1.0",
"doctrine/dbal": "^2.5",
"franzl/whoops-middleware": "^0.2.0",
"illuminate/bus": "5.1.*",
"illuminate/cache": "5.1.*",
"illuminate/config": "5.1.*",
"illuminate/container": "5.1.*",
"illuminate/contracts": "5.1.*",
"illuminate/database": "5.1.*",
"illuminate/database": "^5.1.31",
"illuminate/events": "5.1.*",
"illuminate/filesystem": "5.1.*",
"illuminate/hashing": "5.1.*",
@@ -34,27 +37,25 @@
"illuminate/support": "5.1.*",
"illuminate/validation": "5.1.*",
"illuminate/view": "5.1.*",
"league/flysystem": "^1.0.11",
"tobscure/json-api": "^0.2.0",
"oyejorge/less.php": "~1.5",
"intervention/image": "^2.3.0",
"s9e/text-formatter": "^0.4.0",
"psr/http-message": "^1.0",
"zendframework/zend-diactoros": "^1.1",
"zendframework/zend-stratigility": "^1.1",
"nikic/fast-route": "^0.6",
"dflydev/fig-cookies": "^1.0",
"symfony/console": "^2.7",
"symfony/yaml": "^2.7",
"symfony/translation": "^2.7",
"doctrine/dbal": "^2.5",
"league/flysystem": "^1.0.11",
"league/oauth2-client": "~1.0",
"matthiasmullie/minify": "^1.3",
"monolog/monolog": "^1.16.0",
"franzl/whoops-middleware": "^0.2.0",
"matthiasmullie/minify": "^1.3"
"nikic/fast-route": "^0.6",
"oyejorge/less.php": "~1.5",
"psr/http-message": "^1.0",
"symfony/console": "^2.7",
"symfony/http-foundation": "^2.7",
"symfony/translation": "^2.7",
"symfony/yaml": "^2.7",
"s9e/text-formatter": "^0.4.12",
"tobscure/json-api": "^0.2.0",
"zendframework/zend-diactoros": "^1.1",
"zendframework/zend-stratigility": "^1.1"
},
"require-dev": {
"mockery/mockery": "^0.9.4",
"squizlabs/php_codesniffer": "2.*",
"phpunit/phpunit": "^4.8"
},
"autoload": {
@@ -71,7 +72,11 @@
}
},
"scripts": {
"test": "vendor/bin/phpunit -c tests/phpunit.xml",
"style": "vendor/bin/phpcs --standard=PSR2 -np src"
"test": "vendor/bin/phpunit -c tests/phpunit.xml"
},
"extra": {
"branch-alias": {
"dev-master": "0.1.x-dev"
}
}
}

13
error/403.html Normal file
View File

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

View File

@@ -1,12 +1,10 @@
var gulp = require('flarum-gulp');
var nodeDir = 'node_modules';
var bowerDir = '../bower_components';
gulp({
includeHelpers: true,
files: [
nodeDir + '/babel-core/external-helpers.js',
bowerDir + '/es6-micro-loader/dist/system-polyfill.js',
bowerDir + '/mithril/mithril.js',

6479
js/admin/dist/app.js vendored

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,7 @@
{
"private": true,
"devDependencies": {
"gulp": "^3.8.11",
"flarum-gulp": "git+https://github.com/flarum/gulp.git",
"babel-core": "^5.0.0"
"gulp": "^3.9.1",
"flarum-gulp": "^0.2.0"
}
}

View File

@@ -21,9 +21,9 @@ export default class AddExtensionModal extends Modal {
content() {
return (
<div className="Modal-body">
<p>One day in the not-too-distant future, this dialog will allow you to add an extension to your forum with ease. We're building an ecosystem as we speak!</p>
<p>In the meantime, if you manage to get your hands on a new extension, simply drop it in your forum's <code>extensions</code> directory.</p>
<p>If you're a developer, you can <a href="http://flarum.org/docs/extend">read the docs</a> and have a go at building your own.</p>
<p>{app.translator.trans('core.admin.add_extension.temporary_text')}</p>
<p>{app.translator.trans('core.admin.add_extension.install_text', {a: <a href="https://discuss.flarum.org/t/extensions" target="_blank"/>})}</p>
<p>{app.translator.trans('core.admin.add_extension.developer_text', {a: <a href="http://flarum.org/docs/extend" target="_blank"/>})}</p>
</div>
);
}

View File

@@ -1,11 +1,13 @@
import Component from 'flarum/Component';
import Page from 'flarum/components/Page';
import Button from 'flarum/components/Button';
import Switch from 'flarum/components/Switch';
import EditCustomCssModal from 'flarum/components/EditCustomCssModal';
import saveSettings from 'flarum/utils/saveSettings';
export default class AppearancePage extends Component {
export default class AppearancePage extends Page {
init() {
super.init();
this.primaryColor = m.prop(app.settings.theme_primary_color);
this.secondaryColor = m.prop(app.settings.theme_secondary_color);
this.darkMode = m.prop(app.settings.theme_dark_mode === '1');

View File

@@ -1,4 +1,4 @@
import Component from 'flarum/Component';
import Page from 'flarum/components/Page';
import FieldSet from 'flarum/components/FieldSet';
import Select from 'flarum/components/Select';
import Button from 'flarum/components/Button';
@@ -6,8 +6,10 @@ import Alert from 'flarum/components/Alert';
import saveSettings from 'flarum/utils/saveSettings';
import ItemList from 'flarum/utils/ItemList';
export default class BasicsPage extends Component {
export default class BasicsPage extends Page {
init() {
super.init();
this.loading = false;
this.fields = [
@@ -145,7 +147,8 @@ export default class BasicsPage extends Component {
.then(() => {
app.alerts.show(this.successAlert = new Alert({type: 'success', children: app.translator.trans('core.admin.basics.saved_message')}));
})
.finally(() => {
.catch(() => {})
.then(() => {
this.loading = false;
m.redraw();
});

View File

@@ -1,11 +1,11 @@
import Component from 'flarum/Component';
import Page from 'flarum/components/Page';
export default class DashboardPage extends Component {
export default class DashboardPage extends Page {
view() {
return (
<div className="DashboardPage">
<div className="container">
<h2>Welcome to Flarum Beta</h2>
<h2>{app.translator.trans('core.admin.dashboard.welcome_text')}</h2>
<p>{app.translator.trans('core.admin.dashboard.version_text', {version: <strong>{app.forum.attribute('version')}</strong>})}</p>
<p>{app.translator.trans('core.admin.dashboard.beta_warning_text', {strong: <strong/>})}</p>
<ul>

View File

@@ -12,13 +12,13 @@ export default class EditCustomCssModal extends Modal {
}
title() {
return 'Edit Custom CSS';
return app.translator.trans('core.admin.edit_css.title');
}
content() {
return (
<div className="Modal-body">
<p>Customize your forum's appearance by adding your own LESS/CSS code to be applied on top of Flarum's default styles. <a href="http://flarum.org/docs/extend/themes/">Read the documentation</a> for more information.</p>
<p>{app.translator.trans('core.admin.edit_css.customize_text', {a: <a href="https://github.com/flarum/core/tree/master/less" target="_blank"/>})}</p>
<div className="Form">
<div className="Form-group">
@@ -29,7 +29,7 @@ export default class EditCustomCssModal extends Modal {
{Button.component({
className: 'Button Button--primary',
type: 'submit',
children: 'Save Changes',
children: app.translator.trans('core.admin.edit_css.submit_button'),
loading: this.loading
})}
</div>

View File

@@ -1,4 +1,4 @@
import Component from 'flarum/Component';
import Page from 'flarum/components/Page';
import LinkButton from 'flarum/components/LinkButton';
import Button from 'flarum/components/Button';
import Dropdown from 'flarum/components/Dropdown';
@@ -9,10 +9,8 @@ import ItemList from 'flarum/utils/ItemList';
import icon from 'flarum/helpers/icon';
import listItems from 'flarum/helpers/listItems';
export default class ExtensionsPage extends Component {
export default class ExtensionsPage extends Page {
view() {
const extensions = Object.keys(app.extensions).map(id => app.extensions[id]);
return (
<div className="ExtensionsPage">
<div className="ExtensionsPage-header">
@@ -29,15 +27,15 @@ export default class ExtensionsPage extends Component {
<div className="ExtensionsPage-list">
<div className="container">
<ul className="ExtensionList">
{extensions
.sort((a, b) => a.extra['flarum-extension'].title.localeCompare(b.extra['flarum-extension'].title))
.map(extension => {
{Object.keys(app.extensions)
.map(id => {
const extension = app.extensions[id];
const controls = this.controlItems(extension.id).toArray();
return <li className={'ExtensionListItem ' + (!this.isEnabled(extension.id) ? 'disabled' : '')}>
<div className="ExtensionListItem-content">
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.extra['flarum-extension'].icon}>
{extension.extra['flarum-extension'].icon ? icon(extension.extra['flarum-extension'].icon.name) : ''}
<span className="ExtensionListItem-icon ExtensionIcon" style={extension.icon}>
{extension.icon ? icon(extension.icon.name) : ''}
</span>
{controls.length ? (
<Dropdown

View File

@@ -0,0 +1,32 @@
import Component from 'flarum/Component';
/**
* The `Page` component
*
* @abstract
*/
export default class Page extends Component {
init() {
app.previous = app.current;
app.current = this;
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);
}
}
}

View File

@@ -1,11 +1,11 @@
import Component from 'flarum/Component';
import Page from 'flarum/components/Page';
import GroupBadge from 'flarum/components/GroupBadge';
import EditGroupModal from 'flarum/components/EditGroupModal';
import Group from 'flarum/models/Group';
import icon from 'flarum/helpers/icon';
import PermissionGrid from 'flarum/components/PermissionGrid';
export default class PermissionsPage extends Component {
export default class PermissionsPage extends Page {
view() {
return (
<div className="PermissionsPage">

View File

@@ -33,7 +33,7 @@ export default class SettingsModal extends Modal {
className="Button Button--primary"
loading={this.loading}
disabled={!this.changed()}>
Save Changes
{app.translator.trans('core.admin.settings.submit_button')}
</Button>
);
}

View File

@@ -7,10 +7,10 @@
"spin.js": "~2.0.1",
"moment": "~2.8.4",
"color-thief": "v2.0",
"mithril": "lhorie/mithril.js#next",
"mithril": "lhorie/mithril.js#v0.2.3",
"es6-micro-loader": "caridy/es6-micro-loader#v0.2.1",
"fastclick": "~1.0.6",
"autolink": "*",
"autolink": "~1.0.0",
"m.attrs.bidi": "tobscure/m.attrs.bidi"
}
}

View File

@@ -1,12 +1,10 @@
var gulp = require('flarum-gulp');
var nodeDir = 'node_modules';
var bowerDir = '../bower_components';
gulp({
includeHelpers: true,
files: [
nodeDir + '/babel-core/external-helpers.js',
bowerDir + '/es6-micro-loader/dist/system-polyfill.js',
bowerDir + '/mithril/mithril.js',

10194
js/forum/dist/app.js vendored

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,7 @@
{
"private": true,
"devDependencies": {
"gulp": "^3.8.11",
"flarum-gulp": "git+https://github.com/flarum/gulp.git",
"babel-core": "^5.0.0"
"gulp": "^3.9.1",
"flarum-gulp": "^0.2.0"
}
}

View File

@@ -5,6 +5,7 @@ import routes from 'flarum/initializers/routes';
import components from 'flarum/initializers/components';
import humanTime from 'flarum/initializers/humanTime';
import boot from 'flarum/initializers/boot';
import alertEmailConfirmation from 'flarum/initializers/alertEmailConfirmation';
const app = new ForumApp();
@@ -15,5 +16,6 @@ app.initializers.add('humanTime', humanTime);
app.initializers.add('preload', preload, -100);
app.initializers.add('boot', boot, -100);
app.initializers.add('alertEmailConfirmation', alertEmailConfirmation, -100);
export default app;

View File

@@ -22,6 +22,13 @@ export default class ChangeEmailModal extends Modal {
* @type {function}
*/
this.email = m.prop(app.session.user.email());
/**
* The value of the password input.
*
* @type {function}
*/
this.password = m.prop('');
}
className() {
@@ -54,8 +61,13 @@ export default class ChangeEmailModal extends Modal {
<div className="Form-group">
<input type="email" name="email" className="FormControl"
placeholder={app.session.user.email()}
value={this.email()}
onchange={m.withAttr('value', this.email)}
bidi={this.email}
disabled={this.loading}/>
</div>
<div className="Form-group">
<input type="password" name="password" className="FormControl"
placeholder={app.translator.trans('core.forum.change_email.confirm_password_label')}
bidi={this.password}
disabled={this.loading}/>
</div>
<div className="Form-group">
@@ -81,10 +93,24 @@ export default class ChangeEmailModal extends Modal {
return;
}
const oldEmail = app.session.user.email();
this.loading = true;
app.session.user.save({email: this.email()}, {errorHandler: this.onerror.bind(this)})
app.session.user.save({email: this.email()}, {
errorHandler: this.onerror.bind(this),
meta: {password: this.password()}
})
.then(() => this.success = true)
.finally(this.loaded.bind(this));
.catch(() => {})
.then(this.loaded.bind(this));
}
onerror(error) {
if (error.status === 401) {
error.alert.props.children = app.translator.trans('core.forum.change_email.incorrect_password_message');
}
super.onerror(error);
}
}

View File

@@ -71,8 +71,7 @@ export default class CommentPost extends Post {
isEditing() {
return app.composer.component instanceof EditPostComposer &&
app.composer.component.props.post === this.props.post &&
app.composer.position !== Composer.PositionEnum.MINIMIZED;
app.composer.component.props.post === this.props.post;
}
attrs() {

View File

@@ -19,13 +19,6 @@ class Composer extends Component {
*/
this.position = Composer.PositionEnum.HIDDEN;
/**
* The composer's previous position.
*
* @type {Composer.PositionEnum}
*/
this.oldPosition = null;
/**
* The composer's intended height, which can be modified by the user
* (by dragging the composer handle).
@@ -66,21 +59,19 @@ class Composer extends Component {
view() {
const classes = {
'normal': this.position === Composer.PositionEnum.NORMAL,
'minimized': this.position === Composer.PositionEnum.MINIMIZED,
'fullScreen': this.position === Composer.PositionEnum.FULLSCREEN,
'active': this.active
};
classes.visible = this.position === Composer.PositionEnum.NORMAL || classes.minimized || classes.fullScreen;
classes.visible = classes.normal || classes.minimized || classes.fullScreen;
// If the composer is minimized, tell the composer's content component that
// it shouldn't let the user interact with it. Set up a handler so that if
// the content IS clicked, the composer will be shown.
if (this.component) this.component.props.disabled = classes.minimized;
const showIfMinimized = () => {
if (this.position === Composer.PositionEnum.MINIMIZED) this.show();
m.redraw.strategy('none');
};
const showIfMinimized = this.position === Composer.PositionEnum.MINIMIZED ? this.show.bind(this) : undefined;
return (
<div className={'Composer ' + classList(classes)}>
@@ -100,8 +91,6 @@ class Composer extends Component {
defaultHeight = this.$().height();
}
this.updateHeight();
if (isInitialized) return;
// Since this component is a part of the global UI that persists between
@@ -213,19 +202,17 @@ class Composer extends Component {
* of any flexible elements inside the composer's body.
*/
updateHeight() {
// TODO: update this in a way that is independent of the TextEditor being
// present.
const height = this.computedHeight();
const $flexible = this.$('.TextEditor-flexible');
const $flexible = this.$('.Composer-flexible');
this.$().height(height);
if ($flexible.length) {
const headerHeight = $flexible.offset().top - this.$().offset().top;
const paddingBottom = parseInt($flexible.css('padding-bottom'), 10);
const footerHeight = this.$('.TextEditor-controls').outerHeight(true);
const footerHeight = this.$('.Composer-footer').outerHeight(true);
$flexible.height(height - headerHeight - paddingBottom - footerHeight);
$flexible.height(this.$().outerHeight() - headerHeight - paddingBottom - footerHeight);
}
}
@@ -236,99 +223,27 @@ class Composer extends Component {
*/
updateBodyPadding() {
const visible = this.position !== Composer.PositionEnum.HIDDEN &&
this.position !== Composer.PositionEnum.MINIMIZED;
this.position !== Composer.PositionEnum.MINIMIZED &&
this.$().css('position') !== 'absolute';
const paddingBottom = visible
? this.computedHeight() - parseInt($('#app').css('padding-bottom'), 10)
: 0;
$('#content').css({paddingBottom});
}
/**
* Update (and animate) the DOM to reflect the composer's current state.
* Determine whether or not the Composer is covering the screen.
*
* This will be true if the Composer is in full-screen mode on desktop, or
* if the Composer is positioned absolutely as on mobile devices.
*
* @return {Boolean}
* @public
*/
update() {
// Before we redraw the composer to its new state, we need to save the
// current height of the composer, as well as the page's scroll position, so
// that we can smoothly transition from the old to the new state.
const $composer = this.$().stop(true);
const oldHeight = $composer.is(':visible') ? $composer.outerHeight() : 0;
const scrollTop = $(window).scrollTop();
m.redraw(true);
// Now that we've redrawn and the composer's DOM has been updated, we want
// to update the composer's height. Once we've done that, we'll capture the
// real value to use as the end point for our animation later on.
$composer.show();
this.updateHeight();
const newHeight = $composer.outerHeight();
switch (this.position) {
case Composer.PositionEnum.NORMAL:
// If the composer is being opened, we will make it visible and animate
// it growing/sliding up from the bottom of the viewport. Or if the user
// has just exited fullscreen mode, we will simply tell the content to
// take focus.
if (this.oldPosition !== Composer.PositionEnum.FULLSCREEN) {
$composer.show()
.css({height: oldHeight})
.animate({bottom: 0, height: newHeight}, 'fast', this.component.focus.bind(this.component));
if ($composer.css('position') === 'absolute') {
$composer.css('top', $(window).scrollTop());
this.$backdrop = $('<div/>')
.addClass('composer-backdrop')
.appendTo('body');
}
} else {
this.component.focus();
}
break;
case Composer.PositionEnum.MINIMIZED:
// If the composer has been minimized, we will animate it shrinking down
// to its new smaller size.
$composer.css({top: 'auto', height: oldHeight})
.animate({height: newHeight}, 'fast');
if (this.$backdrop) this.$backdrop.remove();
break;
case Composer.PositionEnum.HIDDEN:
// If the composer has been hidden, then we will animate it sliding down
// beyond the edge of the viewport. Once the animation is complete, we
// un-draw the composer's component.
$composer.css({top: 'auto', height: oldHeight})
.animate({bottom: -newHeight}, 'fast', () => {
$composer.hide();
this.clear();
m.redraw();
});
if (this.$backdrop) this.$backdrop.remove();
break;
case Composer.PositionEnum.FULLSCREEN:
this.component.focus();
break;
default:
// no default
}
// Provided the composer isn't in fullscreen mode, we'll want to update the
// body's padding to make sure all of the page's content can still be seen.
// Plus, we'll scroll back to where we were before the composer was opened,
// as its opening may have changed the content of the page.
if (this.position !== Composer.PositionEnum.FULLSCREEN) {
this.updateBodyPadding();
$('html, body').scrollTop(scrollTop);
}
this.oldPosition = this.position;
isFullScreen() {
return this.position === Composer.PositionEnum.FULLSCREEN || this.$().css('position') === 'absolute';
}
/**
@@ -378,20 +293,76 @@ class Composer extends Component {
this.component = null;
}
/**
* Animate the Composer into the given position.
*
* @param {Composer.PositionEnum} position
*/
animateToPosition(position) {
// Before we redraw the composer to its new state, we need to save the
// current height of the composer, as well as the page's scroll position, so
// that we can smoothly transition from the old to the new state.
const oldPosition = this.position;
const $composer = this.$().stop(true);
const oldHeight = $composer.outerHeight();
const scrollTop = $(window).scrollTop();
this.position = position;
m.redraw(true);
// Now that we've redrawn and the composer's DOM has been updated, we want
// to update the composer's height. Once we've done that, we'll capture the
// real value to use as the end point for our animation later on.
$composer.show();
this.updateHeight();
const newHeight = $composer.outerHeight();
if (oldPosition === Composer.PositionEnum.HIDDEN) {
$composer.css({bottom: -newHeight, height: newHeight});
} else {
$composer.css({height: oldHeight});
}
$composer.animate({bottom: 0, height: newHeight}, 'fast', () => this.component.focus());
this.updateBodyPadding();
$(window).scrollTop(scrollTop);
}
/**
* Show the Composer backdrop.
*/
showBackdrop() {
this.$backdrop = $('<div/>')
.addClass('composer-backdrop')
.appendTo('body');
}
/**
* Hide the Composer backdrop.
*/
hideBackdrop() {
if (this.$backdrop) this.$backdrop.remove();
}
/**
* Show the composer.
*
* @public
*/
show() {
// If the composer is hidden or minimized, we'll need to update its
// position. Otherwise, if the composer is already showing (whether it's
// fullscreen or not), we can leave it as is.
if ([Composer.PositionEnum.MINIMIZED, Composer.PositionEnum.HIDDEN].indexOf(this.position) !== -1) {
this.position = Composer.PositionEnum.NORMAL;
if (this.position === Composer.PositionEnum.NORMAL || this.position === Composer.PositionEnum.FULLSCREEN) {
return;
}
this.update();
this.animateToPosition(Composer.PositionEnum.NORMAL);
if (this.isFullScreen()) {
this.$().css('top', $(window).scrollTop());
this.showBackdrop();
}
}
/**
@@ -400,8 +371,20 @@ class Composer extends Component {
* @public
*/
hide() {
this.position = Composer.PositionEnum.HIDDEN;
this.update();
const $composer = this.$();
// Animate the composer sliding down off the bottom edge of the viewport.
// Only when the animation is completed, update the Composer state flag and
// other elements on the page.
$composer.stop(true).animate({bottom: -$composer.height()}, 'fast', () => {
this.position = Composer.PositionEnum.HIDDEN;
this.clear();
m.redraw();
$composer.hide();
this.hideBackdrop();
this.updateBodyPadding();
});
}
/**
@@ -422,10 +405,12 @@ class Composer extends Component {
* @public
*/
minimize() {
if (this.position !== Composer.PositionEnum.HIDDEN) {
this.position = Composer.PositionEnum.MINIMIZED;
this.update();
}
if (this.position === Composer.PositionEnum.HIDDEN) return;
this.animateToPosition(Composer.PositionEnum.MINIMIZED);
this.$().css('top', 'auto');
this.hideBackdrop();
}
/**
@@ -437,7 +422,9 @@ class Composer extends Component {
fullScreen() {
if (this.position !== Composer.PositionEnum.HIDDEN) {
this.position = Composer.PositionEnum.FULLSCREEN;
this.update();
m.redraw();
this.updateHeight();
this.component.focus();
}
}
@@ -449,7 +436,9 @@ class Composer extends Component {
exitFullScreen() {
if (this.position === Composer.PositionEnum.FULLSCREEN) {
this.position = Composer.PositionEnum.NORMAL;
this.update();
m.redraw();
this.updateHeight();
this.component.focus();
}
}

View File

@@ -56,7 +56,7 @@ export default class ComposerBody extends Component {
this.editor.props.disabled = this.loading;
return (
<div className="ComposerBody">
<div className={'ComposerBody ' + (this.props.className || '')}>
{avatar(this.props.user, {className: 'ComposerBody-avatar'})}
<div className="ComposerBody-content">
<ul className="ComposerBody-header">{listItems(this.headerItems().toArray())}</ul>

View File

@@ -31,12 +31,15 @@ export default class DiscussionComposer extends ComposerBody {
props.submitLabel = props.submitLabel || app.translator.trans('core.forum.composer_discussion.submit_button');
props.confirmExit = props.confirmExit || extractText(app.translator.trans('core.forum.composer_discussion.discard_confirmation'));
props.titlePlaceholder = props.titlePlaceholder || extractText(app.translator.trans('core.forum.composer_discussion.title_placeholder'));
props.className = 'ComposerBody--discussion';
}
headerItems() {
const items = super.headerItems();
items.add('title', (
items.add('title', <h3>{app.translator.trans('core.forum.composer_discussion.title')}</h3>, 100);
items.add('discussionTitle', (
<h3>
<input className="FormControl"
value={this.title()}

View File

@@ -115,7 +115,7 @@ export default class DiscussionList extends Component {
map.latest = '-lastTime';
map.top = '-commentsCount';
map.newest = '-startTime';
map.oldest = '+startTime';
map.oldest = 'startTime';
return map;
}

View File

@@ -184,7 +184,7 @@ export default class DiscussionPage extends Page {
// the specific post that was routed to.
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.stream.goToNumber(m.route.param('near') || (includedPosts[0] && includedPosts[0].number()), true);
}
/**

View File

@@ -1,6 +1,13 @@
import ComposerBody from 'flarum/components/ComposerBody';
import icon from 'flarum/helpers/icon';
function minimizeComposerIfFullScreen(e) {
if (app.composer.isFullScreen()) {
app.composer.minimize();
e.stopPropagation();
}
}
/**
* The `EditPostComposer` component displays the composer content for editing a
* post. It sets the initial content to the content of the post that is being
@@ -15,7 +22,9 @@ export default class EditPostComposer extends ComposerBody {
init() {
super.init();
this.editor.props.preview = () => {
this.editor.props.preview = e => {
minimizeComposerIfFullScreen(e);
m.route(app.route.post(this.props.post));
};
}
@@ -35,10 +44,16 @@ export default class EditPostComposer extends ComposerBody {
const items = super.headerItems();
const post = this.props.post;
const routeAndMinimize = function(element, isInitialized) {
if (isInitialized) return;
$(element).on('click', minimizeComposerIfFullScreen);
m.route.apply(this, arguments);
};
items.add('title', (
<h3>
{icon('pencil')} {' '}
<a href={app.route.discussion(post.discussion(), post.number())} config={m.route}>
<a href={app.route.discussion(post.discussion(), post.number())} config={routeAndMinimize}>
{app.translator.trans('core.forum.composer_edit.post_link', {number: post.number(), discussion: post.discussion().title()})}
</a>
</h3>

View File

@@ -85,12 +85,22 @@ export default class ForgotPasswordModal extends Modal {
app.request({
method: 'POST',
url: app.forum.attribute('apiUrl') + '/forgot',
data: {email: this.email()}
data: {email: this.email()},
errorHandler: this.onerror.bind(this)
})
.then(() => {
this.success = true;
this.alert = null;
})
.finally(this.loaded.bind(this));
.catch(() => {})
.then(this.loaded.bind(this));
}
onerror(error) {
if (error.status === 404) {
error.alert.props.children = app.translator.trans('core.forum.forgot_password.not_found_message');
}
super.onerror(error);
}
}

View File

@@ -98,14 +98,19 @@ export default class IndexPage extends Page {
// Work out the difference between the height of this hero and that of the
// previous hero. Maintain the same scroll position relative to the bottom
// of the hero so that the 'fixed' sidebar doesn't jump around.
const heroHeight = this.$('.Hero').outerHeight();
// of the hero so that the sidebar doesn't jump around.
const oldHeroHeight = app.cache.heroHeight;
const heroHeight = app.cache.heroHeight = this.$('.Hero').outerHeight();
const scrollTop = app.cache.scrollTop;
$('#app').css('min-height', $(window).height() + heroHeight);
$(window).scrollTop(scrollTop - (app.cache.heroHeight - heroHeight));
app.cache.heroHeight = heroHeight;
// Scroll to the remembered position. We do this after a short delay so that
// it happens after the browser has done its own "back button" scrolling,
// which isn't right. https://github.com/flarum/core/issues/835
const scroll = () => $(window).scrollTop(scrollTop - oldHeroHeight + heroHeight);
scroll();
setTimeout(scroll, 1);
// If we've just returned from a discussion page, then the constructor will
// have set the `lastDiscussion` property. If this is the case, we want to
@@ -199,16 +204,17 @@ export default class IndexPage extends Page {
*/
viewItems() {
const items = new ItemList();
const sortMap = app.cache.discussionList.sortMap();
const sortOptions = {};
for (const i in app.cache.discussionList.sortMap()) {
for (const i in sortMap) {
sortOptions[i] = app.translator.trans('core.forum.index_sort.' + i + '_button');
}
items.add('sort',
Select.component({
options: sortOptions,
value: this.params().sort,
value: this.params().sort || Object.keys(sortMap)[0],
onchange: this.changeSort.bind(this)
})
);
@@ -358,6 +364,10 @@ export default class IndexPage extends Page {
* @return void
*/
markAllAsRead() {
app.session.user.save({readTime: new Date()});
const confirmation = confirm(app.translator.trans('core.forum.index.mark_all_as_read_confirmation'));
if (confirmation) {
app.session.user.save({readTime: new Date()});
}
}
}

View File

@@ -13,8 +13,8 @@ export default class LogInButton extends Button {
props.className = (props.className || '') + ' LogInButton';
props.onclick = function() {
const width = 1000;
const height = 500;
const width = 600;
const height = 400;
const $window = $(window);
window.open(app.forum.attribute('baseUrl') + props.path, 'logInPopup',

View File

@@ -48,16 +48,14 @@ export default class LogInModal extends Modal {
<div className="Form Form--centered">
<div className="Form-group">
<input className="FormControl" name="email" placeholder={extractText(app.translator.trans('core.forum.log_in.username_or_email_placeholder'))}
value={this.email()}
onchange={m.withAttr('value', this.email)}
<input className="FormControl" name="email" type="text" placeholder={extractText(app.translator.trans('core.forum.log_in.username_or_email_placeholder'))}
bidi={this.email}
disabled={this.loading} />
</div>
<div className="Form-group">
<input className="FormControl" name="password" type="password" placeholder={extractText(app.translator.trans('core.forum.log_in.password_placeholder'))}
value={this.password()}
onchange={m.withAttr('value', this.password)}
bidi={this.password}
disabled={this.loading} />
</div>
@@ -124,18 +122,15 @@ export default class LogInModal extends Modal {
const email = this.email();
const password = this.password();
app.session.login(email, password, {errorHandler: this.onerror.bind(this)})
.catch(this.loaded.bind(this));
app.session.login(email, password, {errorHandler: this.onerror.bind(this)}).then(
() => window.location.reload(),
this.loaded.bind(this)
);
}
onerror(error) {
if (error.status === 401) {
if (error.response.emailConfirmationRequired) {
error.alert.props.children = app.translator.trans('core.forum.log_in.confirmation_required_message', {email: error.response.emailConfirmationRequired});
delete error.alert.props.type;
} else {
error.alert.props.children = app.translator.trans('core.forum.log_in.invalid_login_message');
}
error.alert.props.children = app.translator.trans('core.forum.log_in.invalid_login_message');
}
super.onerror(error);

View File

@@ -120,7 +120,8 @@ export default class NotificationList extends Component {
app.session.user.pushAttributes({newNotificationsCount: 0});
app.cache.notifications = notifications.sort((a, b) => b.time() - a.time());
})
.finally(() => {
.catch(() => {})
.then(() => {
this.loading = false;
m.redraw();
});

View File

@@ -18,6 +18,8 @@ import ItemList from 'flarum/utils/ItemList';
*/
export default class Post extends Component {
init() {
this.loading = false;
/**
* Set up a subtree retainer so that the post will not be redrawn
* unless new data comes in.
@@ -37,7 +39,7 @@ export default class Post extends Component {
view() {
const attrs = this.attrs();
attrs.className = 'Post ' + (attrs.className || '');
attrs.className = 'Post ' + (this.loading ? 'Post--loading ' : '') + (attrs.className || '');
return (
<article {...attrs}>

View File

@@ -5,6 +5,7 @@ import anchorScroll from 'flarum/utils/anchorScroll';
import mixin from 'flarum/utils/mixin';
import evented from 'flarum/utils/evented';
import ReplyPlaceholder from 'flarum/components/ReplyPlaceholder';
import Button from 'flarum/components/Button';
/**
* The `PostStream` component displays an infinitely-scrollable wall of posts in
@@ -54,7 +55,9 @@ class PostStream extends Component {
return this.goToLast().then(() => {
$('html,body').stop(true).animate({
scrollTop: $(document).height() - $(window).height()
}, 'fast');
}, 'fast', () => {
this.flashItem(this.$('.PostStream-item:last-child'));
});
});
}
@@ -194,60 +197,72 @@ class PostStream extends Component {
this.visibleEnd = this.sanitizeIndex(this.visibleEnd);
this.viewingEnd = this.visibleEnd === this.count();
const posts = this.posts();
const postIds = this.discussion.postIds();
const items = posts.map((post, i) => {
let content;
const attrs = {'data-index': this.visibleStart + i};
if (post) {
const time = post.time();
const PostComponent = app.postComponents[post.contentType()];
content = PostComponent ? PostComponent.component({post}) : '';
attrs.key = 'post' + post.id();
attrs.config = fadeIn;
attrs['data-time'] = time.toISOString();
attrs['data-number'] = post.number();
attrs['data-id'] = post.id();
attrs['data-type'] = post.contentType();
// If the post before this one was more than 4 hours ago, we will
// display a 'time gap' indicating how long it has been in between
// the posts.
const dt = time - lastTime;
if (dt > 1000 * 60 * 60 * 24 * 4) {
content = [
<div className="PostStream-timeGap">
<span>{app.translator.trans('core.forum.post_stream.time_lapsed_text', {period: moment.duration(dt).humanize()})}</span>
</div>,
content
];
}
lastTime = time;
} else {
attrs.key = 'post' + postIds[this.visibleStart + i];
content = PostLoading.component();
}
return <div className="PostStream-item" {...attrs}>{content}</div>;
});
if (!this.viewingEnd && posts[this.visibleEnd - this.visibleStart - 1]) {
items.push(
<div className="PostStream-loadMore" key="loadMore">
<Button className="Button" onclick={this.loadNext.bind(this)}>
{app.translator.trans('core.forum.post_stream.load_more_button')}
</Button>
</div>
);
}
// If we're viewing the end of the discussion, the user can reply, and
// is not already doing so, then show a 'write a reply' placeholder.
if (this.viewingEnd && (!app.session.user || this.discussion.canReply())) {
items.push(
<div className="PostStream-item" key="reply">
{ReplyPlaceholder.component({discussion: this.discussion})}
</div>
);
}
return (
<div className="PostStream">
{this.posts().map((post, i) => {
let content;
const attrs = {'data-index': this.visibleStart + i};
if (post) {
const time = post.time();
const PostComponent = app.postComponents[post.contentType()];
content = PostComponent ? PostComponent.component({post}) : '';
attrs.key = 'post' + post.id();
attrs.config = fadeIn;
attrs['data-time'] = time.toISOString();
attrs['data-number'] = post.number();
attrs['data-id'] = post.id();
// If the post before this one was more than 4 hours ago, we will
// display a 'time gap' indicating how long it has been in between
// the posts.
const dt = time - lastTime;
if (dt > 1000 * 60 * 60 * 24 * 4) {
content = [
<div className="PostStream-timeGap">
<span>{app.translator.trans('core.forum.post_stream.time_lapsed_text', {period: moment.duration(dt).humanize()})}</span>
</div>,
content
];
}
lastTime = time;
} else {
attrs.key = 'post' + postIds[this.visibleStart + i];
content = PostLoading.component();
}
return <div className="PostStream-item" {...attrs}>{content}</div>;
})}
{
// If we're viewing the end of the discussion, the user can reply, and
// is not already doing so, then show a 'write a reply' placeholder.
this.viewingEnd &&
(!app.session.user || this.discussion.canReply())
? (
<div className="PostStream-item" key="reply">
{ReplyPlaceholder.component({discussion: this.discussion})}
</div>
) : ''
}
{items}
</div>
);
}

View File

@@ -39,16 +39,6 @@ export default class PostStreamScrubber extends Component {
*/
this.description = '';
/**
* The integer index of the last item that is visible in the viewport. This
* is displayed on the scrubber (i.e. X of 100 posts).
*
* @return {Integer}
*/
this.visibleIndex = computed('index', 'visible', 'count', function(index, visible, count) {
return Math.min(count, Math.ceil(Math.max(0, index) + visible));
});
// When the post stream begins loading posts at a certain index, we want our
// scrubber scrollbar to jump to that position.
this.props.stream.on('unpaused', this.handlers.streamWasUnpaused = this.streamWasUnpaused.bind(this));
@@ -68,10 +58,10 @@ export default class PostStreamScrubber extends Component {
const retain = this.subtree.retain();
const count = this.count();
const unreadCount = this.props.stream.discussion.unreadCount();
const unreadPercent = Math.min(count - this.index, unreadCount) / count;
const unreadPercent = count ? Math.min(count - this.index, unreadCount) / count : 0;
const viewing = app.translator.transChoice('core.forum.post_scrubber.viewing_text', count, {
index: <span className="Scrubber-index">{retain || formatNumber(this.visibleIndex())}</span>,
index: <span className="Scrubber-index">{retain || formatNumber(Math.ceil(this.index + this.visible))}</span>,
count: <span className="Scrubber-count">{formatNumber(count)}</span>
});
@@ -200,6 +190,7 @@ export default class PostStreamScrubber extends Component {
const marginTop = stream.getMarginTop();
const viewportTop = scrollTop + marginTop;
const viewportHeight = $(window).height() - marginTop;
const viewportBottom = viewportTop + viewportHeight;
// Before looping through all of the posts, we reset the scrollbar
// properties to a 'default' state. These values reflect what would be
@@ -219,32 +210,28 @@ export default class PostStreamScrubber extends Component {
const height = $this.outerHeight(true);
// If this item is above the top of the viewport, skip to the next
// post. If it's below the bottom of the viewport, break out of the
// one. If it's below the bottom of the viewport, break out of the
// loop.
if (top + height < viewportTop) {
visible = (top + height - viewportTop) / height;
index = parseFloat($this.data('index')) + 1 - visible;
return true;
}
if (top > viewportTop + viewportHeight) {
return false;
}
// If the bottom half of this item is visible at the top of the
// viewport, then set the start of the visible proportion as our index.
if (top <= viewportTop && top + height > viewportTop) {
visible = (top + height - viewportTop) / height;
index = parseFloat($this.data('index')) + 1 - visible;
//
// If the top half of this item is visible at the bottom of the
// viewport, then add the visible proportion to the visible
// counter.
} else if (top + height >= viewportTop + viewportHeight) {
visible += (viewportTop + viewportHeight - top) / height;
//
// If the whole item is visible in the viewport, then increment the
// visible counter.
} else visible++;
// Work out how many pixels of this item are visible inside the viewport.
// Then add the proportion of this item's total height to the index.
const visibleTop = Math.max(0, viewportTop - top);
const visibleBottom = Math.min(height, viewportTop + viewportHeight - top);
const visiblePost = visibleBottom - visibleTop;
if (top <= viewportTop) {
index = parseFloat($this.data('index')) + visibleTop / height;
}
if (visiblePost > 0) {
visible += visiblePost / height;
}
// If this item has a time associated with it, then set the
// scrollbar's current period to a formatted version of this time.
@@ -328,7 +315,7 @@ export default class PostStreamScrubber extends Component {
const visible = this.visible || 1;
const $scrubber = this.$();
$scrubber.find('.Scrubber-index').text(formatNumber(this.visibleIndex()));
$scrubber.find('.Scrubber-index').text(formatNumber(Math.ceil(index + visible)));
$scrubber.find('.Scrubber-description').text(this.description);
$scrubber.toggleClass('disabled', this.disabled());

View File

@@ -2,6 +2,7 @@ import Component from 'flarum/Component';
import UserCard from 'flarum/components/UserCard';
import avatar from 'flarum/helpers/avatar';
import username from 'flarum/helpers/username';
import userOnline from 'flarum/helpers/userOnline';
import listItems from 'flarum/helpers/listItems';
/**
@@ -45,6 +46,7 @@ export default class PostUser extends Component {
return (
<div className="PostUser">
{userOnline(user)}
<h3>
<a href={app.route.user(user)} config={m.route}>
{avatar(user, {className: 'PostUser-avatar'})}{' '}{username(user)}

View File

@@ -65,9 +65,9 @@ export default class PostsUserPage extends UserPage {
{this.posts.map(post => (
<li>
<div className="PostsUserPage-discussion">
In <a href={app.route.post(post)} config={m.route}>{post.discussion().title()}</a>
{app.translator.trans('core.forum.user.in_discussion_text', {discussion: <a href={app.route.post(post)} config={m.route}>{post.discussion().title()}</a>})}
</div>
{CommentPost.component({post, showDiscussionTitle: true})}
{CommentPost.component({post})}
</li>
))}
</ul>

View File

@@ -4,6 +4,13 @@ import Button from 'flarum/components/Button';
import icon from 'flarum/helpers/icon';
import extractText from 'flarum/utils/extractText';
function minimizeComposerIfFullScreen(e) {
if (app.composer.isFullScreen()) {
app.composer.minimize();
e.stopPropagation();
}
}
/**
* The `ReplyComposer` component displays the composer content for replying to a
* discussion.
@@ -17,7 +24,9 @@ export default class ReplyComposer extends ComposerBody {
init() {
super.init();
this.editor.props.preview = () => {
this.editor.props.preview = e => {
minimizeComposerIfFullScreen(e);
m.route(app.route.discussion(this.props.discussion, 'reply'));
};
}
@@ -34,10 +43,16 @@ export default class ReplyComposer extends ComposerBody {
const items = super.headerItems();
const discussion = this.props.discussion;
const routeAndMinimize = function(element, isInitialized) {
if (isInitialized) return;
$(element).on('click', minimizeComposerIfFullScreen);
m.route.apply(this, arguments);
};
items.add('title', (
<h3>
{icon('reply')} {' '}
<a href={app.route.discussion(discussion)} config={m.route}>{discussion.title()}</a>
<a href={app.route.discussion(discussion)} config={routeAndMinimize}>{discussion.title()}</a>
</h3>
));

View File

@@ -23,7 +23,7 @@ export default class Search extends Component {
*
* @type {Function}
*/
this.value = m.prop();
this.value = m.prop('');
/**
* Whether or not the search input has focus.
@@ -131,7 +131,11 @@ export default class Search extends Component {
break;
case 13: // Return
m.route(this.getItem(this.index).find('a').attr('href'));
if (this.value()) {
m.route(this.getItem(this.index).find('a').attr('href'));
} else {
this.clear();
}
this.$('input').blur();
break;

View File

@@ -48,7 +48,7 @@ export default class SettingsPage extends UserPage {
FieldSet.component({
label: app.translator.trans('core.forum.settings.notifications_heading'),
className: 'Settings-notifications',
children: [NotificationGrid.component({user: this.user})]
children: this.notificationsItems().toArray()
})
);
@@ -90,6 +90,19 @@ export default class SettingsPage extends UserPage {
return items;
}
/**
* Build an item list for the user's notification settings.
*
* @return {ItemList}
*/
notificationsItems() {
const items = new ItemList();
items.add('notificationGrid', NotificationGrid.component({user: this.user}));
return items;
}
/**
* Generate a callback that will save a value to the given preference.
*

View File

@@ -39,13 +39,6 @@ export default class SignUpModal extends Modal {
* @type {Function}
*/
this.password = m.prop(this.props.password || '');
/**
* The user that has been signed up and that should be welcomed.
*
* @type {null|User}
*/
this.welcomeUser = null;
}
className() {
@@ -68,12 +61,12 @@ export default class SignUpModal extends Modal {
}
body() {
const body = [
return [
this.props.token ? '' : <LogInButtons/>,
<div className="Form Form--centered">
<div className="Form-group">
<input className="FormControl" name="username" placeholder={extractText(app.translator.trans('core.forum.sign_up.username_placeholder'))}
<input className="FormControl" name="username" type="text" placeholder={extractText(app.translator.trans('core.forum.sign_up.username_placeholder'))}
value={this.username()}
onchange={m.withAttr('value', this.username)}
disabled={this.loading} />
@@ -105,36 +98,6 @@ export default class SignUpModal extends Modal {
</div>
</div>
];
if (this.welcomeUser) {
const user = this.welcomeUser;
const fadeIn = (element, isInitialized) => {
if (isInitialized) return;
$(element).hide().fadeIn();
};
body.push(
<div className="SignUpModal-welcome" style={{background: user.color()}} config={fadeIn}>
<div className="darkenBackground">
<div className="container">
{avatar(user)}
<h3>{app.translator.trans('core.forum.sign_up.welcome_text', {user})}</h3>
<p>{app.translator.trans('core.forum.sign_up.confirmation_message', {email: <strong>{user.email()}</strong>})}</p>
<p>
<Button className="Button Button--primary" onclick={this.hide.bind(this)}>
{app.translator.trans('core.forum.sign_up.dismiss_button')}
</Button>
</p>
</div>
</div>
</div>
);
}
return body;
}
footer() {
@@ -181,19 +144,7 @@ export default class SignUpModal extends Modal {
data,
errorHandler: this.onerror.bind(this)
}).then(
payload => {
const user = app.store.pushPayload(payload);
// If the user's new account has been activated, then we can assume
// that they have been logged in too. Thus, we will reload the page.
// Otherwise, we will show a message asking them to check their email.
if (user.isActivated()) {
window.location.reload();
} else {
this.welcomeUser = user;
this.loaded();
}
},
() => window.location.reload(),
this.loaded.bind(this)
);
}
@@ -216,6 +167,10 @@ export default class SignUpModal extends Modal {
data.password = this.password();
}
if (this.props.avatarUrl) {
data.avatarUrl = this.props.avatarUrl;
}
return data;
}
}

View File

@@ -19,7 +19,7 @@ export default class TextEditor extends Component {
/**
* The value of the textarea.
*
* @type {[type]}
* @type {String}
*/
this.value = m.prop(this.props.value || '');
}
@@ -27,14 +27,14 @@ export default class TextEditor extends Component {
view() {
return (
<div className="TextEditor">
<textarea className="FormControl TextEditor-flexible"
<textarea className="FormControl Composer-flexible"
config={this.configTextarea.bind(this)}
oninput={m.withAttr('value', this.oninput.bind(this))}
placeholder={this.props.placeholder || ''}
disabled={!!this.props.disabled}
value={this.value()}/>
<ul className="TextEditor-controls">
<ul className="TextEditor-controls Composer-footer">
{listItems(this.controlItems().toArray())}
</ul>
</div>
@@ -72,6 +72,7 @@ export default class TextEditor extends Component {
children: this.props.submitLabel,
icon: 'check',
className: 'Button Button--primary',
itemClassName: 'App-primaryControl',
onclick: this.onsubmit.bind(this)
})
);

View File

@@ -91,7 +91,12 @@ export default class UserBio extends Component {
if (user.bio() !== value) {
this.loading = true;
user.save({bio: value}).finally(this.loaded.bind(this));
user.save({bio: value})
.catch(() => {})
.then(() => {
this.loading = false;
m.redraw();
});
}
this.editing = false;

View File

@@ -134,7 +134,8 @@ export default class UserPage extends Page {
href: app.route('user.posts', {username: user.username()}),
children: [app.translator.trans('core.forum.user.posts_link'), <span className="Button-badge">{user.commentsCount()}</span>],
icon: 'comment-o'
})
}),
100
);
items.add('discussions',
@@ -142,17 +143,19 @@ export default class UserPage extends Page {
href: app.route('user.discussions', {username: user.username()}),
children: [app.translator.trans('core.forum.user.discussions_link'), <span className="Button-badge">{user.discussionsCount()}</span>],
icon: 'reorder'
})
}),
90
);
if (app.session.user === user) {
items.add('separator', Separator.component());
items.add('separator', Separator.component(), -90);
items.add('settings',
LinkButton.component({
href: app.route('settings'),
children: app.translator.trans('core.forum.user.settings_link'),
icon: 'cog'
})
}),
-100
);
}

View File

@@ -1,5 +1,6 @@
import highlight from 'flarum/helpers/highlight';
import avatar from 'flarum/helpers/avatar';
import username from 'flarum/helpers/username';
/**
* The `UsersSearchSource` finds and displays user search results in the search
@@ -23,14 +24,19 @@ export default class UsersSearchResults {
return [
<li className="Dropdown-header">{app.translator.trans('core.forum.search.users_heading')}</li>,
results.map(user => (
<li className="UserSearchResult" data-index={'users' + user.id()}>
<a href={app.route.user(user)} config={m.route}>
{avatar(user)}
{highlight(user.username(), query)}
</a>
</li>
))
results.map(user => {
const name = username(user);
name.children[0] = highlight(name.children[0], query);
return (
<li className="UserSearchResult" data-index={'users' + user.id()}>
<a href={app.route.user(user)} config={m.route}>
{avatar(user)}
{name}
</a>
</li>
);
})
];
}
}

View File

@@ -0,0 +1,55 @@
import Alert from 'flarum/components/Alert';
import Button from 'flarum/components/Button';
import icon from 'flarum/helpers/icon';
/**
* Shows an alert if the user has not yet confirmed their email address.
*
* @param {ForumApp} app
*/
export default function alertEmailConfirmation(app) {
const user = app.session.user;
if (!user || user.isActivated()) return;
const resendButton = Button.component({
className: 'Button Button--link',
children: app.translator.trans('core.forum.user_email_confirmation.resend_button'),
onclick: function() {
resendButton.props.loading = true;
m.redraw();
app.request({
method: 'POST',
url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/send-confirmation',
}).then(() => {
resendButton.props.loading = false;
resendButton.props.children = [icon('check'), ' ', app.translator.trans('core.forum.user_email_confirmation.sent_message')];
resendButton.props.disabled = true;
m.redraw();
}).catch(() => {
resendButton.props.loading = false;
m.redraw();
});
}
});
class ContainedAlert extends Alert {
view() {
const vdom = super.view();
vdom.children = [<div className="container">{vdom.children}</div>];
return vdom;
}
}
m.mount(
$('<div/>').insertBefore('#content')[0],
ContainedAlert.component({
dismissible: false,
children: app.translator.trans('core.forum.user_email_confirmation.alert_message', {email: <strong>{user.email()}</strong>}),
controls: [resendButton]
})
);
}

View File

@@ -217,18 +217,19 @@ export default {
*/
deleteAction() {
if (confirm(extractText(app.translator.trans('core.forum.discussion_controls.delete_confirmation')))) {
// If there is a discussion list in the cache, remove this discussion.
if (app.cache.discussionList) {
app.cache.discussionList.removeDiscussion(this);
}
// If we're currently viewing the discussion that was deleted, go back
// to the previous page.
if (app.viewingDiscussion(this)) {
app.history.back();
}
return this.delete();
return this.delete().then(() => {
// If there is a discussion list in the cache, remove this discussion.
if (app.cache.discussionList) {
app.cache.discussionList.removeDiscussion(this);
m.redraw();
}
});
}
},

View File

@@ -78,7 +78,7 @@ export default {
* @return {ItemList}
* @protected
*/
destructiveControls(post) {
destructiveControls(post, context) {
const items = new ItemList();
if (post.contentType() === 'comment' && !post.isHidden()) {
@@ -97,11 +97,11 @@ export default {
onclick: this.restoreAction.bind(post)
}));
}
if (post.canDelete() && post.number() !== 1) {
if (post.canDelete()) {
items.add('delete', Button.component({
icon: 'times',
children: app.translator.trans('core.forum.post_controls.delete_forever_button'),
onclick: this.deleteAction.bind(post)
onclick: this.deleteAction.bind(post, context)
}));
}
}
@@ -144,9 +144,32 @@ export default {
*
* @return {Promise}
*/
deleteAction() {
this.discussion().removePost(this.id());
deleteAction(context) {
if (context) context.loading = true;
return this.delete();
return this.delete()
.then(() => {
const discussion = this.discussion();
discussion.removePost(this.id());
// If this was the last post in the discussion, then we will assume that
// the whole discussion was deleted too.
if (!discussion.postIds().length) {
// If there is a discussion list in the cache, remove this discussion.
if (app.cache.discussionList) {
app.cache.discussionList.removeDiscussion(discussion);
}
if (app.viewingDiscussion(discussion)) {
app.history.back();
}
}
})
.catch(() => {})
.then(() => {
if (context) context.loading = false;
m.redraw();
});
}
};

View File

@@ -2,6 +2,7 @@ import ItemList from 'flarum/utils/ItemList';
import Alert from 'flarum/components/Alert';
import Button from 'flarum/components/Button';
import RequestErrorModal from 'flarum/components/RequestErrorModal';
import ConfirmPasswordModal from 'flarum/components/ConfirmPasswordModal';
import Translator from 'flarum/Translator';
import extract from 'flarum/utils/extract';
import patchMithril from 'flarum/utils/patchMithril';
@@ -182,20 +183,23 @@ export default class App {
* @return {Promise}
* @public
*/
request(options) {
request(originalOptions) {
const options = Object.assign({}, originalOptions);
// Set some default options if they haven't been overridden. We want to
// authenticate all requests with the session token. We also want all
// requests to run asynchronously in the background, so that they don't
// prevent redraws from occurring.
options.config = options.config || this.session.authorize.bind(this.session);
options.background = options.background || true;
extend(options, 'config', (result, xhr) => xhr.setRequestHeader('X-CSRF-Token', this.session.csrfToken));
// If the method is something like PATCH or DELETE, which not all servers
// support, then we'll send it as a POST request with a the intended method
// specified in the X-Fake-Http-Method header.
// and clients support, then we'll send it as a POST request with the
// intended method specified in the X-HTTP-Method-Override header.
if (options.method !== 'GET' && options.method !== 'POST') {
const method = options.method;
extend(options, 'config', (result, xhr) => xhr.setRequestHeader('X-Fake-Http-Method', method));
extend(options, 'config', (result, xhr) => xhr.setRequestHeader('X-HTTP-Method-Override', method));
options.method = 'POST';
}
@@ -218,7 +222,7 @@ export default class App {
if (original) {
responseText = original(xhr.responseText);
} else {
responseText = xhr.responseText.length > 0 ? xhr.responseText : null;
responseText = xhr.responseText || null;
}
const status = xhr.status;
@@ -227,6 +231,11 @@ export default class App {
throw new RequestError(status, responseText, options, xhr);
}
if (xhr.getResponseHeader) {
const csrfToken = xhr.getResponseHeader('X-CSRF-Token');
if (csrfToken) app.session.csrfToken = csrfToken;
}
try {
return JSON.parse(responseText);
} catch (e) {
@@ -238,7 +247,9 @@ export default class App {
// Now make the request. If it's a failure, inspect the error that was
// returned and show an alert containing its contents.
return m.request(options).then(null, error => {
const deferred = m.deferred();
m.request(options).then(response => deferred.resolve(response), error => {
this.requestError = error;
let children;
@@ -283,8 +294,10 @@ export default class App {
this.alerts.show(error.alert);
}
throw error;
deferred.reject(error);
});
return deferred.promise;
}
/**

View File

@@ -54,6 +54,14 @@ export default class Component {
*/
this.element = null;
/**
* Whether or not to retain the component's subtree on redraw.
*
* @type {boolean}
* @public
*/
this.retain = false;
this.init();
}
@@ -91,7 +99,7 @@ export default class Component {
* @public
*/
render() {
const vdom = this.view();
const vdom = this.retain ? {subtree: 'retain'} : this.view();
// Override the root element's config attribute with our own function, which
// will set the component instance's element property to the root DOM

View File

@@ -150,14 +150,17 @@ export default class Model {
// Before we update the model's data, we should make a copy of the model's
// old data so that we can revert back to it if something goes awry during
// persistence.
const oldData = JSON.parse(JSON.stringify(this.data));
const oldData = this.copyData();
this.pushData(data);
const request = {data};
if (options.meta) request.meta = options.meta;
return app.request(Object.assign({
method: this.exists ? 'PATCH' : 'POST',
url: app.forum.attribute('apiUrl') + this.apiEndpoint(),
data: {data}
data: request
}, options)).then(
// If everything went well, we'll make sure the store knows that this
// model exists now (if it didn't already), and we'll push the data that
@@ -209,6 +212,10 @@ export default class Model {
return '/' + this.data.type + (this.exists ? '/' + this.data.id : '');
}
copyData() {
return JSON.parse(JSON.stringify(this.data));
}
/**
* Generate a function which returns the value of the given attribute.
*

View File

@@ -3,7 +3,7 @@
* to the current authenticated user, and provides methods to log in/out.
*/
export default class Session {
constructor(token, user) {
constructor(user, csrfToken) {
/**
* The current authenticated user.
*
@@ -13,12 +13,12 @@ export default class Session {
this.user = user;
/**
* The token that was used for authentication.
* The CSRF token.
*
* @type {String|null}
* @public
*/
this.token = token;
this.csrfToken = csrfToken;
}
/**
@@ -35,8 +35,7 @@ export default class Session {
method: 'POST',
url: app.forum.attribute('baseUrl') + '/login',
data: {identification, password}
}, options))
.then(() => window.location.reload());
}, options));
}
/**
@@ -45,19 +44,6 @@ export default class Session {
* @public
*/
logout() {
window.location = app.forum.attribute('baseUrl') + '/logout?token=' + this.token;
}
/**
* Apply an authorization header with the current token to the given
* XMLHttpRequest object.
*
* @param {XMLHttpRequest} xhr
* @public
*/
authorize(xhr) {
if (this.token) {
xhr.setRequestHeader('Authorization', 'Token ' + this.token);
}
window.location = app.forum.attribute('baseUrl') + '/logout?token=' + this.csrfToken;
}
}

View File

@@ -24,11 +24,6 @@ export default class Badge extends Component {
attrs.className = 'Badge ' + (type ? 'Badge--' + type : '') + ' ' + (attrs.className || '');
attrs.title = extract(attrs, 'label') || '';
// Give the badge a unique key so that when badges are displayed together,
// and then one is added/removed, Mithril will correctly redraw the series
// of badges.
attrs.key = attrs.type;
return (
<span {...attrs}>
{iconName ? icon(iconName, {className: 'Badge-icon'}) : m.trust('&nbsp;')}

View File

@@ -47,13 +47,20 @@ export default class Dropdown extends Component {
// bottom of the viewport. If it does, we will apply class to make it show
// above the toggle button instead of below it.
this.$().on('shown.bs.dropdown', () => {
const $menu = this.$('.Dropdown-menu').removeClass('Dropdown-menu--top');
const $menu = this.$('.Dropdown-menu');
const isRight = $menu.hasClass('Dropdown-menu--right');
$menu.removeClass('Dropdown-menu--top Dropdown-menu--right');
$menu.toggleClass(
'Dropdown-menu--top',
$menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height()
);
$menu.toggleClass(
'Dropdown-menu--right',
isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width()
);
if (this.props.onshow) {
this.props.onshow();
m.redraw();

View File

@@ -98,7 +98,10 @@ export default class Modal extends Component {
* Focus on the first input when the modal is ready to be used.
*/
onready() {
this.$('form :input:first').focus().select();
this.$('form').find('input, select, textarea').first().focus().select();
}
onhide() {
}
/**

View File

@@ -46,6 +46,8 @@ export default class ModalManager extends Component {
this.showing = true;
this.component = component;
app.current.retain = true;
m.redraw(true);
this.$().modal({backdrop: this.component.isDismissible() ? true : 'static'}).modal('show');
@@ -77,8 +79,14 @@ export default class ModalManager extends Component {
* @protected
*/
clear() {
if (this.component) {
this.component.onhide();
}
this.component = null;
app.current.retain = false;
m.lazyRedraw();
}

View File

@@ -34,6 +34,14 @@ export default function listItems(items) {
const active = item.component && item.component.isActive && item.component.isActive(item.props);
const className = item.props ? item.props.itemClassName : item.itemClassName;
if (isListItem) {
item.attrs = item.attrs || {};
item.attrs.key = item.attrs.key || item.itemName;
}
const space = new String(' ');
space.attrs = {key: '_space_'+item.itemName};
return [
isListItem
? item
@@ -41,10 +49,11 @@ export default function listItems(items) {
(item.itemName ? 'item-' + item.itemName : ''),
className,
(active ? 'active' : '')
])}>
])}
key={item.itemName}>
{item}
</li>,
' '
space
];
});
}

View File

@@ -0,0 +1,13 @@
import icon from 'flarum/helpers/icon';
/**
* The `useronline` helper displays a green circle if the user is online
*
* @param {User} user
* @return {Object}
*/
export default function userOnline(user) {
if (user.lastSeenTime() && user.isOnline()) {
return <span className="UserOnline">{icon('circle')}</span>;
}
}

View File

@@ -6,7 +6,7 @@
* @return {Object}
*/
export default function username(user) {
const name = (user && user.username()) || app.translator.trans('core.lib.deleted_user_text');
const name = (user && user.username()) || app.translator.trans('core.lib.username.deleted_text');
return <span className="username">{name}</span>;
}

View File

@@ -18,7 +18,7 @@ export default function preload(app) {
app.forum = app.store.getById('forums', 1);
app.session = new Session(
app.preload.session.token,
app.store.getById('users', app.preload.session.userId)
app.store.getById('users', app.preload.session.userId),
app.preload.session.csrfToken
);
}

View File

@@ -2,14 +2,13 @@ import Model from 'flarum/Model';
import mixin from 'flarum/utils/mixin';
import computed from 'flarum/utils/computed';
import ItemList from 'flarum/utils/ItemList';
import { slug } from 'flarum/utils/string';
import Badge from 'flarum/components/Badge';
export default class Discussion extends Model {}
Object.assign(Discussion.prototype, {
title: Model.attribute('title'),
slug: computed('title', slug),
slug: Model.attribute('slug'),
startTime: Model.attribute('startTime', Model.transformDate),
startUser: Model.hasOne('startUser'),
@@ -99,7 +98,9 @@ Object.assign(Discussion.prototype, {
* @public
*/
postIds() {
return this.data.relationships.posts.data.map(link => link.id);
const posts = this.data.relationships.posts;
return posts ? posts.data.map(link => link.id) : [];
}
});

View File

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

View File

@@ -14,15 +14,21 @@
}
@media @desktop, @desktop-hd {
.App-nav {
position: fixed;
position: absolute;
top: @header-height;
bottom: 0;
height: ~"calc(100vh - @{header-height})";
width: @admin-pane-width;
.box-shadow(0 6px 6px @shadow-color);
background: @body-bg;
border-top: 1px solid @control-bg;
z-index: @zindex-pane;
overflow: auto;
.affix & {
position: fixed;
bottom: 0;
height: auto;
}
}
.App-content .sideNavOffset {
margin-left: @admin-pane-width;

View File

@@ -39,12 +39,15 @@
h3 {
margin: 0;
line-height: 1.5em;
color: @secondary-color;
&, input, a {
color: @secondary-color;
font-size: 14px;
font-weight: normal;
}
input, a {
color: inherit;
}
input {
font-size: 16px;
width: 500px;
@@ -142,6 +145,9 @@
}
}
}
.Composer-controls .fa-minus:before {
content: @fa-var-times;
}
.composer-backdrop {
position: fixed;
top: 0;
@@ -172,6 +178,21 @@
border-bottom: 0;
padding: 15px;
}
.normal &:first-child {
margin: -@header-height-phone 50px 0;
text-align: center;
position: relative;
z-index: @zindex-header + 1;
border: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
h3 {
color: @header-control-color;
}
}
}
h3 {
&, a, input {
@@ -184,10 +205,9 @@
}
.ComposerBody-editor {
padding: 15px;
textarea {
height: 50vh !important;
}
}
.ComposerBody-editor .TextEditor-controls .item-submit {
position: absolute !important;
}
}
@@ -284,6 +304,9 @@
font-size: 16px;
}
}
.ComposerBody--discussion .ComposerBody-header .item-title {
display: none;
}
}
@media @desktop-up {

View File

@@ -32,7 +32,7 @@
pointer-events: none;
.Badge {
margin-left: -15px;
margin-left: -10px;
position: relative;
pointer-events: auto;
}
@@ -199,7 +199,7 @@
transition: background 0.2s;
&:hover {
background: @control-bg;
background: mix(@control-bg, @body-bg, 50%);
}
&:hover .DiscussionListItem-controls,
.DiscussionListItem-controls.open {

View File

@@ -128,7 +128,6 @@
.DiscussionPage-list {
.panePinned & {
left: 0;
z-index: @zindex-composer - 1;
.transition(none);
}
}

View File

@@ -6,8 +6,7 @@
margin-bottom: 20px;
.Button {
display: block;
width: 100%;
.Button--block();
}
}
}

View File

@@ -1,5 +1,11 @@
.LogInButton {
&:extend(.Button--block);
.Button-icon {
font-size: 18px;
vertical-align: -1px;
margin-right: 5px;
}
}
.LogInButtons {
width: 200px;

View File

@@ -32,7 +32,7 @@
position: absolute;
top: 1px;
left: 17px;
background: @control-color;
background: @header-control-color;
color: @header-bg;
font-size: 11px;
font-weight: bold;
@@ -42,6 +42,11 @@
border: 1px solid @header-bg;
min-width: 18px;
height: 18px;
text-align: center;
@media @phone {
left: 20px;
}
.new & {
background: @header-color;

View File

@@ -49,6 +49,16 @@
font-size: 14px;
}
.UserOnline {
& .icon {
font-size: 12px;
}
& .fa-circle {
color: @online-user-circle-color;
}
}
.UserCard {
position: absolute;
top: -10px;
@@ -71,7 +81,7 @@
pointer-events: none;
.Badge {
margin-left: -15px;
margin-left: -10px;
position: relative;
pointer-events: auto;
}
@@ -167,6 +177,9 @@
color: @muted-more-color;
}
}
.Post--loading {
opacity: 0.5;
}
.PostMeta {
display: inline;
}
@@ -250,7 +263,6 @@
margin-top: -5px;
float: right;
position: relative;
z-index: 1;
.transition(opacity 0.2s);
.EventPost &, .Post--hidden:not(.revealContent) & {
@@ -275,9 +287,6 @@
.Post:hover &, &.open {
opacity: 1;
}
&.open {
z-index: 2;
}
}
.PostPreview {
@@ -370,7 +379,6 @@
border: 2px dashed @control-bg;
color: @muted-color;
border-radius: 10px;
padding: 20px;
.Post-header {
margin: 0;
@@ -379,9 +387,6 @@
}
@media @tablet-up {
.ReplyPlaceholder {
margin-left: -20px;
margin-right: -20px;
padding-left: 110px;
border-color: transparent;
transition: border-color 0.2s;

View File

@@ -7,13 +7,17 @@
padding: 0;
> li {
margin-bottom: 40px;
margin-bottom: 25px;
}
}
fieldset > ul {
list-style: none;
margin: 0;
padding: 0;
> li {
margin-bottom: 15px;
}
}
}
.Settings-account {

View File

@@ -96,6 +96,8 @@
> .Button {
color: @header-color;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
}
.App-titleControl--text {
@@ -179,17 +181,17 @@
display: none;
}
.Header-title {
border-bottom: 1px solid @header-control-bg;
padding: 13px 10px;
font-size: 16px;
font-weight: normal;
margin: 0;
line-height: @header-height-phone - 1;
white-space: nowrap;
text-align: center;
}
.Header-controls {
margin-top: 10px;
> li {
padding: 10px 10px 0;
padding: 0 10px 0;
}
.FormControl, .ButtonGroup, .Button {
width: 100%;
@@ -201,6 +203,9 @@
}
}
}
.Header-secondary .Search {
margin: 10px 0;
}
}
// On other devices, we stick the header up the top of the page, overlaying

View File

@@ -1,12 +1,11 @@
.Badge {
.Badge--size(22px);
border: 1px solid @body-bg;
background: @muted-color;
color: #fff;
display: inline-block;
vertical-align: middle;
text-align: center;
.box-shadow(0 2px 6px @shadow-color);
.box-shadow(0 2px 4px @shadow-color);
.Badge-label {
display: none;
@@ -17,7 +16,7 @@
width: @size;
height: @size;
border-radius: @size / 2;
line-height: @size - 3px;
line-height: @size - 1px;
&, .Badge-icon {
font-size: 0.56 * @size;

View File

@@ -192,6 +192,8 @@
.Button--block {
display: block;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
// Vertically space out multiple block buttons
+ .Button--block {
@@ -217,7 +219,7 @@
border-radius: 18px;
.Avatar {
margin: -2px 5px -2px -5px;
margin: -2px 5px -2px -6px;
.Avatar--size(24px);
}
}

View File

@@ -33,6 +33,8 @@
border: 0;
background: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.box-shadow(none);
text-align: left;
font-size: 13px;
@@ -127,9 +129,11 @@
}
}
.Dropdown-menu li:first-child {
&, + li.Dropdown-separator {
display: none;
@media @tablet-up {
.Dropdown-menu li:first-child {
&, + li.Dropdown-separator {
display: none;
}
}
}
}

View File

@@ -6,6 +6,7 @@
overflow: hidden;
text-overflow: ellipsis;
padding-left: 8px;
padding-right: 8px;
.icon {
font-size: 14px;

View File

@@ -2,12 +2,15 @@
position: relative;
}
@media @tablet-up {
.Search.focused {
margin-left: -400px;
.transition(all 0.4s);
.Search {
.transition(margin-left 0.4s);
input, .Search-results {
width: 400px;
&.focused {
margin-left: -400px;
input, .Search-results {
width: 400px;
}
}
}
}

View File

@@ -1,5 +1,3 @@
@import url(//fonts.googleapis.com/css?family=Open+Sans:400italic,700italic,400,700,600);
@import "font-awesome.less";
@fa-font-path: "../../assets/fonts";

View File

@@ -1,18 +1,21 @@
// This is a mixin which styles components (buttons, inputs, etc.) for use on
// dark backgrounds.
.light-contents(@color: #fff, @control-bg: fade(#000, 10%), @control-color: #fff) {
&, a, .Button--link, .Search-input {
&, a {
color: @color;
}
.Button--link, .Search-input {
color: @control-color;
}
.FormControl {
background: @control-bg;
border: 0;
color: @control-color;
.placeholder(fade(@control-color, 80%));
.placeholder(@control-color);
&:focus {
color: @control-color;
background: fadein(@control-bg, 5%);
color: @color;
background: fadein(darken(@control-bg, 5%), 10%);
}
}
.Button, .Button:hover {

View File

@@ -66,6 +66,7 @@
> ul > li, .Dropdown-menu > li {
display: inline-block;
margin: 0 20px 0 0;
vertical-align: top;
}
.Dropdown-separator {
display: none;
@@ -94,7 +95,7 @@
float: left;
&, > ul {
width: 175px;
width: 190px;
}
> ul {
margin-top: 30px;
@@ -118,6 +119,6 @@
@media @desktop-up {
.sideNavOffset {
margin-top: 30px;
margin-left: 225px;
margin-left: 240px;
}
}

View File

@@ -81,8 +81,8 @@
.define-header(true) {
@header-bg: @primary-color;
@header-color: @body-bg;
@header-control-bg: fade(#000, 10%);
@header-control-color: @body-bg;
@header-control-bg: mix(#000, @header-bg, 10%);
@header-control-color: mix(@body-bg, @header-bg, 60%);
}
// ---------------------------------
@@ -95,10 +95,10 @@
@border-radius: 4px;
@zindex-dropdown: 1000;
@zindex-header: 1010;
@zindex-header: 1000;
@zindex-pane: 1010;
@zindex-composer: 1020;
@zindex-pane: 1030;
@zindex-dropdown: 1030;
@zindex-modal-background: 1040;
@zindex-modal: 1050;
@zindex-alerts: 1060;

View File

@@ -8,25 +8,15 @@
* file that was distributed with this source code.
*/
namespace Flarum\Core\Migration;
use Flarum\Database\AbstractMigration;
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateAccessTokensTable extends AbstractMigration
{
public function up()
{
$this->schema->create('access_tokens', function (Blueprint $table) {
$table->string('id', 100)->primary();
$table->integer('user_id')->unsigned();
$table->timestamp('created_at');
$table->timestamp('expires_at');
});
return Migration::createTable(
'access_tokens',
function (Blueprint $table) {
$table->string('id', 100)->primary();
$table->integer('user_id')->unsigned();
$table->timestamp('created_at');
$table->timestamp('expires_at');
}
public function down()
{
$this->schema->drop('access_tokens');
}
}
);

View File

@@ -8,22 +8,12 @@
* file that was distributed with this source code.
*/
namespace Flarum\Core\Migration;
use Flarum\Database\AbstractMigration;
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateApiKeysTable extends AbstractMigration
{
public function up()
{
$this->schema->create('api_keys', function (Blueprint $table) {
$table->string('id', 100)->primary();
});
return Migration::createTable(
'api_keys',
function (Blueprint $table) {
$table->string('id', 100)->primary();
}
public function down()
{
$this->schema->drop('api_keys');
}
}
);

View File

@@ -8,23 +8,13 @@
* file that was distributed with this source code.
*/
namespace Flarum\Core\Migration;
use Flarum\Database\AbstractMigration;
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateConfigTable extends AbstractMigration
{
public function up()
{
$this->schema->create('config', function (Blueprint $table) {
$table->string('key', 100)->primary();
$table->binary('value')->nullable();
});
return Migration::createTable(
'config',
function (Blueprint $table) {
$table->string('key', 100)->primary();
$table->binary('value')->nullable();
}
public function down()
{
$this->schema->drop('config');
}
}
);

View File

@@ -8,35 +8,25 @@
* file that was distributed with this source code.
*/
namespace Flarum\Core\Migration;
use Flarum\Database\AbstractMigration;
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateDiscussionsTable extends AbstractMigration
{
public function up()
{
$this->schema->create('discussions', function (Blueprint $table) {
$table->increments('id');
$table->string('title', 200);
$table->integer('comments_count')->unsigned()->default(0);
$table->integer('participants_count')->unsigned()->default(0);
$table->integer('number_index')->unsigned()->default(0);
return Migration::createTable(
'discussions',
function (Blueprint $table) {
$table->increments('id');
$table->string('title', 200);
$table->integer('comments_count')->unsigned()->default(0);
$table->integer('participants_count')->unsigned()->default(0);
$table->integer('number_index')->unsigned()->default(0);
$table->dateTime('start_time');
$table->integer('start_user_id')->unsigned()->nullable();
$table->integer('start_post_id')->unsigned()->nullable();
$table->dateTime('start_time');
$table->integer('start_user_id')->unsigned()->nullable();
$table->integer('start_post_id')->unsigned()->nullable();
$table->dateTime('last_time')->nullable();
$table->integer('last_user_id')->unsigned()->nullable();
$table->integer('last_post_id')->unsigned()->nullable();
$table->integer('last_post_number')->unsigned()->nullable();
});
$table->dateTime('last_time')->nullable();
$table->integer('last_user_id')->unsigned()->nullable();
$table->integer('last_post_id')->unsigned()->nullable();
$table->integer('last_post_number')->unsigned()->nullable();
}
public function down()
{
$this->schema->drop('discussions');
}
}
);

View File

@@ -8,25 +8,15 @@
* file that was distributed with this source code.
*/
namespace Flarum\Core\Migration;
use Flarum\Database\AbstractMigration;
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateEmailTokensTable extends AbstractMigration
{
public function up()
{
$this->schema->create('email_tokens', function (Blueprint $table) {
$table->string('id', 100)->primary();
$table->string('email', 150);
$table->integer('user_id')->unsigned();
$table->timestamp('created_at');
});
return Migration::createTable(
'email_tokens',
function (Blueprint $table) {
$table->string('id', 100)->primary();
$table->string('email', 150);
$table->integer('user_id')->unsigned();
$table->timestamp('created_at');
}
public function down()
{
$this->schema->drop('email_tokens');
}
}
);

View File

@@ -8,26 +8,16 @@
* file that was distributed with this source code.
*/
namespace Flarum\Core\Migration;
use Flarum\Database\AbstractMigration;
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateGroupsTable extends AbstractMigration
{
public function up()
{
$this->schema->create('groups', function (Blueprint $table) {
$table->increments('id');
$table->string('name_singular', 100);
$table->string('name_plural', 100);
$table->string('color', 20)->nullable();
$table->string('icon', 100)->nullable();
});
return Migration::createTable(
'groups',
function (Blueprint $table) {
$table->increments('id');
$table->string('name_singular', 100);
$table->string('name_plural', 100);
$table->string('color', 20)->nullable();
$table->string('icon', 100)->nullable();
}
public function down()
{
$this->schema->drop('groups');
}
}
);

View File

@@ -8,31 +8,21 @@
* file that was distributed with this source code.
*/
namespace Flarum\Core\Migration;
use Flarum\Database\AbstractMigration;
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateNotificationsTable extends AbstractMigration
{
public function up()
{
$this->schema->create('notifications', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id')->unsigned();
$table->integer('sender_id')->unsigned()->nullable();
$table->string('type', 100);
$table->string('subject_type', 200)->nullable();
$table->integer('subject_id')->unsigned()->nullable();
$table->binary('data')->nullable();
$table->dateTime('time');
$table->boolean('is_read')->default(0);
$table->boolean('is_deleted')->default(0);
});
return Migration::createTable(
'notifications',
function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id')->unsigned();
$table->integer('sender_id')->unsigned()->nullable();
$table->string('type', 100);
$table->string('subject_type', 200)->nullable();
$table->integer('subject_id')->unsigned()->nullable();
$table->binary('data')->nullable();
$table->dateTime('time');
$table->boolean('is_read')->default(0);
$table->boolean('is_deleted')->default(0);
}
public function down()
{
$this->schema->drop('notifications');
}
}
);

View File

@@ -8,24 +8,14 @@
* file that was distributed with this source code.
*/
namespace Flarum\Core\Migration;
use Flarum\Database\AbstractMigration;
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreatePasswordTokensTable extends AbstractMigration
{
public function up()
{
$this->schema->create('password_tokens', function (Blueprint $table) {
$table->string('id', 100)->primary();
$table->integer('user_id')->unsigned();
$table->timestamp('created_at');
});
return Migration::createTable(
'password_tokens',
function (Blueprint $table) {
$table->string('id', 100)->primary();
$table->integer('user_id')->unsigned();
$table->timestamp('created_at');
}
public function down()
{
$this->schema->drop('password_tokens');
}
}
);

View File

@@ -8,24 +8,14 @@
* file that was distributed with this source code.
*/
namespace Flarum\Core\Migration;
use Flarum\Database\AbstractMigration;
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreatePermissionsTable extends AbstractMigration
{
public function up()
{
$this->schema->create('permissions', function (Blueprint $table) {
$table->integer('group_id')->unsigned();
$table->string('permission', 100);
$table->primary(['group_id', 'permission']);
});
return Migration::createTable(
'permissions',
function (Blueprint $table) {
$table->integer('group_id')->unsigned();
$table->string('permission', 100);
$table->primary(['group_id', 'permission']);
}
public function down()
{
$this->schema->drop('permissions');
}
}
);

View File

@@ -8,16 +8,14 @@
* file that was distributed with this source code.
*/
namespace Flarum\Core\Migration;
use Flarum\Database\AbstractMigration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Schema\Builder;
class CreatePostsTable extends AbstractMigration
{
public function up()
{
$this->schema->create('posts', function (Blueprint $table) {
// We need a full custom migration here, because we need to add the fulltext
// index for the content with a raw SQL statement after creating the table.
return [
'up' => function (Builder $schema) {
$schema->create('posts', function (Blueprint $table) {
$table->increments('id');
$table->integer('discussion_id')->unsigned();
$table->integer('number')->unsigned()->nullable();
@@ -37,12 +35,11 @@ class CreatePostsTable extends AbstractMigration
$table->engine = 'MyISAM';
});
$prefix = $this->schema->getConnection()->getTablePrefix();
$this->schema->getConnection()->statement('ALTER TABLE '.$prefix.'posts ADD FULLTEXT content (content)');
}
$prefix = $schema->getConnection()->getTablePrefix();
$schema->getConnection()->statement('ALTER TABLE '.$prefix.'posts ADD FULLTEXT content (content)');
},
public function down()
{
$this->schema->drop('posts');
'down' => function (Builder $schema) {
$schema->drop('posts');
}
}
];

View File

@@ -8,26 +8,16 @@
* file that was distributed with this source code.
*/
namespace Flarum\Core\Migration;
use Flarum\Database\AbstractMigration;
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateUsersDiscussionsTable extends AbstractMigration
{
public function up()
{
$this->schema->create('users_discussions', function (Blueprint $table) {
$table->integer('user_id')->unsigned();
$table->integer('discussion_id')->unsigned();
$table->dateTime('read_time')->nullable();
$table->integer('read_number')->unsigned()->nullable();
$table->primary(['user_id', 'discussion_id']);
});
return Migration::createTable(
'users_discussions',
function (Blueprint $table) {
$table->integer('user_id')->unsigned();
$table->integer('discussion_id')->unsigned();
$table->dateTime('read_time')->nullable();
$table->integer('read_number')->unsigned()->nullable();
$table->primary(['user_id', 'discussion_id']);
}
public function down()
{
$this->schema->drop('users_discussions');
}
}
);

View File

@@ -8,24 +8,14 @@
* file that was distributed with this source code.
*/
namespace Flarum\Core\Migration;
use Flarum\Database\AbstractMigration;
use Flarum\Database\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateUsersGroupsTable extends AbstractMigration
{
public function up()
{
$this->schema->create('users_groups', function (Blueprint $table) {
$table->integer('user_id')->unsigned();
$table->integer('group_id')->unsigned();
$table->primary(['user_id', 'group_id']);
});
return Migration::createTable(
'users_groups',
function (Blueprint $table) {
$table->integer('user_id')->unsigned();
$table->integer('group_id')->unsigned();
$table->primary(['user_id', 'group_id']);
}
public function down()
{
$this->schema->drop('users_groups');
}
}
);

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