1
0
mirror of https://github.com/flarum/core.git synced 2025-08-17 22:01:44 +02:00

Compare commits

..

6 Commits

Author SHA1 Message Date
Toby Zerner
fd3f484eaf WIP: move some things around 2017-06-27 14:01:19 +09:30
Toby Zerner
8b3971f202 WIP: set up webpack, TypeScript environment, start JS app refactor 2017-06-24 23:00:43 +09:30
Sajjad Hashemian
5b68b80e73 Remove depricated function bind 2017-06-24 10:50:03 +02:00
Sajjad Hashemian
ef4c9d4f8a Upgrade to jQuery 3.1.1 2017-06-24 10:49:47 +02:00
Toby Zerner
4585002118 Prevent crash if extension loads an empty module 2017-06-24 10:49:33 +02:00
Toby Zerner
5451aac693 Clean up Group model 2017-06-24 10:49:21 +02:00
957 changed files with 17284 additions and 29039 deletions

View File

@@ -15,5 +15,5 @@ indent_size = 2
[*.{diff,md}]
trim_trailing_whitespace = false
[*.{php,xml}]
[*.php]
indent_size = 4

4
.gitattributes vendored
View File

@@ -1,8 +1,6 @@
.gitattributes export-ignore
.gitignore export-ignore
.gitmodules export-ignore
.github export-ignore
.travis export-ignore
.travis.yml export-ignore
.editorconfig export-ignore
.styleci.yml export-ignore
@@ -10,4 +8,4 @@
phpunit.xml export-ignore
tests export-ignore
js/dist/* -diff
js/*/dist/*.js -diff

3
.github/CONTRIBUTING.md vendored Normal file
View File

@@ -0,0 +1,3 @@
# Contributing to Flarum
Howdy! We're really excited that you are interested in contributing to Flarum. Before submitting your contribution, please take a moment and read through the [Contributing Guidelines](https://github.com/flarum/flarum/blob/master/CONTRIBUTING.md).

3
.github/FUNDING.yml vendored
View File

@@ -1,3 +0,0 @@
github: flarum
open_collective: flarum
tidelift: packagist/flarum/core

26
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,26 @@
> Issues on Github are meant for bug reporting. Please post feature requests on the [discussion forum](https://discuss.flarum.org/t/features).
---
> Try to complete the below form as far as you are able and are willing to share. Add a screenshot of the issue if you can.
## Bug report
- Version of Flarum: x.y.z
- Website URL where the bug is visible: http://example.com
- The webserver you are running: apache, nginx or something else
- PHP version: x.y.z
- Hosted environment: shared or vps
- Hosting provider: http://some-amazing-provider.com
## Flarum info
```
Output of "php flarum info", run this in terminal in your Flarum directory.
```
## Additional comments
Some additional information you'd like to share, eg what have you tried so far.
## Log files
```
Put any relevant logs here.
```

View File

@@ -1,39 +0,0 @@
---
name: "🐛 Bug Report"
about: "If something isn't working as expected"
---
## Bug Report
**Current Behavior**
A clear and concise description of the behavior.
**Steps to Reproduce**
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected Behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environment**
- Flarum version: x.y.z
- Website URL: http://example.com
- Webserver: [e.g. apache, nginx]
- Hosting environment: [e.g. shared, vps]
- PHP version: x.y.z
- Browser: [e.g. chrome 67, safari 11]
```
Output of "php flarum info", run this in terminal in your Flarum directory.
```
**Possible Solution**
<!--- Only if you have suggestions or a fix for the bug -->
**Additional Context**
Add any other context about the problem here.

View File

@@ -1,26 +0,0 @@
---
name: "🚀 Feature Request"
about: "I have a suggestion (and may want to implement it!)"
---
<!--
IMPORTANT: Feature requests on this GitHub issue tracker are only accepted in case they have been approved by a core developer or contain extensive argumentation and directions for implementation. For all other feature requests, ideas and feedback please post in the Flarum Community: https://discuss.flarum.org/t/feedback.
-->
## Feature Request
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. eg. I have an issue when [...]
**Describe the solution you'd like**
A detailed description of your proposed solution. Include:
- How the feature would work/behave
- Any potential drawbacks
- Maybe a screenshot, design, or example code
**Justify why this feature belongs in Flarum's core, rather than in a third-party extension**
Consider who this change will be useful to  most Flarum forums, or just a few?
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

View File

@@ -1,11 +0,0 @@
---
name: "🙋‍ Support Question"
about: "If you have a question, please check out our forum or Discord!"
---
We primarily use GitHub as an issue tracker; for usage and support questions, please check out these resources below. Thanks!
* Flarum Community: https://discuss.flarum.org/
* Discord Chat: https://flarum.org/discord/
* Twitter: https://twitter.com/Flarum

View File

@@ -1,24 +0,0 @@
<!--
IMPORTANT: We applaud pull requests, they excite us every single time. As we have an obligation to maintain a healthy code standard and quality, we take sufficient time for reviews. Please do create a separate pull request per change/issue/feature; we will ask you to split bundled pull requests.
-->
**Fixes #0000**
**Changes proposed in this pull request:**
<!-- fill this out, mention the pages and/or components which have been impacted -->
**Reviewers should focus on:**
<!-- fill this out, ask for feedback on specific changes you are unsure about -->
**Screenshot**
<!-- include an image of the most relevant user-facing change, if any -->
**Confirmed**
- [ ] Frontend changes: tested on a local Flarum installation.
- [ ] Backend changes: tests are green (run `composer test`).
**Required changes:**
- [ ] Related documentation PR: (Remove if irrelevant)
- [ ] Related core extension PRs: (Remove if irrelevant)

13
.github/SECURITY.md vendored
View File

@@ -1,13 +0,0 @@
# Security Policy
## Supported Versions
During the beta phase, we will only patch security vulnerabilities in the latest beta release.
## Reporting a Vulnerability
If you discover a security vulnerability within Flarum, please send an email to security@flarum.org so we can address it promptly.
We will get back to you as time allows.
Discussions may commence internally, so you may not hear back immediately.
When reporting a vulnerability, please provide your GitHub username (if available), so that we can invite you to collaborate on a [security advisory on GitHub](https://help.github.com/en/articles/about-maintainer-security-advisories).

26
.github/stale.yml vendored
View File

@@ -1,26 +0,0 @@
daysUntilStale: 90
daysUntilClose: 30
staleLabel: stale
exemptLabels:
- org/keep
- type/bug
- type/regression
- critical
- security
exemptAssignees: true
exemptMilestones: true
exemptProjects: true
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. We do this
to keep the amount of open issues to a manageable minimum.
In any case, thanks for taking an interest in this software and contributing
by opening the issue in the first place!
closeComment: >
We are closing this issue as it seems to have grown stale. If you still
encounter this problem with the latest version, feel free to re-open it.

View File

@@ -1,16 +0,0 @@
name: JavaScript
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: flarum/action-build@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,65 +0,0 @@
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
php: [7.1, 7.2, 7.3]
service: ['mysql:5.7', mariadb]
prefix: ['', flarum_]
include:
- service: 'mysql:5.7'
db: MySQL
- service: mariadb
db: MariaDB
- prefix: flarum_
prefixStr: (prefix)
exclude:
- php: 7.1
service: 'mysql:5.7'
prefix: flarum_
- php: 7.1
service: mariadb
prefix: flarum_
- php: 7.2
service: 'mysql:5.7'
prefix: flarum_
- php: 7.2
service: mariadb
prefix: flarum_
services:
mysql:
image: ${{ matrix.service }}
ports:
- 13306:3306
name: 'PHP ${{ matrix.php }} / ${{ matrix.db }} ${{ matrix.prefixStr }}'
steps:
- uses: actions/checkout@master
- name: Select PHP version
run: sudo update-alternatives --set php $(which php${{ matrix.php }})
- name: Create MySQL Database
run: mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306
- name: Install Composer dependencies
run: composer install
- name: Setup Composer tests
run: composer test:setup
env:
DB_PORT: 13306
DB_PASSWORD: root
DB_PREFIX: ${{ matrix.prefix }}
- name: Run Composer tests
run: composer test

6
.gitignore vendored
View File

@@ -1,9 +1,9 @@
/vendor
composer.lock
composer.phar
node_modules
.DS_Store
Thumbs.db
/tests/integration/tmp
tests/_output/*
.vagrant
.idea/*
node_modules
bower_components

35
.travis.yml Normal file
View File

@@ -0,0 +1,35 @@
language: php
php:
- 5.6
- 7.0
- 7.1
- hhvm
matrix:
allow_failures:
- php: hhvm
fast_finish: true
before_script:
- if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then phpenv config-rm xdebug.ini; fi;
- composer self-update
- composer install
script:
- vendor/bin/phpunit --coverage-clover=coverage.xml
notifications:
email:
on_failure: change
webhooks:
urls:
- https://webhooks.gitter.im/e/7b9e9827a03b44a16588
on_success: always
on_failure: always
on_start: false
after_success:
- bash <(curl -s https://codecov.io/bash)
sudo: false

View File

@@ -1,119 +0,0 @@
# Changelog
## [0.1.0-beta.11.1](https://github.com/flarum/core/compare/v0.1.0-beta.11...v0.1.0-beta.11.1)
### Fixed
- Saving custom css in admin failed (#1946)
## [0.1.0-beta.11](https://github.com/flarum/core/compare/v0.1.0-beta.10...v0.1.0-beta.11)
### Added
- Comments have an additional class `Post--by-actor` when posted by the user (#1927)
### Changed
- Improved support for URL identification during installation (#1861)
- KeyboardNavigatable now has a callback ability (#1922)
- Links are no longer opened with target `_blank` but in the same window (#859)
- Links now have `nofollow ugc` by default as their `rel` attribute (#859, #1884)
- Improved performance of the full text gambit when searching for users (#1877)
- The Queue implementation is now available under its Illuminate contract
### Fixed
- No error handling was possible in the console/cli (#1789)
- Enable scrollbars in log in modals so it fits for GitHub (#1716)
- Reduce log in modal for SSO so it fits for Facebook (#1727)
- Deleting discussions permanently did not delete its posts (#1909)
- Fixed the queue:restart command (#1932)
- Deleted posts were visible to all visitors (#1827)
- Old avatars weren't being deleted when replaced (#1918)
- The search performance regression was reverted (#1764)
- No profile background could be set for remote images (#445)
- Back button sends to home even though it could actually go back (#1942)
- Debug button no longer visible (#1687)
- Modals on smaller screens use the whole width of the page
## [0.1.0-beta.10](https://github.com/flarum/core/compare/v0.1.0-beta.9...v0.1.0-beta.10)
### Added
- Initial queue support: Infrastructure for offloading long-running tasks (e.g. email sending) to background workers (#1773)
- Notifications can now be marked as read without visiting a discussion (#151)
- SEO: The discussion list now has a `rel="canonical"` meta tag, preventing duplicate content (#1134, #1814)
- The "Edit User" permission can now be edited in the UI (#1845)
- New status message and redirect after user deletion (#1750, #1777)
- Errors in Flarum's boot process are now presented with more detailed information (#1607)
### Changed
- Better, more detailed and extensible error handling (#1641, #1843)
- Error pages in debug mode now return the same HTTP status codes as in production (#1648)
- Tweak HTTP status codes for authentication / authorization errors (#1854)
- Already-used links from account activation emails now show a better error message (#1337)
### Fixed
- Security vulnerabilities in dependencies
- Performance: High CPU usage when scrolling in a discussion (#1222)
- Special characters crashed the search (#1498)
- Missing declarations for language and text direction in HTML output (#1772)
- Private messages were counted in user post counts (#1695)
- Extensions could not change the forum's default page (#1819)
- API requests authenticated using access tokens needed to provide a CSRF token (#1828)
- Accessibility: Screenreaders did not read the "Back to discussion list" link (#1835)
## [0.1.0-beta.9](https://github.com/flarum/core/compare/v0.1.0-beta.8.2...v0.1.0-beta.9)
### Added
- New `hasPermission()` helper method for `Group` objects ([9684fbc](https://github.com/flarum/core/commit/9684fbc4da07d32aa322d9228302a23418412cb9))
- Expose supported mail drivers in IoC container ([208bad3](https://github.com/flarum/core/commit/208bad393f37bfdb76007afcddfa4b7451563e9d))
- More test for some API endpoints ([1670590](https://github.com/flarum/core/commit/167059027e5a066d618599c90164ef1b5a509148))
- The `Formatter\Rendering` event now receives the HTTP request instance as well ([0ab9fac](https://github.com/flarum/core/commit/0ab9facc4bd59a260575e6fc650793c663e5866a))
- More and better validation in installer UIs
- Check and enforce minimum MariaDB ([7ff9a90](https://github.com/flarum/core/commit/7ff9a90204923293adc520d3c02dc984845d4f9f))
- Revert publication of assets when installation fails ([ed9591c](https://github.com/flarum/core/commit/ed9591c16fb2ea7a4be3387b805d855a53e0a7d5))
- Benefit from Laravel's database reconnection logic in long-running tasks ([e0becd0](https://github.com/flarum/core/commit/e0becd0c7bda939048923c1f86648793feee78d5))
- The "vendor path" (where Composer dependencies can be found) can now be configured ([5e1680c](https://github.com/flarum/core/commit/5e1680c458cd3ba274faeb92de3ac2053789131e))
### Changed
- Performance: Actually cache translations on disk ([0d16fac](https://github.com/flarum/core/commit/0d16fac001bb735ee66e82871183516aeac269b7))
- Allow per-site extenders to override extension extenders ([ba594de](https://github.com/flarum/core/commit/ba594de13a033480834d53d73f747b05fe9796f8))
- Do not resolve objects from the IoC container (in service providers and extenders) until they are actually used
- Replace event subscribers (that resolve objects from the IoC container) with listeners (that resolve lazily)
- Use custom service provider for Mail component ([ac5e26a](https://github.com/flarum/core/commit/ac5e26a254d89e21bd4c115b6cbd40338e2e4b4b))
- Update to Laravel 5.7, revert custom logic for building database index names
- Refactored installer, extracted Installation class and pipeline for reuse in CLI and web installers ([790d5be](https://github.com/flarum/core/commit/790d5beee5e283178716bc8f9901c758d9e5b6a0))
- Use whitelist for enabling pre-installed extensions during installation ([4585f03](https://github.com/flarum/core/commit/4585f03ee356c92942fbc2ae8c683c651b473954))
- Update minimum MySQL version ([7ff9a90](https://github.com/flarum/core/commit/7ff9a90204923293adc520d3c02dc984845d4f9f))
### Fixed
- Signing up via OAuth providers was broken ([67f9375](https://github.com/flarum/core/commit/67f9375d4745add194ae3249d526197c32fd5461))
- Group badges were overlapping ([16eb1fa](https://github.com/flarum/core/commit/16eb1fa63b6d7b80ec30c24c0e406a2b7ab09934))
- API: Endpoint for uninstalling extensions returned an error ([c761802](https://github.com/flarum/core/commit/c76180290056ddbab67baf5ede814fcedf1dcf14))
- Documentation links in installer were outdated ([b58380e](https://github.com/flarum/core/commit/b58380e224ee54abdade3d0a4cc107ef5c91c9a9))
- Event posts where counted when aggregating user posts ([671fdec](https://github.com/flarum/core/commit/671fdec8d0a092ccceb5d4d5f657d0f4287fc4c7))
- Admins could not reset user passwords ([c67fb2d](https://github.com/flarum/core/commit/c67fb2d4b6a128c71d65dc6703310c0b62f91be2))
- Several down migrations were invalid
- Validation errors on reset password page resulted in HTTP 404 ([4611abe](https://github.com/flarum/core/commit/4611abe5db8b94ca3dc7bf9c447fca7c67358ee3))
- `is:unread` gambit generated an invalid query ([e17bb0b](https://github.com/flarum/core/commit/e17bb0b4331f2c92459292195c6b7db8cde1f9f3))
- Entire forum was breaking when the `custom_less` setting was missing from the database ([bf2c5a5](https://github.com/flarum/core/commit/bf2c5a5564dff3f5ef13efe7a8d69f2617570ce6))
- Dropdown icon was not showing in user card when on user page ([12fdfc9](https://github.com/flarum/core/commit/12fdfc9b544a27f6fe59c82ad6bddd3420cc0181))
- Requests were missing the `original*` attributes, which broke installations in subfolders ([56fde28](https://github.com/flarum/core/commit/56fde28e436f52fee0c03c538f0a6049bc584b53))
- Special characters such as `%` and `_` could return incorrect results ([ee3640e](https://github.com/flarum/core/commit/ee3640e1605ff67fef4b3d5cd0596f14a6ae73c9))
- FontAwesome component package changed paths in version 5.9.0 ([5eb69e1](https://github.com/flarum/core/commit/5eb69e1f59fa73fdfd5badbf41a05a6a040e7426))
- Some server environments had problems accessing the system-wide tmp path for storing JS file maps ([54660eb](https://github.com/flarum/core/commit/54660ebd6311f9ea142f1b573263d0d907400786))
- Content length of posts.content was not migrated to mediumText in 2017 ([590b311](https://github.com/flarum/core/commit/590b3115708bf94a9c7f169d98c6126380c7056e))
- An error occurred when going to the previous route if there was no previous route found ([985b87da](https://github.com/flarum/core/commit/985b87da6c9942c568a1a192e2fdcfde72e030ee))
### Removed
- `php flarum install --defaults` - this was meant to be used in our old development VM ([44c9109](https://github.com/flarum/core/commit/44c91099cd77138bb5fc29f14fb1e81a9781272d))
- Obsolete `id` attributes in JSON-API responses ([ecc3b5e](https://github.com/flarum/core/commit/ecc3b5e2271f8d9b38d52cd54476d86995dbe32e) and [7a44086](https://github.com/flarum/core/commit/7a44086bf3a0e3ba907dceb13d07ac695eca05ea))
## [0.1.0-beta.8.1](https://github.com/flarum/core/compare/v0.1.0-beta.8...v0.1.0-beta.8.1)
### Fixed
- Fix live output in `migrate:reset` command ([f591585](https://github.com/flarum/core/commit/f591585d02f8c4ff0211c5bf4413dd6baa724c05))
- Fix search with database prefix ([7705a2b](https://github.com/flarum/core/commit/7705a2b7d751943ef9d0c7379ec34f8530b99310))
- Fix invalid join time of admin user created by installer ([57f73c9](https://github.com/flarum/core/commit/57f73c9638eeb825f9e336ed3c443afccfd8995e))
- Ensure InnoDB engine is used for all tables ([fb6b51b](https://github.com/flarum/core/commit/fb6b51b1cfef0af399607fe038603c8240800b2b), [6370f7e](https://github.com/flarum/core/commit/6370f7ecffa9ea7d5fb64d9551400edbc63318db))
- Fix dropping foreign keys in `down` migrations ([57d5846](https://github.com/flarum/core/commit/57d5846b647881009d9e60f9ffca20b1bb77776e))
- Fix discussion list scroll position not being maintained when hero is not visible ([40dc6ac](https://github.com/flarum/core/commit/40dc6ac604c2a0973356b38217aa8d09352daae5))
- Fix empty meta description tag ([88e43cc](https://github.com/flarum/core/commit/88e43cc6940ee30d6529e9ce659471ec4fb1c474))
- Remove empty attributes on `<html>` tag ([796b577](https://github.com/flarum/core/commit/796b57753d34d4ea741dbebcbc550b17808f6c94))

View File

@@ -1,7 +1,6 @@
The MIT License (MIT)
Copyright (c) 2019-2020 Stichting Flarum (Flarum Foundation)
Copyright (c) 2014-2019 Toby Zerner (toby.zerner@gmail.com)
Copyright (c) 2014-2017 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

@@ -1,35 +1,7 @@
<p align="center"><img src="https://flarum.org/img/logo.png"></p>
# Flarum Core
<p align="center">
<a href="https://travis-ci.org/flarum/core"><img src="https://travis-ci.org/flarum/core.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/d/total.svg" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/v/stable.svg" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/flarum/core"><img src="https://poser.pugx.org/flarum/core/license.svg" alt="License"></a>
</p>
## About Flarum
**[Flarum](https://flarum.org/) is a delightfully simple discussion platform for your website.** It's fast and easy to use, with all the features you need to run a successful community. It is designed to be:
* **Fast and simple.** No clutter, no bloat, no complex dependencies. Flarum is built with PHP so its quick and easy to deploy. The interface is powered by Mithril, a performant JavaScript framework with a tiny footprint.
* **Beautiful and responsive.** This is forum software for humans. Flarum is carefully designed to be consistent and intuitive across platforms, out-of-the-box.
* **Powerful and extensible.** Customize, extend, and integrate Flarum to suit your community. Flarums architecture is amazingly flexible, with a powerful Extension API.
## Installation
This repository contains Flarum's core code. If you want to set up a forum, visit the [Flarum skeleton repository](https://github.com/flarum/flarum).
This repository contains Flarum's core code. If you want to set up a forum, visit the [main Flarum repository](http://github.com/flarum/flarum).
## Contributing
Thank you for considering contributing to Flarum! Please read the **[Contributing guide](https://flarum.org/docs/contributing.html)** to learn how you can help.
## Security Vulnerabilities
If you discover a security vulnerability within Flarum, please send an e-mail to [security@flarum.org](mailto:security@flarum.org). All security vulnerabilities will be promptly addressed. More details can be found in our [security policy](https://github.com/flarum/core/security/policy).
## License
Flarum is open-source software licensed under the [MIT License](https://github.com/flarum/flarum/blob/master/LICENSE).
Flarum is open-source and we would love your help building it! Please read the [Contributing Guide](https://github.com/flarum/flarum/blob/master/CONTRIBUTING.md) to learn how you can help.

View File

@@ -2,7 +2,7 @@
"name": "flarum/core",
"description": "Delightfully simple forum software.",
"keywords": ["forum", "discussion"],
"homepage": "https://flarum.org/",
"homepage": "http://flarum.org",
"license": "MIT",
"authors": [
{
@@ -12,66 +12,52 @@
{
"name": "Franz Liedke",
"email": "franz@develophp.org"
},
{
"name": "Daniel Klabbers",
"email": "daniel@klabbers.email"
}
],
"support": {
"issues": "https://github.com/flarum/core/issues",
"source": "https://github.com/flarum/core",
"docs": "https://flarum.org/docs/"
"docs": "http://flarum.org/docs"
},
"require": {
"php": ">=7.1",
"axy/sourcemap": "^0.1.4",
"components/font-awesome": "5.9.*",
"php": ">=5.6.0",
"dflydev/fig-cookies": "^1.0.2",
"doctrine/dbal": "^2.7",
"doctrine/dbal": "^2.5",
"components/font-awesome": "^4.6",
"franzl/whoops-middleware": "^0.4.0",
"illuminate/bus": "5.7.*",
"illuminate/cache": "5.7.*",
"illuminate/config": "5.7.*",
"illuminate/container": "5.7.*",
"illuminate/contracts": "5.7.*",
"illuminate/database": "5.7.*",
"illuminate/events": "5.7.*",
"illuminate/filesystem": "5.7.*",
"illuminate/hashing": "5.7.*",
"illuminate/mail": "5.7.*",
"illuminate/queue": "5.7.*",
"illuminate/session": "5.7.*",
"illuminate/support": "5.7.*",
"illuminate/validation": "5.7.*",
"illuminate/view": "5.7.*",
"illuminate/bus": "5.1.*",
"illuminate/cache": "5.1.*",
"illuminate/config": "5.1.*",
"illuminate/container": "5.1.*",
"illuminate/contracts": "5.1.*",
"illuminate/database": "^5.1.31",
"illuminate/events": "5.1.*",
"illuminate/filesystem": "5.1.*",
"illuminate/hashing": "5.1.*",
"illuminate/mail": "5.1.*",
"illuminate/support": "5.1.*",
"illuminate/validation": "5.1.*",
"illuminate/view": "5.1.*",
"intervention/image": "^2.3.0",
"laminas/laminas-diactoros": "^1.8.4",
"laminas/laminas-httphandlerrunner": "^1.0",
"laminas/laminas-stratigility": "^3.0",
"laminas/laminas-zendframework-bridge": "^1.0",
"league/flysystem": "^1.0.11",
"league/oauth2-client": "~1.0",
"matthiasmullie/minify": "^1.3",
"middlewares/base-path": "^1.1",
"middlewares/base-path-router": "^0.2.1",
"middlewares/request-handler": "^1.2",
"monolog/monolog": "^1.16.0",
"nikic/fast-route": "^0.6",
"oyejorge/less.php": "^1.7",
"oyejorge/less.php": "~1.5",
"psr/http-message": "^1.0",
"psr/http-server-handler": "^1.0",
"psr/http-server-middleware": "^1.0",
"s9e/text-formatter": "^1.2.0",
"symfony/config": "^3.3",
"symfony/console": "^4.2",
"symfony/event-dispatcher": "^4.3.2",
"symfony/translation": "^3.3",
"symfony/yaml": "^3.3",
"tobscure/json-api": "^0.3.0"
"symfony/console": "^2.7",
"symfony/http-foundation": "^2.7",
"symfony/translation": "^2.7",
"symfony/yaml": "^2.7",
"s9e/text-formatter": "^0.8.1",
"tobscure/json-api": "^0.3.0",
"zendframework/zend-diactoros": "^1.1",
"zendframework/zend-stratigility": "^1.3"
},
"require-dev": {
"mockery/mockery": "^1.0",
"phpunit/phpunit": "^7.0"
"mockery/mockery": "^0.9.4",
"phpunit/phpunit": "^4.8"
},
"autoload": {
"psr-4": {
@@ -83,30 +69,12 @@
},
"autoload-dev": {
"psr-4": {
"Flarum\\Tests\\": "tests/"
"Tests\\": "tests/"
}
},
"config": {
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-master": "0.1.x-dev"
}
},
"scripts": {
"test": [
"@test:unit",
"@test:integration"
],
"test:unit": "phpunit -c tests/phpunit.unit.xml",
"test:integration": "phpunit -c tests/phpunit.integration.xml",
"test:setup": "@php tests/integration/setup.php"
},
"scripts-descriptions": {
"test": "Runs all tests.",
"test:unit": "Runs all unit tests.",
"test:integration": "Runs all integration tests.",
"test:setup": "Sets up a database for use with integration tests. Execute this only once."
}
}

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>

13
error/404.html Normal file
View File

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

13
error/500.html Normal file
View File

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

13
error/503.html Normal file
View File

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

1
js/admin/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
dist

View File

@@ -0,0 +1,53 @@
import * as m from 'mithril';
import Application from './lib/Application';
import routes from './routes';
import Nav from './components/Nav';
export default class AdminApplication extends Application {
/**
* A map of extension names to their settings callbacks.
*
* @type {Object}
*/
extensionSettings = {};
/**
* Construct a list of permissions required to have the given permission.
*
* @param {String} permission
* @return {Array}
*/
getRequiredPermissions(permission) {
const required = [];
if (permission === 'startDiscussion' || permission.indexOf('discussion.') === 0) {
required.push('viewDiscussions');
}
if (permission === 'discussion.delete') {
required.push('discussion.hide');
}
if (permission === 'discussion.deletePosts') {
required.push('discussion.editPosts');
}
return required;
}
/**
* @inheritdoc
*/
mount() {
m.route.prefix('#');
super.mount();
m.mount(document.getElementById('nav'), <Nav/>);
}
/**
* @inheritdoc
*/
registerDefaultRoutes(router) {
routes(router);
}
}

View File

@@ -0,0 +1,40 @@
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import flarum from 'flarum';
import Modal from 'flarum/lib/components/Modal';
export default class AddExtensionModal extends Modal {
/**
* @inheritdoc
*/
className() {
return super.className() + ' AddExtensionModal Modal--small';
}
/**
* @inheritdoc
*/
title() {
return flarum.translator.trans('admin.add_extension.title');
}
/**
* @inheritdoc
*/
content() {
return (
<div className="Modal-body">
<p>{flarum.translator.trans('admin.add_extension.temporary_text')}</p>
<p>{flarum.translator.trans('admin.add_extension.install_text', {a: <a href="https://discuss.flarum.org/t/extensions" target="_blank"/>})}</p>
<p>{flarum.translator.trans('admin.add_extension.developer_text', {a: <a href="http://flarum.org/docs/extend" target="_blank"/>})}</p>
</div>
);
}
}

View File

@@ -0,0 +1,33 @@
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import LinkButton from 'flarum/lib/components/LinkButton';
/**
*
*/
export default class AdminLinkButton extends LinkButton {
/**
* @inheritdoc
*/
className() {
return super.className() + ' AdminLinkButton';
}
/**
* @inheritdoc
*/
content() {
const content = super.content();
content.push(<div className="AdminLinkButton-description">{this.attrs.description}</div>);
return content;
}
}

View File

@@ -0,0 +1,52 @@
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import Component from 'flarum/lib/Component';
import ItemList from 'flarum/lib/utils/ItemList';
import AdminLinkButton from 'flarum/components/AdminLinkButton';
import SelectDropdown from 'flarum/components/SelectDropdown';
function addLink(items, route, icon) {
items.add(route, <AdminLinkButton
href={flarum.router.to(route)}
icon={icon}
children={flarum.translator.trans(`admin.nav.${route}_button`)}
description={flarum.translator.trans(`admin.nav.${route}_text`)}/>);
}
/**
*
*/
export default class Nav extends Component {
/**
* @inheritdoc
*/
view() {
return <SelectDropdown className="Nav" buttonClassName="Button" children={this.items().toArray()}/>;
}
/**
* Build an item list of links to show in the admin navigation.
*
* @return {ItemList}
* @public
*/
items() {
const items = new ItemList();
addLink(items, 'dashboard', 'bar-chart'));
addLink(items, 'basics', 'pencil'));
addLink(items, 'mail', 'envelope'));
addLink(items, 'permissions', 'key'));
addLink(items, 'appearance', 'paint-brush'));
addLink(items, 'extensions', 'puzzle-piece'));
return items;
}
}

View File

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

View File

@@ -1,11 +1,11 @@
import Page from './Page';
import FieldSet from '../../common/components/FieldSet';
import Select from '../../common/components/Select';
import Button from '../../common/components/Button';
import Alert from '../../common/components/Alert';
import saveSettings from '../utils/saveSettings';
import ItemList from '../../common/utils/ItemList';
import Switch from '../../common/components/Switch';
import Page from 'flarum/components/Page';
import FieldSet from 'flarum/components/FieldSet';
import Select from 'flarum/components/Select';
import Button from 'flarum/components/Button';
import Alert from 'flarum/components/Alert';
import saveSettings from 'flarum/utils/saveSettings';
import ItemList from 'flarum/utils/ItemList';
import Switch from 'flarum/components/Switch';
export default class BasicsPage extends Page {
init() {
@@ -25,7 +25,7 @@ export default class BasicsPage extends Page {
this.values = {};
const settings = app.data.settings;
this.fields.forEach(key => this.values[key] = m.prop(settings[key]));
this.fields.forEach(key => this.values[key] = m.prop(settings[key] || false));
this.localeOptions = {};
const locales = app.data.locales;
@@ -66,16 +66,19 @@ export default class BasicsPage extends Page {
options: this.localeOptions,
value: this.values.default_locale(),
onchange: this.values.default_locale
}),
Switch.component({
state: this.values.show_language_selector(),
onchange: this.values.show_language_selector,
children: app.translator.trans('core.admin.basics.show_language_selector_label'),
})
]
})
: ''}
{Switch.component({
state: this.values.show_language_selector(),
onchange: this.values.show_language_selector,
children: app.translator.trans('core.admin.basics.show_language_selector_label'),
})}
<br/>
{FieldSet.component({
label: app.translator.trans('core.admin.basics.home_page_heading'),
className: 'BasicsPage-homePage',

View File

@@ -0,0 +1,22 @@
import Page from 'flarum/components/Page';
export default class DashboardPage extends Page {
view() {
return (
<div className="DashboardPage">
<div className="container">
<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>
<li>{app.translator.trans('core.admin.dashboard.contributing_text', {a: <a href="http://flarum.org/docs/contributing" target="_blank"/>})}</li>
<li>{app.translator.trans('core.admin.dashboard.troubleshooting_text', {a: <a href="http://flarum.org/docs/troubleshooting" target="_blank"/>})}</li>
<li>{app.translator.trans('core.admin.dashboard.support_text', {a: <a href="http://discuss.flarum.org/t/support" target="_blank"/>})}</li>
<li>{app.translator.trans('core.admin.dashboard.features_text', {a: <a href="http://discuss.flarum.org/t/features" target="_blank"/>})}</li>
<li>{app.translator.trans('core.admin.dashboard.extension_text', {a: <a href="http://flarum.org/docs/extend" target="_blank"/>})}</li>
</ul>
</div>
</div>
);
}
}

View File

@@ -1,4 +1,4 @@
import SettingsModal from './SettingsModal';
import SettingsModal from 'flarum/components/SettingsModal';
export default class EditCustomCssModal extends SettingsModal {
className() {

View File

@@ -1,4 +1,4 @@
import SettingsModal from './SettingsModal';
import SettingsModal from 'flarum/components/SettingsModal';
export default class EditCustomHeaderModal extends SettingsModal {
className() {

View File

@@ -0,0 +1,102 @@
import Modal from 'flarum/components/Modal';
import Button from 'flarum/components/Button';
import Badge from 'flarum/components/Badge';
import Group from 'flarum/models/Group';
/**
* The `EditGroupModal` component shows a modal dialog which allows the user
* to create or edit a group.
*/
export default class EditGroupModal extends Modal {
init() {
this.group = this.props.group || app.store.createRecord('groups');
this.nameSingular = m.prop(this.group.nameSingular() || '');
this.namePlural = m.prop(this.group.namePlural() || '');
this.icon = m.prop(this.group.icon() || '');
this.color = m.prop(this.group.color() || '');
}
className() {
return 'EditGroupModal Modal--small';
}
title() {
return [
this.color() || this.icon() ? Badge.component({
icon: this.icon(),
style: {backgroundColor: this.color()}
}) : '',
' ',
this.namePlural() || app.translator.trans('core.admin.edit_group.title')
];
}
content() {
return (
<div className="Modal-body">
<div className="Form">
<div className="Form-group">
<label>{app.translator.trans('core.admin.edit_group.name_label')}</label>
<div className="EditGroupModal-name-input">
<input className="FormControl" placeholder={app.translator.trans('core.admin.edit_group.singular_placeholder')} value={this.nameSingular()} oninput={m.withAttr('value', this.nameSingular)}/>
<input className="FormControl" placeholder={app.translator.trans('core.admin.edit_group.plural_placeholder')} value={this.namePlural()} oninput={m.withAttr('value', this.namePlural)}/>
</div>
</div>
<div className="Form-group">
<label>{app.translator.trans('core.admin.edit_group.color_label')}</label>
<input className="FormControl" placeholder="#aaaaaa" value={this.color()} oninput={m.withAttr('value', this.color)}/>
</div>
<div className="Form-group">
<label>{app.translator.trans('core.admin.edit_group.icon_label')}</label>
<div className="helpText">
{app.translator.trans('core.admin.edit_group.icon_text', {a: <a href="http://fortawesome.github.io/Font-Awesome/icons/" tabindex="-1"/>})}
</div>
<input className="FormControl" placeholder="bolt" value={this.icon()} oninput={m.withAttr('value', this.icon)}/>
</div>
<div className="Form-group">
{Button.component({
type: 'submit',
className: 'Button Button--primary EditGroupModal-save',
loading: this.loading,
children: app.translator.trans('core.admin.edit_group.submit_button')
})}
{this.group.exists && this.group.id() !== Group.ADMINISTRATOR_ID ? (
<button type="button" className="Button EditGroupModal-delete" onclick={this.deleteGroup.bind(this)}>
{app.translator.trans('core.admin.edit_group.delete_button')}
</button>
) : ''}
</div>
</div>
</div>
);
}
onsubmit(e) {
e.preventDefault();
this.loading = true;
this.group.save({
nameSingular: this.nameSingular(),
namePlural: this.namePlural(),
color: this.color(),
icon: this.icon()
}, {errorHandler: this.onerror.bind(this)})
.then(this.hide.bind(this))
.catch(() => {
this.loading = false;
m.redraw();
});
}
deleteGroup() {
if (confirm(app.translator.trans('core.admin.edit_group.delete_confirmation'))) {
this.group.delete().then(() => m.redraw());
this.hide();
}
}
}

View File

@@ -1,13 +1,13 @@
import Page from './Page';
import LinkButton from '../../common/components/LinkButton';
import Button from '../../common/components/Button';
import Dropdown from '../../common/components/Dropdown';
import Separator from '../../common/components/Separator';
import AddExtensionModal from './AddExtensionModal';
import LoadingModal from './LoadingModal';
import ItemList from '../../common/utils/ItemList';
import icon from '../../common/helpers/icon';
import listItems from '../../common/helpers/listItems';
import Page from 'flarum/components/Page';
import LinkButton from 'flarum/components/LinkButton';
import Button from 'flarum/components/Button';
import Dropdown from 'flarum/components/Dropdown';
import Separator from 'flarum/components/Separator';
import AddExtensionModal from 'flarum/components/AddExtensionModal';
import LoadingModal from 'flarum/components/LoadingModal';
import ItemList from 'flarum/utils/ItemList';
import icon from 'flarum/helpers/icon';
import listItems from 'flarum/helpers/listItems';
export default class ExtensionsPage extends Page {
view() {
@@ -17,7 +17,7 @@ export default class ExtensionsPage extends Page {
<div className="container">
{Button.component({
children: app.translator.trans('core.admin.extensions.add_button'),
icon: 'fas fa-plus',
icon: 'plus',
className: 'Button Button--primary',
onclick: () => app.modal.show(new AddExtensionModal())
})}
@@ -42,18 +42,15 @@ export default class ExtensionsPage extends Page {
className="ExtensionListItem-controls"
buttonClassName="Button Button--icon Button--flat"
menuClassName="Dropdown-menu--right"
icon="fas fa-ellipsis-h">
icon="ellipsis-h">
{controls}
</Dropdown>
) : ''}
<div className="ExtensionListItem-main">
<label className="ExtensionListItem-title">
<input type="checkbox" checked={this.isEnabled(extension.id)} onclick={this.toggle.bind(this, extension.id)}/> {' '}
{extension.extra['flarum-extension'].title}
</label>
<div className="ExtensionListItem-version">{extension.version}</div>
<div className="ExtensionListItem-description">{extension.description}</div>
</div>
<label className="ExtensionListItem-title">
<input type="checkbox" checked={this.isEnabled(extension.id)} onclick={this.toggle.bind(this, extension.id)}/> {' '}
{extension.extra['flarum-extension'].title}
</label>
<div className="ExtensionListItem-version">{extension.version}</div>
</div>
</li>;
})}
@@ -70,7 +67,7 @@ export default class ExtensionsPage extends Page {
if (app.extensionSettings[name]) {
items.add('settings', Button.component({
icon: 'fas fa-cog',
icon: 'cog',
children: app.translator.trans('core.admin.extensions.settings_button'),
onclick: app.extensionSettings[name]
}));
@@ -78,7 +75,7 @@ export default class ExtensionsPage extends Page {
if (!enabled) {
items.add('uninstall', Button.component({
icon: 'far fa-trash-alt',
icon: 'trash-o',
children: app.translator.trans('core.admin.extensions.uninstall_button'),
onclick: () => {
app.request({

View File

@@ -0,0 +1,25 @@
import Component from 'flarum/Component';
import ItemList from 'flarum/utils/ItemList';
import SessionDropdown from './SessionDropdown';
export default class Header extends Component {
/**
* @inheritdoc
*/
view() {
return this.items().toVnodes();
}
/**
* Build an item list for the header contents.
*
* @return {ItemList}
*/
items() {
const items = new ItemList();
items.add('session', <SessionDropdown/>);
return items;
}
}

View File

@@ -1,4 +1,4 @@
import Modal from '../../common/components/Modal';
import Modal from 'flarum/components/Modal';
export default class LoadingModal extends Modal {
isDismissible() {

View File

@@ -1,57 +1,37 @@
import Page from './Page';
import FieldSet from '../../common/components/FieldSet';
import Button from '../../common/components/Button';
import Alert from '../../common/components/Alert';
import Select from '../../common/components/Select';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import saveSettings from '../utils/saveSettings';
import Page from 'flarum/components/Page';
import FieldSet from 'flarum/components/FieldSet';
import Button from 'flarum/components/Button';
import Alert from 'flarum/components/Alert';
import saveSettings from 'flarum/utils/saveSettings';
export default class MailPage extends Page {
init() {
super.init();
this.loading = true;
this.saving = false;
this.loading = false;
this.driverFields = {};
this.fields = ['mail_driver', 'mail_from'];
this.fields = [
'mail_driver',
'mail_host',
'mail_from',
'mail_port',
'mail_username',
'mail_password',
'mail_encryption'
];
this.values = {};
const settings = app.data.settings;
this.fields.forEach(key => this.values[key] = m.prop(settings[key]));
app.request({
method: 'GET',
url: app.forum.attribute('apiUrl') + '/mail-drivers'
}).then(response => {
this.driverFields = response['data'].reduce(
(hash, driver) => ({...hash, [driver['id']]: driver['attributes']['fields']}),
{}
);
for (const driver in this.driverFields) {
for (const field in this.driverFields[driver]) {
this.fields.push(field);
this.values[field] = m.prop(settings[field]);
}
}
this.loading = false;
m.redraw();
});
this.localeOptions = {};
const locales = app.locales;
for (const i in locales) {
this.localeOptions[i] = `${locales[i]} (${i})`;
}
}
view() {
if (this.loading) {
return (
<div className="MailPage">
<div className="container">
<LoadingIndicator />
</div>
</div>
);
}
return (
<div className="MailPage">
<div className="container">
@@ -61,6 +41,36 @@ export default class MailPage extends Page {
{app.translator.trans('core.admin.email.text')}
</div>
{FieldSet.component({
label: app.translator.trans('core.admin.email.server_heading'),
className: 'MailPage-MailSettings',
children: [
<div className="MailPage-MailSettings-input">
<label>{app.translator.trans('core.admin.email.driver_label')}</label>
<input className="FormControl" value={this.values.mail_driver() || ''} oninput={m.withAttr('value', this.values.mail_driver)} />
<label>{app.translator.trans('core.admin.email.host_label')}</label>
<input className="FormControl" value={this.values.mail_host() || ''} oninput={m.withAttr('value', this.values.mail_host)} />
<label>{app.translator.trans('core.admin.email.port_label')}</label>
<input className="FormControl" value={this.values.mail_port() || ''} oninput={m.withAttr('value', this.values.mail_port)} />
<label>{app.translator.trans('core.admin.email.encryption_label')}</label>
<input className="FormControl" value={this.values.mail_encryption() || ''} oninput={m.withAttr('value', this.values.mail_encryption)} />
</div>
]
})}
{FieldSet.component({
label: app.translator.trans('core.admin.email.account_heading'),
className: 'MailPage-MailSettings',
children: [
<div className="MailPage-MailSettings-input">
<label>{app.translator.trans('core.admin.email.username_label')}</label>
<input className="FormControl" value={this.values.mail_username() || ''} oninput={m.withAttr('value', this.values.mail_username)} />
<label>{app.translator.trans('core.admin.email.password_label')}</label>
<input className="FormControl" value={this.values.mail_password() || ''} oninput={m.withAttr('value', this.values.mail_password)} />
</div>
]
})}
{FieldSet.component({
label: app.translator.trans('core.admin.email.addresses_heading'),
className: 'MailPage-MailSettings',
@@ -72,35 +82,11 @@ export default class MailPage extends Page {
]
})}
{FieldSet.component({
label: app.translator.trans('core.admin.email.driver_heading'),
className: 'MailPage-MailSettings',
children: [
<div className="MailPage-MailSettings-input">
<label>{app.translator.trans('core.admin.email.driver_label')}</label>
<Select value={this.values.mail_driver()} options={Object.keys(this.driverFields).reduce((memo, val) => ({...memo, [val]: val}), {})} onchange={this.values.mail_driver} />
</div>
]
})}
{Object.keys(this.driverFields[this.values.mail_driver()]).length > 0 && FieldSet.component({
label: app.translator.trans(`core.admin.email.${this.values.mail_driver()}_heading`),
className: 'MailPage-MailSettings',
children: [
<div className="MailPage-MailSettings-input">
{Object.keys(this.driverFields[this.values.mail_driver()]).map(field => [
<label>{app.translator.trans(`core.admin.email.${field}_label`)}</label>,
this.renderField(field),
])}
</div>
]
})}
{Button.component({
type: 'submit',
className: 'Button Button--primary',
children: app.translator.trans('core.admin.email.submit_button'),
loading: this.saving,
loading: this.loading,
disabled: !this.changed()
})}
</form>
@@ -109,18 +95,6 @@ export default class MailPage extends Page {
);
}
renderField(name) {
const driver = this.values.mail_driver();
const field = this.driverFields[driver][name];
const prop = this.values[name];
if (typeof field === 'string') {
return <input className="FormControl" value={prop() || ''} oninput={m.withAttr('value', prop)}/>;
} else {
return <Select value={prop()} options={field} onchange={prop} />;
}
}
changed() {
return this.fields.some(key => this.values[key]() !== app.data.settings[key]);
}
@@ -128,9 +102,9 @@ export default class MailPage extends Page {
onsubmit(e) {
e.preventDefault();
if (this.saving) return;
if (this.loading) return;
this.saving = true;
this.loading = true;
app.alerts.dismiss(this.successAlert);
const settings = {};
@@ -143,7 +117,7 @@ export default class MailPage extends Page {
})
.catch(() => {})
.then(() => {
this.saving = false;
this.loading = false;
m.redraw();
});
}

View File

@@ -1,4 +1,4 @@
import Component from '../../common/Component';
import Component from 'flarum/Component';
/**
* The `Page` component

View File

@@ -1,9 +1,9 @@
import Dropdown from '../../common/components/Dropdown';
import Button from '../../common/components/Button';
import Separator from '../../common/components/Separator';
import Group from '../../common/models/Group';
import Badge from '../../common/components/Badge';
import GroupBadge from '../../common/components/GroupBadge';
import Dropdown from 'flarum/components/Dropdown';
import Button from 'flarum/components/Button';
import Separator from 'flarum/components/Separator';
import Group from 'flarum/models/Group';
import Badge from 'flarum/components/Badge';
import GroupBadge from 'flarum/components/GroupBadge';
function badgeForId(id) {
const group = app.store.getById('groups', id);
@@ -52,9 +52,9 @@ export default class PermissionDropdown extends Dropdown {
const adminGroup = app.store.getById('groups', Group.ADMINISTRATOR_ID);
if (everyone) {
this.props.label = Badge.component({icon: 'fas fa-globe'});
this.props.label = Badge.component({icon: 'globe'});
} else if (members) {
this.props.label = Badge.component({icon: 'fas fa-user'});
this.props.label = Badge.component({icon: 'user'});
} else {
this.props.label = [
badgeForId(Group.ADMINISTRATOR_ID),
@@ -66,8 +66,8 @@ export default class PermissionDropdown extends Dropdown {
if (this.props.allowGuest) {
this.props.children.push(
Button.component({
children: [Badge.component({icon: 'fas fa-globe'}), ' ', app.translator.trans('core.admin.permissions_controls.everyone_button')],
icon: everyone ? 'fas fa-check' : true,
children: [Badge.component({icon: 'globe'}), ' ', app.translator.trans('core.admin.permissions_controls.everyone_button')],
icon: everyone ? 'check' : true,
onclick: () => this.save([Group.GUEST_ID]),
disabled: this.isGroupDisabled(Group.GUEST_ID)
})
@@ -76,8 +76,8 @@ export default class PermissionDropdown extends Dropdown {
this.props.children.push(
Button.component({
children: [Badge.component({icon: 'fas fa-user'}), ' ', app.translator.trans('core.admin.permissions_controls.members_button')],
icon: members ? 'fas fa-check' : true,
children: [Badge.component({icon: 'user'}), ' ', app.translator.trans('core.admin.permissions_controls.members_button')],
icon: members ? 'check' : true,
onclick: () => this.save([Group.MEMBER_ID]),
disabled: this.isGroupDisabled(Group.MEMBER_ID)
}),
@@ -86,7 +86,7 @@ export default class PermissionDropdown extends Dropdown {
Button.component({
children: [badgeForId(adminGroup.id()), ' ', adminGroup.namePlural()],
icon: !everyone && !members ? 'fas fa-check' : true,
icon: !everyone && !members ? 'check' : true,
disabled: !everyone && !members,
onclick: e => {
if (e.shiftKey) e.stopPropagation();
@@ -101,7 +101,7 @@ export default class PermissionDropdown extends Dropdown {
.filter(group => [Group.ADMINISTRATOR_ID, Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
.map(group => Button.component({
children: [badgeForId(group.id()), ' ', group.namePlural()],
icon: groupIds.indexOf(group.id()) !== -1 ? 'fas fa-check' : true,
icon: groupIds.indexOf(group.id()) !== -1 ? 'check' : true,
onclick: (e) => {
if (e.shiftKey) e.stopPropagation();
this.toggle(group.id());

View File

@@ -1,9 +1,9 @@
import Component from '../../common/Component';
import PermissionDropdown from './PermissionDropdown';
import SettingDropdown from './SettingDropdown';
import Button from '../../common/components/Button';
import ItemList from '../../common/utils/ItemList';
import icon from '../../common/helpers/icon';
import Component from 'flarum/Component';
import PermissionDropdown from 'flarum/components/PermissionDropdown';
import SettingDropdown from 'flarum/components/SettingDropdown';
import Button from 'flarum/components/Button';
import ItemList from 'flarum/utils/ItemList';
import icon from 'flarum/helpers/icon';
export default class PermissionGrid extends Component {
init() {
@@ -29,7 +29,7 @@ export default class PermissionGrid extends Component {
{scopes.map(scope => (
<th>
{scope.label}{' '}
{scope.onremove ? Button.component({icon: 'fas fa-times', className: 'Button Button--text PermissionGrid-removeScope', onclick: scope.onremove}) : ''}
{scope.onremove ? Button.component({icon: 'times', className: 'Button Button--text PermissionGrid-removeScope', onclick: scope.onremove}) : ''}
</th>
))}
<th>{this.scopeControlItems().toArray()}</th>
@@ -85,21 +85,21 @@ export default class PermissionGrid extends Component {
const items = new ItemList();
items.add('viewDiscussions', {
icon: 'fas fa-eye',
icon: 'eye',
label: app.translator.trans('core.admin.permissions.view_discussions_label'),
permission: 'viewDiscussions',
allowGuest: true
}, 100);
items.add('viewUserList', {
icon: 'fas fa-users',
icon: 'users',
label: app.translator.trans('core.admin.permissions.view_user_list_label'),
permission: 'viewUserList',
allowGuest: true
}, 100);
items.add('signUp', {
icon: 'fas fa-user-plus',
icon: 'user-plus',
label: app.translator.trans('core.admin.permissions.sign_up_label'),
setting: () => SettingDropdown.component({
key: 'allow_sign_up',
@@ -110,12 +110,6 @@ export default class PermissionGrid extends Component {
})
}, 90);
items.add('viewLastSeenAt', {
icon: 'far fa-clock',
label: app.translator.trans('core.admin.permissions.view_last_seen_at_label'),
permission: 'user.viewLastSeenAt',
});
return items;
}
@@ -123,13 +117,13 @@ export default class PermissionGrid extends Component {
const items = new ItemList();
items.add('start', {
icon: 'fas fa-edit',
icon: 'edit',
label: app.translator.trans('core.admin.permissions.start_discussions_label'),
permission: 'startDiscussion'
}, 100);
items.add('allowRenaming', {
icon: 'fas fa-i-cursor',
icon: 'i-cursor',
label: app.translator.trans('core.admin.permissions.allow_renaming_label'),
setting: () => {
const minutes = parseInt(app.data.settings.allow_renaming, 10);
@@ -155,13 +149,13 @@ export default class PermissionGrid extends Component {
const items = new ItemList();
items.add('reply', {
icon: 'fas fa-reply',
icon: 'reply',
label: app.translator.trans('core.admin.permissions.reply_to_discussions_label'),
permission: 'discussion.reply'
}, 100);
items.add('allowPostEditing', {
icon: 'fas fa-pencil-alt',
icon: 'pencil',
label: app.translator.trans('core.admin.permissions.allow_post_editing_label'),
setting: () => {
const minutes = parseInt(app.data.settings.allow_post_editing, 10);
@@ -187,53 +181,41 @@ export default class PermissionGrid extends Component {
const items = new ItemList();
items.add('viewIpsPosts', {
icon: 'fas fa-bullseye',
icon: 'bullseye',
label: app.translator.trans('core.admin.permissions.view_post_ips_label'),
permission: 'discussion.viewIpsPosts'
}, 110);
items.add('renameDiscussions', {
icon: 'fas fa-i-cursor',
icon: 'i-cursor',
label: app.translator.trans('core.admin.permissions.rename_discussions_label'),
permission: 'discussion.rename'
}, 100);
items.add('hideDiscussions', {
icon: 'far fa-trash-alt',
icon: 'trash-o',
label: app.translator.trans('core.admin.permissions.delete_discussions_label'),
permission: 'discussion.hide'
}, 90);
items.add('deleteDiscussions', {
icon: 'fas fa-times',
icon: 'times',
label: app.translator.trans('core.admin.permissions.delete_discussions_forever_label'),
permission: 'discussion.delete'
}, 80);
items.add('editPosts', {
icon: 'fas fa-pencil-alt',
label: app.translator.trans('core.admin.permissions.edit_posts_label'),
icon: 'pencil',
label: app.translator.trans('core.admin.permissions.edit_and_delete_posts_label'),
permission: 'discussion.editPosts'
}, 70);
items.add('hidePosts', {
icon: 'far fa-trash-alt',
label: app.translator.trans('core.admin.permissions.delete_posts_label'),
permission: 'discussion.hidePosts'
}, 60);
items.add('deletePosts', {
icon: 'fas fa-times',
icon: 'times',
label: app.translator.trans('core.admin.permissions.delete_posts_forever_label'),
permission: 'discussion.deletePosts'
}, 60);
items.add('userEdit', {
icon: 'fas fa-user-cog',
label: app.translator.trans('core.admin.permissions.edit_users_label'),
permission: 'user.edit'
}, 60);
return items;
}

View File

@@ -1,9 +1,9 @@
import Page from './Page';
import GroupBadge from '../../common/components/GroupBadge';
import EditGroupModal from './EditGroupModal';
import Group from '../../common/models/Group';
import icon from '../../common/helpers/icon';
import PermissionGrid from './PermissionGrid';
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 Page {
view() {
@@ -24,7 +24,7 @@ export default class PermissionsPage extends Page {
</button>
))}
<button className="Button Group Group--add" onclick={() => app.modal.show(new EditGroupModal())}>
{icon('fas fa-plus', {className: 'Group-icon'})}
{icon('plus', {className: 'Group-icon'})}
<span className="Group-name">{app.translator.trans('core.admin.permissions.new_group_button')}</span>
</button>
</div>

View File

@@ -1,8 +1,8 @@
import avatar from '../../common/helpers/avatar';
import username from '../../common/helpers/username';
import Dropdown from '../../common/components/Dropdown';
import Button from '../../common/components/Button';
import ItemList from '../../common/utils/ItemList';
import avatar from 'flarum/helpers/avatar';
import username from 'flarum/helpers/username';
import Dropdown from 'flarum/components/Dropdown';
import Button from 'flarum/components/Button';
import ItemList from 'flarum/utils/ItemList';
/**
* The `SessionDropdown` component shows a button with the current user's
@@ -42,7 +42,7 @@ export default class SessionDropdown extends Dropdown {
items.add('logOut',
Button.component({
icon: 'fas fa-sign-out-alt',
icon: 'sign-out',
children: app.translator.trans('core.admin.header.log_out_button'),
onclick: app.session.logout.bind(app.session)
}),

View File

@@ -1,6 +1,6 @@
import SelectDropdown from '../../common/components/SelectDropdown';
import Button from '../../common/components/Button';
import saveSettings from '../utils/saveSettings';
import SelectDropdown from 'flarum/components/SelectDropdown';
import Button from 'flarum/components/Button';
import saveSettings from 'flarum/utils/saveSettings';
export default class SettingDropdown extends SelectDropdown {
static initProps(props) {
@@ -8,7 +8,7 @@ export default class SettingDropdown extends SelectDropdown {
props.className = 'SettingDropdown';
props.buttonClassName = 'Button Button--text';
props.caretIcon = 'fas fa-caret-down';
props.caretIcon = 'caret-down';
props.defaultLabel = 'Custom';
props.children = props.options.map(({value, label}) => {
@@ -16,7 +16,7 @@ export default class SettingDropdown extends SelectDropdown {
return Button.component({
children: label,
icon: active ? 'fas fa-check' : true,
icon: active ? 'check' : true,
onclick: saveSettings.bind(this, {[props.key]: value}),
active
});

View File

@@ -1,6 +1,6 @@
import Modal from '../../common/components/Modal';
import Button from '../../common/components/Button';
import saveSettings from '../utils/saveSettings';
import Modal from 'flarum/components/Modal';
import Button from 'flarum/components/Button';
import saveSettings from 'flarum/utils/saveSettings';
export default class SettingsModal extends Modal {
init() {

View File

@@ -1,4 +1,4 @@
import Button from '../../common/components/Button';
import Button from 'flarum/components/Button';
export default class UploadImageButton extends Button {
init() {

9
js/admin/src/index.ts Normal file
View File

@@ -0,0 +1,9 @@
import AdminApplication from './AdminApplication';
export const app = new AdminApplication();
export const extensions = [];
// Export public API
// export { default as Extend } from './Extend';
// export { IndexPage, DicsussionList } from './components';

1
js/admin/src/lib Symbolic link
View File

@@ -0,0 +1 @@
/Users/toby/Projects/Flarum/app/packages/core/js/lib

15
js/admin/src/routes.ts Normal file
View File

@@ -0,0 +1,15 @@
import DashboardPage from './components/DashboardPage';
import BasicsPage from './components/BasicsPage';
import PermissionsPage from './components/PermissionsPage';
import AppearancePage from './components/AppearancePage';
import ExtensionsPage from './components/ExtensionsPage';
import MailPage from './components/MailPage';
export default function(router) {
router.add('dashboard', '/', DashboardPage);
router.add('basics', '/basics', BasicsPage);
router.add('permissions', '/permissions', PermissionsPage);
router.add('appearance', '/appearance', AppearancePage);
router.add('extensions', '/extensions', ExtensionsPage);
router.add('mail', '/mail', MailPage);
}

View File

@@ -0,0 +1,22 @@
import flarum from 'flarum';
/**
* Make a request to save the given settings to the database.
*
* @param {Object} settings
* @return {Promise}
*/
export default function saveSettings(settings) {
const oldSettings = JSON.parse(JSON.stringify(flarum.data.settings));
Object.assign(flarum.data.settings, settings);
return flarum.ajax.request({
method: 'POST',
url: flarum.forum.apiUrl + '/settings',
data: settings
}).catch(error => {
flarum.data.settings = oldSettings;
throw error;
});
}

23
js/admin/tsconfig.json Normal file
View File

@@ -0,0 +1,23 @@
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": false,
"module": "commonjs",
"target": "es5",
"jsx": "react",
"jsxFactory": "m",
"declaration": true,
"lib": ["dom", "es2015"],
"types": [
"mithril",
"classnames"
]
},
"include": [
"./src/**/*"
],
"exclude": [
"node_modules"
]
}

View File

@@ -0,0 +1,30 @@
const path = require('path');
const { CheckerPlugin } = require('awesome-typescript-loader');
module.exports = {
entry: path.resolve(__dirname, 'src/index.tsx'),
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx']
},
devtool: 'source-map',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
library: 'flarum',
libraryTarget: 'var'
},
module: {
loaders: [
{
test: /\.tsx?$/,
loader: 'awesome-typescript-loader'
}
]
},
plugins: [
new CheckerPlugin()
],
externals: {
mithril: 'm'
}
};

26
js/dist/admin.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

53
js/dist/forum.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
js/forum/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
dist

View File

@@ -0,0 +1,86 @@
import Application from './lib/Application';
import routes from './routes';
import Search from './components/Search';
export default class ForumApplication extends Application {
/**
* The page's search component instance.
*
* @type {SearchBox}
*/
search = new Search();
/**
* A map of notification types to their components.
*
* @type {Object}
*/
notificationComponents = {};
/**
* A map of post types to their components.
*
* @type {Object}
*/
postComponents = {};
//app.postComponents.comment = CommentPost;
//app.postComponents.discussionRenamed = DiscussionRenamedPost;
// app.notificationComponents.discussionRenamed = DiscussionRenamedNotification;
/**
* @inheritdoc
*/
registerDefaultRoutes(router) {
routes(router);
}
// TODO: work out where to put these helper functions
// /**
// * Check whether or not the user is currently composing a reply to a
// * discussion.
// *
// * @param {Discussion} discussion
// * @return {Boolean}
// */
// composingReplyTo(discussion) {
// return this.composer.component instanceof ReplyComposer &&
// this.composer.component.props.discussion === discussion &&
// this.composer.position !== Composer.PositionEnum.HIDDEN;
// }
//
// /**
// * Check whether or not the user is currently viewing a discussion.
// *
// * @param {Discussion} discussion
// * @return {Boolean}
// */
// viewingDiscussion(discussion) {
// return this.current instanceof DiscussionPage &&
// this.current.discussion === discussion;
// }
//
// /**
// * Callback for when an external authenticator (social login) action has
// * completed.
// *
// * If the payload indicates that the user has been logged in, then the page
// * will be reloaded. Otherwise, a SignUpModal will be opened, prefilled
// * with the provided details.
// *
// * @param {Object} payload A dictionary of props to pass into the sign up
// * modal. A truthy `authenticated` prop indicates that the user has logged
// * in, and thus the page is reloaded.
// * @public
// */
// authenticationComplete(payload) {
// if (payload.authenticated) {
// window.location.reload();
// } else {
// const modal = new SignUpModal(payload);
// this.modal.show(modal);
// modal.$('[name=password]').focus();
// }
// }
}

View File

@@ -1,10 +1,10 @@
import Component from '../../common/Component';
import avatar from '../../common/helpers/avatar';
import icon from '../../common/helpers/icon';
import listItems from '../../common/helpers/listItems';
import ItemList from '../../common/utils/ItemList';
import Button from '../../common/components/Button';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import Component from 'flarum/Component';
import avatar from 'flarum/helpers/avatar';
import icon from 'flarum/helpers/icon';
import listItems from 'flarum/helpers/listItems';
import ItemList from 'flarum/utils/ItemList';
import Button from 'flarum/components/Button';
import LoadingIndicator from 'flarum/components/LoadingIndicator';
/**
* The `AvatarEditor` component displays a user's avatar along with a dropdown
@@ -23,13 +23,6 @@ export default class AvatarEditor extends Component {
* @type {Boolean}
*/
this.loading = false;
/**
* Whether or not an image has been dragged over the dropzone.
*
* @type {Boolean}
*/
this.isDraggedOver = false;
}
static initProps(props) {
@@ -42,18 +35,13 @@ export default class AvatarEditor extends Component {
const user = this.props.user;
return (
<div className={'AvatarEditor Dropdown ' + this.props.className + (this.loading ? ' loading' : '') + (this.isDraggedOver ? ' dragover' : '')}>
<div className={'AvatarEditor Dropdown ' + this.props.className + (this.loading ? ' loading' : '')}>
{avatar(user)}
<a className={ user.avatarUrl() ? "Dropdown-toggle" : "Dropdown-toggle AvatarEditor--noAvatar" }
title={app.translator.trans('core.forum.user.avatar_upload_tooltip')}
data-toggle="dropdown"
onclick={this.quickUpload.bind(this)}
ondragover={this.enableDragover.bind(this)}
ondragenter={this.enableDragover.bind(this)}
ondragleave={this.disableDragover.bind(this)}
ondragend={this.disableDragover.bind(this)}
ondrop={this.dropUpload.bind(this)}>
{this.loading ? LoadingIndicator.component() : (user.avatarUrl() ? icon('fas fa-pencil-alt') : icon('fas fa-plus-circle'))}
onclick={this.quickUpload.bind(this)}>
{this.loading ? LoadingIndicator.component() : (user.avatarUrl() ? icon('pencil') : icon('plus-circle'))}
</a>
<ul className="Dropdown-menu Menu">
{listItems(this.controlItems().toArray())}
@@ -72,15 +60,15 @@ export default class AvatarEditor extends Component {
items.add('upload',
Button.component({
icon: 'fas fa-upload',
icon: 'upload',
children: app.translator.trans('core.forum.user.avatar_upload_button'),
onclick: this.openPicker.bind(this)
onclick: this.upload.bind(this)
})
);
items.add('remove',
Button.component({
icon: 'fas fa-times',
icon: 'times',
children: app.translator.trans('core.forum.user.avatar_remove_button'),
onclick: this.remove.bind(this)
})
@@ -89,40 +77,6 @@ export default class AvatarEditor extends Component {
return items;
}
/**
* Enable dragover style
*
* @param {Event} e
*/
enableDragover(e) {
e.preventDefault();
e.stopPropagation();
this.isDraggedOver = true;
}
/**
* Disable dragover style
*
* @param {Event} e
*/
disableDragover(e) {
e.preventDefault();
e.stopPropagation();
this.isDraggedOver = false;
}
/**
* Upload avatar when file is dropped into dropzone.
*
* @param {Event} e
*/
dropUpload(e) {
e.preventDefault();
e.stopPropagation();
this.isDraggedOver = false;
this.upload(e.dataTransfer.files[0]);
}
/**
* If the user doesn't have an avatar, there's no point in showing the
* controls dropdown, because only one option would be viable: uploading.
@@ -135,14 +89,14 @@ export default class AvatarEditor extends Component {
if (!this.props.user.avatarUrl()) {
e.preventDefault();
e.stopPropagation();
this.openPicker();
this.upload();
}
}
/**
* Upload avatar using file picker
* Prompt the user to upload a new avatar.
*/
openPicker() {
upload() {
if (this.loading) return;
// Create a hidden HTML input element and click on it so the user can select
@@ -151,36 +105,24 @@ export default class AvatarEditor extends Component {
const $input = $('<input type="file">');
$input.appendTo('body').hide().click().on('change', e => {
this.upload($(e.target)[0].files[0]);
const data = new FormData();
data.append('avatar', $(e.target)[0].files[0]);
this.loading = true;
m.redraw();
app.request({
method: 'POST',
url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/avatar',
serialize: raw => raw,
data
}).then(
this.success.bind(this),
this.failure.bind(this)
);
});
}
/**
* Upload avatar
*
* @param {File} file
*/
upload(file) {
if (this.loading) return;
const user = this.props.user;
const data = new FormData();
data.append('avatar', file);
this.loading = true;
m.redraw();
app.request({
method: 'POST',
url: app.forum.attribute('apiUrl') + '/users/' + user.id() + '/avatar',
serialize: raw => raw,
data
}).then(
this.success.bind(this),
this.failure.bind(this)
);
}
/**
* Remove the user's avatar.
*/

View File

@@ -1,5 +1,5 @@
import Modal from '../../common/components/Modal';
import Button from '../../common/components/Button';
import Modal from 'flarum/components/Modal';
import Button from 'flarum/components/Button';
/**
* The `ChangeEmailModal` component shows a modal dialog which allows the user

View File

@@ -1,5 +1,5 @@
import Modal from '../../common/components/Modal';
import Button from '../../common/components/Button';
import Modal from 'flarum/components/Modal';
import Button from 'flarum/components/Button';
/**
* The `ChangePasswordModal` component shows a modal dialog which allows the

View File

@@ -1,14 +1,14 @@
/*global s9e, hljs*/
import Post from './Post';
import classList from '../../common/utils/classList';
import PostUser from './PostUser';
import PostMeta from './PostMeta';
import PostEdited from './PostEdited';
import EditPostComposer from './EditPostComposer';
import ItemList from '../../common/utils/ItemList';
import listItems from '../../common/helpers/listItems';
import Button from '../../common/components/Button';
import Post from 'flarum/components/Post';
import classList from 'flarum/utils/classList';
import PostUser from 'flarum/components/PostUser';
import PostMeta from 'flarum/components/PostMeta';
import PostEdited from 'flarum/components/PostEdited';
import EditPostComposer from 'flarum/components/EditPostComposer';
import ItemList from 'flarum/utils/ItemList';
import listItems from 'flarum/helpers/listItems';
import Button from 'flarum/components/Button';
/**
* The `CommentPost` component displays a standard `comment`-typed post. This
@@ -80,7 +80,7 @@ export default class CommentPost extends Post {
const post = this.props.post;
const attrs = super.attrs();
attrs.className = (attrs.className || '') + ' ' + classList({
attrs.className += ' '+classList({
'CommentPost': true,
'Post--hidden': post.isHidden(),
'Post--edited': post.isEdited(),
@@ -142,7 +142,7 @@ export default class CommentPost extends Post {
items.add('toggle', (
Button.component({
className: 'Button Button--default Button--more',
icon: 'fas fa-ellipsis-h',
icon: 'ellipsis-h',
onclick: this.toggleContent.bind(this)
})
));

View File

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

View File

@@ -1,9 +1,9 @@
import Component from '../../common/Component';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import TextEditor from './TextEditor';
import avatar from '../../common/helpers/avatar';
import listItems from '../../common/helpers/listItems';
import ItemList from '../../common/utils/ItemList';
import Component from 'flarum/Component';
import LoadingIndicator from 'flarum/components/LoadingIndicator';
import TextEditor from 'flarum/components/TextEditor';
import avatar from 'flarum/helpers/avatar';
import listItems from 'flarum/helpers/listItems';
import ItemList from 'flarum/utils/ItemList';
/**
* The `ComposerBody` component handles the body, or the content, of the

View File

@@ -1,4 +1,4 @@
import Button from '../../common/components/Button';
import Button from 'flarum/components/Button';
/**
* The `ComposerButton` component displays a button suitable for the composer

View File

@@ -1,5 +1,5 @@
import ComposerBody from './ComposerBody';
import extractText from '../../common/utils/extractText';
import ComposerBody from 'flarum/components/ComposerBody';
import extractText from 'flarum/utils/extractText';
/**
* The `DiscussionComposer` component displays the composer content for starting

View File

@@ -1,6 +1,6 @@
import Component from '../../common/Component';
import ItemList from '../../common/utils/ItemList';
import listItems from '../../common/helpers/listItems';
import Component from 'flarum/Component';
import ItemList from 'flarum/utils/ItemList';
import listItems from 'flarum/helpers/listItems';
/**
* The `DiscussionHero` component displays the hero on a discussion page.

View File

@@ -1,8 +1,8 @@
import Component from '../../common/Component';
import DiscussionListItem from './DiscussionListItem';
import Button from '../../common/components/Button';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import Placeholder from '../../common/components/Placeholder';
import Component from 'flarum/Component';
import DiscussionListItem from 'flarum/components/DiscussionListItem';
import Button from 'flarum/components/Button';
import LoadingIndicator from 'flarum/components/LoadingIndicator';
import Placeholder from 'flarum/components/Placeholder';
/**
* The `DiscussionList` component displays a list of discussions.
@@ -62,7 +62,7 @@ export default class DiscussionList extends Component {
}
return (
<div className={'DiscussionList'+(this.props.params.q ? ' DiscussionList--searchResults' : '')}>
<div className="DiscussionList">
<ul className="DiscussionList-discussions">
{this.discussions.map(discussion => {
return (
@@ -87,14 +87,14 @@ export default class DiscussionList extends Component {
* @api
*/
requestParams() {
const params = {include: ['user', 'lastPostedUser'], filter: {}};
const params = {include: ['startUser', 'lastUser'], filter: {}};
params.sort = this.sortMap()[this.props.params.sort];
if (this.props.params.q) {
params.filter.q = this.props.params.q;
params.include.push('mostRelevantPost', 'mostRelevantPost.user');
params.include.push('relevantPosts', 'relevantPosts.discussion', 'relevantPosts.user');
}
return params;
@@ -112,10 +112,10 @@ export default class DiscussionList extends Component {
if (this.props.params.q) {
map.relevance = '';
}
map.latest = '-lastPostedAt';
map.top = '-commentCount';
map.newest = '-createdAt';
map.oldest = 'createdAt';
map.latest = '-lastTime';
map.top = '-commentsCount';
map.newest = '-startTime';
map.oldest = 'startTime';
return map;
}
@@ -150,7 +150,7 @@ export default class DiscussionList extends Component {
* @return {Promise}
*/
loadResults(offset) {
const preloadedDiscussions = app.preloadedApiDocument();
const preloadedDiscussions = app.preloadedDocument();
if (preloadedDiscussions) {
return m.deferred().resolve(preloadedDiscussions).promise;

View File

@@ -1,19 +1,19 @@
import Component from '../../common/Component';
import avatar from '../../common/helpers/avatar';
import listItems from '../../common/helpers/listItems';
import highlight from '../../common/helpers/highlight';
import icon from '../../common/helpers/icon';
import humanTime from '../../common/utils/humanTime';
import ItemList from '../../common/utils/ItemList';
import abbreviateNumber from '../../common/utils/abbreviateNumber';
import Dropdown from '../../common/components/Dropdown';
import TerminalPost from './TerminalPost';
import PostPreview from './PostPreview';
import SubtreeRetainer from '../../common/utils/SubtreeRetainer';
import DiscussionControls from '../utils/DiscussionControls';
import slidable from '../utils/slidable';
import extractText from '../../common/utils/extractText';
import classList from '../../common/utils/classList';
import Component from 'flarum/Component';
import avatar from 'flarum/helpers/avatar';
import listItems from 'flarum/helpers/listItems';
import highlight from 'flarum/helpers/highlight';
import icon from 'flarum/helpers/icon';
import humanTime from 'flarum/utils/humanTime';
import ItemList from 'flarum/utils/ItemList';
import abbreviateNumber from 'flarum/utils/abbreviateNumber';
import Dropdown from 'flarum/components/Dropdown';
import TerminalPost from 'flarum/components/TerminalPost';
import PostPreview from 'flarum/components/PostPreview';
import SubtreeRetainer from 'flarum/utils/SubtreeRetainer';
import DiscussionControls from 'flarum/utils/DiscussionControls';
import slidable from 'flarum/utils/slidable';
import extractText from 'flarum/utils/extractText';
import classList from 'flarum/utils/classList';
/**
* The `DiscussionListItem` component shows a single discussion in the
@@ -35,7 +35,7 @@ export default class DiscussionListItem extends Component {
this.subtree = new SubtreeRetainer(
() => this.props.discussion.freshness,
() => {
const time = app.session.user && app.session.user.markedAllAsReadAt();
const time = app.session.user && app.session.user.readTime();
return time && time.getTime();
},
() => this.active()
@@ -58,30 +58,20 @@ export default class DiscussionListItem extends Component {
if (retain) return retain;
const discussion = this.props.discussion;
const user = discussion.user();
const startUser = discussion.startUser();
const isUnread = discussion.isUnread();
const isRead = discussion.isRead();
const showUnread = !this.showRepliesCount() && isUnread;
let jumpTo = 0;
const jumpTo = Math.min(discussion.lastPostNumber(), (discussion.readNumber() || 0) + 1);
const relevantPosts = this.props.params.q ? discussion.relevantPosts() : [];
const controls = DiscussionControls.controls(discussion, this).toArray();
const attrs = this.attrs();
if (this.props.params.q) {
const post = discussion.mostRelevantPost();
if (post) {
jumpTo = post.number();
}
const phrase = this.props.params.q;
this.highlightRegExp = new RegExp(phrase+'|'+phrase.trim().replace(/\s+/g, '|'), 'gi');
} else {
jumpTo = Math.min(discussion.lastPostNumber(), (discussion.lastReadPostNumber() || 0) + 1);
}
return (
<div {...attrs}>
{controls.length ? Dropdown.component({
icon: 'fas fa-ellipsis-v',
icon: 'ellipsis-v',
children: controls,
className: 'DiscussionListItem-controls',
buttonClassName: 'Button Button--icon Button--flat Slidable-underneath Slidable-underneath--right'
@@ -89,18 +79,18 @@ export default class DiscussionListItem extends Component {
<a className={'Slidable-underneath Slidable-underneath--left Slidable-underneath--elastic' + (isUnread ? '' : ' disabled')}
onclick={this.markAsRead.bind(this)}>
{icon('fas fa-check')}
{icon('check')}
</a>
<div className={'DiscussionListItem-content Slidable-content' + (isUnread ? ' unread' : '') + (isRead ? ' read' : '')}>
<a href={user ? app.route.user(user) : '#'}
<a href={startUser ? app.route.user(startUser) : '#'}
className="DiscussionListItem-author"
title={extractText(app.translator.trans('core.forum.discussion_list.started_text', {user: user, ago: humanTime(discussion.createdAt())}))}
title={extractText(app.translator.trans('core.forum.discussion_list.started_text', {user: startUser, ago: humanTime(discussion.startTime())}))}
config={function(element) {
$(element).tooltip({placement: 'right'});
m.route.apply(this, arguments);
}}>
{avatar(user, {title: ''})}
{avatar(startUser, {title: ''})}
</a>
<ul className="DiscussionListItem-badges badges">
@@ -110,15 +100,22 @@ export default class DiscussionListItem extends Component {
<a href={app.route.discussion(discussion, jumpTo)}
config={m.route}
className="DiscussionListItem-main">
<h3 className="DiscussionListItem-title">{highlight(discussion.title(), this.highlightRegExp)}</h3>
<h3 className="DiscussionListItem-title">{highlight(discussion.title(), this.props.params.q)}</h3>
<ul className="DiscussionListItem-info">{listItems(this.infoItems().toArray())}</ul>
</a>
<span className="DiscussionListItem-count"
onclick={this.markAsRead.bind(this)}
title={showUnread ? app.translator.trans('core.forum.discussion_list.mark_as_read_tooltip') : ''}>
{abbreviateNumber(discussion[showUnread ? 'unreadCount' : 'replyCount']())}
{abbreviateNumber(discussion[showUnread ? 'unreadCount' : 'repliesCount']())}
</span>
{relevantPosts && relevantPosts.length
? <div className="DiscussionListItem-relevantPosts">
{relevantPosts.map(post => PostPreview.component({post, highlight: this.props.params.q}))}
</div>
: ''}
</div>
</div>
);
@@ -156,7 +153,7 @@ export default class DiscussionListItem extends Component {
*
* @return {Boolean}
*/
showFirstPost() {
showStartPost() {
return ['newest', 'oldest'].indexOf(this.props.params.sort) !== -1;
}
@@ -177,7 +174,7 @@ export default class DiscussionListItem extends Component {
const discussion = this.props.discussion;
if (discussion.isUnread()) {
discussion.save({lastReadPostNumber: discussion.lastPostNumber()});
discussion.save({readNumber: discussion.lastPostNumber()});
m.redraw();
}
}
@@ -191,21 +188,12 @@ export default class DiscussionListItem extends Component {
infoItems() {
const items = new ItemList();
if (this.props.params.q) {
const post = this.props.discussion.mostRelevantPost() || this.props.discussion.firstPost();
if (post && post.contentType() === 'comment') {
const excerpt = highlight(post.contentPlain(), this.highlightRegExp, 175);
items.add('excerpt', excerpt, -100);
}
} else {
items.add('terminalPost',
TerminalPost.component({
discussion: this.props.discussion,
lastPost: !this.showFirstPost()
})
);
}
items.add('terminalPost',
TerminalPost.component({
discussion: this.props.discussion,
lastPost: !this.showStartPost()
})
);
return items;
}

View File

@@ -1,12 +1,12 @@
import Page from './Page';
import ItemList from '../../common/utils/ItemList';
import DiscussionHero from './DiscussionHero';
import PostStream from './PostStream';
import PostStreamScrubber from './PostStreamScrubber';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import SplitDropdown from '../../common/components/SplitDropdown';
import listItems from '../../common/helpers/listItems';
import DiscussionControls from '../utils/DiscussionControls';
import Page from 'flarum/components/Page';
import ItemList from 'flarum/utils/ItemList';
import DiscussionHero from 'flarum/components/DiscussionHero';
import PostStream from 'flarum/components/PostStream';
import PostStreamScrubber from 'flarum/components/PostStreamScrubber';
import LoadingIndicator from 'flarum/components/LoadingIndicator';
import SplitDropdown from 'flarum/components/SplitDropdown';
import listItems from 'flarum/helpers/listItems';
import DiscussionControls from 'flarum/utils/DiscussionControls';
/**
* The `DiscussionPage` component displays a whole discussion page, including
@@ -115,14 +115,6 @@ export default class DiscussionPage extends Page {
);
}
config(...args) {
super.config(...args);
if (this.discussion) {
app.setTitle(this.discussion.title());
}
}
/**
* Clear and reload the discussion.
*/
@@ -130,7 +122,7 @@ export default class DiscussionPage extends Page {
this.near = m.route.param('near') || 0;
this.discussion = null;
const preloadedDiscussion = app.preloadedApiDocument();
const preloadedDiscussion = app.preloadedDocument();
if (preloadedDiscussion) {
// We must wrap this in a setTimeout because if we are mounting this
// component for the first time on page load, then any calls to m.redraw
@@ -168,6 +160,7 @@ export default class DiscussionPage extends Page {
this.discussion = discussion;
app.history.push('discussion', discussion.title());
app.setTitle(discussion.title());
app.setTitleCount(0);
// When the API responds with a discussion, it will also include a number of
@@ -179,13 +172,8 @@ export default class DiscussionPage extends Page {
// the 'discussion' relationship linked, then sorting and splicing.
let includedPosts = [];
if (discussion.payload && discussion.payload.included) {
const discussionId = discussion.id();
includedPosts = discussion.payload.included
.filter(record => record.type === 'posts'
&& record.relationships
&& record.relationships.discussion
&& record.relationships.discussion.data.id === discussionId)
.filter(record => record.type === 'posts' && record.relationships && record.relationships.discussion)
.map(record => app.store.getById('posts', record.id))
.sort((a, b) => a.id() - b.id())
.slice(0, 20);
@@ -252,7 +240,7 @@ export default class DiscussionPage extends Page {
items.add('controls',
SplitDropdown.component({
children: DiscussionControls.controls(this.discussion, this).toArray(),
icon: 'fas fa-ellipsis-v',
icon: 'ellipsis-v',
className: 'App-primaryControl',
buttonClassName: 'Button--primary'
})
@@ -290,8 +278,8 @@ export default class DiscussionPage extends Page {
// If the user hasn't read past here before, then we'll update their read
// state and redraw.
if (app.session.user && endNumber > (discussion.lastReadPostNumber() || 0)) {
discussion.save({lastReadPostNumber: endNumber});
if (app.session.user && endNumber > (discussion.readNumber() || 0)) {
discussion.save({readNumber: endNumber});
m.redraw();
}
}

View File

@@ -1,4 +1,4 @@
import Notification from './Notification';
import Notification from 'flarum/components/Notification';
/**
* The `DiscussionRenamedNotification` component displays a notification which
@@ -10,7 +10,7 @@ import Notification from './Notification';
*/
export default class DiscussionRenamedNotification extends Notification {
icon() {
return 'fas fa-pencil-alt';
return 'pencil';
}
href() {
@@ -20,6 +20,6 @@ export default class DiscussionRenamedNotification extends Notification {
}
content() {
return app.translator.trans('core.forum.notifications.discussion_renamed_text', {user: this.props.notification.fromUser()});
return app.translator.trans('core.forum.notifications.discussion_renamed_text', {user: this.props.notification.sender()});
}
}

View File

@@ -1,5 +1,5 @@
import EventPost from './EventPost';
import extractText from '../../common/utils/extractText';
import EventPost from 'flarum/components/EventPost';
import extractText from 'flarum/utils/extractText';
/**
* The `DiscussionRenamedPost` component displays a discussion event post
@@ -11,7 +11,7 @@ import extractText from '../../common/utils/extractText';
*/
export default class DiscussionRenamedPost extends EventPost {
icon() {
return 'fas fa-pencil-alt';
return 'pencil';
}
description(data) {

View File

@@ -1,5 +1,5 @@
import highlight from '../../common/helpers/highlight';
import LinkButton from '../../common/components/LinkButton';
import highlight from 'flarum/helpers/highlight';
import LinkButton from 'flarum/components/LinkButton';
/**
* The `DiscussionsSearchSource` finds and displays discussion search results in
@@ -20,7 +20,7 @@ export default class DiscussionsSearchSource {
const params = {
filter: {q: query},
page: {limit: 3},
include: 'mostRelevantPost'
include: 'relevantPosts,relevantPosts.discussion,relevantPosts.user'
};
return app.store.find('discussions', params).then(results => this.results[query] = results);
@@ -35,19 +35,20 @@ export default class DiscussionsSearchSource {
<li className="Dropdown-header">{app.translator.trans('core.forum.search.discussions_heading')}</li>,
<li>
{LinkButton.component({
icon: 'fas fa-search',
icon: 'search',
children: app.translator.trans('core.forum.search.all_discussions_button', {query}),
href: app.route('index', {q: query})
})}
</li>,
results.map(discussion => {
const mostRelevantPost = discussion.mostRelevantPost();
const relevantPosts = discussion.relevantPosts();
const post = relevantPosts && relevantPosts[0];
return (
<li className="DiscussionSearchResult" data-index={'discussions' + discussion.id()}>
<a href={app.route.discussion(discussion, mostRelevantPost && mostRelevantPost.number())} config={m.route}>
<a href={app.route.discussion(discussion, post && post.number())} config={m.route}>
<div className="DiscussionSearchResult-title">{highlight(discussion.title(), query)}</div>
{mostRelevantPost ? <div className="DiscussionSearchResult-excerpt">{highlight(mostRelevantPost.contentPlain(), query, 100)}</div> : ''}
{post ? <div className="DiscussionSearchResult-excerpt">{highlight(post.contentPlain(), query, 100)}</div> : ''}
</a>
</li>
);

View File

@@ -1,5 +1,5 @@
import UserPage from './UserPage';
import DiscussionList from './DiscussionList';
import UserPage from 'flarum/components/UserPage';
import DiscussionList from 'flarum/components/DiscussionList';
/**
* The `DiscussionsUserPage` component shows a discussion list inside of a user
@@ -17,8 +17,7 @@ export default class DiscussionsUserPage extends UserPage {
<div className="DiscussionsUserPage">
{DiscussionList.component({
params: {
q: 'author:' + this.user.username(),
sort: 'newest'
q: 'author:' + this.user.username()
}
})}
</div>

View File

@@ -1,5 +1,5 @@
import ComposerBody from './ComposerBody';
import icon from '../../common/helpers/icon';
import ComposerBody from 'flarum/components/ComposerBody';
import icon from 'flarum/helpers/icon';
function minimizeComposerIfFullScreen(e) {
if (app.composer.isFullScreen()) {
@@ -52,7 +52,7 @@ export default class EditPostComposer extends ComposerBody {
items.add('title', (
<h3>
{icon('fas fa-pencil-alt')} {' '}
{icon('pencil')} {' '}
<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>

View File

@@ -0,0 +1,165 @@
import Modal from 'flarum/components/Modal';
import Button from 'flarum/components/Button';
import GroupBadge from 'flarum/components/GroupBadge';
import Group from 'flarum/models/Group';
import extractText from 'flarum/utils/extractText';
/**
* The `EditUserModal` component displays a modal dialog with a login form.
*/
export default class EditUserModal extends Modal {
init() {
super.init();
const user = this.props.user;
this.username = m.prop(user.username() || '');
this.email = m.prop(user.email() || '');
this.isActivated = m.prop(user.isActivated() || false);
this.setPassword = m.prop(false);
this.password = m.prop(user.password() || '');
this.groups = {};
app.store.all('groups')
.filter(group => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
.forEach(group => this.groups[group.id()] = m.prop(user.groups().indexOf(group) !== -1));
}
className() {
return 'EditUserModal Modal--small';
}
title() {
return app.translator.trans('core.forum.edit_user.title');
}
content() {
return (
<div className="Modal-body">
<div className="Form">
<div className="Form-group">
<label>{app.translator.trans('core.forum.edit_user.username_heading')}</label>
<input className="FormControl" placeholder={extractText(app.translator.trans('core.forum.edit_user.username_label'))}
bidi={this.username} />
</div>
{app.session.user !== this.props.user ? [
<div className="Form-group">
<label>{app.translator.trans('core.forum.edit_user.email_heading')}</label>
<div>
<input className="FormControl" placeholder={extractText(app.translator.trans('core.forum.edit_user.email_label'))}
bidi={this.email} />
</div>
{!this.isActivated() ? (
<div>
{Button.component({
className: 'Button Button--block',
children: app.translator.trans('core.forum.edit_user.activate_button'),
loading: this.loading,
onclick: this.activate.bind(this)
})}
</div>
) : ''}
</div>,
<div className="Form-group">
<label>{app.translator.trans('core.forum.edit_user.password_heading')}</label>
<div>
<label className="checkbox">
<input type="checkbox" checked={this.setPassword()} onchange={e => {
this.setPassword(e.target.checked);
m.redraw(true);
if (e.target.checked) this.$('[name=password]').select();
m.redraw.strategy('none');
}}/>
{app.translator.trans('core.forum.edit_user.set_password_label')}
</label>
{this.setPassword() ? (
<input className="FormControl" type="password" name="password" placeholder={extractText(app.translator.trans('core.forum.edit_user.password_label'))}
bidi={this.password} />
) : ''}
</div>
</div>
] : ''}
<div className="Form-group EditUserModal-groups">
<label>{app.translator.trans('core.forum.edit_user.groups_heading')}</label>
<div>
{Object.keys(this.groups)
.map(id => app.store.getById('groups', id))
.map(group => (
<label className="checkbox">
<input type="checkbox"
bidi={this.groups[group.id()]}
disabled={this.props.user.id() === '1' && group.id() === Group.ADMINISTRATOR_ID} />
{GroupBadge.component({group, label: ''})} {group.nameSingular()}
</label>
))}
</div>
</div>
<div className="Form-group">
{Button.component({
className: 'Button Button--primary',
type: 'submit',
loading: this.loading,
children: app.translator.trans('core.forum.edit_user.submit_button')
})}
</div>
</div>
</div>
);
}
activate() {
this.loading = true;
const data = {
username: this.username(),
isActivated: true,
};
this.props.user.save(data, {errorHandler: this.onerror.bind(this)})
.then(() => {
this.isActivated(true);
this.loading = false;
m.redraw();
})
.catch(() => {
this.loading = false;
m.redraw();
});
}
data() {
const groups = Object.keys(this.groups)
.filter(id => this.groups[id]())
.map(id => app.store.getById('groups', id));
const data = {
username: this.username(),
relationships: {groups}
};
if (app.session.user !== this.props.user) {
data.email = this.email();
}
if (this.setPassword()) {
data.password = this.password();
}
return data;
}
onsubmit(e) {
e.preventDefault();
this.loading = true;
this.props.user.save(this.data(), {errorHandler: this.onerror.bind(this)})
.then(this.hide.bind(this))
.catch(() => {
this.loading = false;
m.redraw();
});
}
}

View File

@@ -1,7 +1,7 @@
import Post from './Post';
import { ucfirst } from '../../common/utils/string';
import usernameHelper from '../../common/helpers/username';
import icon from '../../common/helpers/icon';
import Post from 'flarum/components/Post';
import { ucfirst } from 'flarum/utils/string';
import usernameHelper from 'flarum/helpers/username';
import icon from 'flarum/helpers/icon';
/**
* The `EventPost` component displays a post which indicating a discussion
@@ -18,7 +18,7 @@ export default class EventPost extends Post {
attrs() {
const attrs = super.attrs();
attrs.className = (attrs.className || '') + ' EventPost ' + ucfirst(this.props.post.contentType()) + 'Post';
attrs.className += ' EventPost ' + ucfirst(this.props.post.contentType()) + 'Post';
return attrs;
}

View File

@@ -1,7 +1,7 @@
import Modal from '../../common/components/Modal';
import Alert from '../../common/components/Alert';
import Button from '../../common/components/Button';
import extractText from '../../common/utils/extractText';
import Modal from 'flarum/components/Modal';
import Alert from 'flarum/components/Alert';
import Button from 'flarum/components/Button';
import extractText from 'flarum/utils/extractText';
/**
* The `ForgotPasswordModal` component displays a modal which allows the user to

View File

@@ -1,6 +1,6 @@
import Component from '../../common/Component';
import ItemList from '../../common/utils/ItemList';
import listItems from '../../common/helpers/listItems';
import Component from 'flarum/Component';
import ItemList from 'flarum/utils/ItemList';
import listItems from 'flarum/helpers/listItems';
/**
* The `HeaderPrimary` component displays primary header controls. On the

View File

@@ -1,12 +1,12 @@
import Component from '../../common/Component';
import Button from '../../common/components/Button';
import LogInModal from './LogInModal';
import SignUpModal from './SignUpModal';
import SessionDropdown from './SessionDropdown';
import SelectDropdown from '../../common/components/SelectDropdown';
import NotificationsDropdown from './NotificationsDropdown';
import ItemList from '../../common/utils/ItemList';
import listItems from '../../common/helpers/listItems';
import Component from 'flarum/Component';
import Button from 'flarum/components/Button';
import LogInModal from 'flarum/components/LogInModal';
import SignUpModal from 'flarum/components/SignUpModal';
import SessionDropdown from 'flarum/components/SessionDropdown';
import SelectDropdown from 'flarum/components/SelectDropdown';
import NotificationsDropdown from 'flarum/components/NotificationsDropdown';
import ItemList from 'flarum/utils/ItemList';
import listItems from 'flarum/helpers/listItems';
/**
* The `HeaderSecondary` component displays secondary header controls, such as
@@ -46,7 +46,7 @@ export default class HeaderSecondary extends Component {
locales.push(Button.component({
active: app.data.locale === locale,
children: app.data.locales[locale],
icon: app.data.locale === locale ? 'fas fa-check' : true,
icon: app.data.locale === locale ? 'check' : true,
onclick: () => {
if (app.session.user) {
app.session.user.savePreferences({locale}).then(() => window.location.reload());

View File

@@ -1,17 +1,17 @@
import { extend } from '../../common/extend';
import Page from './Page';
import ItemList from '../../common/utils/ItemList';
import listItems from '../../common/helpers/listItems';
import icon from '../../common/helpers/icon';
import DiscussionList from './DiscussionList';
import WelcomeHero from './WelcomeHero';
import DiscussionComposer from './DiscussionComposer';
import LogInModal from './LogInModal';
import DiscussionPage from './DiscussionPage';
import Dropdown from '../../common/components/Dropdown';
import Button from '../../common/components/Button';
import LinkButton from '../../common/components/LinkButton';
import SelectDropdown from '../../common/components/SelectDropdown';
import { extend } from 'flarum/extend';
import Page from 'flarum/components/Page';
import ItemList from 'flarum/utils/ItemList';
import listItems from 'flarum/helpers/listItems';
import icon from 'flarum/helpers/icon';
import DiscussionList from 'flarum/components/DiscussionList';
import WelcomeHero from 'flarum/components/WelcomeHero';
import DiscussionComposer from 'flarum/components/DiscussionComposer';
import LogInModal from 'flarum/components/LogInModal';
import DiscussionPage from 'flarum/components/DiscussionPage';
import Select from 'flarum/components/Select';
import Button from 'flarum/components/Button';
import LinkButton from 'flarum/components/LinkButton';
import SelectDropdown from 'flarum/components/SelectDropdown';
/**
* The `IndexPage` component displays the index page, including the welcome
@@ -55,7 +55,7 @@ export default class IndexPage extends Page {
app.cache.discussionList = new DiscussionList({params});
}
app.history.push('index', app.translator.trans('core.forum.header.back_to_index_tooltip'));
app.history.push('index', icon('bars'));
this.bodyClass = 'App--index';
}
@@ -71,17 +71,15 @@ export default class IndexPage extends Page {
<div className="IndexPage">
{this.hero()}
<div className="container">
<div className="sideNavContainer">
<nav className="IndexPage-nav sideNav">
<ul>{listItems(this.sidebarItems().toArray())}</ul>
</nav>
<div className="IndexPage-results sideNavOffset">
<div className="IndexPage-toolbar">
<ul className="IndexPage-toolbar-view">{listItems(this.viewItems().toArray())}</ul>
<ul className="IndexPage-toolbar-action">{listItems(this.actionItems().toArray())}</ul>
</div>
{app.cache.discussionList.render()}
<nav className="IndexPage-nav sideNav">
<ul>{listItems(this.sidebarItems().toArray())}</ul>
</nav>
<div className="IndexPage-results sideNavOffset">
<div className="IndexPage-toolbar">
<ul className="IndexPage-toolbar-view">{listItems(this.viewItems().toArray())}</ul>
<ul className="IndexPage-toolbar-action">{listItems(this.actionItems().toArray())}</ul>
</div>
{app.cache.discussionList.render()}
</div>
</div>
</div>
@@ -102,7 +100,7 @@ export default class IndexPage extends Page {
// previous hero. Maintain the same scroll position relative to the bottom
// of the hero so that the sidebar doesn't jump around.
const oldHeroHeight = app.cache.heroHeight;
const heroHeight = app.cache.heroHeight = this.$('.Hero').outerHeight() || 0;
const heroHeight = app.cache.heroHeight = this.$('.Hero').outerHeight();
const scrollTop = app.cache.scrollTop;
$('#app').css('min-height', $(window).height() + heroHeight);
@@ -156,10 +154,10 @@ export default class IndexPage extends Page {
items.add('newDiscussion',
Button.component({
children: app.translator.trans(canStartDiscussion ? 'core.forum.index.start_discussion_button' : 'core.forum.index.cannot_start_discussion_button'),
icon: 'fas fa-edit',
icon: 'edit',
className: 'Button Button--primary IndexPage-newDiscussion',
itemClassName: 'App-primaryControl',
onclick: this.newDiscussionAction.bind(this),
onclick: this.newDiscussion.bind(this),
disabled: !canStartDiscussion
})
);
@@ -189,7 +187,7 @@ export default class IndexPage extends Page {
LinkButton.component({
href: app.route('index', params),
children: app.translator.trans('core.forum.index.all_discussions_link'),
icon: 'far fa-comments'
icon: 'comments-o'
}),
100
);
@@ -214,20 +212,10 @@ export default class IndexPage extends Page {
}
items.add('sort',
Dropdown.component({
buttonClassName: 'Button',
label: sortOptions[this.params().sort] || Object.keys(sortMap).map(key => sortOptions[key])[0],
children: Object.keys(sortOptions).map(value => {
const label = sortOptions[value];
const active = (this.params().sort || Object.keys(sortMap)[0]) === value;
return Button.component({
children: label,
icon: active ? 'fas fa-check' : true,
onclick: this.changeSort.bind(this, value),
active: active,
})
}),
Select.component({
options: sortOptions,
value: this.params().sort || Object.keys(sortMap)[0],
onchange: this.changeSort.bind(this)
})
);
@@ -246,7 +234,7 @@ export default class IndexPage extends Page {
items.add('refresh',
Button.component({
title: app.translator.trans('core.forum.index.refresh_tooltip'),
icon: 'fas fa-sync',
icon: 'refresh',
className: 'Button Button--icon',
onclick: () => {
app.cache.discussionList.refresh();
@@ -262,7 +250,7 @@ export default class IndexPage extends Page {
items.add('markAllAsRead',
Button.component({
title: app.translator.trans('core.forum.index.mark_all_as_read_tooltip'),
icon: 'fas fa-check',
icon: 'check',
className: 'Button Button--icon',
onclick: this.markAllAsRead.bind(this)
})
@@ -339,29 +327,43 @@ export default class IndexPage extends Page {
}
/**
* Open the composer for a new discussion or prompt the user to login.
* Log the user in and then open the composer for a new discussion.
*
* @return {Promise}
*/
newDiscussionAction() {
newDiscussion() {
const deferred = m.deferred();
if (app.session.user) {
const component = new DiscussionComposer({ user: app.session.user });
app.composer.load(component);
app.composer.show();
deferred.resolve(component);
this.composeNewDiscussion(deferred);
} else {
deferred.reject();
app.modal.show(new LogInModal());
app.modal.show(
new LogInModal({
onlogin: this.composeNewDiscussion.bind(this, deferred)
})
);
}
return deferred.promise;
}
/**
* Initialize the composer for a new discussion.
*
* @param {Deferred} deferred
* @return {Promise}
*/
composeNewDiscussion(deferred) {
const component = new DiscussionComposer({user: app.session.user});
app.composer.load(component);
app.composer.show();
deferred.resolve(component);
return deferred.promise;
}
/**
* Mark all discussions as read.
*
@@ -371,7 +373,7 @@ export default class IndexPage extends Page {
const confirmation = confirm(app.translator.trans('core.forum.index.mark_all_as_read_confirmation'));
if (confirmation) {
app.session.user.save({markedAllAsReadAt: new Date()});
app.session.user.save({readTime: new Date()});
}
}
}

View File

@@ -1,5 +1,5 @@
import Component from '../../common/Component';
import avatar from '../../common/helpers/avatar';
import Component from 'flarum/Component';
import avatar from 'flarum/helpers/avatar';
/**
* The `LoadingPost` component shows a placeholder that looks like a post,

View File

@@ -1,4 +1,4 @@
import Button from '../../common/components/Button';
import Button from 'flarum/components/Button';
/**
* The `LogInButton` component displays a social login button which will open
@@ -13,7 +13,7 @@ export default class LogInButton extends Button {
props.className = (props.className || '') + ' LogInButton';
props.onclick = function() {
const width = 580;
const width = 600;
const height = 400;
const $window = $(window);
@@ -22,7 +22,7 @@ export default class LogInButton extends Button {
`height=${height},` +
`top=${$window.height() / 2 - height / 2},` +
`left=${$window.width() / 2 - width / 2},` +
'status=no,scrollbars=yes,resizable=no');
'status=no,scrollbars=no,resizable=no');
};
super.initProps(props);

View File

@@ -1,5 +1,5 @@
import Component from '../../common/Component';
import ItemList from '../../common/utils/ItemList';
import Component from 'flarum/Component';
import ItemList from 'flarum/utils/ItemList';
/**
* The `LogInButtons` component displays a collection of social login buttons.

View File

@@ -0,0 +1,157 @@
import Modal from 'flarum/components/Modal';
import ForgotPasswordModal from 'flarum/components/ForgotPasswordModal';
import SignUpModal from 'flarum/components/SignUpModal';
import Alert from 'flarum/components/Alert';
import Button from 'flarum/components/Button';
import LogInButtons from 'flarum/components/LogInButtons';
import Switch from 'flarum/components/Switch';
import extractText from 'flarum/utils/extractText';
/**
* The `LogInModal` component displays a modal dialog with a login form.
*
* ### Props
*
* - `identification`
* - `password`
*/
export default class LogInModal extends Modal {
init() {
super.init();
/**
* The value of the identification input.
*
* @type {Function}
*/
this.identification = m.prop(this.props.identification || '');
/**
* The value of the password input.
*
* @type {Function}
*/
this.password = m.prop(this.props.password || '');
/**
* The value of the remember me input.
*
* @type {Function}
*/
this.remember = m.prop(this.props.remember && true);
}
className() {
return 'LogInModal Modal--small';
}
title() {
return app.translator.trans('core.forum.log_in.title');
}
content() {
return [
<div className="Modal-body">
<LogInButtons/>
<div className="Form Form--centered">
<div className="Form-group">
<input className="FormControl" name="identification" type="text" placeholder={extractText(app.translator.trans('core.forum.log_in.username_or_email_placeholder'))}
bidi={this.identification}
disabled={this.loading} />
</div>
<div className="Form-group">
<input className="FormControl" name="password" type="password" placeholder={extractText(app.translator.trans('core.forum.log_in.password_placeholder'))}
bidi={this.password}
disabled={this.loading} />
</div>
<div className="Form-group">
{Switch.component({
children: app.translator.trans('core.forum.log_in.remember_me_label'),
disabled: this.loading,
onchange: this.remember,
state: this.remember()
})}
</div>
<div className="Form-group">
{Button.component({
className: 'Button Button--primary Button--block',
type: 'submit',
loading: this.loading,
children: app.translator.trans('core.forum.log_in.submit_button')
})}
</div>
</div>
</div>,
<div className="Modal-footer">
<p className="LogInModal-forgotPassword">
<a onclick={this.forgotPassword.bind(this)}>{app.translator.trans('core.forum.log_in.forgot_password_link')}</a>
</p>
{app.forum.attribute('allowSignUp') ? (
<p className="LogInModal-signUp">
{app.translator.trans('core.forum.log_in.sign_up_text', {a: <a onclick={this.signUp.bind(this)}/>})}
</p>
) : ''}
</div>
];
}
/**
* Open the forgot password modal, prefilling it with an email if the user has
* entered one.
*
* @public
*/
forgotPassword() {
const email = this.identification();
const props = email.indexOf('@') !== -1 ? {email} : undefined;
app.modal.show(new ForgotPasswordModal(props));
}
/**
* Open the sign up modal, prefilling it with an email/username/password if
* the user has entered one.
*
* @public
*/
signUp() {
const props = {password: this.password()};
const identification = this.identification();
props[identification.indexOf('@') !== -1 ? 'email' : 'username'] = identification;
app.modal.show(new SignUpModal(props));
}
onready() {
this.$('[name=' + (this.identification() ? 'password' : 'identification') + ']').select();
}
onsubmit(e) {
e.preventDefault();
this.loading = true;
const identification = this.identification();
const password = this.password();
const remember = this.remember();
app.session.login({identification, password, remember}, {errorHandler: this.onerror.bind(this)})
.then(
() => window.location.reload(),
this.loaded.bind(this)
);
}
onerror(error) {
if (error.status === 401) {
error.alert.props.children = app.translator.trans('core.forum.log_in.invalid_login_message');
}
super.onerror(error);
}
}

View File

@@ -1,8 +1,7 @@
import Component from '../../common/Component';
import avatar from '../../common/helpers/avatar';
import icon from '../../common/helpers/icon';
import humanTime from '../../common/helpers/humanTime';
import Button from '../../common/components/Button';
import Component from 'flarum/Component';
import avatar from 'flarum/helpers/avatar';
import icon from 'flarum/helpers/icon';
import humanTime from 'flarum/helpers/humanTime';
/**
* The `Notification` component abstract displays a single notification.
@@ -27,21 +26,10 @@ export default class Notification extends Component {
if (!isInitialized) $(element).click(this.markAsRead.bind(this));
}}>
{!notification.isRead() && Button.component({
className: 'Notification-action Button Button--icon Button--link',
icon: 'fas fa-check',
title: app.translator.trans('core.forum.notifications.mark_as_read_tooltip'),
onclick: e => {
e.preventDefault();
e.stopPropagation();
this.markAsRead();
}
})}
{avatar(notification.fromUser())}
{avatar(notification.sender())}
{icon(this.icon(), {className: 'Notification-icon'})}
<span className="Notification-content">{this.content()}</span>
{humanTime(notification.createdAt())}
{humanTime(notification.time())}
<div className="Notification-excerpt">
{this.excerpt()}
</div>
@@ -91,7 +79,7 @@ export default class Notification extends Component {
markAsRead() {
if (this.props.notification.isRead()) return;
app.session.user.pushAttributes({unreadNotificationCount: app.session.user.unreadNotificationCount() - 1});
app.session.user.pushAttributes({unreadNotificationsCount: app.session.user.unreadNotificationsCount() - 1});
this.props.notification.save({isRead: true});
}

View File

@@ -1,7 +1,7 @@
import Component from '../../common/Component';
import Checkbox from '../../common/components/Checkbox';
import icon from '../../common/helpers/icon';
import ItemList from '../../common/utils/ItemList';
import Component from 'flarum/Component';
import Checkbox from 'flarum/components/Checkbox';
import icon from 'flarum/helpers/icon';
import ItemList from 'flarum/utils/ItemList';
/**
* The `NotificationGrid` component displays a table of notification types and
@@ -18,7 +18,10 @@ export default class NotificationGrid extends Component {
*
* @type {Array}
*/
this.methods = this.notificationMethods().toArray();
this.methods = [
{name: 'alert', icon: 'bell', label: app.translator.trans('core.forum.settings.notify_by_web_heading')},
{name: 'email', icon: 'envelope-o', label: app.translator.trans('core.forum.settings.notify_by_email_heading')}
];
/**
* A map of notification type-method combinations to the checkbox instances
@@ -31,7 +34,7 @@ export default class NotificationGrid extends Component {
/**
* Information about the available notification types.
*
* @type {Array}
* @type {Object}
*/
this.types = this.notificationTypes().toArray();
@@ -86,12 +89,12 @@ export default class NotificationGrid extends Component {
config(isInitialized) {
if (isInitialized) return;
this.$('thead .NotificationGrid-groupToggle').bind('mouseenter mouseleave', function(e) {
this.$('thead .NotificationGrid-groupToggle').on('mouseenter mouseleave', function(e) {
const i = parseInt($(this).index(), 10) + 1;
$(this).parents('table').find('td:nth-child(' + i + ')').toggleClass('highlighted', e.type === 'mouseenter');
});
this.$('tbody .NotificationGrid-groupToggle').bind('mouseenter mouseleave', function(e) {
this.$('tbody .NotificationGrid-groupToggle').on('mouseenter mouseleave', function(e) {
$(this).parent().find('td').toggleClass('highlighted', e.type === 'mouseenter');
});
}
@@ -161,42 +164,12 @@ export default class NotificationGrid extends Component {
return 'notify_' + type + '_' + method;
}
/**
* Build an item list for the notification methods to display in the grid.
*
* Each notification method is an object which has the following properties:
*
* - `name` The name of the notification method.
* - `icon` The icon to display in the column header.
* - `label` The label to display in the column header.
*
* @return {ItemList}
*/
notificationMethods() {
const items = new ItemList();
items.add('alert', {
name: 'alert',
icon: 'fas fa-bell',
label: app.translator.trans('core.forum.settings.notify_by_web_heading'),
});
items.add('email', {
name: 'email',
icon: 'far fa-envelope',
label: app.translator.trans('core.forum.settings.notify_by_email_heading'),
});
return items;
}
/**
* Build an item list for the notification types to display in the grid.
*
* Each notification type is an object which has the following properties:
*
* - `name` The name of the notification type.
* - `icon` The icon to display in the notification grid row.
* - `label` The label to display in the notification grid row.
*
* @return {ItemList}
@@ -206,7 +179,7 @@ export default class NotificationGrid extends Component {
items.add('discussionRenamed', {
name: 'discussionRenamed',
icon: 'fas fa-pencil-alt',
icon: 'pencil',
label: app.translator.trans('core.forum.settings.notify_discussion_renamed_label')
});

View File

@@ -0,0 +1,145 @@
import Component from 'flarum/Component';
import listItems from 'flarum/helpers/listItems';
import Button from 'flarum/components/Button';
import LoadingIndicator from 'flarum/components/LoadingIndicator';
import Discussion from 'flarum/models/Discussion';
/**
* The `NotificationList` component displays a list of the logged-in user's
* notifications, grouped by discussion.
*/
export default class NotificationList extends Component {
init() {
/**
* Whether or not the notifications are loading.
*
* @type {Boolean}
*/
this.loading = false;
}
view() {
const groups = [];
if (app.cache.notifications) {
const discussions = {};
// Build an array of discussions which the notifications are related to,
// and add the notifications as children.
app.cache.notifications.forEach(notification => {
const subject = notification.subject();
if (typeof subject === 'undefined') return;
// Get the discussion that this notification is related to. If it's not
// directly related to a discussion, it may be related to a post or
// other entity which is related to a discussion.
let discussion = false;
if (subject instanceof Discussion) discussion = subject;
else if (subject && subject.discussion) discussion = subject.discussion();
// If the notification is not related to a discussion directly or
// indirectly, then we will assign it to a neutral group.
const key = discussion ? discussion.id() : 0;
discussions[key] = discussions[key] || {discussion: discussion, notifications: []};
discussions[key].notifications.push(notification);
if (groups.indexOf(discussions[key]) === -1) {
groups.push(discussions[key]);
}
});
}
return (
<div className="NotificationList">
<div className="NotificationList-header">
<div className="App-primaryControl">
{Button.component({
className: 'Button Button--icon Button--link',
icon: 'check',
title: app.translator.trans('core.forum.notifications.mark_all_as_read_tooltip'),
onclick: this.markAllAsRead.bind(this)
})}
</div>
<h4 className="App-titleControl App-titleControl--text">{app.translator.trans('core.forum.notifications.title')}</h4>
</div>
<div className="NotificationList-content">
{groups.length
? groups.map(group => {
const badges = group.discussion && group.discussion.badges().toArray();
return (
<div className="NotificationGroup">
{group.discussion
? (
<a className="NotificationGroup-header"
href={app.route.discussion(group.discussion)}
config={m.route}>
{badges && badges.length ? <ul className="NotificationGroup-badges badges">{listItems(badges)}</ul> : ''}
{group.discussion.title()}
</a>
) : (
<div className="NotificationGroup-header">
{app.forum.attribute('title')}
</div>
)}
<ul className="NotificationGroup-content">
{group.notifications.map(notification => {
const NotificationComponent = app.notificationComponents[notification.contentType()];
return NotificationComponent ? <li>{NotificationComponent.component({notification})}</li> : '';
})}
</ul>
</div>
);
})
: !this.loading
? <div className="NotificationList-empty">{app.translator.trans('core.forum.notifications.empty_text')}</div>
: LoadingIndicator.component({className: 'LoadingIndicator--block'})}
</div>
</div>
);
}
/**
* Load notifications into the application's cache if they haven't already
* been loaded.
*/
load() {
if (app.cache.notifications && !app.session.user.newNotificationsCount()) {
return;
}
this.loading = true;
m.redraw();
app.store.find('notifications')
.then(notifications => {
app.session.user.pushAttributes({newNotificationsCount: 0});
app.cache.notifications = notifications.sort((a, b) => b.time() - a.time());
})
.catch(() => {})
.then(() => {
this.loading = false;
m.redraw();
});
}
/**
* Mark all of the notifications as read.
*/
markAllAsRead() {
if (!app.cache.notifications) return;
app.session.user.pushAttributes({unreadNotificationsCount: 0});
app.cache.notifications.forEach(notification => notification.pushAttributes({isRead: true}));
app.request({
url: app.forum.attribute('apiUrl') + '/notifications/read',
method: 'POST'
});
}
}

View File

@@ -1,6 +1,6 @@
import Dropdown from '../../common/components/Dropdown';
import icon from '../../common/helpers/icon';
import NotificationList from './NotificationList';
import Dropdown from 'flarum/components/Dropdown';
import icon from 'flarum/helpers/icon';
import NotificationList from 'flarum/components/NotificationList';
export default class NotificationsDropdown extends Dropdown {
static initProps(props) {
@@ -8,7 +8,7 @@ export default class NotificationsDropdown extends Dropdown {
props.buttonClassName = props.buttonClassName || 'Button Button--flat';
props.menuClassName = props.menuClassName || 'Dropdown-menu--right';
props.label = props.label || app.translator.trans('core.forum.notifications.tooltip');
props.icon = props.icon || 'fas fa-bell';
props.icon = props.icon || 'bell';
super.initProps(props);
}
@@ -62,11 +62,11 @@ export default class NotificationsDropdown extends Dropdown {
}
getUnreadCount() {
return app.session.user.unreadNotificationCount();
return app.session.user.unreadNotificationsCount();
}
getNewCount() {
return app.session.user.newNotificationCount();
return app.session.user.newNotificationsCount();
}
menuClick(e) {

View File

@@ -1,5 +1,5 @@
import Page from './Page';
import NotificationList from './NotificationList';
import Page from 'flarum/components/Page';
import NotificationList from 'flarum/components/NotificationList';
/**
* The `NotificationsPage` component shows the notifications list. It is only

View File

@@ -1,4 +1,4 @@
import Component from '../../common/Component';
import Component from 'flarum/Component';
/**
* The `Page` component

View File

@@ -1,9 +1,9 @@
import Component from '../../common/Component';
import SubtreeRetainer from '../../common/utils/SubtreeRetainer';
import Dropdown from '../../common/components/Dropdown';
import PostControls from '../utils/PostControls';
import listItems from '../../common/helpers/listItems';
import ItemList from '../../common/utils/ItemList';
import Component from 'flarum/Component';
import SubtreeRetainer from 'flarum/utils/SubtreeRetainer';
import Dropdown from 'flarum/components/Dropdown';
import PostControls from 'flarum/utils/PostControls';
import listItems from 'flarum/helpers/listItems';
import ItemList from 'flarum/utils/ItemList';
/**
* The `Post` component displays a single post. The basic post template just
@@ -39,7 +39,7 @@ export default class Post extends Component {
view() {
const attrs = this.attrs();
attrs.className = this.classes(attrs.className).join(' ');
attrs.className = 'Post ' + (this.loading ? 'Post--loading ' : '') + (attrs.className || '');
return (
<article {...attrs}>
@@ -57,7 +57,7 @@ export default class Post extends Component {
className="Post-controls"
buttonClassName="Button Button--icon Button--flat"
menuClassName="Dropdown-menu--right"
icon="fas fa-ellipsis-h"
icon="ellipsis-h"
onshow={() => this.$('.Post-actions').addClass('open')}
onhide={() => this.$('.Post-actions').removeClass('open')}>
{controls}
@@ -98,26 +98,6 @@ export default class Post extends Component {
return [];
}
/**
* Get the post's classes.
*
* @param string classes
* @returns {string[]}
*/
classes(existing) {
let classes = (existing || '').split(' ').concat(['Post']);
if (this.loading) {
classes.push('Post--loading');
}
if (this.props.post.user() === app.session.user) {
classes.push('Post--by-actor');
}
return classes;
}
/**
* Build an item list for the post's actions.
*

View File

@@ -1,6 +1,6 @@
import Component from '../../common/Component';
import humanTime from '../../common/utils/humanTime';
import extractText from '../../common/utils/extractText';
import Component from 'flarum/Component';
import humanTime from 'flarum/utils/humanTime';
import extractText from 'flarum/utils/extractText';
/**
* The `PostEdited` component displays information about when and by whom a post
@@ -18,10 +18,10 @@ export default class PostEdited extends Component {
view() {
const post = this.props.post;
const editedUser = post.editedUser();
const editUser = post.editUser();
const editedInfo = extractText(app.translator.trans(
'core.forum.post.edited_tooltip',
{user: editedUser, ago: humanTime(post.editedAt())}
{user: editUser, ago: humanTime(post.editTime())}
));
if (editedInfo !== this.oldEditedInfo) {
this.shouldUpdateTooltip = true;

View File

@@ -1,6 +1,6 @@
import Component from '../../common/Component';
import humanTime from '../../common/helpers/humanTime';
import fullTime from '../../common/helpers/fullTime';
import Component from 'flarum/Component';
import humanTime from 'flarum/helpers/humanTime';
import fullTime from 'flarum/helpers/fullTime';
/**
* The `PostMeta` component displays the time of a post, and when clicked, shows
@@ -14,7 +14,7 @@ import fullTime from '../../common/helpers/fullTime';
export default class PostMeta extends Component {
view() {
const post = this.props.post;
const time = post.createdAt();
const time = post.time();
const permalink = this.getPermalink(post);
const touch = 'ontouchstart' in document.documentElement;

View File

@@ -1,7 +1,7 @@
import Component from '../../common/Component';
import avatar from '../../common/helpers/avatar';
import username from '../../common/helpers/username';
import highlight from '../../common/helpers/highlight';
import Component from 'flarum/Component';
import avatar from 'flarum/helpers/avatar';
import username from 'flarum/helpers/username';
import highlight from 'flarum/helpers/highlight';
/**
* The `PostPreview` component shows a link to a post containing the avatar and

View File

@@ -1,10 +1,11 @@
import Component from '../../common/Component';
import ScrollListener from '../../common/utils/ScrollListener';
import PostLoading from './LoadingPost';
import anchorScroll from '../../common/utils/anchorScroll';
import evented from '../../common/utils/evented';
import ReplyPlaceholder from './ReplyPlaceholder';
import Button from '../../common/components/Button';
import Component from 'flarum/Component';
import ScrollListener from 'flarum/utils/ScrollListener';
import PostLoading from 'flarum/components/LoadingPost';
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
@@ -121,11 +122,11 @@ class PostStream extends Component {
* @public
*/
update() {
if (!this.viewingEnd) return m.deferred().resolve().promise;
if (!this.viewingEnd) return;
this.visibleEnd = this.count();
return this.loadRange(this.visibleStart, this.visibleEnd).then(() => m.redraw());
this.loadRange(this.visibleStart, this.visibleEnd).then(() => m.redraw());
}
/**
@@ -204,7 +205,7 @@ class PostStream extends Component {
const attrs = {'data-index': this.visibleStart + i};
if (post) {
const time = post.createdAt();
const time = post.time();
const PostComponent = app.postComponents[post.contentType()];
content = PostComponent ? PostComponent.component({post}) : '';
@@ -585,7 +586,7 @@ class PostStream extends Component {
*/
unpause() {
this.paused = false;
this.scrollListener.update();
this.scrollListener.update(true);
this.trigger('unpaused');
}
}

View File

@@ -1,8 +1,9 @@
import Component from '../../common/Component';
import icon from '../../common/helpers/icon';
import ScrollListener from '../../common/utils/ScrollListener';
import SubtreeRetainer from '../../common/utils/SubtreeRetainer';
import formatNumber from '../../common/utils/formatNumber';
import Component from 'flarum/Component';
import icon from 'flarum/helpers/icon';
import ScrollListener from 'flarum/utils/ScrollListener';
import SubtreeRetainer from 'flarum/utils/SubtreeRetainer';
import computed from 'flarum/utils/computed';
import formatNumber from 'flarum/utils/formatNumber';
/**
* The `PostStreamScrubber` component displays a scrubber which can be used to
@@ -83,13 +84,13 @@ export default class PostStreamScrubber extends Component {
return (
<div className={'PostStreamScrubber Dropdown ' + (this.disabled() ? 'disabled ' : '') + (this.props.className || '')}>
<button className="Button Dropdown-toggle" data-toggle="dropdown">
{viewing} {icon('fas fa-sort')}
{viewing} {icon('sort')}
</button>
<div className="Dropdown-menu dropdown-menu">
<div className="Scrubber">
<a className="Scrubber-first" onclick={this.goToFirst.bind(this)}>
{icon('fas fa-angle-double-up')} {app.translator.trans('core.forum.post_scrubber.original_post_link')}
{icon('angle-double-up')} {app.translator.trans('core.forum.post_scrubber.original_post_link')}
</a>
<div className="Scrubber-scrollbar">
@@ -98,7 +99,7 @@ export default class PostStreamScrubber extends Component {
<div className="Scrubber-bar"/>
<div className="Scrubber-info">
<strong>{viewing}</strong>
<span className="Scrubber-description">{retain || this.description}</span>
<span class="Scrubber-description">{retain || this.description}</span>
</div>
</div>
<div className="Scrubber-after"/>
@@ -109,7 +110,7 @@ export default class PostStreamScrubber extends Component {
</div>
<a className="Scrubber-last" onclick={this.goToLast.bind(this)}>
{icon('fas fa-angle-double-down')} {app.translator.trans('core.forum.post_scrubber.now_link')}
{icon('angle-double-down')} {app.translator.trans('core.forum.post_scrubber.now_link')}
</a>
</div>
</div>
@@ -131,7 +132,7 @@ export default class PostStreamScrubber extends Component {
*/
goToLast() {
this.props.stream.goToLast();
this.index = this.count();
this.index = this.props.stream.count();
this.renderScrollbar(true);
}
@@ -189,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
@@ -256,12 +258,12 @@ export default class PostStreamScrubber extends Component {
// When any part of the whole scrollbar is clicked, we want to jump to
// that position.
this.$('.Scrubber-scrollbar')
.bind('click', this.onclick.bind(this))
.click(this.onclick.bind(this))
// Now we want to make the scrollbar handle draggable. Let's start by
// preventing default browser events from messing things up.
.css({ cursor: 'pointer', 'user-select': 'none' })
.bind('dragstart mousedown touchstart', e => e.preventDefault());
.on('dragstart mousedown touchstart', e => e.preventDefault());
// When the mouse is pressed on the scrollbar handle, we capture some
// information about its current position. We will store this
@@ -273,7 +275,7 @@ export default class PostStreamScrubber extends Component {
this.$('.Scrubber-handle')
.css('cursor', 'move')
.bind('mousedown touchstart', this.onmousedown.bind(this))
.on('mousedown touchstart', this.onmousedown.bind(this))
// Exempt the scrollbar handle from the 'jump to' click event.
.click(e => e.stopPropagation());
@@ -313,7 +315,7 @@ export default class PostStreamScrubber extends Component {
const visible = this.visible || 1;
const $scrubber = this.$();
$scrubber.find('.Scrubber-index').text(formatNumber(Math.min(Math.ceil(index + visible), count)));
$scrubber.find('.Scrubber-index').text(formatNumber(Math.ceil(index + visible)));
$scrubber.find('.Scrubber-description').text(this.description);
$scrubber.toggleClass('disabled', this.disabled());
@@ -364,7 +366,7 @@ export default class PostStreamScrubber extends Component {
}
onresize() {
this.scrollListener.update();
this.scrollListener.update(true);
// Adjust the height of the scrollbar so that it fills the height of
// the sidebar and doesn't overlap the footer.

View File

@@ -1,9 +1,9 @@
import Component from '../../common/Component';
import UserCard from './UserCard';
import avatar from '../../common/helpers/avatar';
import username from '../../common/helpers/username';
import userOnline from '../../common/helpers/userOnline';
import listItems from '../../common/helpers/listItems';
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';
/**
* The `PostUser` component shows the avatar and username of a post's author.
@@ -46,11 +46,10 @@ 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'})}
{userOnline(user)}
{username(user)}
{avatar(user, {className: 'PostUser-avatar'})}{' '}{username(user)}
</a>
</h3>
<ul className="PostUser-badges badges">

View File

@@ -1,8 +1,7 @@
import UserPage from './UserPage';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import Button from '../../common/components/Button';
import Placeholder from '../../common/components/Placeholder';
import CommentPost from './CommentPost';
import UserPage from 'flarum/components/UserPage';
import LoadingIndicator from 'flarum/components/LoadingIndicator';
import Button from 'flarum/components/Button';
import CommentPost from 'flarum/components/CommentPost';
/**
* The `PostsUserPage` component shows a user's activity feed inside of their
@@ -44,14 +43,6 @@ export default class PostsUserPage extends UserPage {
}
content() {
if (this.posts.length === 0 && ! this.loading) {
return (
<div className="PostsUserPage">
<Placeholder text={app.translator.trans('core.forum.user.posts_empty_text')} />
</div>
);
}
let footer;
if (this.loading) {
@@ -80,9 +71,7 @@ export default class PostsUserPage extends UserPage {
</li>
))}
</ul>
<div className="PostsUserPage-loadMore">
{footer}
</div>
{footer}
</div>
);
}
@@ -125,7 +114,7 @@ export default class PostsUserPage extends UserPage {
type: 'comment'
},
page: {offset, limit: this.loadLimit},
sort: '-createdAt'
sort: '-time'
});
}

View File

@@ -1,5 +1,5 @@
import Modal from '../../common/components/Modal';
import Button from '../../common/components/Button';
import Modal from 'flarum/components/Modal';
import Button from 'flarum/components/Button';
/**
* The 'RenameDiscussionModal' displays a modal dialog with an input to rename a discussion
@@ -24,13 +24,13 @@ export default class RenameDiscussionModal extends Modal {
content() {
return (
<div className="Modal-body">
<div className="Form Form--centered">
<div className="Form">
<div className="Form-group">
<input className="FormControl" bidi={this.newTitle} type="text" />
<input className="FormControl title" placeholder={this.currentTitle} bidi={this.newTitle} />
</div>
<div className="Form-group">
{Button.component({
className: 'Button Button--primary Button--block',
className: 'Button Button--primary',
type: 'submit',
loading: this.loading,
children: app.translator.trans('core.forum.rename_discussion.submit_button')
@@ -59,9 +59,6 @@ export default class RenameDiscussionModal extends Modal {
}
m.redraw();
this.hide();
}).catch(() => {
this.loading = false;
m.redraw();
});
} else {
this.hide();

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