1
0
mirror of https://github.com/flarum/core.git synced 2025-08-15 04:44:08 +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
872 changed files with 14817 additions and 26465 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).

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).

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,86 +0,0 @@
# Changelog
## [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,6 +1,6 @@
The MIT License (MIT)
Copyright (c) Toby Zerner
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.
## 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,64 +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",
"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/translation": "^3.3",
"symfony/yaml": "^3.3",
"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.8.4",
"zendframework/zend-httphandlerrunner": "^1.0",
"zendframework/zend-stratigility": "^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": {
@@ -81,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,56 +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']}),
{}
);
Object.keys(this.driverFields).flatMap(key => this.driverFields[key]).forEach(
key => {
this.fields.push(key);
this.values[key] = m.prop(settings[key]);
}
);
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">
@@ -60,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',
@@ -71,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">
{this.driverFields[this.values.mail_driver()].flatMap(field => [
<label>{app.translator.trans(`core.admin.email.${field}_label`)}</label>,
<input className="FormControl" value={this.values[field]() || ''} oninput={m.withAttr('value', this.values[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>
@@ -115,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 = {};
@@ -130,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'
}
};

40
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

67
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

View File

@@ -1,11 +0,0 @@
/*
* 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.
*/
export * from './src/common';
export * from './src/forum';

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

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
@@ -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}

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();

View File

@@ -1,8 +1,8 @@
import ComposerBody from './ComposerBody';
import Alert from '../../common/components/Alert';
import Button from '../../common/components/Button';
import icon from '../../common/helpers/icon';
import extractText from '../../common/utils/extractText';
import ComposerBody from 'flarum/components/ComposerBody';
import Alert from 'flarum/components/Alert';
import Button from 'flarum/components/Button';
import icon from 'flarum/helpers/icon';
import extractText from 'flarum/utils/extractText';
function minimizeComposerIfFullScreen(e) {
if (app.composer.isFullScreen()) {
@@ -51,7 +51,7 @@ export default class ReplyComposer extends ComposerBody {
items.add('title', (
<h3>
{icon('fas fa-reply')} {' '}
{icon('reply')} {' '}
<a href={app.route.discussion(discussion)} config={routeAndMinimize}>{discussion.title()}</a>
</h3>
));
@@ -82,10 +82,9 @@ export default class ReplyComposer extends ComposerBody {
app.store.createRecord('posts').save(data).then(
post => {
// If we're currently viewing the discussion which this reply was made
// in, then we can update the post stream and scroll to the post.
// in, then we can update the post stream.
if (app.viewingDiscussion(discussion)) {
app.current.stream.update().then(() => app.current.stream.goToNumber(post.number()));
app.current.stream.update();
} else {
// Otherwise, we'll create an alert message to inform the user that
// their reply has been posted, containing a button which will

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